create-walle 0.9.25 → 0.9.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/bin/create-walle.js +815 -45
- package/package.json +2 -2
- package/template/bin/ctm-dev-cleanup.js +90 -4
- package/template/bin/ctm-launch.sh +49 -1
- package/template/bin/dev.sh +45 -1
- package/template/bin/ensure-stable-node.js +132 -0
- package/template/bin/install-service.sh +9 -0
- package/template/claude-task-manager/api-prompts.js +899 -119
- package/template/claude-task-manager/approval-agent.js +360 -40
- package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
- package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
- package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
- package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
- package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
- package/template/claude-task-manager/db.js +399 -48
- package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
- package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
- package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
- package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
- package/template/claude-task-manager/lib/approval-hook.js +200 -0
- package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
- package/template/claude-task-manager/lib/auth-rules.js +11 -0
- package/template/claude-task-manager/lib/background-llm.js +32 -4
- package/template/claude-task-manager/lib/codesign-identity.js +140 -0
- package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
- package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
- package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
- package/template/claude-task-manager/lib/codex-paths.js +73 -0
- package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
- package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
- package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
- package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
- package/template/claude-task-manager/lib/command-targets.js +163 -0
- package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
- package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
- package/template/claude-task-manager/lib/escalation-review.js +80 -3
- package/template/claude-task-manager/lib/flow-control.js +52 -0
- package/template/claude-task-manager/lib/fs-watcher.js +24 -15
- package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
- package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
- package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
- package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
- package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
- package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
- package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
- package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
- package/template/claude-task-manager/lib/perf-tracker.js +29 -2
- package/template/claude-task-manager/lib/permission-match.js +146 -16
- package/template/claude-task-manager/lib/project-slug.js +33 -0
- package/template/claude-task-manager/lib/prompt-intent.js +51 -4
- package/template/claude-task-manager/lib/read-pool-client.js +48 -3
- package/template/claude-task-manager/lib/real-node.js +73 -0
- package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
- package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
- package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
- package/template/claude-task-manager/lib/session-history.js +5 -7
- package/template/claude-task-manager/lib/session-host-manager.js +19 -0
- package/template/claude-task-manager/lib/session-jobs.js +6 -0
- package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
- package/template/claude-task-manager/lib/session-messages-page.js +211 -0
- package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
- package/template/claude-task-manager/lib/session-standup.js +8 -0
- package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
- package/template/claude-task-manager/lib/session-token-usage.js +30 -8
- package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
- package/template/claude-task-manager/lib/storage-migration.js +2 -1
- package/template/claude-task-manager/lib/transcript-store.js +179 -12
- package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
- package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
- package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
- package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
- package/template/claude-task-manager/package.json +5 -2
- package/template/claude-task-manager/prompt-harvest.js +31 -11
- package/template/claude-task-manager/providers/claude-code.js +29 -1
- package/template/claude-task-manager/providers/codex.js +13 -1
- package/template/claude-task-manager/public/css/setup.css +11 -0
- package/template/claude-task-manager/public/css/walle-session.css +132 -4
- package/template/claude-task-manager/public/css/walle.css +89 -0
- package/template/claude-task-manager/public/icon-16.png +0 -0
- package/template/claude-task-manager/public/icon-32.png +0 -0
- package/template/claude-task-manager/public/icon-512.png +0 -0
- package/template/claude-task-manager/public/index.html +2483 -165
- package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
- package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
- package/template/claude-task-manager/public/js/message-renderer.js +60 -1
- package/template/claude-task-manager/public/js/prompts.js +13 -1
- package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
- package/template/claude-task-manager/public/js/setup.js +54 -10
- package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
- package/template/claude-task-manager/public/js/stream-view.js +78 -0
- package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
- package/template/claude-task-manager/public/js/tool-state.js +155 -0
- package/template/claude-task-manager/public/js/walle-session.js +887 -326
- package/template/claude-task-manager/public/js/walle.js +306 -195
- package/template/claude-task-manager/public/m/app.css +1 -0
- package/template/claude-task-manager/public/m/app.js +33 -3
- package/template/claude-task-manager/queue-engine.js +45 -1
- package/template/claude-task-manager/server.js +3367 -540
- package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
- package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
- package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
- package/template/claude-task-manager/workers/session-host-process.js +10 -0
- package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
- package/template/package.json +2 -3
- package/template/shared/icons/AppIcon-ctm.icns +0 -0
- package/template/shared/icons/AppIcon-walle.icns +0 -0
- package/template/wall-e/agent.js +139 -18
- package/template/wall-e/api-walle.js +201 -22
- package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
- package/template/wall-e/brain.js +1049 -39
- package/template/wall-e/chat.js +427 -86
- package/template/wall-e/coding/acceptance-contract.js +26 -1
- package/template/wall-e/coding/action-memory-policy.js +353 -0
- package/template/wall-e/coding/action-memory-store.js +814 -0
- package/template/wall-e/coding/initial-messages.js +197 -0
- package/template/wall-e/coding/no-progress-guard.js +327 -0
- package/template/wall-e/coding/permission-service.js +88 -22
- package/template/wall-e/coding/session-workspaces.js +81 -0
- package/template/wall-e/coding/shell-sandbox.js +124 -0
- package/template/wall-e/coding/stream-processor.js +63 -2
- package/template/wall-e/coding/tool-execution-controller.js +14 -1
- package/template/wall-e/coding/tool-registry.js +1 -1
- package/template/wall-e/coding/transcript-writer.js +3 -0
- package/template/wall-e/coding-orchestrator.js +636 -35
- package/template/wall-e/coding-prompts.js +51 -2
- package/template/wall-e/docs/model-routing-policy.md +59 -0
- package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
- package/template/wall-e/extraction/knowledge-extractor.js +76 -23
- package/template/wall-e/http/chat-api.js +30 -12
- package/template/wall-e/http/model-admin.js +93 -1
- package/template/wall-e/lib/background-lanes.js +133 -0
- package/template/wall-e/lib/boot-profile.js +11 -0
- package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
- package/template/wall-e/lib/brain-read-pool-client.js +311 -0
- package/template/wall-e/lib/diagnostics-flags.js +87 -0
- package/template/wall-e/lib/event-loop-monitor.js +74 -3
- package/template/wall-e/lib/mcp-integration.js +7 -1
- package/template/wall-e/lib/real-node.js +98 -0
- package/template/wall-e/lib/runtime-health.js +206 -0
- package/template/wall-e/lib/runtime-worker-pool.js +101 -0
- package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
- package/template/wall-e/lib/scheduler.js +446 -17
- package/template/wall-e/lib/service-health.js +61 -2
- package/template/wall-e/lib/service-readiness.js +258 -0
- package/template/wall-e/lib/usage.js +152 -0
- package/template/wall-e/lib/worker-thread-pool.js +389 -0
- package/template/wall-e/llm/client.js +81 -4
- package/template/wall-e/llm/default-fallback.js +54 -8
- package/template/wall-e/llm/mlx.js +536 -73
- package/template/wall-e/llm/mlx.plugin.json +1 -1
- package/template/wall-e/llm/ollama.js +342 -43
- package/template/wall-e/llm/provider-error.js +18 -1
- package/template/wall-e/llm/provider-health-state.js +176 -0
- package/template/wall-e/llm/routing-policy.js +796 -0
- package/template/wall-e/llm/supported-models.js +5 -0
- package/template/wall-e/loops/tasks.js +60 -14
- package/template/wall-e/loops/think.js +89 -24
- package/template/wall-e/mcp-server.js +192 -28
- package/template/wall-e/server.js +32 -7
- package/template/wall-e/skills/script-skill-runner.js +8 -1
- package/template/wall-e/skills/skill-planner.js +64 -1
- package/template/wall-e/tools/builtin-middleware.js +67 -2
- package/template/wall-e/tools/local-tools.js +116 -26
- package/template/wall-e/tools/permission-checker.js +52 -4
- package/template/wall-e/tools/permission-rules.js +36 -0
- package/template/wall-e/tools/shell-analyzer.js +46 -1
- package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
- package/template/wall-e/training/real-trajectory-miner.js +2617 -0
- package/template/wall-e/training/replay-eval-analysis.js +151 -0
- package/template/wall-e/training/run-shell-command-selector.js +277 -0
- package/template/wall-e/training/tool-sft-dataset.js +312 -0
- package/template/wall-e/training/tool-sft-renderers.js +144 -0
- package/template/wall-e/training/tool-trace-harvester.js +1440 -0
- package/template/wall-e/training/trajectory-action-selector.js +364 -0
- package/template/wall-e/weather-runtime.js +232 -0
- package/template/wall-e/workers/brain-owner-worker.js +162 -0
- package/template/wall-e/workers/brain-read-worker.js +148 -0
- package/template/wall-e/workers/runtime-worker.js +145 -0
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env swift
|
|
2
2
|
// ctm-hotkey — Global hotkey daemon for CTM
|
|
3
|
-
// Registers Cmd+Shift+S
|
|
3
|
+
// Registers Cmd+Shift+S as a system-wide hotkey via Carbon's RegisterEventHotKey.
|
|
4
4
|
// When triggered, POSTs to CTM's screenshot API.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
6
|
+
// NO special permission required. RegisterEventHotKey is narrowly scoped — the process only
|
|
7
|
+
// ever learns that this one combo was pressed, never any other input — so macOS does NOT gate
|
|
8
|
+
// it behind the Accessibility ("can read all keystrokes") permission that a CGEventTap or an
|
|
9
|
+
// NSEvent global monitor would require. This is the same API Electron/VS Code/Slack/Alfred use.
|
|
10
|
+
// Removing the Accessibility dependency is the whole point: the prior CGEventTap build bricked
|
|
11
|
+
// itself whenever the end user didn't grant Accessibility in time.
|
|
12
|
+
//
|
|
13
|
+
// Install: swiftc -> launchd plist (see install-hotkey.sh)
|
|
8
14
|
|
|
9
15
|
import Cocoa
|
|
16
|
+
import Carbon
|
|
10
17
|
import Foundation
|
|
11
18
|
|
|
12
19
|
// --- Configuration ---
|
|
@@ -14,9 +21,9 @@ let CTM_PORT = ProcessInfo.processInfo.environment["CTM_PORT"] ?? "3456"
|
|
|
14
21
|
let CTM_URL = "http://localhost:\(CTM_PORT)/api/screenshot"
|
|
15
22
|
|
|
16
23
|
// --- Hotkey: Cmd+Shift+S ---
|
|
17
|
-
//
|
|
18
|
-
let
|
|
19
|
-
let
|
|
24
|
+
// kVK_ANSI_S == 1 ('S'); Carbon modifier masks cmdKey | shiftKey.
|
|
25
|
+
let HOTKEY_KEYCODE: UInt32 = UInt32(kVK_ANSI_S)
|
|
26
|
+
let HOTKEY_MODIFIERS: UInt32 = UInt32(cmdKey | shiftKey)
|
|
20
27
|
|
|
21
28
|
func triggerScreenshot() {
|
|
22
29
|
guard let url = URL(string: CTM_URL) else {
|
|
@@ -25,94 +32,73 @@ func triggerScreenshot() {
|
|
|
25
32
|
}
|
|
26
33
|
var request = URLRequest(url: url)
|
|
27
34
|
request.httpMethod = "POST"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
//
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if let
|
|
38
|
-
|
|
35
|
+
// Generous timeout: the server's response only returns AFTER capture+save, which can take a few
|
|
36
|
+
// seconds on a slow machine or cold start. We don't need the body — this is fire-and-forget — so
|
|
37
|
+
// a long ceiling just avoids a scary "Request failed: timed out" line when capture is merely slow.
|
|
38
|
+
request.timeoutInterval = 60
|
|
39
|
+
|
|
40
|
+
print("[ctm-hotkey] Screenshot triggered") // log the FIRE immediately, before the async result
|
|
41
|
+
URLSession.shared.dataTask(with: request) { _, response, error in
|
|
42
|
+
if let http = response as? HTTPURLResponse, http.statusCode != 200 {
|
|
43
|
+
print("[ctm-hotkey] Server returned HTTP \(http.statusCode)")
|
|
44
|
+
} else if let error = error {
|
|
45
|
+
// CTM might not be running — that's fine, fail quietly.
|
|
46
|
+
print("[ctm-hotkey] Request error: \(error.localizedDescription)")
|
|
39
47
|
}
|
|
40
48
|
}.resume()
|
|
41
49
|
}
|
|
42
50
|
|
|
43
|
-
// ---
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if type == .tapDisabledByTimeout || type == .tapDisabledByUserInput {
|
|
50
|
-
if let tap = globalTap {
|
|
51
|
-
CGEvent.tapEnable(tap: tap, enable: true)
|
|
52
|
-
}
|
|
53
|
-
return Unmanaged.passRetained(event)
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
guard type == .keyDown else {
|
|
57
|
-
return Unmanaged.passRetained(event)
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
let keyCode = event.getIntegerValueField(.keyboardEventKeycode)
|
|
61
|
-
let flags = event.flags
|
|
62
|
-
|
|
63
|
-
// Check: Cmd+Shift held, S key pressed
|
|
64
|
-
// Mask out caps lock and other irrelevant modifiers
|
|
65
|
-
let relevantFlags = flags.intersection([.maskCommand, .maskShift, .maskControl, .maskAlternate])
|
|
66
|
-
if keyCode == TARGET_KEYCODE && relevantFlags == TARGET_MODIFIERS {
|
|
67
|
-
triggerScreenshot()
|
|
68
|
-
// Return nil to consume the event (prevent "Save As" in other apps)
|
|
69
|
-
return nil
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
return Unmanaged.passRetained(event)
|
|
51
|
+
// --- Carbon hot-key handler ---
|
|
52
|
+
// A registered hot key is delivered to us BEFORE the focused app sees it, so the combo is
|
|
53
|
+
// naturally consumed (no "Save As" in the foreground app) without the old return-nil tap hack.
|
|
54
|
+
func hotKeyHandler(_ next: EventHandlerCallRef?, _ event: EventRef?, _ userData: UnsafeMutableRawPointer?) -> OSStatus {
|
|
55
|
+
triggerScreenshot()
|
|
56
|
+
return noErr
|
|
73
57
|
}
|
|
74
58
|
|
|
75
59
|
// --- Main ---
|
|
60
|
+
setbuf(stdout, nil) // unbuffered: this is a launchd daemon, its log must reflect state live
|
|
76
61
|
print("[ctm-hotkey] Starting global hotkey daemon (Cmd+Shift+S -> CTM Screenshot)")
|
|
77
62
|
print("[ctm-hotkey] CTM endpoint: \(CTM_URL)")
|
|
78
63
|
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
64
|
+
// A launchd LaunchAgent starts as a background-only process with no window-server connection,
|
|
65
|
+
// so Carbon's hot-key dispatcher would never deliver events to it. Promote to an "accessory"
|
|
66
|
+
// app (no Dock icon, no menu bar) to establish that connection — exactly what menu-bar-less
|
|
67
|
+
// hot-key utilities do. This needs no permission.
|
|
68
|
+
NSApplication.shared.setActivationPolicy(.accessory)
|
|
69
|
+
|
|
70
|
+
// Install the hot-key event handler once on the dispatcher target.
|
|
71
|
+
var eventType = EventTypeSpec(eventClass: OSType(kEventClassKeyboard), eventKind: UInt32(kEventHotKeyPressed))
|
|
72
|
+
InstallEventHandler(GetEventDispatcherTarget(), hotKeyHandler, 1, &eventType, nil, nil)
|
|
73
|
+
|
|
74
|
+
var hotKeyRef: EventHotKeyRef?
|
|
75
|
+
let hotKeyID = EventHotKeyID(signature: OSType(0x43544D4B) /* 'CTMK' */, id: 1)
|
|
76
|
+
|
|
77
|
+
func registerHotKey() -> Bool {
|
|
78
|
+
if hotKeyRef != nil { return true }
|
|
79
|
+
let status = RegisterEventHotKey(
|
|
80
|
+
HOTKEY_KEYCODE, HOTKEY_MODIFIERS, hotKeyID,
|
|
81
|
+
GetEventDispatcherTarget(), 0, &hotKeyRef
|
|
82
|
+
)
|
|
83
|
+
return status == noErr && hotKeyRef != nil
|
|
88
84
|
}
|
|
89
85
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
)
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
guard let tap = machPort else {
|
|
104
|
-
print("[ctm-hotkey] ERROR: Failed to create event tap.")
|
|
105
|
-
print("[ctm-hotkey] This usually means Accessibility permission is not granted.")
|
|
106
|
-
print("[ctm-hotkey] Go to: System Settings > Privacy & Security > Accessibility")
|
|
107
|
-
exit(1)
|
|
86
|
+
if registerHotKey() {
|
|
87
|
+
print("[ctm-hotkey] Global hotkey registered. Press Cmd+Shift+S anywhere to screenshot.")
|
|
88
|
+
} else {
|
|
89
|
+
// NEVER exit on failure — that's what bricked the old CGEventTap daemon (clean exit +
|
|
90
|
+
// KeepAlive/SuccessfulExit:false → launchd refuses to restart). Stay alive and retry so a
|
|
91
|
+
// transient failure (e.g. another app momentarily holding the combo) self-heals.
|
|
92
|
+
print("[ctm-hotkey] WARNING: hotkey registration failed; retrying every 5s…")
|
|
93
|
+
Timer.scheduledTimer(withTimeInterval: 5, repeats: true) { timer in
|
|
94
|
+
if registerHotKey() {
|
|
95
|
+
print("[ctm-hotkey] Global hotkey registered. Press Cmd+Shift+S anywhere to screenshot.")
|
|
96
|
+
timer.invalidate()
|
|
97
|
+
}
|
|
98
|
+
}
|
|
108
99
|
}
|
|
109
100
|
|
|
110
|
-
let runLoopSource = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, tap, 0)
|
|
111
|
-
CFRunLoopAddSource(CFRunLoopGetCurrent(), runLoopSource, .commonModes)
|
|
112
|
-
CGEvent.tapEnable(tap: tap, enable: true)
|
|
113
|
-
|
|
114
|
-
print("[ctm-hotkey] Global hotkey registered. Press Cmd+Shift+S anywhere to screenshot.")
|
|
115
101
|
print("[ctm-hotkey] Press Ctrl+C to stop.")
|
|
116
102
|
|
|
117
|
-
// Run the
|
|
118
|
-
|
|
103
|
+
// Run the Cocoa run loop (pumps Carbon hot-key events); blocks forever.
|
|
104
|
+
NSApp.run()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// ctm-screen-auth — request macOS Screen Recording ("Screen & System Audio Recording")
|
|
2
|
+
// permission for the current macOS responsible process.
|
|
3
|
+
//
|
|
4
|
+
// `screencapture` is checked by TCC against its responsible process, not only its own
|
|
5
|
+
// binary. CTM may capture directly under the CTM bundle identity, or through the
|
|
6
|
+
// ctm-disclaim + real-node bridge. This helper must run through that same responsible
|
|
7
|
+
// process path so CGRequestScreenCaptureAccess() prompts for the identity that the next
|
|
8
|
+
// `screencapture` call will actually use.
|
|
9
|
+
//
|
|
10
|
+
// `screencapture` never reliably surfaces the permission prompt; it just fails.
|
|
11
|
+
// CGRequestScreenCaptureAccess() DOES surface the prompt. After the user enables the
|
|
12
|
+
// permission, CTM should be relaunched for macOS to honor the new Screen Recording grant.
|
|
13
|
+
//
|
|
14
|
+
// Output (stdout): "granted" if access is already present, "requested" if we triggered the
|
|
15
|
+
// system prompt, "denied" if the request returned without access. Exit 0 only when granted.
|
|
16
|
+
|
|
17
|
+
import CoreGraphics
|
|
18
|
+
import Foundation
|
|
19
|
+
|
|
20
|
+
func emit(_ s: String) { FileHandle.standardOutput.write((s + "\n").data(using: .utf8)!) }
|
|
21
|
+
|
|
22
|
+
// --preflight: report the current grant WITHOUT ever surfacing a prompt. Used to probe several
|
|
23
|
+
// candidate node identities and pick one that is ALREADY granted, so the common case needs no
|
|
24
|
+
// user action at all. (CGRequestScreenCaptureAccess would pop a dialog for each ungranted probe.)
|
|
25
|
+
if CommandLine.arguments.contains("--preflight") {
|
|
26
|
+
let ok = CGPreflightScreenCaptureAccess()
|
|
27
|
+
emit(ok ? "granted" : "denied")
|
|
28
|
+
exit(ok ? 0 : 1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if CGPreflightScreenCaptureAccess() {
|
|
32
|
+
emit("granted")
|
|
33
|
+
exit(0)
|
|
34
|
+
}
|
|
35
|
+
let granted = CGRequestScreenCaptureAccess()
|
|
36
|
+
emit(granted ? "granted" : "requested")
|
|
37
|
+
exit(granted ? 0 : 2)
|
|
@@ -1,29 +1,89 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# install-hotkey.sh —
|
|
2
|
+
# install-hotkey.sh — Build + install the CTM global screenshot hotkey as a signed .app bundle.
|
|
3
3
|
# Usage: bash install-hotkey.sh [install|uninstall|status]
|
|
4
|
+
#
|
|
5
|
+
# Why a .app bundle (not a bare binary): RegisterEventHotKey only delivers a global hotkey to a GUI
|
|
6
|
+
# app the window server recognizes. A bare launchd executable — even with setActivationPolicy(.accessory)
|
|
7
|
+
# — registers but never receives the keypress. Wrapping it in an LSUIElement .app (Info.plist + bundle
|
|
8
|
+
# identity), a peer of Coding Task Manager.app / Wall-E.app, is what makes it fire. NO TCC permission
|
|
9
|
+
# is needed (Carbon RegisterEventHotKey is not gated behind Accessibility).
|
|
4
10
|
set -euo pipefail
|
|
5
11
|
|
|
6
12
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
7
|
-
|
|
13
|
+
SOURCE="$SCRIPT_DIR/ctm-hotkey.swift"
|
|
14
|
+
BUNDLE_DIR="$HOME/.walle/bundles/CTM-Screenshot.app"
|
|
15
|
+
EXEC="$BUNDLE_DIR/Contents/MacOS/ctm-hotkey"
|
|
16
|
+
BUNDLE_ID="com.walle.ctm.hotkey"
|
|
17
|
+
LEGACY_BINARY="$HOME/.local/bin/ctm-hotkey" # pre-bundle build; removed on install
|
|
8
18
|
PLIST_NAME="com.ctm.hotkey"
|
|
9
19
|
PLIST="$HOME/Library/LaunchAgents/${PLIST_NAME}.plist"
|
|
10
|
-
SOURCE="$SCRIPT_DIR/ctm-hotkey.swift"
|
|
11
20
|
|
|
12
21
|
case "${1:-install}" in
|
|
13
22
|
install)
|
|
14
23
|
echo "=== CTM Global Hotkey Installer ==="
|
|
15
24
|
echo ""
|
|
16
25
|
|
|
17
|
-
# 1. Compile
|
|
18
|
-
echo "[1/
|
|
19
|
-
mkdir -p "$(dirname "$
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
26
|
+
# 1. Compile the Carbon swift straight into the bundle's Contents/MacOS.
|
|
27
|
+
echo "[1/4] Compiling hotkey app..."
|
|
28
|
+
mkdir -p "$(dirname "$EXEC")"
|
|
29
|
+
# Carbon: RegisterEventHotKey / InstallEventHandler. Cocoa: NSApplication (GUI app context).
|
|
30
|
+
swiftc -O -o "$EXEC" "$SOURCE" -framework Cocoa -framework Carbon
|
|
31
|
+
chmod +x "$EXEC"
|
|
32
|
+
echo " Exec: $EXEC"
|
|
33
|
+
|
|
34
|
+
# 2. Write the bundle Info.plist (LSUIElement menu-bar accessory; bundle identity is what lets
|
|
35
|
+
# RegisterEventHotKey receive the global hotkey).
|
|
36
|
+
echo "[2/4] Writing Info.plist..."
|
|
37
|
+
cat > "$BUNDLE_DIR/Contents/Info.plist" <<PLISTEOF
|
|
38
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
39
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
40
|
+
<plist version="1.0">
|
|
41
|
+
<dict>
|
|
42
|
+
<key>CFBundleName</key><string>CTM Screenshot</string>
|
|
43
|
+
<key>CFBundleDisplayName</key><string>CTM Screenshot</string>
|
|
44
|
+
<key>CFBundleIdentifier</key><string>${BUNDLE_ID}</string>
|
|
45
|
+
<key>CFBundleExecutable</key><string>ctm-hotkey</string>
|
|
46
|
+
<key>CFBundlePackageType</key><string>APPL</string>
|
|
47
|
+
<key>CFBundleVersion</key><string>1.0</string>
|
|
48
|
+
<key>CFBundleShortVersionString</key><string>1.0</string>
|
|
49
|
+
<key>LSUIElement</key><true/>
|
|
50
|
+
<key>LSMinimumSystemVersion</key><string>11.0</string>
|
|
51
|
+
<key>NSPrincipalClass</key><string>NSApplication</string>
|
|
52
|
+
</dict>
|
|
53
|
+
</plist>
|
|
54
|
+
PLISTEOF
|
|
23
55
|
|
|
24
|
-
#
|
|
25
|
-
|
|
26
|
-
|
|
56
|
+
# 3. Sign the whole bundle so it runs for every user (arm64 won't execute an unsigned binary):
|
|
57
|
+
# Developer ID Application first (stable, branded Team Identifier — like the daemon bundles),
|
|
58
|
+
# else the stable self-signed local cert, else ad-hoc. The hotkey needs NO TCC permission, so
|
|
59
|
+
# any valid signature suffices for it to register and fire.
|
|
60
|
+
echo "[3/4] Signing bundle..."
|
|
61
|
+
DEVID_HASH="$(security find-identity -v -p codesigning 2>/dev/null | awk '/Developer ID Application/ { print $2; exit }')"
|
|
62
|
+
if [ -n "$DEVID_HASH" ] && codesign --force --deep --sign "$DEVID_HASH" --identifier "$BUNDLE_ID" "$BUNDLE_DIR" 2>/dev/null; then
|
|
63
|
+
echo " Signed (Developer ID)"
|
|
64
|
+
else
|
|
65
|
+
SIGN_KEYCHAIN="$HOME/.walle/walle-signing.keychain-db"
|
|
66
|
+
SIGN_IDENTITY=""
|
|
67
|
+
if [ -f "$SIGN_KEYCHAIN" ]; then
|
|
68
|
+
security unlock-keychain -p walle "$SIGN_KEYCHAIN" 2>/dev/null || true
|
|
69
|
+
SIGN_IDENTITY="$(security find-certificate -a -c "Wall-E Local Signing" -Z "$SIGN_KEYCHAIN" 2>/dev/null | awk '/SHA-1 hash:/ { print $3; exit }')"
|
|
70
|
+
fi
|
|
71
|
+
if [ -n "$SIGN_IDENTITY" ] && {
|
|
72
|
+
security set-key-partition-list -S apple-tool:,apple: -s -k walle "$SIGN_KEYCHAIN" >/dev/null 2>&1 || true
|
|
73
|
+
codesign --force --deep --sign "$SIGN_IDENTITY" --identifier "$BUNDLE_ID" --keychain "$SIGN_KEYCHAIN" "$BUNDLE_DIR" 2>/dev/null
|
|
74
|
+
}; then
|
|
75
|
+
echo " Signed (Wall-E Local Signing)"
|
|
76
|
+
else
|
|
77
|
+
codesign --force --deep --sign - "$BUNDLE_DIR" 2>/dev/null && echo " Signed (ad-hoc)" || echo " Signing failed; continuing (hotkey needs no permission)."
|
|
78
|
+
fi
|
|
79
|
+
fi
|
|
80
|
+
rm -f "$LEGACY_BINARY" 2>/dev/null || true # migrate off any pre-bundle bare binary
|
|
81
|
+
|
|
82
|
+
# 4. Create launchd plist (points at the bundle exec) + load it. Modern domain-target API:
|
|
83
|
+
# legacy `load`/`unload` fail "5: Input/output error" when the label carries a stale *disabled*
|
|
84
|
+
# override; `enable` clears it so bootstrap can re-load.
|
|
85
|
+
echo "[4/4] Installing + starting LaunchAgent..."
|
|
86
|
+
mkdir -p "$(dirname "$PLIST")" "$HOME/.local/log"
|
|
27
87
|
cat > "$PLIST" <<PLISTEOF
|
|
28
88
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
29
89
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
@@ -33,7 +93,7 @@ case "${1:-install}" in
|
|
|
33
93
|
<string>${PLIST_NAME}</string>
|
|
34
94
|
<key>ProgramArguments</key>
|
|
35
95
|
<array>
|
|
36
|
-
<string>${
|
|
96
|
+
<string>${EXEC}</string>
|
|
37
97
|
</array>
|
|
38
98
|
<key>EnvironmentVariables</key>
|
|
39
99
|
<dict>
|
|
@@ -43,7 +103,10 @@ case "${1:-install}" in
|
|
|
43
103
|
<key>RunAtLoad</key>
|
|
44
104
|
<true/>
|
|
45
105
|
<key>KeepAlive</key>
|
|
46
|
-
<
|
|
106
|
+
<dict>
|
|
107
|
+
<key>SuccessfulExit</key>
|
|
108
|
+
<false/>
|
|
109
|
+
</dict>
|
|
47
110
|
<key>StandardOutPath</key>
|
|
48
111
|
<string>${HOME}/.local/log/ctm-hotkey.log</string>
|
|
49
112
|
<key>StandardErrorPath</key>
|
|
@@ -52,58 +115,43 @@ case "${1:-install}" in
|
|
|
52
115
|
</plist>
|
|
53
116
|
PLISTEOF
|
|
54
117
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
launchctl
|
|
60
|
-
launchctl
|
|
118
|
+
UID_N="$(id -u)"
|
|
119
|
+
SVC="gui/${UID_N}/${PLIST_NAME}"
|
|
120
|
+
launchctl bootout "$SVC" 2>/dev/null || true
|
|
121
|
+
launchctl enable "$SVC" 2>/dev/null || true
|
|
122
|
+
launchctl bootstrap "gui/${UID_N}" "$PLIST" 2>/dev/null || launchctl load "$PLIST"
|
|
123
|
+
launchctl kickstart -k "$SVC" 2>/dev/null || true
|
|
61
124
|
|
|
62
125
|
echo ""
|
|
63
126
|
echo "=== Installed! ==="
|
|
64
127
|
echo " Hotkey: Cmd+Shift+S (global, works in any app)"
|
|
65
|
-
echo "
|
|
66
|
-
echo " Daemon: Auto-starts on login"
|
|
128
|
+
echo " App: $BUNDLE_DIR"
|
|
67
129
|
echo " Log: ~/.local/log/ctm-hotkey.log"
|
|
68
130
|
echo ""
|
|
69
|
-
echo "
|
|
70
|
-
echo "
|
|
71
|
-
echo " Add: $BINARY"
|
|
131
|
+
echo "No permission prompt needed — the hotkey works immediately."
|
|
132
|
+
echo "(The screenshot capture itself uses Screen Recording, handled by CTM.)"
|
|
72
133
|
echo ""
|
|
73
134
|
echo "To uninstall: bash $0 uninstall"
|
|
74
135
|
;;
|
|
75
136
|
|
|
76
137
|
uninstall)
|
|
77
138
|
echo "=== CTM Global Hotkey Uninstaller ==="
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
rm -f "$BINARY"
|
|
86
|
-
echo "Removed: $BINARY"
|
|
87
|
-
fi
|
|
139
|
+
# bootout (not `disable`): tear the job down but leave the label *enabled* so a future reinstall
|
|
140
|
+
# isn't blocked by a disabled override.
|
|
141
|
+
echo "Stopping daemon..."
|
|
142
|
+
launchctl bootout "gui/$(id -u)/${PLIST_NAME}" 2>/dev/null || true
|
|
143
|
+
rm -f "$PLIST" 2>/dev/null && echo "Removed: $PLIST" || true
|
|
144
|
+
rm -rf "$BUNDLE_DIR" 2>/dev/null && echo "Removed: $BUNDLE_DIR" || true
|
|
145
|
+
rm -f "$LEGACY_BINARY" 2>/dev/null || true
|
|
88
146
|
echo "Done. Hotkey disabled."
|
|
89
147
|
;;
|
|
90
148
|
|
|
91
149
|
status)
|
|
92
150
|
echo "=== CTM Global Hotkey Status ==="
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
echo "
|
|
97
|
-
fi
|
|
98
|
-
if [ -f "$PLIST" ]; then
|
|
99
|
-
echo "Plist: $PLIST (exists)"
|
|
100
|
-
else
|
|
101
|
-
echo "Plist: not installed"
|
|
102
|
-
fi
|
|
103
|
-
if launchctl list | grep -q "$PLIST_NAME" 2>/dev/null; then
|
|
104
|
-
echo "Daemon: running"
|
|
105
|
-
PID=$(launchctl list | grep "$PLIST_NAME" | awk '{print $1}')
|
|
106
|
-
echo "PID: $PID"
|
|
151
|
+
[ -x "$EXEC" ] && echo "App: $BUNDLE_DIR (installed)" || echo "App: not installed"
|
|
152
|
+
[ -f "$PLIST" ] && echo "Plist: $PLIST (exists)" || echo "Plist: not installed"
|
|
153
|
+
if launchctl list 2>/dev/null | grep -q "$PLIST_NAME"; then
|
|
154
|
+
echo "Daemon: running (PID $(launchctl list | awk -v l="$PLIST_NAME" '$0 ~ l {print $1; exit}'))"
|
|
107
155
|
else
|
|
108
156
|
echo "Daemon: not running"
|
|
109
157
|
fi
|
|
@@ -10,6 +10,20 @@ CTM_PORT="${CTM_PORT:-3456}"
|
|
|
10
10
|
LABEL="com.walle.server"
|
|
11
11
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
12
|
|
|
13
|
+
# Provision a STABLE-IDENTITY daemon node BEFORE the restart, so the node launchd respawns runs
|
|
14
|
+
# under a notarized / Developer-ID-signed identity whose macOS TCC grants persist across restarts
|
|
15
|
+
# (stops the recurring "access data from other apps" prompt). Best-effort and OFF the launchd boot
|
|
16
|
+
# path: the FIRST run may download the notarized Node once (cached under ~/.walle/notarized-node);
|
|
17
|
+
# every later run is instant. Never blocks the restart — ctm-launch.sh just prefers the result.
|
|
18
|
+
if [[ "$(uname -s)" == "Darwin" ]]; then
|
|
19
|
+
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
|
20
|
+
PROV_NODE="$(bash "$REPO_ROOT/bin/node-bin.sh" 2>/dev/null || command -v node 2>/dev/null || true)"
|
|
21
|
+
if [[ -n "$PROV_NODE" && -f "$REPO_ROOT/bin/ensure-stable-node.js" ]]; then
|
|
22
|
+
STABLE_NODE="$("$PROV_NODE" "$REPO_ROOT/bin/ensure-stable-node.js" 2>/dev/null || true)"
|
|
23
|
+
[[ -n "$STABLE_NODE" ]] && echo "Stable daemon node: $STABLE_NODE"
|
|
24
|
+
fi
|
|
25
|
+
fi
|
|
26
|
+
|
|
13
27
|
_health_ok() { curl -sf "http://localhost:${CTM_PORT}/api/health" >/dev/null 2>&1; }
|
|
14
28
|
|
|
15
29
|
# True iff launchd reports a live (non-zero) pid for the job -- i.e. it respawned CTM,
|