@sayknow-cli/coding-agent 0.3.12 → 0.3.15

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 (248) hide show
  1. package/dist/types/autoresearch/tools/init-experiment.d.ts +1 -1
  2. package/dist/types/autoresearch/tools/log-experiment.d.ts +2 -2
  3. package/dist/types/cli/classify-install-target.d.ts +0 -10
  4. package/dist/types/cli/daemon-cli.d.ts +2 -0
  5. package/dist/types/cli/initial-message.d.ts +1 -1
  6. package/dist/types/cli/malloc-env-guard.d.ts +65 -0
  7. package/dist/types/cli/malloc-env-guard.test.d.ts +1 -0
  8. package/dist/types/cli/notify-cli.d.ts +3 -1
  9. package/dist/types/cli/session-picker.d.ts +4 -3
  10. package/dist/types/cli/update-cli.d.ts +22 -3
  11. package/dist/types/cli.d.ts +5 -0
  12. package/dist/types/commands/daemon.d.ts +6 -2
  13. package/dist/types/commands/launch.d.ts +1 -0
  14. package/dist/types/commands/notify.d.ts +6 -0
  15. package/dist/types/commands/web-search.d.ts +1 -1
  16. package/dist/types/commit/agentic/tools/propose-changelog.d.ts +4 -4
  17. package/dist/types/commit/agentic/tools/propose-commit.d.ts +9 -9
  18. package/dist/types/commit/agentic/tools/schemas.d.ts +9 -9
  19. package/dist/types/commit/agentic/tools/split-commit.d.ts +9 -9
  20. package/dist/types/commit/analysis/conventional.d.ts +1 -1
  21. package/dist/types/commit/analysis/summary.d.ts +1 -1
  22. package/dist/types/commit/changelog/generate.d.ts +3 -3
  23. package/dist/types/commit/changelog/index.d.ts +2 -2
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +1 -1
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +1 -1
  26. package/dist/types/commit/shared-llm.d.ts +9 -9
  27. package/dist/types/config/keybindings.d.ts +134 -129
  28. package/dist/types/config/model-profile-activation.d.ts +1 -0
  29. package/dist/types/config/model-registry.d.ts +35 -35
  30. package/dist/types/config/models-config-schema.d.ts +111 -111
  31. package/dist/types/config/settings-schema.d.ts +56 -16
  32. package/dist/types/config/settings.d.ts +2 -0
  33. package/dist/types/coordinator/contract.d.ts +1 -1
  34. package/dist/types/coordinator-mcp/model-preset.d.ts +45 -0
  35. package/dist/types/coordinator-mcp/policy.d.ts +3 -0
  36. package/dist/types/coordinator-mcp/server.d.ts +79 -6
  37. package/dist/types/coordinator-mcp/session-reaper.d.ts +53 -0
  38. package/dist/types/daemon/control-types.d.ts +10 -0
  39. package/dist/types/daemon/operator-contract.d.ts +47 -0
  40. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.d.ts +4 -0
  41. package/dist/types/defaults/skc/extensions/grok-cli-vendor/src/shared/errors.d.ts +12 -12
  42. package/dist/types/discovery/helpers.d.ts +1 -1
  43. package/dist/types/discovery/substitute-plugin-root.d.ts +0 -4
  44. package/dist/types/edit/modes/patch.d.ts +2 -2
  45. package/dist/types/extensibility/extensions/types.d.ts +3 -1
  46. package/dist/types/goals/tools/goal-tool.d.ts +6 -6
  47. package/dist/types/i18n/messages/en.d.ts +8 -8
  48. package/dist/types/internal-urls/local-protocol.d.ts +13 -9
  49. package/dist/types/lsp/index.d.ts +9 -9
  50. package/dist/types/lsp/types.d.ts +9 -9
  51. package/dist/types/main.d.ts +66 -2
  52. package/dist/types/modes/acp/acp-agent.d.ts +19 -2
  53. package/dist/types/modes/acp/acp-mode.d.ts +12 -0
  54. package/dist/types/modes/components/assistant-message.d.ts +1 -1
  55. package/dist/types/modes/components/custom-editor.d.ts +2 -0
  56. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  57. package/dist/types/modes/components/irc-sidebar.d.ts +32 -0
  58. package/dist/types/modes/components/pet-selector.d.ts +11 -0
  59. package/dist/types/modes/components/sayknow-pet-widget.d.ts +59 -0
  60. package/dist/types/modes/components/session-selector.d.ts +22 -9
  61. package/dist/types/modes/components/settings-selector.d.ts +2 -0
  62. package/dist/types/modes/components/status-line/types.d.ts +1 -1
  63. package/dist/types/modes/components/status-line.d.ts +0 -10
  64. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  65. package/dist/types/modes/components/user-message.d.ts +2 -1
  66. package/dist/types/modes/controllers/event-controller.d.ts +4 -0
  67. package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
  68. package/dist/types/modes/interactive-mode.d.ts +24 -2
  69. package/dist/types/modes/irc-observation-ledger.d.ts +22 -0
  70. package/dist/types/modes/print-mode.d.ts +3 -4
  71. package/dist/types/modes/rpc/rpc-client.d.ts +7 -1
  72. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -1
  73. package/dist/types/modes/rpc/rpc-types.d.ts +17 -1
  74. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +8 -0
  75. package/dist/types/modes/shared/agent-wire/unattended-run-controller.d.ts +5 -0
  76. package/dist/types/modes/shared/agent-wire/unattended-session.d.ts +45 -0
  77. package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +42 -2
  78. package/dist/types/modes/theme/defaults/index.d.ts +568 -568
  79. package/dist/types/modes/types.d.ts +28 -2
  80. package/dist/types/modes/utils/abort-message.d.ts +1 -1
  81. package/dist/types/modes/utils/context-usage.d.ts +4 -19
  82. package/dist/types/modes/utils/irc-message.d.ts +38 -0
  83. package/dist/types/modes/utils/ui-helpers.d.ts +13 -4
  84. package/dist/types/notifications/config.d.ts +34 -0
  85. package/dist/types/notifications/daemon-paths.d.ts +7 -0
  86. package/dist/types/notifications/index.d.ts +27 -0
  87. package/dist/types/notifications/lifecycle-control-runtime.d.ts +12 -4
  88. package/dist/types/notifications/notification-service.d.ts +144 -0
  89. package/dist/types/notifications/rate-limit-pool.d.ts +27 -9
  90. package/dist/types/notifications/telegram-daemon-control.d.ts +6 -0
  91. package/dist/types/notifications/telegram-daemon.d.ts +130 -3
  92. package/dist/types/notifications/telegram-reference.d.ts +19 -2
  93. package/dist/types/notifications/topic-registry.d.ts +24 -4
  94. package/dist/types/registry/agent-registry.d.ts +2 -0
  95. package/dist/types/rlm/index.d.ts +1 -1
  96. package/dist/types/sdk.d.ts +2 -0
  97. package/dist/types/secrets/obfuscator.d.ts +1 -1
  98. package/dist/types/session/agent-session.d.ts +40 -3
  99. package/dist/types/session/cache-economics.d.ts +79 -0
  100. package/dist/types/session/context-estimation.d.ts +31 -0
  101. package/dist/types/session/provider-safety-stop.d.ts +5 -0
  102. package/dist/types/session/session-manager.d.ts +124 -4
  103. package/dist/types/session/session-storage.d.ts +155 -13
  104. package/dist/types/setup/credential-auto-import.d.ts +2 -2
  105. package/dist/types/skc-runtime/session-state-sidecar.d.ts +34 -3
  106. package/dist/types/skc-runtime/state-schema.d.ts +19 -19
  107. package/dist/types/skc-runtime/tmux-common.d.ts +6 -0
  108. package/dist/types/skc-runtime/tmux-owner-isolation-cli.d.ts +5 -0
  109. package/dist/types/skc-runtime/tmux-owner-isolation.d.ts +309 -0
  110. package/dist/types/skc-runtime/tmux-sessions.d.ts +49 -17
  111. package/dist/types/task/agents.d.ts +2 -2
  112. package/dist/types/task/executor.d.ts +3 -0
  113. package/dist/types/task/receipt.d.ts +3 -1
  114. package/dist/types/task/types.d.ts +35 -27
  115. package/dist/types/tools/ask-answer-registry.d.ts +5 -0
  116. package/dist/types/tools/bash.d.ts +1 -1
  117. package/dist/types/tools/browser/tab-supervisor.d.ts +6 -0
  118. package/dist/types/tools/browser.d.ts +28 -28
  119. package/dist/types/tools/computer.d.ts +20 -20
  120. package/dist/types/tools/cron.d.ts +2 -2
  121. package/dist/types/tools/debug.d.ts +40 -40
  122. package/dist/types/tools/gh.d.ts +10 -10
  123. package/dist/types/tools/image-gen.d.ts +2 -2
  124. package/dist/types/tools/index.d.ts +58 -6
  125. package/dist/types/tools/monitor.d.ts +6 -6
  126. package/dist/types/tools/skill-discovery.d.ts +4 -4
  127. package/dist/types/tools/subagent.d.ts +12 -12
  128. package/dist/types/tools/telegram-send.d.ts +7 -0
  129. package/dist/types/tools/todo-write.d.ts +8 -8
  130. package/dist/types/utils/external-editor.d.ts +2 -0
  131. package/dist/types/web/insane/bridge.d.ts +9 -9
  132. package/dist/types/web/search/index.d.ts +4 -4
  133. package/package.json +8 -8
  134. package/scripts/g004-tmux-smoke.ts +196 -36
  135. package/src/cli/daemon-cli.ts +19 -29
  136. package/src/cli/malloc-env-guard.test.ts +104 -0
  137. package/src/cli/malloc-env-guard.ts +149 -0
  138. package/src/cli/notify-cli.ts +78 -14
  139. package/src/cli/session-picker.ts +23 -40
  140. package/src/cli/update-cli.ts +56 -37
  141. package/src/cli.ts +51 -10
  142. package/src/commands/daemon.ts +19 -5
  143. package/src/commands/harness.ts +515 -47
  144. package/src/commands/launch.ts +11 -3
  145. package/src/commands/notify.ts +5 -1
  146. package/src/commands/session.ts +6 -1
  147. package/src/config/keybindings.ts +5 -0
  148. package/src/config/model-profile-activation.ts +1 -1
  149. package/src/config/model-profiles.ts +14 -14
  150. package/src/config/settings-schema.ts +41 -1
  151. package/src/config/settings.ts +96 -45
  152. package/src/coordinator/contract.ts +1 -0
  153. package/src/coordinator-mcp/model-preset.ts +100 -0
  154. package/src/coordinator-mcp/policy.ts +30 -0
  155. package/src/coordinator-mcp/server.ts +2958 -430
  156. package/src/coordinator-mcp/session-reaper.ts +129 -0
  157. package/src/daemon/control-types.ts +11 -0
  158. package/src/daemon/operator-contract.ts +121 -0
  159. package/src/defaults/skc/extensions/grok-cli-vendor/src/models/catalog.ts +57 -17
  160. package/src/defaults/skc/extensions/grok-cli-vendor/src/payload/sanitize.ts +15 -2
  161. package/src/defaults/skc/extensions/grok-cli-vendor/src/provider/register.ts +5 -1
  162. package/src/extensibility/extensions/runner.ts +1 -1
  163. package/src/extensibility/extensions/types.ts +3 -1
  164. package/src/internal-urls/docs-index.generated.ts +10 -9
  165. package/src/internal-urls/local-protocol.ts +29 -10
  166. package/src/main.ts +301 -80
  167. package/src/modes/acp/acp-agent.ts +730 -100
  168. package/src/modes/acp/acp-mode.ts +29 -2
  169. package/src/modes/components/assistant-message.ts +32 -3
  170. package/src/modes/components/bash-execution.ts +43 -13
  171. package/src/modes/components/custom-editor.ts +5 -0
  172. package/src/modes/components/footer.ts +3 -3
  173. package/src/modes/components/hook-selector.ts +12 -0
  174. package/src/modes/components/irc-sidebar.ts +206 -0
  175. package/src/modes/components/pet-selector.ts +55 -0
  176. package/src/modes/components/sayknow-pet-widget.ts +360 -0
  177. package/src/modes/components/session-selector.ts +222 -221
  178. package/src/modes/components/settings-selector.ts +10 -0
  179. package/src/modes/components/status-line/segments.ts +14 -7
  180. package/src/modes/components/status-line/types.ts +1 -1
  181. package/src/modes/components/status-line.ts +4 -171
  182. package/src/modes/components/tool-execution.ts +47 -6
  183. package/src/modes/components/user-message.ts +26 -9
  184. package/src/modes/controllers/command-controller.ts +28 -2
  185. package/src/modes/controllers/event-controller.ts +159 -36
  186. package/src/modes/controllers/extension-ui-controller.ts +37 -18
  187. package/src/modes/controllers/input-controller.ts +8 -2
  188. package/src/modes/controllers/selector-controller.ts +62 -15
  189. package/src/modes/interactive-mode.ts +199 -7
  190. package/src/modes/irc-observation-ledger.ts +127 -0
  191. package/src/modes/print-mode.ts +228 -92
  192. package/src/modes/rpc/rpc-client.ts +58 -1
  193. package/src/modes/rpc/rpc-mode.ts +286 -85
  194. package/src/modes/rpc/rpc-types.ts +15 -1
  195. package/src/modes/shared/agent-wire/command-dispatch.ts +33 -1
  196. package/src/modes/shared/agent-wire/command-validation.ts +10 -0
  197. package/src/modes/shared/agent-wire/deep-interview-gate.ts +45 -2
  198. package/src/modes/shared/agent-wire/scopes.ts +1 -0
  199. package/src/modes/shared/agent-wire/unattended-session.ts +288 -1
  200. package/src/modes/shared/agent-wire/workflow-gate-broker.ts +130 -20
  201. package/src/modes/types.ts +34 -2
  202. package/src/modes/utils/context-usage.ts +56 -92
  203. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  204. package/src/modes/utils/irc-message.ts +138 -0
  205. package/src/modes/utils/ui-helpers.ts +216 -50
  206. package/src/notifications/config.ts +44 -0
  207. package/src/notifications/daemon-paths.ts +8 -0
  208. package/src/notifications/index.ts +535 -75
  209. package/src/notifications/lifecycle-control-runtime.ts +930 -77
  210. package/src/notifications/notification-service.ts +710 -0
  211. package/src/notifications/rate-limit-pool.ts +46 -12
  212. package/src/notifications/telegram-daemon-control.ts +27 -2
  213. package/src/notifications/telegram-daemon.ts +635 -94
  214. package/src/notifications/telegram-reference.ts +73 -13
  215. package/src/notifications/topic-registry.ts +86 -9
  216. package/src/prompts/system/irc-peer-roster.md +1 -0
  217. package/src/prompts/system/subagent-system-prompt.md +1 -0
  218. package/src/prompts/system/system-prompt.md +1 -0
  219. package/src/prompts/tools/irc.md +1 -1
  220. package/src/registry/agent-registry.ts +3 -0
  221. package/src/sdk.ts +52 -1
  222. package/src/secrets/obfuscator.ts +4 -2
  223. package/src/session/agent-session.ts +670 -217
  224. package/src/session/cache-economics.ts +275 -0
  225. package/src/session/context-estimation.ts +91 -0
  226. package/src/session/provider-safety-stop.ts +9 -0
  227. package/src/session/session-dump-format.ts +6 -0
  228. package/src/session/session-manager.ts +809 -39
  229. package/src/session/session-storage.ts +582 -38
  230. package/src/skc-runtime/session-state-sidecar.ts +756 -135
  231. package/src/skc-runtime/tmux-common.ts +26 -7
  232. package/src/skc-runtime/tmux-gc.ts +52 -9
  233. package/src/skc-runtime/tmux-owner-isolation-cli.ts +185 -0
  234. package/src/skc-runtime/tmux-owner-isolation.ts +2060 -0
  235. package/src/skc-runtime/tmux-sessions.ts +871 -68
  236. package/src/slash-commands/builtin-registry.ts +153 -9
  237. package/src/slash-commands/helpers/context-report.ts +26 -4
  238. package/src/task/executor.ts +12 -3
  239. package/src/task/index.ts +8 -0
  240. package/src/task/receipt.ts +7 -2
  241. package/src/task/types.ts +26 -0
  242. package/src/tools/ask-answer-registry.ts +25 -0
  243. package/src/tools/browser/tab-supervisor.ts +63 -6
  244. package/src/tools/browser.ts +8 -3
  245. package/src/tools/index.ts +81 -6
  246. package/src/tools/sqlite-reader.ts +8 -4
  247. package/src/tui/output-block.ts +18 -6
  248. package/src/utils/external-editor.ts +6 -1
@@ -0,0 +1,104 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ buildMallocReexecArgv,
4
+ formatMallocEnvNotice,
5
+ hasMacOSMallocEnv,
6
+ MACOS_MALLOC_ENV_VARS,
7
+ MALLOC_ENV_REEXEC_GUARD,
8
+ shouldReexecForMacOSMallocEnv,
9
+ } from "./malloc-env-guard";
10
+
11
+ describe("hasMacOSMallocEnv", () => {
12
+ it("detects MallocStackLogging", () => {
13
+ expect(hasMacOSMallocEnv({ MallocStackLogging: "0" })).toBe(true);
14
+ });
15
+
16
+ it("detects MallocStackLoggingNoCompact", () => {
17
+ expect(hasMacOSMallocEnv({ MallocStackLoggingNoCompact: "1" })).toBe(true);
18
+ });
19
+
20
+ it("is false when neither var is present", () => {
21
+ expect(hasMacOSMallocEnv({ PATH: "/usr/bin", HOME: "/home/x" })).toBe(false);
22
+ });
23
+
24
+ it("is false for an empty environment", () => {
25
+ expect(hasMacOSMallocEnv({})).toBe(false);
26
+ });
27
+ });
28
+
29
+ describe("shouldReexecForMacOSMallocEnv", () => {
30
+ it("is true on darwin with a malloc var and no loop guard", () => {
31
+ expect(shouldReexecForMacOSMallocEnv({ MallocStackLogging: "0" }, "darwin")).toBe(true);
32
+ });
33
+
34
+ it("is false off darwin even when a malloc var is present", () => {
35
+ expect(shouldReexecForMacOSMallocEnv({ MallocStackLogging: "0" }, "linux")).toBe(false);
36
+ expect(shouldReexecForMacOSMallocEnv({ MallocStackLogging: "0" }, "win32")).toBe(false);
37
+ });
38
+
39
+ it("is false when the loop guard is already set (already re-exec'd)", () => {
40
+ expect(shouldReexecForMacOSMallocEnv({ MallocStackLogging: "0", [MALLOC_ENV_REEXEC_GUARD]: "1" }, "darwin")).toBe(
41
+ false,
42
+ );
43
+ });
44
+
45
+ it("is false on darwin when no malloc var is present", () => {
46
+ expect(shouldReexecForMacOSMallocEnv({ PATH: "/usr/bin" }, "darwin")).toBe(false);
47
+ });
48
+ });
49
+
50
+ describe("buildMallocReexecArgv", () => {
51
+ it("drops the embedded virtual entry for a POSIX compiled binary", () => {
52
+ expect(buildMallocReexecArgv(["/exe", "/$bunfs/root/cli", "run", "--flag"], "/exe")).toEqual([
53
+ "/exe",
54
+ "run",
55
+ "--flag",
56
+ ]);
57
+ });
58
+
59
+ it("drops the embedded virtual entry for a Windows compiled binary", () => {
60
+ expect(buildMallocReexecArgv(["C:/exe.exe", "B:\\~BUN\\root\\cli", "run"], "C:/exe.exe")).toEqual([
61
+ "C:/exe.exe",
62
+ "run",
63
+ ]);
64
+ });
65
+
66
+ it("drops the percent-encoded embedded virtual entry", () => {
67
+ expect(buildMallocReexecArgv(["/exe", "B:\\%7EBUN\\root\\cli", "run"], "/exe")).toEqual(["/exe", "run"]);
68
+ });
69
+
70
+ it("keeps the on-disk script for a source/dev-link run", () => {
71
+ expect(
72
+ buildMallocReexecArgv(["/usr/bin/bun", "/repo/packages/coding-agent/src/cli.ts", "run", "-x"], "/usr/bin/bun"),
73
+ ).toEqual(["/usr/bin/bun", "/repo/packages/coding-agent/src/cli.ts", "run", "-x"]);
74
+ });
75
+
76
+ it("handles a source run with no user args", () => {
77
+ expect(buildMallocReexecArgv(["/usr/bin/bun", "/repo/cli.ts"], "/usr/bin/bun")).toEqual([
78
+ "/usr/bin/bun",
79
+ "/repo/cli.ts",
80
+ ]);
81
+ });
82
+ });
83
+
84
+ describe("formatMallocEnvNotice", () => {
85
+ it("names both env vars and the permanent launchctl fix", () => {
86
+ const notice = formatMallocEnvNotice();
87
+ for (const name of MACOS_MALLOC_ENV_VARS) {
88
+ expect(notice).toContain(name);
89
+ }
90
+ expect(notice).toContain("launchctl unsetenv MallocStackLogging");
91
+ expect(notice).toContain("launchctl unsetenv MallocStackLoggingNoCompact");
92
+ expect(notice.endsWith("\n")).toBe(true);
93
+ });
94
+ });
95
+
96
+ describe("module constants", () => {
97
+ it("exposes exactly the two macOS malloc env vars", () => {
98
+ expect([...MACOS_MALLOC_ENV_VARS]).toEqual(["MallocStackLogging", "MallocStackLoggingNoCompact"]);
99
+ });
100
+
101
+ it("uses a namespaced loop-guard variable", () => {
102
+ expect(MALLOC_ENV_REEXEC_GUARD).toBe("SKC_MALLOC_ENV_REEXEC");
103
+ });
104
+ });
@@ -0,0 +1,149 @@
1
+ /**
2
+ * macOS malloc-stack-logging launch boundary.
3
+ *
4
+ * On macOS, an inherited `MallocStackLogging` / `MallocStackLoggingNoCompact`
5
+ * environment variable (from an Xcode scheme's malloc diagnostics, Instruments,
6
+ * `launchctl setenv`, or a debug-attached shell) makes libmalloc print
7
+ *
8
+ * MallocStackLogging: can't turn off malloc stack logging because it was not enabled.
9
+ *
10
+ * to the stderr of every TTY-attached process. Bun snapshots the spawn-default
11
+ * environment at process startup — before any JS runs — so neither
12
+ * `delete process.env.X` nor a real libc `unsetenv()` cleans the environment
13
+ * that children inherit by default (verified on Bun 1.3.14). Inside the TUI,
14
+ * every PTY-spawned child then repeats the warning straight into the rendered
15
+ * frame and corrupts the display.
16
+ *
17
+ * The only way to hand children a clean startup snapshot is to start the primary
18
+ * process with a clean environment. This module re-execs the exact invocation
19
+ * once (darwin-only) with a `filterProcessEnv`-scrubbed environment and a
20
+ * `SKC_MALLOC_ENV_REEXEC` loop guard, before any fast path or subprocess spawn.
21
+ * The re-exec'd process — and therefore every downstream lane: `Bun.spawn`
22
+ * defaults, `node:child_process`, the native PTY, the tmux owner, plugin
23
+ * installs, and subagents — starts from a clean snapshot.
24
+ *
25
+ * The initial contaminated process's own libmalloc line is emitted by libc
26
+ * before any JS runs and cannot be suppressed from inside the process; it
27
+ * appears at most once per launch. A one-time actionable notice is written to
28
+ * interactive stderr (outside the rendered frame) pointing at the permanent
29
+ * `launchctl unsetenv` fix.
30
+ */
31
+
32
+ import { filterProcessEnv } from "@sayknow-cli/utils/env";
33
+ import type { Subprocess } from "bun";
34
+
35
+ /** Env vars that make macOS libmalloc write to a TTY when inherited. */
36
+ export const MACOS_MALLOC_ENV_VARS = ["MallocStackLogging", "MallocStackLoggingNoCompact"] as const;
37
+
38
+ /** Loop guard set on the scrubbed re-exec so it never re-execs itself again. */
39
+ export const MALLOC_ENV_REEXEC_GUARD = "SKC_MALLOC_ENV_REEXEC";
40
+
41
+ /** True when at least one macOS malloc-stack-logging var is present in `env`. */
42
+ export function hasMacOSMallocEnv(env: Record<string, string | undefined>): boolean {
43
+ return MACOS_MALLOC_ENV_VARS.some(name => env[name] !== undefined);
44
+ }
45
+
46
+ /**
47
+ * True iff we must re-exec to escape a contaminated macOS startup snapshot:
48
+ * darwin, not already inside the scrubbed re-exec, and at least one malloc var
49
+ * present. Off darwin or on the re-exec'd child this is always false, so the
50
+ * common launch path pays only two cheap lookups.
51
+ */
52
+ export function shouldReexecForMacOSMallocEnv(
53
+ env: Record<string, string | undefined>,
54
+ platform: NodeJS.Platform,
55
+ ): boolean {
56
+ return platform === "darwin" && env[MALLOC_ENV_REEXEC_GUARD] === undefined && hasMacOSMallocEnv(env);
57
+ }
58
+
59
+ /**
60
+ * Reconstruct the argv that re-runs the exact same program image.
61
+ *
62
+ * - Compiled single-file executable: argv[1] is the embedded virtual entry
63
+ * (`/$bunfs/…` on POSIX, `B:\~BUN\…` on Windows), which must be dropped — the
64
+ * executable re-embeds its own entry. Re-run `[exe, ...userArgs]`.
65
+ * - Source / dev-link run: argv[1] is the on-disk script that must be passed
66
+ * back to the interpreter. Re-run `[interpreter, script, ...userArgs]`.
67
+ */
68
+ export function buildMallocReexecArgv(argv: readonly string[], execPath: string): string[] {
69
+ const entry = argv[1];
70
+ const isCompiledEntry =
71
+ typeof entry === "string" && (entry.includes("/$bunfs/") || entry.includes("~BUN") || entry.includes("%7EBUN"));
72
+ if (isCompiledEntry) {
73
+ return [execPath, ...argv.slice(2)];
74
+ }
75
+ return [execPath, ...argv.slice(1)];
76
+ }
77
+
78
+ /** One-time, human-actionable notice pointing at the permanent environment fix. */
79
+ export function formatMallocEnvNotice(): string {
80
+ return (
81
+ "sayknow-cli: detected macOS malloc-stack-logging environment variables " +
82
+ "(MallocStackLogging / MallocStackLoggingNoCompact); relaunching once with a " +
83
+ "scrubbed environment so they do not flood the terminal.\n" +
84
+ " Remove them permanently with:\n" +
85
+ " unset MallocStackLogging MallocStackLoggingNoCompact\n" +
86
+ " launchctl unsetenv MallocStackLogging\n" +
87
+ " launchctl unsetenv MallocStackLoggingNoCompact\n"
88
+ );
89
+ }
90
+
91
+ /**
92
+ * Re-exec the current process once with a scrubbed environment and propagate the
93
+ * child's exit code. Callers MUST gate this behind `shouldReexecForMacOSMallocEnv`.
94
+ *
95
+ * Returns the child exit code, or `null` if the re-exec could not be spawned (in
96
+ * which case the caller falls back to running in the current, contaminated
97
+ * process — degraded but functional; the native PTY boundary and
98
+ * `filterProcessEnv` still keep managed children clean).
99
+ */
100
+ export async function reexecWithScrubbedMallocEnv(): Promise<number | null> {
101
+ const cleanEnv = filterProcessEnv(process.env);
102
+ cleanEnv[MALLOC_ENV_REEXEC_GUARD] = "1";
103
+ const childArgv = buildMallocReexecArgv(process.argv, process.execPath);
104
+
105
+ // Surface the diagnostic exactly once, before the TUI enters its alternate
106
+ // screen, and only on an interactive terminal (piped/CI stderr is not a TTY,
107
+ // so libmalloc stays silent there and the notice would just be noise).
108
+ if (process.stderr.isTTY) {
109
+ process.stderr.write(formatMallocEnvNotice());
110
+ }
111
+
112
+ let child: Subprocess;
113
+ try {
114
+ child = Bun.spawn(childArgv, {
115
+ env: cleanEnv,
116
+ stdin: "inherit",
117
+ stdout: "inherit",
118
+ stderr: "inherit",
119
+ });
120
+ } catch {
121
+ return null;
122
+ }
123
+
124
+ // Keep the wrapper alive on SIGINT/SIGTERM so it reaps the child and
125
+ // propagates its exit code instead of dying first, and forward the signal so
126
+ // the scrubbed child terminates too. Interactive Ctrl-C is delivered to the
127
+ // whole foreground process group (and the TUI's raw mode turns it into a 0x03
128
+ // byte, not a signal, anyway), so re-forwarding is a harmless no-op once the
129
+ // child is already handling or exiting. Targeted delivery — `kill -INT <pid>`
130
+ // or a supervisor's `child.kill("SIGINT")` — reaches only the wrapper, so
131
+ // without forwarding the launch would be uncancellable outside a terminal.
132
+ const forwardSignal = (signal: NodeJS.Signals) => () => {
133
+ try {
134
+ child.kill(signal);
135
+ } catch {
136
+ // Child already gone; nothing to forward.
137
+ }
138
+ };
139
+ const onSigint = forwardSignal("SIGINT");
140
+ const onSigterm = forwardSignal("SIGTERM");
141
+ process.on("SIGINT", onSigint);
142
+ process.on("SIGTERM", onSigterm);
143
+ try {
144
+ return await child.exited;
145
+ } finally {
146
+ process.off("SIGINT", onSigint);
147
+ process.off("SIGTERM", onSigterm);
148
+ }
149
+ }
@@ -7,9 +7,19 @@ import { createInterface } from "node:readline/promises";
7
7
  import { APP_NAME } from "@sayknow-cli/utils/dirs";
8
8
  import chalk from "chalk";
9
9
  import type { Settings } from "../config/settings";
10
- import { getNotificationConfig, maskToken } from "../notifications/config";
11
-
12
- export type NotifyAction = "setup" | "status" | "daemon-internal";
10
+ import { maskToken } from "../notifications/config";
11
+ import {
12
+ buildNotificationStatusReport,
13
+ checkNotificationHealth,
14
+ formatNotificationHealthReport,
15
+ formatNotificationRecoveryReport,
16
+ formatNotificationStatusReport,
17
+ formatNotificationTestResult,
18
+ recoverNotifications,
19
+ sendNotificationTest,
20
+ } from "../notifications/notification-service";
21
+
22
+ export type NotifyAction = "setup" | "status" | "health" | "test" | "recovery" | "daemon-internal";
13
23
 
14
24
  export interface NotifyCommandArgs {
15
25
  action: NotifyAction;
@@ -18,6 +28,8 @@ export interface NotifyCommandArgs {
18
28
  token?: string;
19
29
  chatId?: string;
20
30
  redact?: boolean;
31
+ probe?: boolean;
32
+ message?: string;
21
33
  }
22
34
 
23
35
  export interface NotifyCommandDeps {
@@ -93,6 +105,19 @@ export function parseNotifyArgs(args: string[]): NotifyCommandArgs | undefined {
93
105
  redact: rest.includes("--redact"),
94
106
  };
95
107
  }
108
+ if (action === "health" || action === "test" || action === "recovery") {
109
+ const rest = args.slice(2);
110
+ const flag = (name: string): string | undefined => {
111
+ const i = rest.indexOf(name);
112
+ return i >= 0 ? rest[i + 1] : undefined;
113
+ };
114
+ return {
115
+ action,
116
+ rawArgs: rest,
117
+ probe: rest.includes("--probe"),
118
+ message: flag("--message"),
119
+ };
120
+ }
96
121
  if (action === "daemon-internal") {
97
122
  return {
98
123
  action,
@@ -117,6 +142,15 @@ export async function runNotifyCommand(cmd: NotifyCommandArgs, deps: NotifyComma
117
142
  case "status":
118
143
  await runStatus(deps);
119
144
  return;
145
+ case "health":
146
+ await runHealth(deps, cmd);
147
+ return;
148
+ case "test":
149
+ await runTest(deps, cmd);
150
+ return;
151
+ case "recovery":
152
+ await runRecovery(deps);
153
+ return;
120
154
  case "daemon-internal": {
121
155
  const m = await import("../notifications/telegram-daemon-cli");
122
156
  if (cmd.smoke) {
@@ -411,20 +445,42 @@ async function verifyThreadedMode(
411
445
 
412
446
  async function runStatus(deps: NotifyCommandDeps): Promise<void> {
413
447
  const settings = await getSettings(deps);
414
- const cfg = getNotificationConfig(settings);
448
+ const report = buildNotificationStatusReport(settings);
415
449
  process.stdout.write(
416
- `${chalk.bold("Notifications")}\n` +
417
- ` enabled: ${cfg.enabled}\n` +
418
- ` telegram.botToken: ${maskToken(cfg.botToken)}\n` +
419
- ` telegram.chatId: ${cfg.chatId ?? "(unset)"}\n` +
420
- ` discord.botToken: ${maskToken(cfg.discord.botToken)}\n` +
421
- ` discord.channelId: ${cfg.discord.channelId ?? "(unset)"}\n` +
422
- ` slack.botToken: ${maskToken(cfg.slack.botToken)}\n` +
423
- ` slack.channelId: ${cfg.slack.channelId ?? "(unset)"}\n` +
424
- ` redact: ${cfg.redact}\n`,
450
+ `${chalk.bold("Notifications")}\n${formatNotificationStatusReport(report).split("\n").slice(1).join("\n")}\n`,
425
451
  );
426
452
  }
427
453
 
454
+ async function runHealth(deps: NotifyCommandDeps, cmd: NotifyCommandArgs): Promise<void> {
455
+ const settings = await getSettings(deps);
456
+ const report = await checkNotificationHealth({
457
+ settings,
458
+ probe: cmd.probe,
459
+ deps: { fetchImpl: deps.fetchImpl, apiBase: deps.apiBase },
460
+ });
461
+ process.stdout.write(`${formatNotificationHealthReport(report)}\n`);
462
+ if (report.overall === "error" && deps.setExitCode) deps.setExitCode(1);
463
+ else if (report.overall === "error") process.exitCode = 1;
464
+ }
465
+
466
+ async function runTest(deps: NotifyCommandDeps, cmd: NotifyCommandArgs): Promise<void> {
467
+ const settings = await getSettings(deps);
468
+ const result = await sendNotificationTest({
469
+ settings,
470
+ text: cmd.message,
471
+ deps: { fetchImpl: deps.fetchImpl, apiBase: deps.apiBase },
472
+ });
473
+ process.stdout.write(`${formatNotificationTestResult(result)}\n`);
474
+ if (!result.ok && deps.setExitCode) deps.setExitCode(1);
475
+ else if (!result.ok) process.exitCode = 1;
476
+ }
477
+
478
+ async function runRecovery(deps: NotifyCommandDeps): Promise<void> {
479
+ const settings = await getSettings(deps);
480
+ const report = await recoverNotifications({ settings });
481
+ process.stdout.write(`${formatNotificationRecoveryReport(report)}\n`);
482
+ }
483
+
428
484
  async function waitForPrivateChat(
429
485
  fetchImpl: typeof fetch,
430
486
  apiBase: string,
@@ -514,15 +570,23 @@ ${chalk.bold("Usage:")}
514
570
  ${APP_NAME} notify setup
515
571
  ${APP_NAME} notify setup --token <botToken> --chat-id <chatId> [--redact]
516
572
  ${APP_NAME} notify status
573
+ ${APP_NAME} notify health [--probe]
574
+ ${APP_NAME} notify test [--message <text>]
575
+ ${APP_NAME} notify recovery
517
576
 
518
577
  ${chalk.bold("Subcommands:")}
519
578
  setup Pair a Telegram bot token with a private chat and verify Threaded Mode capability
520
579
  status Show notification configuration without secrets
580
+ health Report config, daemon-ownership and endpoint health (--probe adds a Telegram reachability check)
581
+ test Send a one-off test notification through the configured Telegram adapter
582
+ recovery Clear dead-owner daemon locks and stale per-session endpoint files (never touches a live owner)
521
583
 
522
584
  ${chalk.bold("Examples:")}
523
585
  ${APP_NAME} notify setup
524
- ${APP_NAME} notify setup --token <botToken> --chat-id <chatId> [--redact]
525
586
  ${APP_NAME} notify status
587
+ ${APP_NAME} notify health --probe
588
+ ${APP_NAME} notify test --message "hello from skc"
589
+ ${APP_NAME} notify recovery
526
590
 
527
591
  ${chalk.bold("Threaded Mode:")}
528
592
  SKC uses Telegram private-chat topics for per-session threads. Setup verifies the bot
@@ -1,49 +1,32 @@
1
1
  import { ProcessTerminal, TUI } from "@sayknow-cli/tui";
2
- import { SessionSelectorComponent } from "../modes/components/session-selector";
3
- import type { SessionInfo } from "../session/session-manager";
2
+ import { type SessionSelectionResult, SessionSelectorComponent } from "../modes/components/session-selector";
3
+ import { type SessionInfo, SessionManager } from "../session/session-manager";
4
4
  import { FileSessionStorage } from "../session/session-storage";
5
5
 
6
- /** Show TUI session selector and return selected session path or null if cancelled */
7
- export async function selectSession(sessions: SessionInfo[]): Promise<string | null> {
8
- const { promise, resolve } = Promise.withResolvers<string | null>();
6
+ /** Show the read-only TUI session picker and return the user's consent intent. */
7
+ export async function selectSession(sessions: SessionInfo[]): Promise<SessionSelectionResult> {
8
+ const { promise, resolve } = Promise.withResolvers<SessionSelectionResult>();
9
9
  const ui = new TUI(new ProcessTerminal());
10
- let resolved = false;
11
10
  const storage = new FileSessionStorage();
12
-
13
- const showSelector = () => {
14
- const selector = new SessionSelectorComponent(
15
- sessions,
16
- (path: string) => {
17
- if (!resolved) {
18
- resolved = true;
19
- ui.stop();
20
- resolve(path);
21
- }
22
- },
23
- () => {
24
- if (!resolved) {
25
- resolved = true;
26
- ui.stop();
27
- resolve(null);
28
- }
29
- },
30
- () => {
31
- if (!resolved) {
32
- resolved = true;
33
- ui.stop();
34
- process.exit(0);
35
- }
36
- },
37
- async (session: SessionInfo) => {
38
- // Delete handler - SessionList will show confirmation internally
39
- await storage.deleteSessionWithArtifacts(session.path);
40
- return true;
41
- },
42
- );
43
- return selector;
11
+ let settled = false;
12
+ const settle = (selection: SessionSelectionResult): void => {
13
+ if (settled) return;
14
+ settled = true;
15
+ ui.stop();
16
+ resolve(selection);
44
17
  };
45
-
46
- const selector = showSelector();
18
+ const selector = new SessionSelectorComponent(
19
+ sessions,
20
+ () => {},
21
+ () => settle({ kind: "cancelled" }),
22
+ () => settle({ kind: "cancelled" }),
23
+ async session => {
24
+ await storage.deleteSessionWithArtifacts(session.path);
25
+ return true;
26
+ },
27
+ SessionManager.inspectSessionTailReadOnly,
28
+ settle,
29
+ );
47
30
  selector.setOnRequestRender(() => ui.requestRender());
48
31
  ui.addChild(selector);
49
32
  ui.setFocus(selector);
@@ -45,7 +45,6 @@ export interface PackageManagerUpdateOptions {
45
45
  expectedVersion: string;
46
46
  runInstall: PackageManagerUpdateRunner;
47
47
  verifyInstalledRuntime: (expectedVersion: string) => Promise<InstalledVersionVerification>;
48
- printVerificationResult?: (expectedVersion: string) => Promise<void>;
49
48
  printRecoveredVerification?: (expectedVersion: string) => void;
50
49
  }
51
50
 
@@ -122,8 +121,11 @@ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
122
121
  return isPathInDirectoryLexical(resolvedFile, dirReal);
123
122
  }
124
123
 
125
- type PackageManagerTarget = { manager: "npm"; packageName: string };
126
- type UpdateTarget = { method: "bun" } | { method: "npm"; packageName: string } | { method: "binary"; path: string };
124
+ export type PackageManagerTarget = { manager: "npm"; packageName: string };
125
+ export type UpdateTarget =
126
+ | { method: "bun" }
127
+ | { method: "npm"; packageName: string }
128
+ | { method: "binary"; path: string };
127
129
 
128
130
  type PathPlatform = NodeJS.Platform;
129
131
  type PackageExists = (packageName: string, packageRoot: string) => boolean;
@@ -423,19 +425,6 @@ export function formatVerificationFailureForTest(
423
425
  return formatVerificationFailure(result, expectedVersion);
424
426
  }
425
427
 
426
- /**
427
- * Print post-update verification result.
428
- */
429
- async function printVerification(expectedVersion: string): Promise<void> {
430
- const result = await verifyInstalledRuntime(expectedVersion);
431
- if (result.ok) {
432
- printSuccessfulVerification(expectedVersion);
433
- return;
434
- }
435
- console.log(chalk.yellow(`\nWarning: ${formatVerificationFailure(result, expectedVersion)}`));
436
- console.log(chalk.yellow(formatManualUpdateInstructions()));
437
- }
438
-
439
428
  async function unlinkIfExists(filePath: string): Promise<void> {
440
429
  try {
441
430
  await fs.promises.unlink(filePath);
@@ -499,6 +488,14 @@ function formatPackageManagerInstallFailure(
499
488
  return `${managerName} install failed with exit code ${result.exitCode ?? "unknown"}${outputSuffix}. ${formatVerificationFailure(verification, expectedVersion)}`;
500
489
  }
501
490
 
491
+ function formatPackageManagerVerificationFailure(
492
+ managerName: string,
493
+ verification: InstalledVersionVerification,
494
+ expectedVersion: string,
495
+ ): string {
496
+ return `${managerName} install exited successfully, but the selected ${APP_NAME} runtime failed verification: ${formatVerificationFailure(verification, expectedVersion)}`;
497
+ }
498
+
502
499
  export async function runPackageManagerUpdateForTest(
503
500
  options: PackageManagerUpdateOptions,
504
501
  ): Promise<InstalledVersionVerification> {
@@ -508,8 +505,14 @@ export async function runPackageManagerUpdateForTest(
508
505
  async function updateViaPackageManager(options: PackageManagerUpdateOptions): Promise<InstalledVersionVerification> {
509
506
  const result = await options.runInstall(options.expectedVersion);
510
507
  if (result.exitCode === 0) {
511
- await (options.printVerificationResult ?? printVerification)(options.expectedVersion);
512
- return await options.verifyInstalledRuntime(options.expectedVersion);
508
+ const verification = await options.verifyInstalledRuntime(options.expectedVersion);
509
+ if (!verification.ok) {
510
+ throw new Error(
511
+ formatPackageManagerVerificationFailure(options.managerName, verification, options.expectedVersion),
512
+ );
513
+ }
514
+ printSuccessfulVerification(options.expectedVersion);
515
+ return verification;
513
516
  }
514
517
 
515
518
  const verification = await options.verifyInstalledRuntime(options.expectedVersion);
@@ -661,16 +664,42 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
661
664
  /**
662
665
  * Run the update command.
663
666
  */
664
- export async function runUpdateCommand(opts: { force: boolean; check: boolean }): Promise<void> {
667
+ export interface UpdateCommandDependencies {
668
+ getLatestRelease?: () => Promise<ReleaseInfo>;
669
+ resolveUpdateTarget?: () => Promise<UpdateTarget>;
670
+ performUpdate?: (target: UpdateTarget, expectedVersion: string) => Promise<void>;
671
+ refreshInstalledDefaultSkills?: () => Promise<void>;
672
+ exit?: (code: number) => never;
673
+ }
674
+
675
+ async function performUpdate(target: UpdateTarget, expectedVersion: string): Promise<void> {
676
+ if (target.method === "bun") {
677
+ await updateViaBun(expectedVersion);
678
+ } else if (target.method === "npm") {
679
+ await updateViaNpm(target.packageName, expectedVersion);
680
+ } else {
681
+ await updateViaBinaryAt(target.path, expectedVersion);
682
+ }
683
+ }
684
+
685
+ export async function runUpdateCommand(
686
+ opts: { force: boolean; check: boolean },
687
+ deps: UpdateCommandDependencies = {},
688
+ ): Promise<void> {
689
+ const lookupRelease = deps.getLatestRelease ?? getLatestRelease;
690
+ const resolveTarget = deps.resolveUpdateTarget ?? resolveUpdateTarget;
691
+ const update = deps.performUpdate ?? performUpdate;
692
+ const refreshDefaults = deps.refreshInstalledDefaultSkills ?? refreshInstalledDefaultSkills;
693
+ const exit = deps.exit ?? process.exit;
694
+
665
695
  console.log(chalk.dim(`Current version: ${VERSION}`));
666
696
 
667
- // Check for updates
668
697
  let release: ReleaseInfo;
669
698
  try {
670
- release = await getLatestRelease();
699
+ release = await lookupRelease();
671
700
  } catch (err) {
672
701
  console.error(chalk.red(`Failed to check for updates: ${err}`));
673
- process.exit(1);
702
+ return exit(1);
674
703
  }
675
704
 
676
705
  const comparison = compareVersions(release.version, VERSION);
@@ -686,27 +715,17 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
686
715
  console.log(chalk.yellow(`Forcing reinstall of ${release.version}`));
687
716
  }
688
717
 
689
- if (opts.check) {
690
- // Just check, don't install
691
- return;
692
- }
718
+ if (opts.check) return;
693
719
 
694
- // Choose update method based on the prioritized skc binary in PATH
695
720
  try {
696
- const target = await resolveUpdateTarget();
697
- if (target.method === "bun") {
698
- await updateViaBun(release.version);
699
- } else if (target.method === "npm") {
700
- await updateViaNpm(target.packageName, release.version);
701
- } else {
702
- await updateViaBinaryAt(target.path, release.version);
703
- }
721
+ const target = await resolveTarget();
722
+ await update(target, release.version);
704
723
  } catch (err) {
705
724
  console.error(chalk.red(`Update failed: ${err}`));
706
- process.exit(1);
725
+ return exit(1);
707
726
  }
708
727
 
709
- await refreshInstalledDefaultSkills();
728
+ await refreshDefaults();
710
729
  }
711
730
 
712
731
  /**