@sayknow-cli/coding-agent 0.3.5 → 0.3.7

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 (238) hide show
  1. package/README.md +76 -0
  2. package/dist/types/capability/mcp.d.ts +2 -0
  3. package/dist/types/cli/local-provider-smoke.d.ts +59 -0
  4. package/dist/types/cli/notify-cli.d.ts +1 -1
  5. package/dist/types/cli/telegram-cli.d.ts +13 -0
  6. package/dist/types/cli/update-cli.d.ts +24 -1
  7. package/dist/types/commands/local-provider.d.ts +34 -0
  8. package/dist/types/commands/telegram.d.ts +22 -0
  9. package/dist/types/config/keybindings.d.ts +12 -6
  10. package/dist/types/config/model-registry.d.ts +5 -0
  11. package/dist/types/config/models-config-schema.d.ts +5 -0
  12. package/dist/types/config/settings-schema.d.ts +284 -4
  13. package/dist/types/config/settings.d.ts +7 -0
  14. package/dist/types/config/telegram-autostart.d.ts +14 -0
  15. package/dist/types/config/telegram-env-bridge.d.ts +18 -0
  16. package/dist/types/coordinator-mcp/server.d.ts +8 -0
  17. package/dist/types/defaults/skc-defaults.d.ts +7 -0
  18. package/dist/types/discovery/cursor.d.ts +4 -1
  19. package/dist/types/discovery/index.d.ts +0 -1
  20. package/dist/types/discovery/windsurf.d.ts +3 -1
  21. package/dist/types/extensibility/extensions/types.d.ts +11 -0
  22. package/dist/types/i18n/messages/en.d.ts +1 -0
  23. package/dist/types/modes/components/custom-editor.d.ts +6 -1
  24. package/dist/types/modes/components/custom-model-preset-wizard.d.ts +1 -1
  25. package/dist/types/modes/components/hook-selector.d.ts +5 -0
  26. package/dist/types/modes/components/model-selector.d.ts +2 -0
  27. package/dist/types/modes/components/settings-selector.d.ts +5 -1
  28. package/dist/types/modes/components/skill-message.d.ts +1 -0
  29. package/dist/types/modes/components/status-line/types.d.ts +5 -7
  30. package/dist/types/modes/components/status-line.d.ts +15 -3
  31. package/dist/types/modes/components/two-column-body.d.ts +9 -0
  32. package/dist/types/modes/components/welcome.d.ts +9 -1
  33. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  34. package/dist/types/modes/controllers/input-controller.d.ts +6 -1
  35. package/dist/types/modes/controllers/selector-controller.d.ts +10 -1
  36. package/dist/types/modes/interactive-mode.d.ts +2 -0
  37. package/dist/types/modes/prompt-action-autocomplete.d.ts +3 -0
  38. package/dist/types/modes/rpc/rpc-types.d.ts +1 -0
  39. package/dist/types/modes/shared/agent-wire/deep-interview-gate.d.ts +41 -7
  40. package/dist/types/modes/slash-command-visibility.d.ts +1 -0
  41. package/dist/types/modes/theme/theme.d.ts +1 -1
  42. package/dist/types/modes/tmux-scroll.d.ts +18 -0
  43. package/dist/types/modes/types.d.ts +5 -1
  44. package/dist/types/modes/utils/terminal-bell.d.ts +3 -0
  45. package/dist/types/notifications/daemon-paths.d.ts +9 -0
  46. package/dist/types/notifications/html-format.d.ts +2 -0
  47. package/dist/types/notifications/recent-activity.d.ts +5 -1
  48. package/dist/types/notifications/telegram-daemon-cli.d.ts +21 -4
  49. package/dist/types/notifications/telegram-daemon-control.d.ts +8 -0
  50. package/dist/types/notifications/telegram-daemon.d.ts +4 -10
  51. package/dist/types/notifications/telegram-reference.d.ts +4 -0
  52. package/dist/types/notifications/threaded-render.d.ts +1 -0
  53. package/dist/types/runtime-mcp/config-writer.d.ts +9 -0
  54. package/dist/types/runtime-mcp/config.d.ts +2 -0
  55. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  56. package/dist/types/runtime-mcp/manager.d.ts +2 -0
  57. package/dist/types/runtime-mcp/types.d.ts +6 -0
  58. package/dist/types/sdk.d.ts +1 -1
  59. package/dist/types/skc-runtime/launch-tmux.d.ts +2 -2
  60. package/dist/types/skc-runtime/ralplan-runtime.d.ts +3 -2
  61. package/dist/types/skc-runtime/restricted-role-agent-bash.d.ts +1 -0
  62. package/dist/types/skc-runtime/session-state-sidecar.d.ts +5 -0
  63. package/dist/types/skc-runtime/state-writer.d.ts +1 -0
  64. package/dist/types/skc-runtime/team-runtime.d.ts +1 -1
  65. package/dist/types/skc-runtime/tmux-common.d.ts +12 -0
  66. package/dist/types/skc-runtime/ultragoal-guard.d.ts +7 -1
  67. package/dist/types/skc-runtime/ultragoal-runtime.d.ts +0 -4
  68. package/dist/types/slash-commands/types.d.ts +2 -0
  69. package/dist/types/system-prompt.d.ts +7 -0
  70. package/dist/types/tools/skill.d.ts +1 -1
  71. package/dist/types/utils/cmux-workspace.d.ts +52 -0
  72. package/dist/types/web/search/index.d.ts +4 -2
  73. package/dist/types/web/search/provider.d.ts +14 -0
  74. package/dist/types/web/search/providers/utils.d.ts +42 -13
  75. package/dist/types/web/search/render.d.ts +1 -0
  76. package/dist/types/workflow/workflow-intent-diff.d.ts +19 -0
  77. package/package.json +8 -8
  78. package/scripts/build-binary.ts +3 -0
  79. package/scripts/verify-insane-vendor.ts +0 -5
  80. package/src/capability/mcp.ts +2 -0
  81. package/src/cli/local-provider-smoke.ts +632 -0
  82. package/src/cli/notify-cli.ts +4 -2
  83. package/src/cli/telegram-cli.ts +156 -0
  84. package/src/cli/update-cli.ts +179 -13
  85. package/src/cli/web-search-cli.ts +5 -7
  86. package/src/cli.ts +20 -1
  87. package/src/commands/launch.ts +47 -2
  88. package/src/commands/local-provider.ts +62 -0
  89. package/src/commands/notify.ts +1 -1
  90. package/src/commands/ralplan.ts +1 -0
  91. package/src/commands/telegram.ts +47 -0
  92. package/src/config/keybindings.ts +15 -6
  93. package/src/config/mcp-schema.json +4 -0
  94. package/src/config/model-registry.ts +55 -1
  95. package/src/config/models-config-schema.ts +9 -0
  96. package/src/config/settings-schema.ts +256 -4
  97. package/src/config/settings.ts +11 -0
  98. package/src/config/telegram-autostart.ts +107 -0
  99. package/src/config/telegram-env-bridge.ts +92 -0
  100. package/src/coordinator-mcp/server.ts +276 -34
  101. package/src/defaults/skc/skills/ralplan/SKILL.md +29 -20
  102. package/src/defaults/skc/skills/ultragoal/SKILL.md +37 -112
  103. package/src/defaults/skc-defaults.ts +16 -0
  104. package/src/discovery/builtin.ts +12 -0
  105. package/src/discovery/cursor.ts +5 -84
  106. package/src/discovery/gemini.ts +3 -89
  107. package/src/discovery/index.ts +4 -1
  108. package/src/discovery/mcp-json.ts +11 -0
  109. package/src/discovery/opencode.ts +5 -122
  110. package/src/discovery/windsurf.ts +4 -82
  111. package/src/edit/read-file.ts +9 -1
  112. package/src/extensibility/extensions/runner.ts +2 -1
  113. package/src/extensibility/extensions/types.ts +11 -0
  114. package/src/extensibility/skills.ts +30 -2
  115. package/src/hooks/skill-state.ts +33 -73
  116. package/src/i18n/messages/de.ts +1 -0
  117. package/src/i18n/messages/en.ts +1 -0
  118. package/src/i18n/messages/es.ts +1 -0
  119. package/src/i18n/messages/fr.ts +1 -0
  120. package/src/i18n/messages/ja.settings.ts +28 -0
  121. package/src/i18n/messages/ja.ts +1 -0
  122. package/src/i18n/messages/ko.settings.ts +28 -0
  123. package/src/i18n/messages/ko.ts +1 -0
  124. package/src/i18n/messages/zh.settings.ts +28 -0
  125. package/src/i18n/messages/zh.ts +1 -0
  126. package/src/internal-urls/docs-index.generated.ts +12 -7
  127. package/src/main.ts +5 -2
  128. package/src/migrate/executor.ts +23 -6
  129. package/src/modes/acp/acp-agent.ts +7 -6
  130. package/src/modes/components/agent-dashboard.ts +3 -35
  131. package/src/modes/components/custom-editor.ts +26 -0
  132. package/src/modes/components/custom-model-preset-wizard.ts +30 -189
  133. package/src/modes/components/extensions/extension-dashboard.ts +2 -48
  134. package/src/modes/components/hook-selector.ts +21 -3
  135. package/src/modes/components/model-selector.ts +246 -11
  136. package/src/modes/components/session-observer-overlay.ts +3 -3
  137. package/src/modes/components/session-selector.ts +19 -9
  138. package/src/modes/components/settings-selector.ts +551 -11
  139. package/src/modes/components/skill-message.ts +18 -10
  140. package/src/modes/components/status-line/presets.ts +11 -0
  141. package/src/modes/components/status-line/segments.ts +16 -19
  142. package/src/modes/components/status-line/types.ts +6 -2
  143. package/src/modes/components/status-line.ts +281 -117
  144. package/src/modes/components/tree-selector.ts +28 -8
  145. package/src/modes/components/two-column-body.ts +36 -0
  146. package/src/modes/components/welcome.ts +165 -30
  147. package/src/modes/controllers/command-controller.ts +26 -2
  148. package/src/modes/controllers/event-controller.ts +102 -4
  149. package/src/modes/controllers/extension-ui-controller.ts +21 -2
  150. package/src/modes/controllers/input-controller.ts +214 -47
  151. package/src/modes/controllers/selector-controller.ts +35 -33
  152. package/src/modes/interactive-mode.ts +36 -30
  153. package/src/modes/prompt-action-autocomplete.ts +48 -8
  154. package/src/modes/rpc/rpc-types.ts +1 -0
  155. package/src/modes/shared/agent-wire/deep-interview-gate.ts +72 -15
  156. package/src/modes/shared/agent-wire/workflow-gate-schema.ts +20 -0
  157. package/src/modes/slash-command-visibility.ts +9 -0
  158. package/src/modes/theme/theme.ts +5 -1
  159. package/src/modes/tmux-scroll.ts +92 -0
  160. package/src/modes/types.ts +8 -1
  161. package/src/modes/utils/hotkeys-markdown.ts +5 -2
  162. package/src/modes/utils/terminal-bell.ts +48 -0
  163. package/src/modes/utils/ui-helpers.ts +76 -3
  164. package/src/notifications/daemon-paths.ts +22 -0
  165. package/src/notifications/html-format.ts +75 -1
  166. package/src/notifications/index.ts +32 -2
  167. package/src/notifications/lifecycle-control-runtime.ts +12 -3
  168. package/src/notifications/recent-activity.ts +32 -8
  169. package/src/notifications/telegram-daemon-cli.ts +125 -13
  170. package/src/notifications/telegram-daemon-control.ts +9 -0
  171. package/src/notifications/telegram-daemon.ts +88 -46
  172. package/src/notifications/telegram-reference.ts +36 -14
  173. package/src/notifications/threaded-render.ts +14 -6
  174. package/src/prompts/agents/architect.md +7 -4
  175. package/src/prompts/agents/critic.md +15 -6
  176. package/src/prompts/agents/planner.md +8 -3
  177. package/src/prompts/system/system-prompt.md +20 -2
  178. package/src/prompts/tools/bash.md +1 -1
  179. package/src/runtime-mcp/config-writer.ts +28 -0
  180. package/src/runtime-mcp/config.ts +7 -0
  181. package/src/runtime-mcp/loader.ts +3 -0
  182. package/src/runtime-mcp/manager.ts +3 -0
  183. package/src/runtime-mcp/types.ts +6 -0
  184. package/src/sdk.ts +10 -11
  185. package/src/session/agent-session.ts +103 -13
  186. package/src/session/session-manager.ts +58 -18
  187. package/src/skc-runtime/launch-tmux.ts +34 -8
  188. package/src/skc-runtime/launch-worktree.ts +16 -2
  189. package/src/skc-runtime/ralplan-runtime.ts +22 -6
  190. package/src/skc-runtime/restricted-role-agent-bash.ts +1 -0
  191. package/src/skc-runtime/session-state-sidecar.ts +167 -24
  192. package/src/skc-runtime/state-runtime.ts +8 -5
  193. package/src/skc-runtime/state-writer.ts +5 -5
  194. package/src/skc-runtime/team-runtime.ts +21 -6
  195. package/src/skc-runtime/tmux-common.ts +16 -0
  196. package/src/skc-runtime/tmux-sessions.ts +13 -9
  197. package/src/skc-runtime/ultragoal-guard.ts +150 -24
  198. package/src/skc-runtime/ultragoal-runtime.ts +16 -124
  199. package/src/skc-runtime/workflow-manifest.generated.json +0 -8
  200. package/src/skc-runtime/workflow-manifest.ts +0 -1
  201. package/src/slash-commands/builtin-registry.ts +136 -23
  202. package/src/slash-commands/types.ts +2 -0
  203. package/src/system-prompt.ts +8 -0
  204. package/src/task/executor.ts +5 -0
  205. package/src/tools/ast-grep.ts +10 -7
  206. package/src/tools/bash.ts +39 -14
  207. package/src/tools/index.ts +8 -1
  208. package/src/tools/skill.ts +1 -1
  209. package/src/utils/cmux-workspace.ts +233 -0
  210. package/src/utils/title-generator.ts +2 -0
  211. package/src/web/kagi.ts +1 -1
  212. package/src/web/parallel.ts +2 -2
  213. package/src/web/search/index.ts +137 -52
  214. package/src/web/search/provider.ts +80 -0
  215. package/src/web/search/providers/anthropic.ts +1 -1
  216. package/src/web/search/providers/brave.ts +1 -1
  217. package/src/web/search/providers/codex.ts +1 -1
  218. package/src/web/search/providers/duckduckgo.ts +1 -1
  219. package/src/web/search/providers/exa.ts +1 -1
  220. package/src/web/search/providers/gemini.ts +7 -3
  221. package/src/web/search/providers/jina.ts +1 -1
  222. package/src/web/search/providers/kimi.ts +6 -1
  223. package/src/web/search/providers/openai-compatible.ts +1 -1
  224. package/src/web/search/providers/parallel.ts +1 -1
  225. package/src/web/search/providers/perplexity.ts +2 -2
  226. package/src/web/search/providers/searxng.ts +1 -1
  227. package/src/web/search/providers/synthetic.ts +1 -1
  228. package/src/web/search/providers/tavily.ts +1 -1
  229. package/src/web/search/providers/utils.ts +67 -17
  230. package/src/web/search/providers/xai.ts +1 -1
  231. package/src/web/search/providers/zai.ts +1 -1
  232. package/src/web/search/render.ts +3 -0
  233. package/src/workflow/workflow-intent-diff.ts +199 -0
  234. package/vendor/insane-search/MANIFEST.json +1 -3
  235. package/vendor/insane-search/engine/tests/test_u1.py +0 -28
  236. package/vendor/insane-search/engine/transport.py +2 -3
  237. package/vendor/insane-search/engine/validators.py +0 -14
  238. package/src/defaults/skc/rules/ponytail.md +0 -68
@@ -0,0 +1,156 @@
1
+ /**
2
+ * `skc telegram` command handlers — start/status/env for the Telegram Remote
3
+ * integration, driven entirely by the `telegram.*` settings group.
4
+ */
5
+
6
+ import { logger } from "@sayknow-cli/utils";
7
+ import { resolveSkcRuntimeSpawnInfo } from "../daemon/runtime";
8
+ import { isSettingsInitialized, Settings, settings } from "../config/settings";
9
+ import type { TelegramSettings } from "../config/settings-schema";
10
+ import { telegramSettingsToEnv, validateTelegramSettings } from "../config/telegram-env-bridge";
11
+
12
+ export type TelegramAction = "start" | "status" | "env" | "__gateway";
13
+
14
+ export interface TelegramCommandArgs {
15
+ action: TelegramAction;
16
+ flags: {
17
+ json?: boolean;
18
+ };
19
+ }
20
+
21
+ export const TELEGRAM_ACTIONS: readonly TelegramAction[] = ["start", "status", "env", "__gateway"];
22
+
23
+ /** Read the full telegram settings group from the settings singleton. */
24
+ function readTelegramSettings(): TelegramSettings {
25
+ return {
26
+ enabled: settings.get("telegram.enabled"),
27
+ botToken: settings.get("telegram.botToken"),
28
+ allowedUserIds: settings.get("telegram.allowedUserIds"),
29
+ allowedChatIds: settings.get("telegram.allowedChatIds"),
30
+ backend: settings.get("telegram.backend"),
31
+ enableStop: settings.get("telegram.enableStop"),
32
+ enableRich: settings.get("telegram.enableRich"),
33
+ enablePush: settings.get("telegram.enablePush"),
34
+ registerCommands: settings.get("telegram.registerCommands"),
35
+ presets: settings.get("telegram.presets"),
36
+ rpcSocket: settings.get("telegram.rpcSocket"),
37
+ stateDir: settings.get("telegram.stateDir"),
38
+ pollTimeoutSec: settings.get("telegram.pollTimeoutSec"),
39
+ apiBase: settings.get("telegram.apiBase"),
40
+ coordinatorCommand: settings.get("telegram.coordinatorCommand") ?? "skc",
41
+ coordinatorArgs: settings.get("telegram.coordinatorArgs") ?? "mcp-serve,coordinator",
42
+ enableEditMessageText: settings.get("telegram.enableEditMessageText"),
43
+ richCallbackTtlMs: settings.get("telegram.richCallbackTtlMs"),
44
+ richCallbackMaxTokens: settings.get("telegram.richCallbackMaxTokens"),
45
+ defaultTaskMaxLen: settings.get("telegram.defaultTaskMaxLen"),
46
+ livenessMs: settings.get("telegram.livenessMs"),
47
+ followTtlMs: settings.get("telegram.followTtlMs"),
48
+ subscriptionsMax: settings.get("telegram.subscriptionsMax"),
49
+ longPollMs: settings.get("telegram.longPollMs"),
50
+ digestThreshold: settings.get("telegram.digestThreshold"),
51
+ allowAttachSocketArg: settings.get("telegram.allowAttachSocketArg"),
52
+ };
53
+ }
54
+
55
+ /** Resolve the gateway spawn command by re-invoking skc's own hidden
56
+ * `telegram __gateway` subcommand: a compiled binary self-spawns the bundled
57
+ * gateway entrypoint, and a source run re-executes cli.ts under bun. */
58
+ function resolveGatewayCommand(): { cmd: string; args: string[] } {
59
+ const rt = resolveSkcRuntimeSpawnInfo();
60
+ return { cmd: rt.execPath, args: [...rt.argsPrefix, "telegram", "__gateway"] };
61
+ }
62
+
63
+ async function runStart(): Promise<void> {
64
+ const config = readTelegramSettings();
65
+
66
+ if (!config.enabled) {
67
+ process.stderr.write(
68
+ "Telegram Remote is disabled. Enable it in Settings → Integrations, or set telegram.enabled.\n",
69
+ );
70
+ process.exit(1);
71
+ }
72
+
73
+ const errors = validateTelegramSettings(config);
74
+ if (errors.length > 0) {
75
+ process.stderr.write("Telegram configuration is incomplete:\n");
76
+ for (const err of errors) process.stderr.write(` • ${err}\n`);
77
+ process.exit(1);
78
+ }
79
+
80
+ const env = telegramSettingsToEnv(config);
81
+ const { cmd, args } = resolveGatewayCommand();
82
+
83
+ logger.info("telegram-remote starting", { cmd });
84
+
85
+ const proc = Bun.spawn([cmd, ...args], {
86
+ env: { ...process.env, ...env },
87
+ stdio: ["inherit", "inherit", "inherit"],
88
+ });
89
+
90
+ // Forward termination signals to the child.
91
+ const forward = (sig: NodeJS.Signals): void => void proc.kill(sig);
92
+ process.on("SIGINT", forward);
93
+ process.on("SIGTERM", forward);
94
+
95
+ const code = await proc.exited;
96
+ process.off("SIGINT", forward);
97
+ process.off("SIGTERM", forward);
98
+ process.exit(code);
99
+ }
100
+
101
+ async function runStatus(flags: TelegramCommandArgs["flags"]): Promise<void> {
102
+ const config = readTelegramSettings();
103
+ const errors = config.enabled ? validateTelegramSettings(config) : [];
104
+
105
+ if (flags.json) {
106
+ process.stdout.write(`${JSON.stringify({ enabled: config.enabled, valid: errors.length === 0, errors })}\n`);
107
+ return;
108
+ }
109
+
110
+ process.stdout.write(`Telegram Remote: ${config.enabled ? "enabled" : "disabled"}\n`);
111
+ process.stdout.write(`Backend: ${config.backend}\n`);
112
+
113
+ if (config.botToken) process.stdout.write(`Bot Token: ✓ set\n`);
114
+ else process.stdout.write(`Bot Token: ✗ missing\n`);
115
+
116
+ if (config.allowedUserIds || config.allowedChatIds) {
117
+ process.stdout.write(`Allowlist: ✓ configured\n`);
118
+ } else {
119
+ process.stdout.write(`Allowlist: ✗ missing\n`);
120
+ }
121
+
122
+ if (errors.length > 0) {
123
+ process.stdout.write("\nConfiguration issues:\n");
124
+ for (const err of errors) process.stdout.write(` • ${err}\n`);
125
+ }
126
+ }
127
+
128
+ async function runEnv(): Promise<void> {
129
+ const config = readTelegramSettings();
130
+ const env = telegramSettingsToEnv(config);
131
+ for (const [key, value] of Object.entries(env)) {
132
+ process.stdout.write(`${key}=${value}\n`);
133
+ }
134
+ }
135
+
136
+ export async function runTelegramCommand(cmd: TelegramCommandArgs): Promise<void> {
137
+ if (!isSettingsInitialized()) await Settings.init();
138
+ switch (cmd.action) {
139
+ case "start":
140
+ await runStart();
141
+ return;
142
+ case "status":
143
+ await runStatus(cmd.flags);
144
+ return;
145
+ case "env":
146
+ await runEnv();
147
+ return;
148
+ case "__gateway": {
149
+ // Hidden entrypoint: run the Telegram Remote gateway in-process. Reached
150
+ // only via the self-spawn from runStart()/autostart, never by users.
151
+ const { loadConfigFromEnv, runService } = await import("../../../telegram-remote/src/index");
152
+ await runService(loadConfigFromEnv(process.env));
153
+ return;
154
+ }
155
+ }
156
+ }
@@ -10,10 +10,13 @@ import { pipeline } from "node:stream/promises";
10
10
  import { $which, APP_NAME, isEnoent, VERSION } from "@sayknow-cli/utils";
11
11
  import { $ } from "bun";
12
12
  import chalk from "chalk";
13
+ import { installDefaultSkcDefinitions } from "../defaults/skc-defaults";
13
14
  import { theme } from "../modes/theme/theme";
14
15
 
15
16
  const RELEASE_REPO = "jaybeyond/Sayknow_CLI";
16
17
  const PACKAGE = "@sayknow-cli/coding-agent";
18
+ const NPM_WRAPPER_PACKAGE = "sayknow-cli";
19
+ const NPM_MANAGED_PACKAGES = [NPM_WRAPPER_PACKAGE, PACKAGE] as const;
17
20
 
18
21
  interface ReleaseInfo {
19
22
  tag: string;
@@ -27,6 +30,23 @@ export interface InstalledVersionVerification {
27
30
  path?: string;
28
31
  smokeTestFailed?: boolean;
29
32
  smokeTestOutput?: string;
33
+ cleanupWarning?: string;
34
+ }
35
+
36
+ export interface PackageManagerUpdateResult {
37
+ exitCode: number | null;
38
+ text: () => string;
39
+ }
40
+
41
+ export type PackageManagerUpdateRunner = (expectedVersion: string) => Promise<PackageManagerUpdateResult>;
42
+
43
+ export interface PackageManagerUpdateOptions {
44
+ managerName: string;
45
+ expectedVersion: string;
46
+ runInstall: PackageManagerUpdateRunner;
47
+ verifyInstalledRuntime: (expectedVersion: string) => Promise<InstalledVersionVerification>;
48
+ printVerificationResult?: (expectedVersion: string) => Promise<void>;
49
+ printRecoveredVerification?: (expectedVersion: string) => void;
30
50
  }
31
51
 
32
52
  /** Paths and verifier used while replacing a downloaded binary update. */
@@ -102,23 +122,76 @@ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
102
122
  return isPathInDirectoryLexical(resolvedFile, dirReal);
103
123
  }
104
124
 
105
- type UpdateTarget = { method: "bun" } | { method: "binary"; path: string };
125
+ type PackageManagerTarget = { manager: "npm"; packageName: string };
126
+ type UpdateTarget = { method: "bun" } | { method: "npm"; packageName: string } | { method: "binary"; path: string };
127
+
128
+ type PathPlatform = NodeJS.Platform;
129
+ type PackageExists = (packageName: string, packageRoot: string) => boolean;
130
+
131
+ function pathApiForPlatform(platform: PathPlatform): typeof path.posix | typeof path.win32 {
132
+ return platform === "win32" ? path.win32 : path.posix;
133
+ }
134
+
135
+ function defaultPackageExists(_packageName: string, packageRoot: string): boolean {
136
+ return fs.existsSync(path.join(packageRoot, "package.json"));
137
+ }
138
+
139
+ function npmPackageRootForBinPath(binPath: string, packageName: string, platform: PathPlatform): string {
140
+ const pathApi = pathApiForPlatform(platform);
141
+ const segments = packageName.split("/");
142
+ return pathApi.join(pathApi.dirname(binPath), "node_modules", ...segments);
143
+ }
144
+
145
+ function resolveNpmManagedTarget(
146
+ ompPath: string,
147
+ platform: PathPlatform = process.platform,
148
+ packageExists: PackageExists = defaultPackageExists,
149
+ ): PackageManagerTarget | undefined {
150
+ if (platform !== "win32") return undefined;
151
+ const pathApi = pathApiForPlatform(platform);
152
+ const extension = pathApi.extname(ompPath).toLowerCase();
153
+ if (extension !== ".cmd" && extension !== ".ps1") return undefined;
154
+ const basename = pathApi.basename(ompPath, extension).toLowerCase();
155
+ if (basename !== APP_NAME.toLowerCase()) return undefined;
156
+
157
+ for (const packageName of NPM_MANAGED_PACKAGES) {
158
+ const packageRoot = npmPackageRootForBinPath(ompPath, packageName, platform);
159
+ if (packageExists(packageName, packageRoot)) return { manager: "npm", packageName };
160
+ }
161
+ return undefined;
162
+ }
106
163
 
107
- function resolveUpdateMethod(ompPath: string, bunBinDir: string | undefined): "bun" | "binary" {
164
+ function resolveUpdateMethod(ompPath: string, bunBinDir: string | undefined): "bun" | "npm" | "binary" {
165
+ if (resolveNpmManagedTarget(ompPath)) return "npm";
108
166
  if (!bunBinDir) return "binary";
109
167
  return isPathInDirectory(ompPath, bunBinDir) ? "bun" : "binary";
110
168
  }
111
169
 
112
- export function resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): "bun" | "binary" {
170
+ export function resolveUpdateMethodForTest(ompPath: string, bunBinDir: string | undefined): "bun" | "npm" | "binary" {
113
171
  return resolveUpdateMethod(ompPath, bunBinDir);
114
172
  }
173
+
174
+ export function resolveNpmManagedTargetForTest(
175
+ ompPath: string,
176
+ platform: PathPlatform,
177
+ packageExists: PackageExists,
178
+ ): PackageManagerTarget | undefined {
179
+ return resolveNpmManagedTarget(ompPath, platform, packageExists);
180
+ }
115
181
  async function resolveUpdateTarget(): Promise<UpdateTarget> {
116
182
  const bunBinDir = await getBunGlobalBinDir();
117
183
  const ompPath = resolveSkcPath();
118
184
 
119
185
  if (ompPath) {
186
+ const npmTarget = resolveNpmManagedTarget(ompPath);
187
+ if (npmTarget) return { method: "npm", packageName: npmTarget.packageName };
120
188
  const method = resolveUpdateMethod(ompPath, bunBinDir);
121
189
  if (method === "bun") return { method };
190
+ if (method === "npm") {
191
+ throw new Error(
192
+ formatUnsupportedTargetMessage(`Could not resolve npm package root for ${APP_NAME} shim ${ompPath}`),
193
+ );
194
+ }
122
195
  return { method, path: ompPath };
123
196
  }
124
197
 
@@ -262,6 +335,11 @@ function printVerifiedVersion(expectedVersion: string): void {
262
335
  console.log(chalk.green(`\n${theme.status.success} Updated to ${expectedVersion}`));
263
336
  }
264
337
 
338
+ function printSuccessfulVerification(expectedVersion: string): void {
339
+ printVerifiedVersion(expectedVersion);
340
+ printRestartGuidance();
341
+ }
342
+
265
343
  function formatBinaryInstallInstruction(platform: NodeJS.Platform = process.platform): string {
266
344
  if (platform === "win32") {
267
345
  return `For a supported binary install, reinstall with PowerShell: irm https://raw.githubusercontent.com/${RELEASE_REPO}/main/scripts/install.ps1 | iex`;
@@ -351,8 +429,7 @@ export function formatVerificationFailureForTest(
351
429
  async function printVerification(expectedVersion: string): Promise<void> {
352
430
  const result = await verifyInstalledRuntime(expectedVersion);
353
431
  if (result.ok) {
354
- printVerifiedVersion(expectedVersion);
355
- printRestartGuidance();
432
+ printSuccessfulVerification(expectedVersion);
356
433
  return;
357
434
  }
358
435
  console.log(chalk.yellow(`\nWarning: ${formatVerificationFailure(result, expectedVersion)}`));
@@ -367,6 +444,19 @@ async function unlinkIfExists(filePath: string): Promise<void> {
367
444
  }
368
445
  }
369
446
 
447
+ function formatBackupCleanupWarning(backupPath: string, err: unknown): string {
448
+ return `Installed update, but could not remove backup file ${backupPath}: ${err}. You can delete it manually after closing shells or antivirus processes that may still hold it.`;
449
+ }
450
+
451
+ async function cleanupVerifiedBackup(backupPath: string): Promise<string | undefined> {
452
+ try {
453
+ await unlinkIfExists(backupPath);
454
+ return undefined;
455
+ } catch (err) {
456
+ return formatBackupCleanupWarning(backupPath, err);
457
+ }
458
+ }
459
+
370
460
  /**
371
461
  * Atomically replace the installed binary and roll back if version verification fails.
372
462
  */
@@ -386,8 +476,8 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
386
476
  }
387
477
 
388
478
  backupReady = false;
389
- await unlinkIfExists(options.backupPath);
390
- return verification;
479
+ const cleanupWarning = await cleanupVerifiedBackup(options.backupPath);
480
+ return cleanupWarning ? { ...verification, cleanupWarning } : verification;
391
481
  } catch (err) {
392
482
  if (backupReady) {
393
483
  await unlinkIfExists(options.targetPath);
@@ -398,17 +488,67 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
398
488
  }
399
489
  }
400
490
 
491
+ function formatPackageManagerInstallFailure(
492
+ managerName: string,
493
+ result: PackageManagerUpdateResult,
494
+ verification: InstalledVersionVerification,
495
+ expectedVersion: string,
496
+ ): string {
497
+ const output = normalizeVerificationOutput(result.text());
498
+ const outputSuffix = output ? `: ${output}` : "";
499
+ return `${managerName} install failed with exit code ${result.exitCode ?? "unknown"}${outputSuffix}. ${formatVerificationFailure(verification, expectedVersion)}`;
500
+ }
501
+
502
+ export async function runPackageManagerUpdateForTest(
503
+ options: PackageManagerUpdateOptions,
504
+ ): Promise<InstalledVersionVerification> {
505
+ return updateViaPackageManager(options);
506
+ }
507
+
508
+ async function updateViaPackageManager(options: PackageManagerUpdateOptions): Promise<InstalledVersionVerification> {
509
+ const result = await options.runInstall(options.expectedVersion);
510
+ if (result.exitCode === 0) {
511
+ await (options.printVerificationResult ?? printVerification)(options.expectedVersion);
512
+ return await options.verifyInstalledRuntime(options.expectedVersion);
513
+ }
514
+
515
+ const verification = await options.verifyInstalledRuntime(options.expectedVersion);
516
+ if (verification.ok) {
517
+ console.warn(
518
+ chalk.yellow(
519
+ `${options.managerName} exited with ${result.exitCode ?? "unknown"}, but ${APP_NAME} now verifies as ${options.expectedVersion}. Treating the update as installed.`,
520
+ ),
521
+ );
522
+ (options.printRecoveredVerification ?? printSuccessfulVerification)(options.expectedVersion);
523
+ return verification;
524
+ }
525
+
526
+ throw new Error(
527
+ formatPackageManagerInstallFailure(options.managerName, result, verification, options.expectedVersion),
528
+ );
529
+ }
530
+
401
531
  /**
402
532
  * Update via bun package manager.
403
533
  */
404
534
  async function updateViaBun(expectedVersion: string): Promise<void> {
405
535
  console.log(chalk.dim("Updating via bun..."));
406
- const result = await $`bun install -g ${PACKAGE}@${expectedVersion}`.nothrow();
407
- if (result.exitCode !== 0) {
408
- throw new Error(`bun install failed with exit code ${result.exitCode}`);
409
- }
536
+ await updateViaPackageManager({
537
+ managerName: "bun",
538
+ expectedVersion,
539
+ runInstall: async version => await $`bun install -g ${PACKAGE}@${version}`.nothrow(),
540
+ verifyInstalledRuntime,
541
+ });
542
+ }
410
543
 
411
- await printVerification(expectedVersion);
544
+ async function updateViaNpm(packageName: string, expectedVersion: string): Promise<void> {
545
+ console.log(chalk.dim(`Updating npm-managed install via npm (${packageName})...`));
546
+ await updateViaPackageManager({
547
+ managerName: "npm",
548
+ expectedVersion,
549
+ runInstall: async version => await $`npm install -g ${packageName}@${version}`.nothrow(),
550
+ verifyInstalledRuntime,
551
+ });
412
552
  }
413
553
 
414
554
  /**
@@ -430,7 +570,7 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
430
570
  await pipeline(response.body, fileStream);
431
571
 
432
572
  console.log(chalk.dim("Installing update..."));
433
- await replaceBinaryForUpdate({
573
+ const verification = await replaceBinaryForUpdate({
434
574
  targetPath,
435
575
  tempPath,
436
576
  backupPath,
@@ -438,6 +578,7 @@ async function updateViaBinaryAt(targetPath: string, expectedVersion: string): P
438
578
  verifyInstalledVersion: verifyInstalledRuntime,
439
579
  });
440
580
  printVerifiedVersion(expectedVersion);
581
+ if (verification.cleanupWarning) console.warn(chalk.yellow(verification.cleanupWarning));
441
582
  printRestartGuidance();
442
583
  }
443
584
 
@@ -479,6 +620,8 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
479
620
  const target = await resolveUpdateTarget();
480
621
  if (target.method === "bun") {
481
622
  await updateViaBun(release.version);
623
+ } else if (target.method === "npm") {
624
+ await updateViaNpm(target.packageName, release.version);
482
625
  } else {
483
626
  await updateViaBinaryAt(target.path, release.version);
484
627
  }
@@ -486,6 +629,29 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
486
629
  console.error(chalk.red(`Update failed: ${err}`));
487
630
  process.exit(1);
488
631
  }
632
+
633
+ await refreshInstalledDefaultSkills();
634
+ }
635
+
636
+ /**
637
+ * Refresh opted-in on-disk default workflow skill copies after a successful
638
+ * update. The four default skills ship embedded in the binary, so most users
639
+ * need nothing here. But users who ran `skc setup defaults` have on-disk copies
640
+ * under the agent dir that shadow the embedded defaults; those would otherwise
641
+ * go stale after an update. Only rewrite files that already exist and differ —
642
+ * never materialize new copies for users who never opted in.
643
+ */
644
+ async function refreshInstalledDefaultSkills(): Promise<void> {
645
+ try {
646
+ const result = await installDefaultSkcDefinitions({ refreshOnly: true });
647
+ if (result.written > 0) {
648
+ console.log(
649
+ chalk.dim(`Refreshed ${result.written} local default workflow skill file(s) at ${result.targetRoot}`),
650
+ );
651
+ }
652
+ } catch (err) {
653
+ console.error(chalk.yellow(`Warning: failed to refresh local default workflow skills: ${err}`));
654
+ }
489
655
  }
490
656
 
491
657
  /**
@@ -9,13 +9,14 @@ import chalk from "chalk";
9
9
  import { Settings } from "../config/settings";
10
10
  import { initTheme, theme } from "../modes/theme/theme";
11
11
  import {
12
+ getConfiguredSearchProviderPreference,
12
13
  isConfigurableSearchProviderId,
13
14
  isSearchProviderPreference,
14
15
  runSearchQuery,
15
16
  type SearchQueryParams,
16
17
  } from "../web/search/index";
17
18
  import { SEARCH_PROVIDER_ORDER, setPreferredSearchProvider, setSearchFallbackProviders } from "../web/search/provider";
18
- import { setSearchHardTimeoutMs } from "../web/search/providers/utils";
19
+ import { applyConfiguredSearchTimeout } from "../web/search/providers/utils";
19
20
  import { renderSearchResult } from "../web/search/render";
20
21
  import type { SearchProviderId } from "../web/search/types";
21
22
 
@@ -170,8 +171,8 @@ export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
170
171
 
171
172
  await initTheme();
172
173
  const settings = await Settings.init();
173
- const configuredProvider = settings.get("providers.webSearch");
174
- if (typeof configuredProvider === "string" && isSearchProviderPreference(configuredProvider)) {
174
+ const configuredProvider = getConfiguredSearchProviderPreference(settings);
175
+ if (isSearchProviderPreference(configuredProvider)) {
175
176
  setPreferredSearchProvider(configuredProvider);
176
177
  }
177
178
  const configuredFallback = settings.get("web_search.fallback");
@@ -180,10 +181,7 @@ export async function runSearchCommand(cmd: SearchCommandArgs): Promise<void> {
180
181
  configuredFallback.filter(value => typeof value === "string" && isConfigurableSearchProviderId(value)),
181
182
  );
182
183
  }
183
- const configuredTimeout = settings.get("web_search.timeout");
184
- if (typeof configuredTimeout === "number" && Number.isFinite(configuredTimeout) && configuredTimeout > 0) {
185
- setSearchHardTimeoutMs(configuredTimeout * 1000);
186
- }
184
+ applyConfiguredSearchTimeout(settings);
187
185
 
188
186
  const params: SearchQueryParams = {
189
187
  query: cmd.query,
package/src/cli.ts CHANGED
@@ -38,6 +38,7 @@ export const commands: CommandEntry[] = [
38
38
  { name: "notify", load: () => import("./commands/notify").then(m => m.default) },
39
39
  { name: "daemon", load: () => import("./commands/daemon").then(m => m.default) },
40
40
  { name: "web-search", aliases: ["q"], load: () => import("./commands/web-search").then(m => m.default) },
41
+ { name: "local-provider", load: () => import("./commands/local-provider").then(m => m.default) },
41
42
  { name: "mcp-serve", load: () => import("./commands/mcp-serve").then(m => m.default) },
42
43
  { name: "mcp", load: () => import("./commands/mcp").then(m => m.default) },
43
44
  {
@@ -51,6 +52,7 @@ export const commands: CommandEntry[] = [
51
52
  { name: "update", load: () => import("./commands/update").then(m => m.default) },
52
53
  { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
53
54
  { name: "launch", load: () => import("./commands/launch").then(m => m.default) },
55
+ { name: "telegram", load: () => import("./commands/telegram").then(m => m.default) },
54
56
  ];
55
57
 
56
58
  async function showHelp(config: CliConfig): Promise<void> {
@@ -79,6 +81,19 @@ async function installRuntimeGlobals(): Promise<void> {
79
81
  delete process.env.MallocStackLoggingNoCompact;
80
82
  }
81
83
 
84
+ function isNotifyDaemonInternalFastPath(argv: string[]): boolean {
85
+ return argv[0] === "notify" && argv[1] === "daemon-internal";
86
+ }
87
+
88
+ async function runNotifyDaemonInternalFastPath(argv: string[]): Promise<void> {
89
+ const { parseNotifyArgs, runNotifyCommand } = await import("./cli/notify-cli");
90
+ const cmd = parseNotifyArgs(argv);
91
+ if (cmd?.action !== "daemon-internal") {
92
+ throw new Error("invalid notify daemon-internal fast path");
93
+ }
94
+ await runNotifyCommand(cmd);
95
+ }
96
+
82
97
  function hasRootFastFlag(argv: string[], flags: readonly string[]): boolean {
83
98
  for (const arg of argv) {
84
99
  if (isSubcommand(arg)) return false;
@@ -126,7 +141,7 @@ class RootHelpCommand extends Command {
126
141
  resume: Flags.string({ char: "r", description: "Resume a session (by ID prefix, path, or picker if omitted)" }),
127
142
  "session-dir": Flags.string({ description: "Directory for session storage and lookup" }),
128
143
  "no-session": Flags.boolean({ description: "Don't save session (ephemeral)" }),
129
- models: Flags.string({ description: "Comma-separated model patterns for Ctrl+P cycling" }),
144
+ models: Flags.string({ description: "Comma-separated model patterns for Alt+N cycling" }),
130
145
  "no-tools": Flags.boolean({ description: "Disable all built-in tools" }),
131
146
  "no-lsp": Flags.boolean({ description: "Disable LSP tools, formatting, and diagnostics" }),
132
147
  "no-pty": Flags.boolean({ description: "Disable PTY-based interactive bash execution" }),
@@ -208,6 +223,10 @@ async function runSmokeTest(): Promise<void> {
208
223
 
209
224
  /** Run the CLI with the given argv (no `process.argv` prefix). */
210
225
  export async function runCli(argv: string[]): Promise<void> {
226
+ if (isNotifyDaemonInternalFastPath(argv)) {
227
+ await runNotifyDaemonInternalFastPath(argv);
228
+ return;
229
+ }
211
230
  if (argv[0] === "--smoke-test") {
212
231
  await runSmokeTest();
213
232
  return;
@@ -2,6 +2,8 @@
2
2
  * Root command for the coding agent CLI.
3
3
  */
4
4
 
5
+ import * as fs from "node:fs/promises";
6
+ import * as path from "node:path";
5
7
  import { THINKING_EFFORTS } from "@sayknow-cli/ai";
6
8
  import { APP_NAME, setProjectDir } from "@sayknow-cli/utils";
7
9
  import { Args, Command, Flags } from "@sayknow-cli/utils/cli";
@@ -10,6 +12,43 @@ import { runRootCommand } from "../main";
10
12
  import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
11
13
  import { launchDefaultTmuxIfNeeded } from "../skc-runtime/launch-tmux";
12
14
  import { prepareLaunchWorktree } from "../skc-runtime/launch-worktree";
15
+ import {
16
+ SKC_COORDINATOR_SESSION_ID_ENV,
17
+ SKC_COORDINATOR_SESSION_STATE_FILE_ENV,
18
+ } from "../skc-runtime/session-state-sidecar";
19
+
20
+ async function persistCoordinatorLaunchFailure(error: unknown, cwd: string): Promise<void> {
21
+ const stateFile = process.env[SKC_COORDINATOR_SESSION_STATE_FILE_ENV]?.trim();
22
+ if (!stateFile) return;
23
+ const message = error instanceof Error ? error.message : String(error);
24
+ const code = message.split(":", 1)[0] || "launch_failed";
25
+ const now = new Date().toISOString();
26
+ const payload = {
27
+ schema_version: 1,
28
+ session_id: process.env[SKC_COORDINATOR_SESSION_ID_ENV]?.trim() || null,
29
+ state: "errored",
30
+ ready_for_input: false,
31
+ updated_at: now,
32
+ current_turn_id: null,
33
+ last_turn_id: null,
34
+ live: false,
35
+ reason: code,
36
+ source: "agent_session_event",
37
+ event: "launch_error",
38
+ cwd,
39
+ session_file: null,
40
+ final_response: {
41
+ text: message,
42
+ format: "markdown",
43
+ source: "launch_error",
44
+ artifact_path: null,
45
+ truncated: false,
46
+ },
47
+ error: { code, message, recoverable: true },
48
+ };
49
+ await fs.mkdir(path.dirname(stateFile), { recursive: true });
50
+ await Bun.write(stateFile, `${JSON.stringify(payload, null, 2)}\n`);
51
+ }
13
52
 
14
53
  export default class Index extends Command {
15
54
  static description = "Sayknow-CLI — an AI coding assistant";
@@ -80,7 +119,7 @@ export default class Index extends Command {
80
119
  description: "Don't save session (ephemeral)",
81
120
  }),
82
121
  models: Flags.string({
83
- description: "Comma-separated model patterns for Ctrl+P cycling",
122
+ description: "Comma-separated model patterns for Alt+N cycling",
84
123
  }),
85
124
  "no-tools": Flags.boolean({
86
125
  description: "Disable all built-in tools",
@@ -157,7 +196,13 @@ export default class Index extends Command {
157
196
  return;
158
197
  }
159
198
 
160
- const launch = prepareLaunchWorktree(process.cwd(), args);
199
+ let launch: ReturnType<typeof prepareLaunchWorktree>;
200
+ try {
201
+ launch = prepareLaunchWorktree(process.cwd(), args);
202
+ } catch (error) {
203
+ await persistCoordinatorLaunchFailure(error, process.cwd());
204
+ throw error;
205
+ }
161
206
  if (launch.worktree.enabled) {
162
207
  process.chdir(launch.cwd);
163
208
  setProjectDir(launch.cwd);
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Test configured local OpenAI-compatible providers.
3
+ */
4
+ import { Args, Command, Flags } from "@sayknow-cli/utils/cli";
5
+ import {
6
+ runLocalProviderDiscoverCommand,
7
+ runLocalProviderSmokeCommand,
8
+ runLocalProviderStatusCommand,
9
+ } from "../cli/local-provider-smoke";
10
+
11
+ export const LOCAL_PROVIDER_ACTIONS = ["status", "diagnose", "discover", "models", "smoke"] as const;
12
+ export const LOCAL_PROVIDER_DEFAULT_ACTION = "status";
13
+
14
+ export default class LocalProvider extends Command {
15
+ static description = "Diagnose configured local OpenAI-compatible providers";
16
+
17
+ static args = {
18
+ action: Args.string({ description: "Action", required: false, options: LOCAL_PROVIDER_ACTIONS }),
19
+ };
20
+
21
+ static flags = {
22
+ model: Flags.string({ description: "Model id to use for smoke (otherwise uses the first /models id)" }),
23
+ "models-path": Flags.string({ description: "Override models config path" }),
24
+ "timeout-ms": Flags.integer({ description: "Request timeout in milliseconds" }),
25
+ smoke: Flags.boolean({ description: "Run optional streaming chat smoke during status/diagnose" }),
26
+ json: Flags.boolean({ description: "Output JSON" }),
27
+ };
28
+
29
+ async run(): Promise<void> {
30
+ const { args, flags } = await this.parse(LocalProvider);
31
+ const action = args.action ?? LOCAL_PROVIDER_DEFAULT_ACTION;
32
+ if (action === "status" || action === "diagnose") {
33
+ await runLocalProviderStatusCommand({
34
+ model: flags.model,
35
+ modelsPath: flags["models-path"],
36
+ timeoutMs: flags["timeout-ms"],
37
+ smoke: flags.smoke,
38
+ json: flags.json,
39
+ });
40
+ return;
41
+ }
42
+ if (action === "discover" || action === "models") {
43
+ await runLocalProviderDiscoverCommand({
44
+ modelsPath: flags["models-path"],
45
+ timeoutMs: flags["timeout-ms"],
46
+ json: flags.json,
47
+ });
48
+ return;
49
+ }
50
+ if (action === "smoke") {
51
+ await runLocalProviderSmokeCommand({
52
+ model: flags.model,
53
+ modelsPath: flags["models-path"],
54
+ timeoutMs: flags["timeout-ms"],
55
+ json: flags.json,
56
+ });
57
+ return;
58
+ }
59
+ process.stderr.write(`Unsupported local-provider action: ${action}\n`);
60
+ process.exitCode = 1;
61
+ }
62
+ }