@sayknow-cli/coding-agent 0.3.1 → 0.3.4

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 (216) hide show
  1. package/bin/skc.js +4 -0
  2. package/dist/types/cli/mcp-cli.d.ts +25 -0
  3. package/dist/types/cli/plugin-cli.d.ts +2 -0
  4. package/dist/types/cli.d.ts +6 -0
  5. package/dist/types/commands/mcp.d.ts +70 -0
  6. package/dist/types/commands/plugin.d.ts +6 -0
  7. package/dist/types/commands/session.d.ts +6 -0
  8. package/dist/types/config/keybindings.d.ts +2 -2
  9. package/dist/types/config/model-profile-activation.d.ts +8 -1
  10. package/dist/types/config/model-profiles.d.ts +2 -2
  11. package/dist/types/config/model-registry.d.ts +3 -3
  12. package/dist/types/config/models-config-schema.d.ts +0 -5
  13. package/dist/types/config/settings-schema.d.ts +79 -68
  14. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  15. package/dist/types/export/html/template.generated.d.ts +1 -1
  16. package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
  17. package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
  18. package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
  19. package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
  20. package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
  21. package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
  22. package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
  23. package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
  24. package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
  25. package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
  26. package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
  27. package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
  28. package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
  29. package/dist/types/i18n/messages/en.d.ts +0 -1
  30. package/dist/types/main.d.ts +2 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  32. package/dist/types/modes/components/model-selector.d.ts +8 -0
  33. package/dist/types/modes/components/settings-defs.d.ts +0 -1
  34. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  35. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  36. package/dist/types/modes/theme/theme.d.ts +1 -1
  37. package/dist/types/notifications/html-format.d.ts +11 -0
  38. package/dist/types/notifications/index.d.ts +149 -1
  39. package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
  40. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  41. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  42. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  43. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  44. package/dist/types/notifications/recent-activity.d.ts +35 -0
  45. package/dist/types/notifications/telegram-daemon.d.ts +114 -16
  46. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  47. package/dist/types/notifications/topic-registry.d.ts +12 -9
  48. package/dist/types/runtime-mcp/types.d.ts +7 -0
  49. package/dist/types/sdk.d.ts +2 -0
  50. package/dist/types/session/agent-session.d.ts +14 -4
  51. package/dist/types/session/blob-store.d.ts +25 -0
  52. package/dist/types/session/session-manager.d.ts +57 -0
  53. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  54. package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
  55. package/dist/types/skc-runtime/state-renderer.d.ts +5 -0
  56. package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
  57. package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
  58. package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
  59. package/dist/types/skc-runtime/ultragoal-guard.d.ts +37 -1
  60. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +80 -0
  61. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  62. package/dist/types/system-prompt.d.ts +2 -0
  63. package/dist/types/task/executor.d.ts +9 -1
  64. package/dist/types/tools/browser/tab-supervisor.d.ts +31 -0
  65. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  66. package/dist/types/tools/computer-gc.d.ts +23 -0
  67. package/dist/types/tools/cron.d.ts +36 -61
  68. package/dist/types/tools/index.d.ts +3 -2
  69. package/dist/types/tools/resource-gc.d.ts +54 -0
  70. package/dist/types/utils/changelog.d.ts +1 -0
  71. package/dist/types/web/insane/url-guard.d.ts +6 -3
  72. package/dist/types/web/scrapers/types.d.ts +5 -0
  73. package/dist/types/web/scrapers/utils.d.ts +7 -1
  74. package/dist/types/web/search/index.d.ts +1 -0
  75. package/dist/types/web/search/providers/utils.d.ts +11 -4
  76. package/package.json +11 -9
  77. package/scripts/g004-tmux-smoke.ts +100 -0
  78. package/scripts/g005-daemon-smoke.ts +180 -0
  79. package/scripts/g011-daemon-path-smoke.ts +153 -0
  80. package/src/cli/args.ts +0 -1
  81. package/src/cli/fast-help.ts +29 -2
  82. package/src/cli/mcp-cli.ts +272 -0
  83. package/src/cli/plugin-cli.ts +66 -3
  84. package/src/cli/web-search-cli.ts +5 -0
  85. package/src/cli.ts +30 -12
  86. package/src/commands/mcp.ts +117 -0
  87. package/src/commands/plugin.ts +4 -0
  88. package/src/commands/session.ts +18 -0
  89. package/src/config/keybindings.ts +2 -2
  90. package/src/config/model-profile-activation.ts +62 -8
  91. package/src/config/model-profiles.ts +3 -4
  92. package/src/config/model-registry.ts +3 -6
  93. package/src/config/models-config-schema.ts +1 -1
  94. package/src/config/settings-schema.ts +88 -103
  95. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  96. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
  97. package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
  98. package/src/defaults/skc/skills/team/SKILL.md +5 -3
  99. package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
  100. package/src/export/html/index.ts +2 -2
  101. package/src/export/html/template.generated.ts +1 -1
  102. package/src/export/html/template.js +0 -12
  103. package/src/extensibility/extensions/runner.ts +1 -0
  104. package/src/extensibility/skc-plugins/compiler.ts +351 -0
  105. package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
  106. package/src/extensibility/skc-plugins/index.ts +9 -0
  107. package/src/extensibility/skc-plugins/injection.ts +109 -0
  108. package/src/extensibility/skc-plugins/installer.ts +434 -0
  109. package/src/extensibility/skc-plugins/loader.ts +3 -1
  110. package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
  111. package/src/extensibility/skc-plugins/observability.ts +84 -0
  112. package/src/extensibility/skc-plugins/paths.ts +1 -1
  113. package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
  114. package/src/extensibility/skc-plugins/registry.ts +180 -0
  115. package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
  116. package/src/extensibility/skc-plugins/schema.ts +250 -20
  117. package/src/extensibility/skc-plugins/session-validation.ts +147 -0
  118. package/src/extensibility/skc-plugins/types.ts +199 -3
  119. package/src/extensibility/skc-plugins/validation.ts +80 -0
  120. package/src/extensibility/skills.ts +15 -0
  121. package/src/goals/tools/goal-tool.ts +14 -1
  122. package/src/hooks/skill-state.ts +57 -0
  123. package/src/i18n/messages/de.ts +0 -1
  124. package/src/i18n/messages/en.ts +0 -1
  125. package/src/i18n/messages/es.ts +0 -1
  126. package/src/i18n/messages/fr.ts +0 -1
  127. package/src/i18n/messages/ja.ts +0 -1
  128. package/src/i18n/messages/ko.ts +0 -1
  129. package/src/i18n/messages/zh.ts +0 -1
  130. package/src/internal-urls/docs-index.generated.ts +14 -11
  131. package/src/main.ts +14 -3
  132. package/src/modes/bridge/bridge-mode.ts +11 -0
  133. package/src/modes/components/assistant-message.ts +49 -1
  134. package/src/modes/components/custom-editor.ts +2 -0
  135. package/src/modes/components/footer.ts +2 -3
  136. package/src/modes/components/hook-editor.ts +1 -1
  137. package/src/modes/components/hook-selector.ts +67 -43
  138. package/src/modes/components/model-selector.ts +65 -12
  139. package/src/modes/components/settings-defs.ts +1 -2
  140. package/src/modes/components/settings-selector.ts +2 -7
  141. package/src/modes/components/status-line/git-utils.ts +25 -0
  142. package/src/modes/components/status-line.ts +10 -11
  143. package/src/modes/components/welcome.ts +2 -3
  144. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  145. package/src/modes/controllers/selector-controller.ts +59 -11
  146. package/src/modes/interactive-mode.ts +4 -1
  147. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  148. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  149. package/src/modes/theme/defaults/index.ts +2 -0
  150. package/src/modes/theme/theme.ts +0 -4
  151. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  152. package/src/notifications/html-format.ts +38 -0
  153. package/src/notifications/index.ts +242 -12
  154. package/src/notifications/lifecycle-commands.ts +238 -0
  155. package/src/notifications/lifecycle-control-runtime.ts +405 -0
  156. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  157. package/src/notifications/operator-runtime.ts +171 -0
  158. package/src/notifications/rate-limit-pool.ts +19 -0
  159. package/src/notifications/recent-activity.ts +132 -0
  160. package/src/notifications/telegram-daemon.ts +778 -257
  161. package/src/notifications/telegram-reference.ts +25 -7
  162. package/src/notifications/topic-registry.ts +23 -9
  163. package/src/prompts/agents/executor.md +2 -2
  164. package/src/prompts/system/system-prompt.md +2 -2
  165. package/src/prompts/tools/cron.md +5 -3
  166. package/src/prompts/tools/read.md +1 -1
  167. package/src/runtime-mcp/transports/stdio.ts +38 -4
  168. package/src/runtime-mcp/types.ts +7 -0
  169. package/src/sdk.ts +162 -10
  170. package/src/session/agent-session.ts +210 -74
  171. package/src/session/blob-store.ts +196 -8
  172. package/src/session/session-manager.ts +762 -12
  173. package/src/skc-runtime/launch-tmux.ts +68 -19
  174. package/src/skc-runtime/psmux-detect.ts +239 -0
  175. package/src/skc-runtime/state-renderer.ts +13 -0
  176. package/src/skc-runtime/team-runtime.ts +56 -23
  177. package/src/skc-runtime/tmux-common.ts +88 -4
  178. package/src/skc-runtime/tmux-sessions.ts +111 -9
  179. package/src/skc-runtime/ultragoal-guard.ts +192 -8
  180. package/src/skc-runtime/ultragoal-runtime.ts +296 -16
  181. package/src/slash-commands/builtin-registry.ts +23 -3
  182. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  183. package/src/slash-commands/helpers/parse.ts +2 -1
  184. package/src/system-prompt.ts +9 -0
  185. package/src/task/executor.ts +31 -7
  186. package/src/task/index.ts +2 -0
  187. package/src/tools/ask.ts +5 -1
  188. package/src/tools/bash.ts +9 -0
  189. package/src/tools/browser/tab-supervisor.ts +86 -2
  190. package/src/tools/composer-bash-policy.ts +96 -0
  191. package/src/tools/computer-gc.ts +66 -0
  192. package/src/tools/computer.ts +2 -0
  193. package/src/tools/cron.ts +75 -112
  194. package/src/tools/fetch.ts +18 -2
  195. package/src/tools/index.ts +5 -9
  196. package/src/tools/read.ts +25 -55
  197. package/src/tools/renderers.ts +0 -2
  198. package/src/tools/resource-gc.ts +291 -0
  199. package/src/tools/ultragoal-ask-guard.ts +7 -1
  200. package/src/utils/changelog.ts +8 -0
  201. package/src/web/insane/url-guard.ts +18 -14
  202. package/src/web/scrapers/types.ts +143 -45
  203. package/src/web/scrapers/utils.ts +70 -19
  204. package/src/web/search/index.ts +1 -0
  205. package/src/web/search/providers/utils.ts +22 -5
  206. package/vendor/insane-search/MANIFEST.json +3 -1
  207. package/vendor/insane-search/engine/__init__.py +14 -0
  208. package/vendor/insane-search/engine/content_safety.py +151 -0
  209. package/vendor/insane-search/engine/fetch_chain.py +32 -0
  210. package/vendor/insane-search/engine/tests/test_u8.py +216 -0
  211. package/dist/types/tools/inspect-image-renderer.d.ts +0 -26
  212. package/dist/types/tools/inspect-image.d.ts +0 -31
  213. package/src/prompts/tools/inspect-image-system.md +0 -20
  214. package/src/prompts/tools/inspect-image.md +0 -32
  215. package/src/tools/inspect-image-renderer.ts +0 -103
  216. package/src/tools/inspect-image.ts +0 -172
@@ -111,5 +111,6 @@ export declare class WebSearchTool implements AgentTool<typeof webSearchSchema,
111
111
  export declare const webSearchCustomTool: CustomTool<typeof webSearchSchema, SearchRenderDetails>;
112
112
  export declare function getSearchTools(): CustomTool<any, any>[];
113
113
  export { getSearchProvider, setPreferredSearchProvider, setSearchFallbackProviders } from "./provider";
114
+ export { setSearchHardTimeoutMs } from "./providers/utils";
114
115
  export type { SearchProviderId as SearchProvider, SearchResponse } from "./types";
115
116
  export { isConfigurableSearchProviderId, isSearchProviderPreference } from "./types";
@@ -15,16 +15,23 @@ import { SearchProviderError, type SearchProviderId, type SearchSource } from ".
15
15
  */
16
16
  export declare function findCredential(storage: AgentStorage | null | undefined, envKey: string | null | undefined, ...storageProviders: string[]): string | null;
17
17
  /**
18
- * Default hard ceiling for a single web-search round-trip. 60s tolerates
18
+ * Default hard ceiling for a single web-search round-trip. 300s tolerates
19
19
  * legitimate slow LLM-mediated responses (anthropic web_search_20250305,
20
20
  * perplexity, gemini, OpenAI code backend) while still guaranteeing the session unfreezes
21
- * within a minute if Bun's `AbortSignal` fails to propagate on Windows.
21
+ * if Bun's `AbortSignal` fails to propagate on Windows.
22
22
  *
23
23
  * Pure search APIs (brave, exa, jina, tavily, searxng, synthetic, zai)
24
24
  * settle far faster in practice; reusing the same ceiling keeps the wiring
25
25
  * uniform without compromising correctness.
26
26
  */
27
- export declare const SEARCH_HARD_TIMEOUT_MS = 60000;
27
+ export declare const SEARCH_HARD_TIMEOUT_MS = 300000;
28
+ /**
29
+ * Override the hard timeout applied to every web-search round-trip.
30
+ *
31
+ * @param ms - Hard timeout in milliseconds. Non-finite or non-positive
32
+ * values reset the timeout to {@link SEARCH_HARD_TIMEOUT_MS}.
33
+ */
34
+ export declare function setSearchHardTimeoutMs(ms: number | undefined): void;
28
35
  /**
29
36
  * Compose a caller-supplied {@link AbortSignal} with a hard timeout so an
30
37
  * outbound `fetch()` is guaranteed to settle within `ms` even when the
@@ -36,7 +43,7 @@ export declare const SEARCH_HARD_TIMEOUT_MS = 60000;
36
43
  * because the user's Esc is never delivered to the native layer.
37
44
  *
38
45
  * @param signal - Caller cancellation signal, if any.
39
- * @param ms - Hard timeout in milliseconds. Defaults to {@link SEARCH_HARD_TIMEOUT_MS}.
46
+ * @param ms - Hard timeout in milliseconds. Defaults to the configured value.
40
47
  */
41
48
  export declare function withHardTimeout(signal: AbortSignal | undefined, ms?: number): AbortSignal;
42
49
  /**
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@sayknow-cli/coding-agent",
4
- "version": "0.3.1",
4
+ "version": "0.3.4",
5
5
  "description": "Sayknow-CLI CLI with read, bash, edit, write tools and session management",
6
6
  "homepage": "https://github.com/jaybeyond/Sayknow_CLI",
7
7
  "author": "jaybeyond",
@@ -28,7 +28,7 @@
28
28
  "main": "./src/index.ts",
29
29
  "types": "./dist/types/index.d.ts",
30
30
  "bin": {
31
- "skc": "src/cli.ts"
31
+ "skc": "bin/skc.js"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "bun scripts/build-binary.ts",
@@ -52,12 +52,12 @@
52
52
  "@agentclientprotocol/sdk": "0.21.0",
53
53
  "@babel/parser": "^7.29.3",
54
54
  "@mozilla/readability": "^0.6.0",
55
- "@sayknow-cli/stats": "0.3.1",
56
- "@sayknow-cli/agent-core": "0.3.1",
57
- "@sayknow-cli/ai": "0.3.1",
58
- "@sayknow-cli/natives": "0.3.1",
59
- "@sayknow-cli/tui": "0.3.1",
60
- "@sayknow-cli/utils": "0.3.1",
55
+ "@sayknow-cli/stats": "0.3.4",
56
+ "@sayknow-cli/agent-core": "0.3.4",
57
+ "@sayknow-cli/ai": "0.3.4",
58
+ "@sayknow-cli/natives": "0.3.4",
59
+ "@sayknow-cli/tui": "0.3.4",
60
+ "@sayknow-cli/utils": "0.3.4",
61
61
  "@puppeteer/browsers": "^2.13.0",
62
62
  "@types/turndown": "5.0.6",
63
63
  "@xterm/headless": "^6.0.0",
@@ -74,13 +74,15 @@
74
74
  "zod": "4.4.3"
75
75
  },
76
76
  "devDependencies": {
77
- "@types/bun": "^1.3.14"
77
+ "@types/bun": "^1.3.14",
78
+ "@types/ws": "^8.5.13"
78
79
  },
79
80
  "engines": {
80
81
  "bun": ">=1.3.14"
81
82
  },
82
83
  "files": [
83
84
  "src",
85
+ "bin",
84
86
  "scripts",
85
87
  "examples",
86
88
  "README.md",
@@ -0,0 +1,100 @@
1
+ // G004 real-tmux smoke: exercises forceCloseSkcTmuxSession against LIVE tmux
2
+ // sessions (tmux 3.6a). Proves the wrapper hard-kills SKC-managed live panes
3
+ // (where remove refuses), refuses non-SKC sessions, and enforces session-id
4
+ // matching. Produces durable evidence; not part of the unit suite.
5
+ import assert from "node:assert";
6
+
7
+ import {
8
+ buildSkcTmuxExactOptionTarget,
9
+ buildSkcTmuxProfileCommands,
10
+ resolveSkcTmuxCommand,
11
+ } from "../src/skc-runtime/tmux-common";
12
+ import { forceCloseSkcTmuxSession, removeSkcTmuxSession, statusSkcTmuxSession } from "../src/skc-runtime/tmux-sessions";
13
+
14
+ const tmux = resolveSkcTmuxCommand(process.env);
15
+
16
+ function sh(args: string[]): { code: number; err: string } {
17
+ const r = Bun.spawnSync([tmux, ...args], { stdout: "pipe", stderr: "pipe" });
18
+ return { code: r.exitCode, err: r.stderr.toString().trim() };
19
+ }
20
+
21
+ function makeRawSession(name: string): void {
22
+ const r = sh(["new-session", "-d", "-s", name, "sleep 600"]);
23
+ if (r.code !== 0) throw new Error(`failed to create tmux session ${name}: ${r.err}`);
24
+ }
25
+
26
+ function tagAsSkc(name: string, sessionId?: string): void {
27
+ const target = buildSkcTmuxExactOptionTarget(name);
28
+ for (const cmd of buildSkcTmuxProfileCommands(target, process.env, { sessionId })) {
29
+ const r = sh(cmd.args);
30
+ if (r.code !== 0) throw new Error(`failed to tag ${name} (${cmd.description}): ${r.err}`);
31
+ }
32
+ }
33
+
34
+ function exists(name: string): boolean {
35
+ return sh(["has-session", "-t", `=${name}`]).code === 0;
36
+ }
37
+
38
+ function killQuiet(name: string): void {
39
+ sh(["kill-session", "-t", `=${name}`]);
40
+ }
41
+
42
+ const suffix = `${process.pid}-${Date.now()}`;
43
+ const live = `skc_g004live_${suffix}`;
44
+ const raw = `g004raw_${suffix}`;
45
+ const mism = `skc_g004mism_${suffix}`;
46
+ const cleanup: string[] = [live, raw, mism];
47
+
48
+ try {
49
+ // 1. SKC-managed LIVE session: remove refuses, force-close hard-kills.
50
+ makeRawSession(live);
51
+ tagAsSkc(live, "sess-g004");
52
+ const status = statusSkcTmuxSession(live);
53
+ assert.equal(status.profile, "1", "session must be recognized as SKC-managed");
54
+ assert.ok(status.panePids.length > 0, "session must have a live pane (sleep)");
55
+ console.log(`[g004] live SKC session up: ${live} panePids=${status.panePids.length}`);
56
+
57
+ let removeRefused = false;
58
+ try {
59
+ removeSkcTmuxSession(live);
60
+ } catch (e) {
61
+ removeRefused = /skc_tmux_session_live/.test(String(e));
62
+ }
63
+ assert.ok(removeRefused, "removeSkcTmuxSession must REFUSE a live pane");
64
+ console.log("[g004] remove refused live session (expected)");
65
+
66
+ const closed = forceCloseSkcTmuxSession(live, process.env, "sess-g004");
67
+ assert.equal(closed.name, live);
68
+ assert.ok(!exists(live), "force-close must hard-kill the live SKC session");
69
+ console.log("[g004] force-close hard-killed the live SKC session (id-matched)");
70
+
71
+ // 2. Non-SKC (untagged) session: force-close must refuse.
72
+ makeRawSession(raw);
73
+ let notManaged = false;
74
+ try {
75
+ forceCloseSkcTmuxSession(raw, process.env);
76
+ } catch (e) {
77
+ notManaged = /skc_tmux_session_(not_managed|not_found)/.test(String(e));
78
+ }
79
+ assert.ok(notManaged, "force-close must refuse a non-SKC tmux session");
80
+ assert.ok(exists(raw), "non-SKC session must be left untouched");
81
+ killQuiet(raw);
82
+ console.log("[g004] force-close refused + preserved non-SKC session (expected)");
83
+
84
+ // 3. SKC session with a MISMATCHED expected session id: must refuse.
85
+ makeRawSession(mism);
86
+ tagAsSkc(mism, "sess-real");
87
+ let idMismatch = false;
88
+ try {
89
+ forceCloseSkcTmuxSession(mism, process.env, "sess-WRONG");
90
+ } catch (e) {
91
+ idMismatch = /skc_tmux_session_id_mismatch/.test(String(e));
92
+ }
93
+ assert.ok(idMismatch, "force-close must refuse on session-id mismatch");
94
+ assert.ok(exists(mism), "mismatched session must be left untouched");
95
+ console.log("[g004] force-close refused on session-id mismatch (expected)");
96
+
97
+ console.log("[g004] PASS: forceCloseSkcTmuxSession verified against live tmux");
98
+ } finally {
99
+ for (const name of cleanup) killQuiet(name);
100
+ }
@@ -0,0 +1,180 @@
1
+ // G005 real end-to-end daemon-orchestration smoke.
2
+ //
3
+ // Drives the lifecycle orchestrator's create -> close path with REAL effects:
4
+ // a real fsynced file-backed idempotency ledger, a real audit JSONL, and a real
5
+ // tmux session spawned/closed via the actual tmux helpers. Proves the daemon
6
+ // orchestration turns a `session_create` frame into a genuinely-spawned,
7
+ // SKC-tagged tmux session and a `session_close` frame into a real hard-close,
8
+ // with idempotent re-ack. Not part of the unit suite.
9
+ import assert from "node:assert";
10
+ import * as crypto from "node:crypto";
11
+ import * as fs from "node:fs";
12
+ import * as os from "node:os";
13
+ import * as path from "node:path";
14
+ import type { SessionCloseFrame, SessionCreateFrame } from "../src/notifications/index";
15
+ import {
16
+ type AuditEvent,
17
+ handleLifecycleRequest,
18
+ type LedgerDoc,
19
+ type LedgerStore,
20
+ type OrchestratorDeps,
21
+ } from "../src/notifications/lifecycle-orchestrator";
22
+ import {
23
+ buildSkcTmuxExactOptionTarget,
24
+ buildSkcTmuxProfileCommands,
25
+ resolveSkcTmuxCommand,
26
+ } from "../src/skc-runtime/tmux-common";
27
+ import { forceCloseSkcTmuxSession, statusSkcTmuxSession } from "../src/skc-runtime/tmux-sessions";
28
+
29
+ const tmux = resolveSkcTmuxCommand(process.env);
30
+ const tmpRoot = fs.mkdtempSync(path.join(os.tmpdir(), "skc-g005-"));
31
+ const ledgerPath = path.join(tmpRoot, "idempotency.json");
32
+ const auditPath = path.join(tmpRoot, "audit.jsonl");
33
+ const created: string[] = [];
34
+
35
+ function sh(args: string[]): number {
36
+ return Bun.spawnSync([tmux, ...args], { stdout: "pipe", stderr: "pipe" }).exitCode;
37
+ }
38
+ function exists(name: string): boolean {
39
+ return sh(["has-session", "-t", `=${name}`]) === 0;
40
+ }
41
+
42
+ // Real fsynced + atomic file ledger store.
43
+ const store: LedgerStore = {
44
+ async read(): Promise<LedgerDoc> {
45
+ try {
46
+ return JSON.parse(fs.readFileSync(ledgerPath, "utf8")) as LedgerDoc;
47
+ } catch {
48
+ return { version: 1, entries: {} };
49
+ }
50
+ },
51
+ async write(doc: LedgerDoc): Promise<void> {
52
+ const tmp = `${ledgerPath}.${process.pid}.tmp`;
53
+ const fd = fs.openSync(tmp, "w", 0o600);
54
+ fs.writeSync(fd, JSON.stringify(doc));
55
+ fs.fsyncSync(fd);
56
+ fs.closeSync(fd);
57
+ fs.renameSync(tmp, ledgerPath);
58
+ },
59
+ };
60
+
61
+ const auditLines: AuditEvent[] = [];
62
+ const deps: OrchestratorDeps = {
63
+ pairedChatId: "42",
64
+ now: () => Date.now(),
65
+ store,
66
+ audit: e => {
67
+ auditLines.push(e);
68
+ fs.appendFileSync(auditPath, `${JSON.stringify(e)}\n`, { mode: 0o600 });
69
+ },
70
+ allowCreate: () => true,
71
+ writeStartupPrompt: async (requestId, prompt) => {
72
+ if (prompt === undefined) return undefined;
73
+ const ref = path.join(tmpRoot, `prompt-${requestId}`);
74
+ const fd = fs.openSync(ref, "w", 0o600);
75
+ fs.writeSync(fd, prompt);
76
+ fs.fsyncSync(fd);
77
+ fs.closeSync(fd);
78
+ return ref;
79
+ },
80
+ // REAL spawn: create a SKC-tagged tmux session carrying the authoritative id.
81
+ spawnCreate: async (_frame, ids) => {
82
+ const name = `skc_g005_${ids.intendedSessionId}`;
83
+ created.push(name);
84
+ if (sh(["new-session", "-d", "-s", name, "sleep 600"]) !== 0) {
85
+ throw new Error(`tmux spawn failed for ${name}`);
86
+ }
87
+ const target = buildSkcTmuxExactOptionTarget(name);
88
+ for (const cmd of buildSkcTmuxProfileCommands(target, process.env, {
89
+ sessionId: ids.intendedSessionId,
90
+ })) {
91
+ if (sh(cmd.args) !== 0) throw new Error(`tag failed for ${name}`);
92
+ }
93
+ return {
94
+ sessionId: ids.intendedSessionId,
95
+ tmuxSession: name,
96
+ endpointUrl: "ws://127.0.0.1:0",
97
+ topicThreadId: "1",
98
+ };
99
+ },
100
+ // REAL close: hard-close the SKC-managed tmux session, id-matched.
101
+ closeSession: async target => {
102
+ forceCloseSkcTmuxSession(target.tmuxSession ?? "", process.env, target.sessionId);
103
+ return { processGone: !exists(target.tmuxSession ?? "") };
104
+ },
105
+ resumeSession: async () => ({ ambiguous: [] }),
106
+ newLifecycleRequestId: () => `lc-${crypto.randomUUID()}`,
107
+ newSessionId: () => `s${crypto.randomUUID().slice(0, 8)}`,
108
+ };
109
+
110
+ const createFrame: SessionCreateFrame = {
111
+ type: "session_create",
112
+ requestId: "lc_g005",
113
+ lifecycleRequestId: "lc_g005",
114
+ intendedSessionId: `g005${Date.now().toString(36)}`,
115
+ updateId: 100,
116
+ chatId: "42",
117
+ token: "control-token",
118
+ target: { kind: "existing_path", path: tmpRoot },
119
+ };
120
+
121
+ async function main(): Promise<void> {
122
+ // 1. CREATE -> real tmux session spawned + SKC-tagged with the authoritative id.
123
+ const createOut = await handleLifecycleRequest(createFrame, deps);
124
+ assert.equal(createOut.status, "ok", "create must succeed");
125
+ const session = createOut.status === "ok" ? createOut.entry.tmuxSession! : "";
126
+ assert.ok(exists(session), "real tmux session must exist after create");
127
+ const status = statusSkcTmuxSession(session);
128
+ assert.equal(status.profile, "1", "spawned session must be SKC-managed");
129
+ assert.equal(status.sessionId, createFrame.intendedSessionId, "authoritative session id propagated to tmux tag");
130
+ console.log(`[g005] CREATE -> live tmux session ${session} (id=${status.sessionId})`);
131
+
132
+ // 2. Idempotent re-ack: same updateId+body must NOT spawn a second session.
133
+ const before = created.length;
134
+ const dupOut = await handleLifecycleRequest(createFrame, deps);
135
+ assert.equal(dupOut.status, "ok", "duplicate create re-acks ok");
136
+ assert.equal(created.length, before, "duplicate create must NOT spawn a second session");
137
+ console.log("[g005] DUPLICATE create re-acked, no second spawn (idempotent)");
138
+
139
+ // 3. CLOSE -> real hard-close of the SKC-managed session, id-matched.
140
+ const closeFrame: SessionCloseFrame = {
141
+ type: "session_close",
142
+ requestId: "lc_g005_close",
143
+ updateId: 101,
144
+ chatId: "42",
145
+ token: "control-token",
146
+ target: { sessionId: createFrame.intendedSessionId, tmuxSession: session },
147
+ force: true,
148
+ };
149
+ const closeOut = await handleLifecycleRequest(closeFrame, deps);
150
+ assert.equal(closeOut.status, "ok", "close must succeed");
151
+ assert.ok(!exists(session), "real tmux session must be gone after close");
152
+ console.log(`[g005] CLOSE -> hard-closed ${session} (id-matched)`);
153
+
154
+ // 4. Durable ledger + audit redaction.
155
+ const doc = JSON.parse(fs.readFileSync(ledgerPath, "utf8")) as LedgerDoc;
156
+ assert.equal(doc.entries["42:100"]?.state, "success");
157
+ assert.equal(doc.entries["42:101"]?.state, "success");
158
+ const auditBlob = fs.readFileSync(auditPath, "utf8");
159
+ assert.ok(!auditBlob.includes("control-token"), "audit must never contain the control token");
160
+ assert.ok(
161
+ auditLines.some(a => a.event === "spawn_started"),
162
+ "audit records spawn_started",
163
+ );
164
+ console.log("[g005] durable fsynced ledger + token-redacted audit verified");
165
+
166
+ console.log("[g005] PASS: real create->close daemon orchestration over live tmux");
167
+ }
168
+
169
+ main()
170
+ .then(() => {
171
+ for (const n of created) sh(["kill-session", "-t", `=${n}`]);
172
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
173
+ process.exit(0);
174
+ })
175
+ .catch(err => {
176
+ for (const n of created) sh(["kill-session", "-t", `=${n}`]);
177
+ fs.rmSync(tmpRoot, { recursive: true, force: true });
178
+ console.error("[g005] FAIL", err);
179
+ process.exit(1);
180
+ });
@@ -0,0 +1,153 @@
1
+ // G011 real daemon-path smoke.
2
+ //
3
+ // Drives a PARSED /session_* command through the LIVE native
4
+ // NotificationControlServer + a real loopback WebSocket client (NO injected/fake
5
+ // seams), wired to the real orchestrator (with stubbed spawn/close effects so the
6
+ // focus is the authenticated wire path, not tmux). Asserts:
7
+ // - a wrong-token handshake is rejected (control token gates the endpoint);
8
+ // - a valid frame is forwarded with the control token STRIPPED from payloadJson;
9
+ // - the host response is routed back to the client by requestId;
10
+ // - control discovery exists while running and is removed after stop.
11
+ import assert from "node:assert";
12
+ import * as crypto from "node:crypto";
13
+ import * as fs from "node:fs";
14
+ import * as os from "node:os";
15
+ import * as path from "node:path";
16
+ import WebSocket from "ws";
17
+
18
+ // Import the WORKTREE native build directly (not @sayknow-cli/natives, which can
19
+ // resolve to a different checkout in this dev environment).
20
+ import { NotificationControlServer } from "../../natives/native/index.js";
21
+ import { parseLifecycleCommand } from "../src/notifications/lifecycle-commands";
22
+ import { attachLifecycleControl, fileAudit, fileLedgerStore } from "../src/notifications/lifecycle-control-runtime";
23
+ import type { OrchestratorDeps } from "../src/notifications/lifecycle-orchestrator";
24
+
25
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "skc-g011-"));
26
+ const token = crypto.randomBytes(32).toString("base64url");
27
+ const ownerId = "daemon-g011";
28
+ const closed: string[] = [];
29
+
30
+ function deps(): OrchestratorDeps {
31
+ return {
32
+ pairedChatId: "42",
33
+ now: () => Date.now(),
34
+ store: fileLedgerStore(path.join(tmp, "idempotency.json")),
35
+ audit: fileAudit(path.join(tmp, "audit.jsonl")),
36
+ allowCreate: () => true,
37
+ writeStartupPrompt: async () => undefined,
38
+ // Stubbed effects: this smoke proves the WIRE path, not tmux (covered by g005).
39
+ spawnCreate: async (_f, ids) => ({
40
+ sessionId: ids.intendedSessionId,
41
+ tmuxSession: `skc-${ids.intendedSessionId}`,
42
+ endpointUrl: "ws://127.0.0.1:0",
43
+ topicThreadId: "1",
44
+ }),
45
+ closeSession: async t => {
46
+ closed.push(t.sessionId);
47
+ return { processGone: true };
48
+ },
49
+ resumeSession: async () => ({ ambiguous: [] }),
50
+ newLifecycleRequestId: () => `lc-${crypto.randomUUID()}`,
51
+ newSessionId: () => `s${crypto.randomUUID().slice(0, 8)}`,
52
+ };
53
+ }
54
+
55
+ function send(ws: WebSocket, frame: unknown): void {
56
+ ws.send(JSON.stringify(frame));
57
+ }
58
+
59
+ async function main(): Promise<void> {
60
+ const server = new NotificationControlServer(token, ownerId, tmp);
61
+
62
+ // Wire the real orchestrator to the real native control server.
63
+ const d = deps();
64
+ attachLifecycleControl(server as never, d);
65
+
66
+ const ep = (await server.start()) as { url: string };
67
+ assert.ok(ep.url.startsWith("ws://127.0.0.1:"), `loopback url, got ${ep.url}`);
68
+ const controlJson = path.join(tmp, "notifications", "control.json");
69
+ assert.ok(fs.existsSync(controlJson), "control discovery must exist while running");
70
+ // The control discovery file MUST NOT persist the privileged control token:
71
+ // the daemon holds it in memory and is the only client.
72
+ const controlRaw = fs.readFileSync(controlJson, "utf8");
73
+ assert.ok(!controlRaw.includes(token), "control token MUST NOT be persisted in control.json");
74
+ assert.ok(
75
+ !("token" in (JSON.parse(controlRaw) as Record<string, unknown>)),
76
+ "control.json must omit any token field",
77
+ );
78
+ console.log(`[g011] live control endpoint ${ep.url}; discovery present, no persisted token`);
79
+
80
+ // 1. Wrong-token handshake must be rejected.
81
+ await new Promise<void>(resolve => {
82
+ const bad = new WebSocket(`${ep.url}/?token=wrong`);
83
+ bad.on("open", () => {
84
+ throw new Error("wrong-token handshake must NOT open");
85
+ });
86
+ bad.on("error", () => {
87
+ console.log("[g011] wrong-token handshake rejected (expected)");
88
+ resolve();
89
+ });
90
+ });
91
+
92
+ // 2. Valid client: send a PARSED /session_close command through the real wire.
93
+ const parsed = parseLifecycleCommand("/session_close sess-g011");
94
+ assert.equal(parsed.kind, "close", "parser must yield a close command");
95
+ const requestId = "lc-g011-1";
96
+ const frame = {
97
+ type: "session_close",
98
+ requestId,
99
+ updateId: 1,
100
+ chatId: "42",
101
+ token,
102
+ target: parsed.kind === "close" ? parsed.target : { sessionId: "sess-g011" },
103
+ force: true,
104
+ };
105
+
106
+ const response = await new Promise<Record<string, unknown>>((resolve, reject) => {
107
+ const ws = new WebSocket(`${ep.url}/?token=${token}`);
108
+ const timer = setTimeout(() => reject(new Error("timed out")), 5000);
109
+ ws.on("open", () => send(ws, frame));
110
+ ws.on("message", data => {
111
+ clearTimeout(timer);
112
+ ws.close();
113
+ resolve(JSON.parse(String(data)) as Record<string, unknown>);
114
+ });
115
+ ws.on("error", reject);
116
+ });
117
+
118
+ assert.equal(response.type, "session_close_response", "response routed back to client");
119
+ assert.equal(response.requestId, requestId, "response correlated by requestId");
120
+ assert.equal(response.sessionId, "sess-g011");
121
+ assert.deepEqual(closed, ["sess-g011"], "orchestrator close effect ran exactly once");
122
+ console.log("[g011] parsed command -> real wire -> orchestrator -> routed response OK");
123
+
124
+ // 3. The real orchestrator path must NOT leak the control token into its
125
+ // durable audit log or idempotency ledger (g002 separately proves the native
126
+ // payloadJson boundary is token-stripped).
127
+ for (const f of ["audit.jsonl", "idempotency.json"]) {
128
+ const p = path.join(tmp, f);
129
+ if (fs.existsSync(p)) {
130
+ assert.ok(!fs.readFileSync(p, "utf8").includes(token), `control token leaked into ${f}`);
131
+ }
132
+ }
133
+ console.log("[g011] no control-token leak in audit/ledger on the real path");
134
+
135
+ // 4. Stop removes control discovery.
136
+ server.stop();
137
+ await new Promise(r => setTimeout(r, 50));
138
+ assert.ok(!fs.existsSync(controlJson), "control discovery removed after stop");
139
+ console.log("[g011] control discovery removed after stop");
140
+
141
+ console.log("[g011] PASS: real daemon-path (parse -> native control endpoint -> orchestrator -> reply)");
142
+ }
143
+
144
+ main()
145
+ .then(() => {
146
+ fs.rmSync(tmp, { recursive: true, force: true });
147
+ process.exit(0);
148
+ })
149
+ .catch(err => {
150
+ fs.rmSync(tmp, { recursive: true, force: true });
151
+ console.error("[g011] FAIL", err);
152
+ process.exit(1);
153
+ });
package/src/cli/args.ts CHANGED
@@ -284,7 +284,6 @@ ${chalk.bold("Available Tools (default-enabled unless noted):")}
284
284
  lsp - Language server protocol (code intelligence)
285
285
  python - Execute Python code (requires: ${APP_NAME} setup python)
286
286
  notebook - Edit Jupyter notebooks
287
- inspect_image - Analyze images with a vision model
288
287
  browser - Browser automation (Puppeteer)
289
288
  task - Launch sub-agents for parallel tasks
290
289
  todo_write - Manage todo/task lists
@@ -1,7 +1,35 @@
1
1
  import { APP_NAME, CONFIG_DIR_NAME } from "@sayknow-cli/utils/dirs";
2
2
 
3
3
  export function getExtraHelpText(): string {
4
- return `Environment Variables:
4
+ return `Commands:
5
+ ${APP_NAME} [prompt] - Start an interactive coding session (default launch command)
6
+ ${APP_NAME} launch - Start an explicit launch/session workflow
7
+ ${APP_NAME} setup - Install SKC defaults or optional dependencies
8
+ ${APP_NAME} session - List, inspect, create, remove, or attach sessions
9
+ ${APP_NAME} state - Inspect or manage persisted SKC state
10
+ ${APP_NAME} harness - Run harness control-plane commands
11
+ ${APP_NAME} coordinator - Manage coordinator/runtime coordination helpers
12
+ ${APP_NAME} team - Run tmux-backed coordinated execution
13
+ ${APP_NAME} ultragoal - Run durable goal execution workflow
14
+ ${APP_NAME} ralplan - Run consensus planning workflow
15
+ ${APP_NAME} deep-interview - Run requirements interview workflow
16
+ ${APP_NAME} skills - List/read embedded workflow skills
17
+ ${APP_NAME} config - List, get, and set configuration values
18
+ ${APP_NAME} notify - Send or test notifications
19
+ ${APP_NAME} daemon - Manage background daemon helpers
20
+ ${APP_NAME} mcp - Manage MCP server registrations
21
+ ${APP_NAME} mcp-serve - Serve the MCP integration endpoint
22
+ ${APP_NAME} contribute-pr - Prepare contribution/PR workflow artifacts
23
+ ${APP_NAME} migrate - Run migration helpers
24
+ ${APP_NAME} rlm - Run RLM helpers
25
+ ${APP_NAME} update - Update SKC installation artifacts
26
+ ${APP_NAME} plugin - Install, remove, and list plugins
27
+ ${APP_NAME} web-search - Search the web from the CLI (alias: q)
28
+ ${APP_NAME} codex-native-hook - Run Codex native hook integration
29
+ ${APP_NAME} gc - Run garbage-collection/cleanup helpers
30
+ ${APP_NAME} <command> --help - Show command-specific help
31
+
32
+ Environment Variables:
5
33
  # Core Providers
6
34
  ANTHROPIC_API_KEY - Anthropic Claude models
7
35
  ANTHROPIC_OAUTH_TOKEN - Anthropic OAuth (takes precedence over API key)
@@ -69,7 +97,6 @@ Available Tools (default-enabled unless noted):
69
97
  lsp - Language server protocol (code intelligence)
70
98
  python - Execute Python code (requires: ${APP_NAME} setup python)
71
99
  notebook - Edit Jupyter notebooks
72
- inspect_image - Analyze images with a vision model
73
100
  browser - Browser automation (Puppeteer)
74
101
  task - Launch sub-agents for parallel tasks
75
102
  todo_write - Manage todo/task lists