25592fa6c2
ci / test (push) Successful in 6m14s
History-free snapshot of the private development repository at 77a82eb. Code, docs and the public catalogue only: no plant or job knowledge layers, no client, job or producer names (tools/scrub.py clean against the widened term list; a second word-level grep clean). The intro deck is left out because its slide images cannot be checked for job drawings.
179 lines
7.4 KiB
Python
179 lines
7.4 KiB
Python
# -*- 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"_.')
|