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.
Files changed (179) hide show
  1. package/README.md +8 -0
  2. package/bin/create-walle.js +815 -45
  3. package/package.json +2 -2
  4. package/template/bin/ctm-dev-cleanup.js +90 -4
  5. package/template/bin/ctm-launch.sh +49 -1
  6. package/template/bin/dev.sh +45 -1
  7. package/template/bin/ensure-stable-node.js +132 -0
  8. package/template/bin/install-service.sh +9 -0
  9. package/template/claude-task-manager/api-prompts.js +899 -119
  10. package/template/claude-task-manager/approval-agent.js +360 -40
  11. package/template/claude-task-manager/bin/ctm-disclaim.c +42 -0
  12. package/template/claude-task-manager/bin/ctm-hotkey.swift +67 -81
  13. package/template/claude-task-manager/bin/ctm-screen-auth.swift +37 -0
  14. package/template/claude-task-manager/bin/install-hotkey.sh +97 -49
  15. package/template/claude-task-manager/bin/restart-ctm.sh +14 -0
  16. package/template/claude-task-manager/db.js +399 -48
  17. package/template/claude-task-manager/docs/approval-hook-sandbox.md +84 -0
  18. package/template/claude-task-manager/docs/codex-app-server-approvals.md +72 -0
  19. package/template/claude-task-manager/docs/codex-native-sandbox.md +47 -0
  20. package/template/claude-task-manager/docs/prompt-editing-tree-design.md +18 -1
  21. package/template/claude-task-manager/lib/approval-hook.js +200 -0
  22. package/template/claude-task-manager/lib/approval-self-adapt.js +1 -0
  23. package/template/claude-task-manager/lib/auth-rules.js +11 -0
  24. package/template/claude-task-manager/lib/background-llm.js +32 -4
  25. package/template/claude-task-manager/lib/codesign-identity.js +140 -0
  26. package/template/claude-task-manager/lib/codex-app-server-client.js +119 -0
  27. package/template/claude-task-manager/lib/codex-approval-bridge.js +118 -0
  28. package/template/claude-task-manager/lib/codex-history-terminal-renderer.js +571 -0
  29. package/template/claude-task-manager/lib/codex-paths.js +73 -0
  30. package/template/claude-task-manager/lib/codex-rollout-snapshot.js +164 -0
  31. package/template/claude-task-manager/lib/codex-rollout-tail.js +72 -0
  32. package/template/claude-task-manager/lib/codex-sandbox-args.js +47 -0
  33. package/template/claude-task-manager/lib/coding-agent-models.js +118 -71
  34. package/template/claude-task-manager/lib/command-targets.js +163 -0
  35. package/template/claude-task-manager/lib/conversation-tail-merge.js +61 -19
  36. package/template/claude-task-manager/lib/db-owner-worker-client.js +29 -1
  37. package/template/claude-task-manager/lib/escalation-review.js +80 -3
  38. package/template/claude-task-manager/lib/flow-control.js +52 -0
  39. package/template/claude-task-manager/lib/fs-watcher.js +24 -15
  40. package/template/claude-task-manager/lib/ingest-cooldown.js +68 -0
  41. package/template/claude-task-manager/lib/jsonl-conversation-parser.js +8 -4
  42. package/template/claude-task-manager/lib/launchd-recovery.js +92 -0
  43. package/template/claude-task-manager/lib/microsoft-dev-tunnel-setup.js +207 -52
  44. package/template/claude-task-manager/lib/mobile-push-store.js +7 -0
  45. package/template/claude-task-manager/lib/model-overview-brain-fallback.js +102 -1
  46. package/template/claude-task-manager/lib/model-overview-cache.js +1 -0
  47. package/template/claude-task-manager/lib/oauth-proxy-supervisor.js +2 -1
  48. package/template/claude-task-manager/lib/perf-tracker.js +29 -2
  49. package/template/claude-task-manager/lib/permission-match.js +146 -16
  50. package/template/claude-task-manager/lib/project-slug.js +33 -0
  51. package/template/claude-task-manager/lib/prompt-intent.js +51 -4
  52. package/template/claude-task-manager/lib/read-pool-client.js +48 -3
  53. package/template/claude-task-manager/lib/real-node.js +73 -0
  54. package/template/claude-task-manager/lib/runtime-work-registry.js +131 -14
  55. package/template/claude-task-manager/lib/session-content-backfill.js +24 -5
  56. package/template/claude-task-manager/lib/session-diagnostics-batch.js +87 -0
  57. package/template/claude-task-manager/lib/session-history.js +5 -7
  58. package/template/claude-task-manager/lib/session-host-manager.js +19 -0
  59. package/template/claude-task-manager/lib/session-jobs.js +6 -0
  60. package/template/claude-task-manager/lib/session-message-response-cache.js +89 -0
  61. package/template/claude-task-manager/lib/session-messages-page.js +211 -0
  62. package/template/claude-task-manager/lib/session-messages-projection.js +170 -0
  63. package/template/claude-task-manager/lib/session-standup.js +8 -0
  64. package/template/claude-task-manager/lib/session-timeline-summary.js +16 -2
  65. package/template/claude-task-manager/lib/session-token-usage.js +30 -8
  66. package/template/claude-task-manager/lib/session-workspace-binding.js +29 -15
  67. package/template/claude-task-manager/lib/storage-migration.js +2 -1
  68. package/template/claude-task-manager/lib/transcript-store.js +179 -12
  69. package/template/claude-task-manager/lib/walle-ctm-history.js +298 -11
  70. package/template/claude-task-manager/lib/walle-permission-reply.js +49 -0
  71. package/template/claude-task-manager/lib/walle-session-cache.js +22 -1
  72. package/template/claude-task-manager/lib/walle-supervisor.js +42 -3
  73. package/template/claude-task-manager/package.json +5 -2
  74. package/template/claude-task-manager/prompt-harvest.js +31 -11
  75. package/template/claude-task-manager/providers/claude-code.js +29 -1
  76. package/template/claude-task-manager/providers/codex.js +13 -1
  77. package/template/claude-task-manager/public/css/setup.css +11 -0
  78. package/template/claude-task-manager/public/css/walle-session.css +132 -4
  79. package/template/claude-task-manager/public/css/walle.css +89 -0
  80. package/template/claude-task-manager/public/icon-16.png +0 -0
  81. package/template/claude-task-manager/public/icon-32.png +0 -0
  82. package/template/claude-task-manager/public/icon-512.png +0 -0
  83. package/template/claude-task-manager/public/index.html +2483 -165
  84. package/template/claude-task-manager/public/js/activation-render-check.js +55 -0
  85. package/template/claude-task-manager/public/js/flow-control-policy.js +52 -0
  86. package/template/claude-task-manager/public/js/message-renderer.js +60 -1
  87. package/template/claude-task-manager/public/js/prompts.js +13 -1
  88. package/template/claude-task-manager/public/js/session-status-precedence.js +9 -3
  89. package/template/claude-task-manager/public/js/setup.js +54 -10
  90. package/template/claude-task-manager/public/js/stream-resize-policy.js +80 -0
  91. package/template/claude-task-manager/public/js/stream-view.js +78 -0
  92. package/template/claude-task-manager/public/js/terminal-reconciler.js +52 -2
  93. package/template/claude-task-manager/public/js/tool-state.js +155 -0
  94. package/template/claude-task-manager/public/js/walle-session.js +887 -326
  95. package/template/claude-task-manager/public/js/walle.js +306 -195
  96. package/template/claude-task-manager/public/m/app.css +1 -0
  97. package/template/claude-task-manager/public/m/app.js +33 -3
  98. package/template/claude-task-manager/queue-engine.js +45 -1
  99. package/template/claude-task-manager/server.js +3367 -540
  100. package/template/claude-task-manager/workers/approval-blocklist.js +130 -17
  101. package/template/claude-task-manager/workers/db-owner-worker.js +31 -1
  102. package/template/claude-task-manager/workers/read-pool-worker.js +92 -5
  103. package/template/claude-task-manager/workers/session-host-process.js +10 -0
  104. package/template/claude-task-manager/workers/state-detectors/codex.js +58 -7
  105. package/template/package.json +2 -3
  106. package/template/shared/icons/AppIcon-ctm.icns +0 -0
  107. package/template/shared/icons/AppIcon-walle.icns +0 -0
  108. package/template/wall-e/agent.js +139 -18
  109. package/template/wall-e/api-walle.js +201 -22
  110. package/template/wall-e/bin/train-gemma-e4b-tooluse.js +1981 -0
  111. package/template/wall-e/brain.js +1049 -39
  112. package/template/wall-e/chat.js +427 -86
  113. package/template/wall-e/coding/acceptance-contract.js +26 -1
  114. package/template/wall-e/coding/action-memory-policy.js +353 -0
  115. package/template/wall-e/coding/action-memory-store.js +814 -0
  116. package/template/wall-e/coding/initial-messages.js +197 -0
  117. package/template/wall-e/coding/no-progress-guard.js +327 -0
  118. package/template/wall-e/coding/permission-service.js +88 -22
  119. package/template/wall-e/coding/session-workspaces.js +81 -0
  120. package/template/wall-e/coding/shell-sandbox.js +124 -0
  121. package/template/wall-e/coding/stream-processor.js +63 -2
  122. package/template/wall-e/coding/tool-execution-controller.js +14 -1
  123. package/template/wall-e/coding/tool-registry.js +1 -1
  124. package/template/wall-e/coding/transcript-writer.js +3 -0
  125. package/template/wall-e/coding-orchestrator.js +636 -35
  126. package/template/wall-e/coding-prompts.js +51 -2
  127. package/template/wall-e/docs/model-routing-policy.md +59 -0
  128. package/template/wall-e/docs/walle-shell-sandbox.md +61 -0
  129. package/template/wall-e/extraction/knowledge-extractor.js +76 -23
  130. package/template/wall-e/http/chat-api.js +30 -12
  131. package/template/wall-e/http/model-admin.js +93 -1
  132. package/template/wall-e/lib/background-lanes.js +133 -0
  133. package/template/wall-e/lib/boot-profile.js +11 -0
  134. package/template/wall-e/lib/brain-owner-worker-client.js +324 -0
  135. package/template/wall-e/lib/brain-read-pool-client.js +311 -0
  136. package/template/wall-e/lib/diagnostics-flags.js +87 -0
  137. package/template/wall-e/lib/event-loop-monitor.js +74 -3
  138. package/template/wall-e/lib/mcp-integration.js +7 -1
  139. package/template/wall-e/lib/real-node.js +98 -0
  140. package/template/wall-e/lib/runtime-health.js +206 -0
  141. package/template/wall-e/lib/runtime-worker-pool.js +101 -0
  142. package/template/wall-e/lib/scheduler-worker-jobs.js +231 -0
  143. package/template/wall-e/lib/scheduler.js +446 -17
  144. package/template/wall-e/lib/service-health.js +61 -2
  145. package/template/wall-e/lib/service-readiness.js +258 -0
  146. package/template/wall-e/lib/usage.js +152 -0
  147. package/template/wall-e/lib/worker-thread-pool.js +389 -0
  148. package/template/wall-e/llm/client.js +81 -4
  149. package/template/wall-e/llm/default-fallback.js +54 -8
  150. package/template/wall-e/llm/mlx.js +536 -73
  151. package/template/wall-e/llm/mlx.plugin.json +1 -1
  152. package/template/wall-e/llm/ollama.js +342 -43
  153. package/template/wall-e/llm/provider-error.js +18 -1
  154. package/template/wall-e/llm/provider-health-state.js +176 -0
  155. package/template/wall-e/llm/routing-policy.js +796 -0
  156. package/template/wall-e/llm/supported-models.js +5 -0
  157. package/template/wall-e/loops/tasks.js +60 -14
  158. package/template/wall-e/loops/think.js +89 -24
  159. package/template/wall-e/mcp-server.js +192 -28
  160. package/template/wall-e/server.js +32 -7
  161. package/template/wall-e/skills/script-skill-runner.js +8 -1
  162. package/template/wall-e/skills/skill-planner.js +64 -1
  163. package/template/wall-e/tools/builtin-middleware.js +67 -2
  164. package/template/wall-e/tools/local-tools.js +116 -26
  165. package/template/wall-e/tools/permission-checker.js +52 -4
  166. package/template/wall-e/tools/permission-rules.js +36 -0
  167. package/template/wall-e/tools/shell-analyzer.js +46 -1
  168. package/template/wall-e/training/gemma-e4b-qlora.js +314 -0
  169. package/template/wall-e/training/real-trajectory-miner.js +2617 -0
  170. package/template/wall-e/training/replay-eval-analysis.js +151 -0
  171. package/template/wall-e/training/run-shell-command-selector.js +277 -0
  172. package/template/wall-e/training/tool-sft-dataset.js +312 -0
  173. package/template/wall-e/training/tool-sft-renderers.js +144 -0
  174. package/template/wall-e/training/tool-trace-harvester.js +1440 -0
  175. package/template/wall-e/training/trajectory-action-selector.js +364 -0
  176. package/template/wall-e/weather-runtime.js +232 -0
  177. package/template/wall-e/workers/brain-owner-worker.js +162 -0
  178. package/template/wall-e/workers/brain-read-worker.js +148 -0
  179. 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 at the macOS event system level.
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
- // Requirements: Accessibility permission (System Settings > Privacy > Accessibility)
7
- // Install: swift build -> launchd plist (see install-hotkey.sh)
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
- // CGEvent key codes: S = 1, modifiers: Cmd = .maskCommand, Shift = .maskShift
18
- let TARGET_KEYCODE: Int64 = 1 // 'S' key
19
- let TARGET_MODIFIERS: CGEventFlags = [.maskCommand, .maskShift]
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
- request.timeoutInterval = 5
29
-
30
- // Fire and forget don't block the event tap callback
31
- URLSession.shared.dataTask(with: request) { data, response, error in
32
- if let error = error {
33
- // CTM might not be running that's fine, fail silently
34
- print("[ctm-hotkey] Request failed: \(error.localizedDescription)")
35
- return
36
- }
37
- if let http = response as? HTTPURLResponse, http.statusCode == 200 {
38
- print("[ctm-hotkey] Screenshot triggered")
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
- // --- CGEvent Tap ---
44
- // Store the tap globally so the callback can re-enable it
45
- var globalTap: CFMachPort?
46
-
47
- func eventTapCallback(proxy: CGEventTapProxy, type: CGEventType, event: CGEvent, refcon: UnsafeMutableRawPointer?) -> Unmanaged<CGEvent>? {
48
- // Re-enable tap if it gets disabled (macOS disables taps that take too long)
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
- // Check Accessibility permission
80
- let trusted = AXIsProcessTrustedWithOptions(
81
- [kAXTrustedCheckOptionPrompt.takeUnretainedValue(): true] as CFDictionary
82
- )
83
- if !trusted {
84
- print("[ctm-hotkey] WARNING: Accessibility permission not granted.")
85
- print("[ctm-hotkey] Go to: System Settings > Privacy & Security > Accessibility")
86
- print("[ctm-hotkey] Add this binary, then restart.")
87
- // Continue anyway the tap will fail gracefully, and the permission dialog is shown
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
- // Create event tap for keyDown events
91
- let eventMask = (1 << CGEventType.keyDown.rawValue)
92
-
93
- let machPort = CGEvent.tapCreate(
94
- tap: .cgSessionEventTap,
95
- place: .headInsertEventTap,
96
- options: .defaultTap,
97
- eventsOfInterest: CGEventMask(eventMask),
98
- callback: eventTapCallback,
99
- userInfo: nil
100
- )
101
-
102
- globalTap = machPort
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 event loop (blocks forever)
118
- CFRunLoopRun()
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 — Compile and install CTM global hotkey daemon
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
- BINARY="$HOME/.local/bin/ctm-hotkey"
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/3] Compiling ctm-hotkey..."
19
- mkdir -p "$(dirname "$BINARY")"
20
- swiftc -O -o "$BINARY" "$SOURCE" -framework Cocoa
21
- chmod +x "$BINARY"
22
- echo " Binary: $BINARY ($(du -h "$BINARY" | cut -f1 | xargs))"
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
- # 2. Create launchd plist
25
- echo "[2/3] Installing launchd agent..."
26
- mkdir -p "$(dirname "$PLIST")"
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>${BINARY}</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
- <true/>
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
- mkdir -p "$HOME/.local/log"
56
-
57
- # 3. Load the agent
58
- echo "[3/3] Starting daemon..."
59
- launchctl unload "$PLIST" 2>/dev/null || true
60
- launchctl load "$PLIST"
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 " Action: Triggers CTM screenshot + annotation editor"
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 "IMPORTANT: You need to grant Accessibility permission:"
70
- echo " System Settings > Privacy & Security > Accessibility"
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
- if [ -f "$PLIST" ]; then
79
- echo "Stopping daemon..."
80
- launchctl unload "$PLIST" 2>/dev/null || true
81
- rm -f "$PLIST"
82
- echo "Removed: $PLIST"
83
- fi
84
- if [ -f "$BINARY" ]; then
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
- if [ -f "$BINARY" ]; then
94
- echo "Binary: $BINARY (exists)"
95
- else
96
- echo "Binary: not installed"
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,