WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# Starts a detached terminal running the `claude` CLI with its working
|
||||
# directory set to the repository root, so the session comes up on CLAUDE.md
|
||||
# and tools/rvt.py -- which is what wires it to THIS running Revit.
|
||||
#
|
||||
# The terminal is a separate process. Nothing runs on Revit's UI thread, so a
|
||||
# long agent session cannot freeze or crash Revit, and this button touches the
|
||||
# model in no way at all: it opens no Transaction, reads no element, and does
|
||||
# not even import `vibe.helpers`' document handle.
|
||||
#
|
||||
# PLAIN `claude`, DEFAULT PERMISSIONS. Standing ruling: this launch must never
|
||||
# pass --dangerously-skip-permissions, --permission-mode bypassPermissions, or
|
||||
# any other auto-approve flag. The agent is being pointed at a live Revit
|
||||
# session and the owner's real model; every tool call it makes is worth a
|
||||
# human seeing first. If a future edit "just needs" the prompts gone, that is
|
||||
# a conversation with the owner, not a flag.
|
||||
#
|
||||
# Interactive, so this deliberately does NOT use the redirect-both-streams
|
||||
# runner from vibe.liftexport.run -- that pattern exists to capture a headless
|
||||
# run without deadlocking on a pipe, and redirecting stdout here would leave
|
||||
# the CLI with no terminal to draw on. What is reused is the other half of the
|
||||
# idiom: a generated .cmd does the cd and holds the window open, so nothing
|
||||
# has to nest quotes inside a wt.exe command line.
|
||||
#
|
||||
# WHAT THIS REPLACED: the button used to run tools/dsh-here.cmd, which starts
|
||||
# `dsh web` (the DeepSeek Harness browser UI) in the same folder. That was the
|
||||
# whole of its behaviour -- a different agent CLI in the same slot -- so it is
|
||||
# gone rather than preserved. tools/dsh-here.cmd is left on disk, now
|
||||
# unreferenced, for the owner to keep or bin.
|
||||
#
|
||||
# This button was called GOD_MOD, then `Claude` for the 2026-08-30 ribbon
|
||||
# rebuild, and is GOD_MOD again after the 2026-08-30 one -- the owner's own
|
||||
# word, underscore and all. Same button, same folder contents throughout; the
|
||||
# folder is `GodMod.pushbutton` because a component name becomes a C# class
|
||||
# name in the generated assembly.
|
||||
#
|
||||
# `__title__` is dead weight while `bundle.yaml` exists -- pyRevit reads it
|
||||
# only `if not self.meta` -- and it is kept in step anyway, so that trimming a
|
||||
# bundle file cannot resurrect a name nobody uses.
|
||||
__title__ = 'GOD_MOD'
|
||||
|
||||
import io
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from System.Diagnostics import Process, ProcessStartInfo
|
||||
|
||||
from pyrevit import forms, script
|
||||
|
||||
TITLE = 'Claude'
|
||||
MARKER = 'CLAUDE.md' # the project instructions the session reads
|
||||
|
||||
|
||||
def repo_root():
|
||||
"""The checkout to start the session in, and whether it looks like one.
|
||||
|
||||
This script lives at
|
||||
<repo>/WillTools.extension/<tab>/<panel>/<button>/script.py
|
||||
so the repo is four levels above the button. Deriving it means the
|
||||
checkout can sit anywhere; ~/pyrevit-dev stays as the fallback for the
|
||||
case where pyRevit does not hand us __file__.
|
||||
|
||||
Returns (folder, found_marker). A folder without CLAUDE.md still gets the
|
||||
session -- it is only missing the project instructions -- but the report
|
||||
says so rather than letting the agent start somewhere surprising in
|
||||
silence.
|
||||
"""
|
||||
home = os.path.join(os.path.expanduser('~'), 'pyrevit-dev')
|
||||
try:
|
||||
root = os.path.dirname(os.path.abspath(__file__))
|
||||
except NameError:
|
||||
return home, os.path.isfile(os.path.join(home, MARKER))
|
||||
for _ in range(4):
|
||||
root = os.path.dirname(root)
|
||||
if os.path.isfile(os.path.join(root, MARKER)):
|
||||
return root, True
|
||||
if os.path.isfile(os.path.join(home, MARKER)):
|
||||
return home, True
|
||||
return root, False
|
||||
|
||||
|
||||
def find_cli():
|
||||
"""Absolute path to the Claude Code CLI, or None.
|
||||
|
||||
`vibe.aiextract.find_claude` already knows the four places a per-user
|
||||
install lands and honours CLAUDE_CODE_EXE; two copies of that list would
|
||||
drift. Imported here rather than at module scope so a broken library
|
||||
still leaves this button able to say what went wrong.
|
||||
"""
|
||||
from vibe.aiextract import find_claude
|
||||
return find_claude()
|
||||
|
||||
|
||||
def write_runner(claude_exe, workdir):
|
||||
"""A one-shot .cmd that cds, runs claude, and keeps the window up.
|
||||
|
||||
It goes in %TEMP%, never in the repo and never beside the model: it holds
|
||||
an absolute path to somebody's install, which is exactly what the scrub
|
||||
gate exists to keep out of the tree. Named per Revit process, because
|
||||
cmd.exe re-reads a batch file as it runs it and two Revits rewriting one
|
||||
file under each other is a class of bug not worth owning.
|
||||
"""
|
||||
folder = os.path.join(tempfile.gettempdir(), 'WillTools')
|
||||
if not os.path.isdir(folder):
|
||||
os.makedirs(folder)
|
||||
runner = os.path.join(folder,
|
||||
'god-mod-claude-{0}.cmd'.format(os.getpid()))
|
||||
# newline='' or the CRLFs below get translated again and every line ends
|
||||
# \r\r\n. cmd.exe survives that, but a batch file nobody can read in a
|
||||
# text editor is a bad place to be debugging a launch from.
|
||||
with io.open(runner, 'w', encoding='utf-8', newline='') as fh:
|
||||
fh.write(u'@echo off\r\n')
|
||||
fh.write(u'chcp 65001 >nul\r\n')
|
||||
fh.write(u'title Claude Code - Revit\r\n')
|
||||
fh.write(u'cd /d "{0}"\r\n'.format(workdir))
|
||||
fh.write(u'echo Revit dev folder: %CD%\r\n')
|
||||
fh.write(u'echo.\r\n')
|
||||
fh.write(u'"{0}"\r\n'.format(claude_exe))
|
||||
fh.write(u'echo.\r\n')
|
||||
fh.write(u'echo [claude exited - this window stays open]\r\n')
|
||||
fh.write(u'pause >nul\r\n')
|
||||
return runner
|
||||
|
||||
|
||||
def launch(runner, workdir):
|
||||
"""Start the runner detached. Windows Terminal when it is there."""
|
||||
psi = ProcessStartInfo()
|
||||
wt = os.path.join(os.environ.get('LOCALAPPDATA', ''),
|
||||
'Microsoft', 'WindowsApps', 'wt.exe')
|
||||
if os.path.isfile(wt):
|
||||
psi.FileName = wt
|
||||
psi.Arguments = '-d "{0}" cmd.exe /c "{1}"'.format(workdir, runner)
|
||||
else:
|
||||
psi.FileName = 'cmd.exe'
|
||||
psi.Arguments = '/c "{0}"'.format(runner)
|
||||
psi.WorkingDirectory = workdir
|
||||
psi.UseShellExecute = True
|
||||
return Process.Start(psi)
|
||||
|
||||
|
||||
WORKDIR, HAS_MARKER = repo_root()
|
||||
|
||||
try:
|
||||
CLAUDE = find_cli()
|
||||
except Exception as ex:
|
||||
forms.alert('Could not look up the Claude Code CLI:\n\n{0}: {1}'.format(
|
||||
type(ex).__name__, ex), title=TITLE, exitscript=True)
|
||||
|
||||
if not CLAUDE:
|
||||
forms.alert(
|
||||
'Claude Code CLI not found.\n\n'
|
||||
'Install it, or set the CLAUDE_CODE_EXE environment variable to the '
|
||||
'full path of claude.cmd / claude.exe.\n\n'
|
||||
'Looked in the usual per-user install folders and on PATH.',
|
||||
title=TITLE, exitscript=True)
|
||||
|
||||
try:
|
||||
RUNNER = write_runner(CLAUDE, WORKDIR)
|
||||
launch(RUNNER, WORKDIR)
|
||||
except Exception as ex:
|
||||
forms.alert('Could not start Claude Code:\n\n{0}: {1}'.format(
|
||||
type(ex).__name__, ex), title=TITLE, exitscript=True)
|
||||
|
||||
output = script.get_output()
|
||||
output.print_md('### Claude Code is starting')
|
||||
output.print_md('Working folder: `{0}`'.format(WORKDIR))
|
||||
output.print_md('CLI: `{0}`'.format(CLAUDE))
|
||||
if not HAS_MARKER:
|
||||
output.print_md(
|
||||
'**No `{0}` in that folder** - the session will start without the '
|
||||
'project instructions, and without them it does not know about the '
|
||||
'bridge to this Revit.'.format(MARKER))
|
||||
output.print_md(
|
||||
'It runs with **default permissions**, so it asks before it acts. '
|
||||
'It can read and drive **this** Revit session through `tools/rvt.py` - '
|
||||
'ask it things like _"how many walls are on level 2?"_ or _"build me a '
|
||||
'button that renumbers the selected doors"_.')
|
||||
Reference in New Issue
Block a user