@sayknow-cli/coding-agent 0.3.1 → 0.3.2

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 (170) 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/settings-schema.d.ts +1 -59
  11. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  12. package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
  13. package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
  14. package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
  15. package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
  16. package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
  17. package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
  18. package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
  19. package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
  20. package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
  21. package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
  22. package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
  23. package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
  24. package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
  25. package/dist/types/i18n/messages/en.d.ts +0 -1
  26. package/dist/types/main.d.ts +2 -0
  27. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  28. package/dist/types/modes/components/model-selector.d.ts +8 -0
  29. package/dist/types/modes/components/settings-defs.d.ts +0 -1
  30. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  31. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  32. package/dist/types/modes/theme/theme.d.ts +1 -1
  33. package/dist/types/notifications/html-format.d.ts +11 -0
  34. package/dist/types/notifications/index.d.ts +149 -1
  35. package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
  36. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  37. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  38. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  39. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  40. package/dist/types/notifications/recent-activity.d.ts +35 -0
  41. package/dist/types/notifications/telegram-daemon.d.ts +114 -16
  42. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  43. package/dist/types/notifications/topic-registry.d.ts +12 -9
  44. package/dist/types/runtime-mcp/types.d.ts +7 -0
  45. package/dist/types/sdk.d.ts +2 -0
  46. package/dist/types/session/agent-session.d.ts +14 -4
  47. package/dist/types/session/blob-store.d.ts +25 -0
  48. package/dist/types/session/session-manager.d.ts +57 -0
  49. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  50. package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
  51. package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
  52. package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
  53. package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
  54. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  55. package/dist/types/system-prompt.d.ts +2 -0
  56. package/dist/types/task/executor.d.ts +9 -1
  57. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  58. package/dist/types/tools/index.d.ts +3 -1
  59. package/dist/types/utils/changelog.d.ts +1 -0
  60. package/dist/types/web/insane/url-guard.d.ts +6 -3
  61. package/dist/types/web/scrapers/types.d.ts +5 -0
  62. package/dist/types/web/scrapers/utils.d.ts +7 -1
  63. package/package.json +11 -9
  64. package/scripts/g004-tmux-smoke.ts +100 -0
  65. package/scripts/g005-daemon-smoke.ts +180 -0
  66. package/scripts/g011-daemon-path-smoke.ts +153 -0
  67. package/src/cli/fast-help.ts +29 -1
  68. package/src/cli/mcp-cli.ts +272 -0
  69. package/src/cli/plugin-cli.ts +66 -3
  70. package/src/cli.ts +30 -12
  71. package/src/commands/mcp.ts +117 -0
  72. package/src/commands/plugin.ts +4 -0
  73. package/src/commands/session.ts +18 -0
  74. package/src/config/keybindings.ts +2 -2
  75. package/src/config/model-profile-activation.ts +55 -7
  76. package/src/config/settings-schema.ts +8 -93
  77. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  78. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
  79. package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
  80. package/src/defaults/skc/skills/team/SKILL.md +5 -3
  81. package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
  82. package/src/export/html/index.ts +2 -2
  83. package/src/extensibility/extensions/runner.ts +1 -0
  84. package/src/extensibility/skc-plugins/compiler.ts +351 -0
  85. package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
  86. package/src/extensibility/skc-plugins/index.ts +9 -0
  87. package/src/extensibility/skc-plugins/injection.ts +109 -0
  88. package/src/extensibility/skc-plugins/installer.ts +434 -0
  89. package/src/extensibility/skc-plugins/loader.ts +3 -1
  90. package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
  91. package/src/extensibility/skc-plugins/observability.ts +84 -0
  92. package/src/extensibility/skc-plugins/paths.ts +1 -1
  93. package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
  94. package/src/extensibility/skc-plugins/registry.ts +180 -0
  95. package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
  96. package/src/extensibility/skc-plugins/schema.ts +250 -20
  97. package/src/extensibility/skc-plugins/session-validation.ts +147 -0
  98. package/src/extensibility/skc-plugins/types.ts +199 -3
  99. package/src/extensibility/skc-plugins/validation.ts +80 -0
  100. package/src/extensibility/skills.ts +15 -0
  101. package/src/hooks/skill-state.ts +57 -0
  102. package/src/i18n/messages/de.ts +0 -1
  103. package/src/i18n/messages/en.ts +0 -1
  104. package/src/i18n/messages/es.ts +0 -1
  105. package/src/i18n/messages/fr.ts +0 -1
  106. package/src/i18n/messages/ja.ts +0 -1
  107. package/src/i18n/messages/ko.ts +0 -1
  108. package/src/i18n/messages/zh.ts +0 -1
  109. package/src/internal-urls/docs-index.generated.ts +12 -8
  110. package/src/main.ts +14 -3
  111. package/src/modes/bridge/bridge-mode.ts +11 -0
  112. package/src/modes/components/assistant-message.ts +49 -1
  113. package/src/modes/components/custom-editor.ts +2 -0
  114. package/src/modes/components/footer.ts +2 -3
  115. package/src/modes/components/hook-editor.ts +1 -1
  116. package/src/modes/components/hook-selector.ts +67 -43
  117. package/src/modes/components/model-selector.ts +56 -11
  118. package/src/modes/components/settings-defs.ts +1 -2
  119. package/src/modes/components/settings-selector.ts +2 -7
  120. package/src/modes/components/status-line/git-utils.ts +25 -0
  121. package/src/modes/components/status-line.ts +10 -11
  122. package/src/modes/components/welcome.ts +2 -3
  123. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  124. package/src/modes/controllers/selector-controller.ts +53 -11
  125. package/src/modes/interactive-mode.ts +4 -1
  126. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  127. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  128. package/src/modes/theme/defaults/index.ts +2 -0
  129. package/src/modes/theme/theme.ts +0 -4
  130. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  131. package/src/notifications/html-format.ts +38 -0
  132. package/src/notifications/index.ts +242 -12
  133. package/src/notifications/lifecycle-commands.ts +238 -0
  134. package/src/notifications/lifecycle-control-runtime.ts +405 -0
  135. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  136. package/src/notifications/operator-runtime.ts +171 -0
  137. package/src/notifications/rate-limit-pool.ts +19 -0
  138. package/src/notifications/recent-activity.ts +132 -0
  139. package/src/notifications/telegram-daemon.ts +778 -257
  140. package/src/notifications/telegram-reference.ts +25 -7
  141. package/src/notifications/topic-registry.ts +23 -9
  142. package/src/prompts/agents/executor.md +2 -2
  143. package/src/runtime-mcp/transports/stdio.ts +38 -4
  144. package/src/runtime-mcp/types.ts +7 -0
  145. package/src/sdk.ts +157 -10
  146. package/src/session/agent-session.ts +166 -74
  147. package/src/session/blob-store.ts +196 -8
  148. package/src/session/session-manager.ts +762 -12
  149. package/src/skc-runtime/launch-tmux.ts +68 -19
  150. package/src/skc-runtime/psmux-detect.ts +239 -0
  151. package/src/skc-runtime/team-runtime.ts +56 -23
  152. package/src/skc-runtime/tmux-common.ts +88 -4
  153. package/src/skc-runtime/tmux-sessions.ts +111 -9
  154. package/src/skc-runtime/ultragoal-guard.ts +25 -8
  155. package/src/skc-runtime/ultragoal-runtime.ts +75 -15
  156. package/src/slash-commands/builtin-registry.ts +23 -3
  157. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  158. package/src/slash-commands/helpers/parse.ts +2 -1
  159. package/src/system-prompt.ts +9 -0
  160. package/src/task/executor.ts +31 -7
  161. package/src/task/index.ts +2 -0
  162. package/src/tools/ask.ts +5 -1
  163. package/src/tools/bash.ts +9 -0
  164. package/src/tools/composer-bash-policy.ts +96 -0
  165. package/src/tools/fetch.ts +18 -2
  166. package/src/tools/index.ts +3 -1
  167. package/src/utils/changelog.ts +8 -0
  168. package/src/web/insane/url-guard.ts +18 -14
  169. package/src/web/scrapers/types.ts +143 -45
  170. package/src/web/scrapers/utils.ts +70 -19
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Direct MCP server registration CLI helpers.
3
+ *
4
+ * This surface only writes explicit user-provided server definitions to SKC's
5
+ * own MCP config. It never imports or inherits live configs from other agents.
6
+ */
7
+ import { getMCPConfigPath, getProjectDir } from "@sayknow-cli/utils";
8
+ import { getMCPServer, readMCPConfigFile, removeMCPServer, upsertMCPServer } from "../runtime-mcp/config-writer";
9
+ import type { MCPConfigFile, MCPServerConfig } from "../runtime-mcp/types";
10
+
11
+ export type MCPAction = "add" | "list" | "remove";
12
+
13
+ export interface MCPCommandArgs {
14
+ action: MCPAction;
15
+ name?: string;
16
+ commandArgs?: string[];
17
+ flags: {
18
+ project?: boolean;
19
+ force?: boolean;
20
+ json?: boolean;
21
+ type?: "stdio" | "http" | "sse";
22
+ command?: string;
23
+ url?: string;
24
+ arg?: string[];
25
+ env?: string[];
26
+ header?: string[];
27
+ cwd?: string;
28
+ timeout?: number;
29
+ };
30
+ cwd?: string;
31
+ }
32
+
33
+ export class MCPArgsError extends Error {}
34
+
35
+ interface ScopedPath {
36
+ scope: "user" | "project";
37
+ path: string;
38
+ }
39
+
40
+ interface RedactedServerEntry {
41
+ name: string;
42
+ config: MCPServerConfig;
43
+ }
44
+
45
+ const REDACTED = "<redacted>";
46
+ const SENSITIVE_KEY_PATTERN =
47
+ /(?:token|secret|key|credential|password|passwd|pwd|authorization|auth|bearer|cookie|session)/i;
48
+
49
+ function resolvePath(args: MCPCommandArgs): ScopedPath {
50
+ const scope = args.flags.project ? "project" : "user";
51
+ return { scope, path: getMCPConfigPath(scope, args.cwd ?? getProjectDir()) };
52
+ }
53
+
54
+ function parsePairs(values: string[] | undefined, label: string): Record<string, string> | undefined {
55
+ if (!values || values.length === 0) return undefined;
56
+ const parsed: Record<string, string> = {};
57
+ for (const value of values) {
58
+ const index = value.indexOf("=");
59
+ if (index <= 0) {
60
+ throw new MCPArgsError(`Invalid ${label}. Use KEY=VALUE.`);
61
+ }
62
+ const key = value.slice(0, index).trim();
63
+ if (!key) {
64
+ throw new MCPArgsError(`Invalid ${label}. Key cannot be empty.`);
65
+ }
66
+ parsed[key] = value.slice(index + 1);
67
+ }
68
+ return parsed;
69
+ }
70
+
71
+ function buildServerConfig(args: MCPCommandArgs): MCPServerConfig {
72
+ const type = args.flags.type ?? (args.flags.url ? "http" : "stdio");
73
+ const timeout = args.flags.timeout;
74
+ const shared = timeout === undefined ? {} : { timeout };
75
+
76
+ if (type === "stdio") {
77
+ const command = args.flags.command ?? args.commandArgs?.[0];
78
+ if (!command) {
79
+ throw new MCPArgsError("`skc mcp add` requires --command <cmd> or a positional command for stdio servers.");
80
+ }
81
+ const config: MCPServerConfig = {
82
+ ...shared,
83
+ type: "stdio",
84
+ command,
85
+ };
86
+ const positionalArgs = args.flags.command ? [] : (args.commandArgs ?? []).slice(1);
87
+ const serverArgs = [...positionalArgs, ...(args.flags.arg ?? [])];
88
+ if (serverArgs.length > 0) config.args = serverArgs;
89
+ const env = parsePairs(args.flags.env, "env");
90
+ if (env) config.env = env;
91
+ if (args.flags.cwd) config.cwd = args.flags.cwd;
92
+ return config;
93
+ }
94
+
95
+ const url = args.flags.url ?? args.commandArgs?.[0];
96
+ if (!url) {
97
+ throw new MCPArgsError(`\`skc mcp add --type ${type}\` requires --url <url> or a positional URL.`);
98
+ }
99
+ const headers = parsePairs(args.flags.header, "header");
100
+ if (type === "http") {
101
+ const config: MCPServerConfig = {
102
+ ...shared,
103
+ type,
104
+ url,
105
+ };
106
+ if (headers) config.headers = headers;
107
+ return config;
108
+ }
109
+ const config: MCPServerConfig = {
110
+ ...shared,
111
+ type,
112
+ url,
113
+ };
114
+ if (headers) config.headers = headers;
115
+ return config;
116
+ }
117
+
118
+ function redactRecord(
119
+ record: Record<string, string> | undefined,
120
+ redactAllValues: boolean,
121
+ ): Record<string, string> | undefined {
122
+ if (!record) return undefined;
123
+ return Object.fromEntries(
124
+ Object.entries(record).map(([key, value]) => [
125
+ key,
126
+ redactAllValues || SENSITIVE_KEY_PATTERN.test(key) ? REDACTED : value,
127
+ ]),
128
+ );
129
+ }
130
+
131
+ export function redactMCPServerConfig(config: MCPServerConfig): MCPServerConfig {
132
+ const redacted = { ...config } as MCPServerConfig;
133
+ if ("env" in redacted) {
134
+ const env = redactRecord(redacted.env, true);
135
+ if (env) redacted.env = env;
136
+ }
137
+ if ("headers" in redacted) {
138
+ const headers = redactRecord(redacted.headers, true);
139
+ if (headers) redacted.headers = headers;
140
+ }
141
+ if (redacted.auth) {
142
+ redacted.auth = {
143
+ type: redacted.auth.type,
144
+ credentialId: redacted.auth.credentialId ? REDACTED : undefined,
145
+ tokenUrl: redacted.auth.tokenUrl,
146
+ clientId: redacted.auth.clientId ? REDACTED : undefined,
147
+ clientSecret: redacted.auth.clientSecret ? REDACTED : undefined,
148
+ };
149
+ }
150
+ if (redacted.oauth) {
151
+ redacted.oauth = {
152
+ clientId: redacted.oauth.clientId ? REDACTED : undefined,
153
+ clientSecret: redacted.oauth.clientSecret ? REDACTED : undefined,
154
+ redirectUri: redacted.oauth.redirectUri,
155
+ callbackPort: redacted.oauth.callbackPort,
156
+ callbackPath: redacted.oauth.callbackPath,
157
+ };
158
+ }
159
+ return redacted;
160
+ }
161
+
162
+ function collectEntries(config: MCPConfigFile): RedactedServerEntry[] {
163
+ return Object.entries(config.mcpServers ?? {})
164
+ .sort(([a], [b]) => a.localeCompare(b))
165
+ .map(([name, serverConfig]) => ({ name, config: redactMCPServerConfig(serverConfig) }));
166
+ }
167
+
168
+ function writeJson(value: unknown): void {
169
+ process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
170
+ }
171
+
172
+ function renderServerLine(entry: RedactedServerEntry): string {
173
+ const config = entry.config;
174
+ if (config.type === "http" || config.type === "sse") {
175
+ return `${entry.name}\t${config.type}\t${config.url}`;
176
+ }
177
+ const args = config.args && config.args.length > 0 ? ` ${config.args.join(" ")}` : "";
178
+ return `${entry.name}\tstdio\t${config.command}${args}`;
179
+ }
180
+
181
+ function renderDetails(entry: RedactedServerEntry): string {
182
+ return `${renderServerLine(entry)}\n${JSON.stringify(entry.config, null, 2)}`;
183
+ }
184
+
185
+ async function runAdd(args: MCPCommandArgs, scoped: ScopedPath): Promise<void> {
186
+ if (!args.name) throw new MCPArgsError("`skc mcp add` requires a server name.");
187
+ const config = buildServerConfig(args);
188
+ const result = await upsertMCPServer(scoped.path, args.name, config, { force: args.flags.force });
189
+ const redacted = redactMCPServerConfig(config);
190
+ if (args.flags.json) {
191
+ writeJson({
192
+ action: "add",
193
+ status: result.status,
194
+ name: args.name,
195
+ scope: scoped.scope,
196
+ path: scoped.path,
197
+ config: redacted,
198
+ });
199
+ return;
200
+ }
201
+ if (result.status === "skipped") {
202
+ process.stdout.write(
203
+ `MCP server "${args.name}" already exists in ${scoped.scope} config. Pass --force to overwrite.\n`,
204
+ );
205
+ return;
206
+ }
207
+ process.stdout.write(`MCP server "${args.name}" ${result.status} in ${scoped.scope} config: ${scoped.path}\n`);
208
+ }
209
+
210
+ async function runList(args: MCPCommandArgs, scoped: ScopedPath): Promise<void> {
211
+ const config = await readMCPConfigFile(scoped.path);
212
+ const entries = collectEntries(config);
213
+ if (args.flags.json) {
214
+ writeJson({ action: "list", scope: scoped.scope, path: scoped.path, servers: entries });
215
+ return;
216
+ }
217
+ if (entries.length === 0) {
218
+ process.stdout.write(`No MCP servers registered in ${scoped.scope} config: ${scoped.path}\n`);
219
+ return;
220
+ }
221
+ process.stdout.write(`MCP servers in ${scoped.scope} config: ${scoped.path}\n`);
222
+ for (const entry of entries) {
223
+ process.stdout.write(`${renderDetails(entry)}\n`);
224
+ }
225
+ }
226
+
227
+ async function runRemove(args: MCPCommandArgs, scoped: ScopedPath): Promise<void> {
228
+ if (!args.name) throw new MCPArgsError("`skc mcp remove` requires a server name.");
229
+ const existing = await getMCPServer(scoped.path, args.name);
230
+ if (!existing) {
231
+ throw new MCPArgsError(`MCP server "${args.name}" not found in ${scoped.scope} config.`);
232
+ }
233
+ await removeMCPServer(scoped.path, args.name);
234
+ const entry = { name: args.name, config: redactMCPServerConfig(existing) };
235
+ if (args.flags.json) {
236
+ writeJson({
237
+ action: "remove",
238
+ status: "removed",
239
+ name: args.name,
240
+ scope: scoped.scope,
241
+ path: scoped.path,
242
+ removed: entry,
243
+ });
244
+ return;
245
+ }
246
+ process.stdout.write(`Removed MCP server "${args.name}" from ${scoped.scope} config: ${scoped.path}\n`);
247
+ process.stdout.write(`${renderDetails(entry)}\n`);
248
+ }
249
+
250
+ export async function runMCPCommand(args: MCPCommandArgs): Promise<void> {
251
+ const scoped = resolvePath(args);
252
+ try {
253
+ switch (args.action) {
254
+ case "add":
255
+ await runAdd(args, scoped);
256
+ return;
257
+ case "list":
258
+ await runList(args, scoped);
259
+ return;
260
+ case "remove":
261
+ await runRemove(args, scoped);
262
+ return;
263
+ }
264
+ } catch (error) {
265
+ if (error instanceof MCPArgsError) {
266
+ process.stderr.write(`${error.message}\n`);
267
+ process.exitCode = 2;
268
+ return;
269
+ }
270
+ throw error;
271
+ }
272
+ }
@@ -16,6 +16,7 @@ import {
16
16
  getPluginsCacheDir,
17
17
  MarketplaceManager,
18
18
  } from "../extensibility/plugins/marketplace/index.js";
19
+ import { installSkcPluginBundle, isSkcPluginBundleSource, readRegistry } from "../extensibility/skc-plugins";
19
20
  import { type ScanReport, scanPluginDir } from "../extensibility/plugins/security-scanner";
20
21
  import { theme } from "../modes/theme/theme";
21
22
 
@@ -50,6 +51,8 @@ export interface PluginCommandArgs {
50
51
  disable?: string;
51
52
  set?: string;
52
53
  scope?: "user" | "project";
54
+ user?: boolean;
55
+ project?: boolean;
53
56
  };
54
57
  }
55
58
 
@@ -111,6 +114,10 @@ export function parsePluginArgs(args: string[]): PluginCommandArgs | undefined {
111
114
  result.flags.dryRun = true;
112
115
  } else if (arg === "-l" || arg === "--local") {
113
116
  result.flags.local = true;
117
+ } else if (arg === "--user") {
118
+ result.flags.user = true;
119
+ } else if (arg === "--project") {
120
+ result.flags.project = true;
114
121
  } else if (arg === "--enable" && i + 1 < args.length) {
115
122
  result.flags.enable = args[++i];
116
123
  } else if (arg === "--disable" && i + 1 < args.length) {
@@ -366,7 +373,14 @@ function printSecurityAdvisory(report: ScanReport): void {
366
373
  async function handleInstall(
367
374
  manager: PluginManager,
368
375
  packages: string[],
369
- flags: { json?: boolean; force?: boolean; dryRun?: boolean; scope?: "user" | "project" },
376
+ flags: {
377
+ json?: boolean;
378
+ force?: boolean;
379
+ dryRun?: boolean;
380
+ scope?: "user" | "project";
381
+ user?: boolean;
382
+ project?: boolean;
383
+ },
370
384
  ): Promise<void> {
371
385
  if (packages.length === 0) {
372
386
  console.error(chalk.red(`Usage: ${APP_NAME} plugin install <package[@version]>[features] ...`));
@@ -381,6 +395,32 @@ async function handleInstall(
381
395
  const knownMarketplaces = new Set((await mktMgr.listMarketplaces()).map(m => m.name));
382
396
 
383
397
  for (const spec of packages) {
398
+ // SKC plugin bundle classifier: a source containing sayknow-plugin.json (or a
399
+ // git/tarball source) routes to the bundle installer BEFORE marketplace/npm.
400
+ if (await isSkcPluginBundleSource(spec)) {
401
+ if (flags.user === flags.project) {
402
+ console.error(
403
+ chalk.red(`SKC plugin bundle install requires exactly one of --user or --project for "${spec}".`),
404
+ );
405
+ process.exit(1);
406
+ }
407
+ const scope: "user" | "project" = flags.user ? "user" : "project";
408
+ try {
409
+ const res = await installSkcPluginBundle(spec, { scope, cwd: process.cwd(), force: flags.force });
410
+ if (flags.json) {
411
+ console.log(JSON.stringify({ name: res.entry.name, status: res.status, scope }, null, 2));
412
+ } else {
413
+ console.log(
414
+ chalk.green(`${theme.status.success} ${res.status} SKC plugin ${res.entry.name} (${scope})`),
415
+ );
416
+ }
417
+ } catch (err) {
418
+ console.error(chalk.red(`${theme.status.error} Failed to install SKC plugin ${spec}: ${err}`));
419
+ process.exit(1);
420
+ }
421
+ continue;
422
+ }
423
+
384
424
  const target = classifyInstallTarget(spec, knownMarketplaces);
385
425
 
386
426
  if (target.type === "marketplace") {
@@ -519,13 +559,16 @@ async function handleList(manager: PluginManager, flags: { json?: boolean }): Pr
519
559
  const npmPlugins = await manager.list();
520
560
  const mktMgr = await makeMarketplaceManager();
521
561
  const mktPlugins = await mktMgr.listInstalledPlugins();
562
+ const cwd = getProjectDir();
563
+ const [skcUser, skcProject] = await Promise.all([readRegistry("user", cwd), readRegistry("project", cwd)]);
564
+ const skcBundles = [...skcUser.plugins, ...skcProject.plugins];
522
565
 
523
566
  if (flags.json) {
524
- console.log(JSON.stringify({ npm: npmPlugins, marketplace: mktPlugins }, null, 2));
567
+ console.log(JSON.stringify({ npm: npmPlugins, marketplace: mktPlugins, skc: skcBundles }, null, 2));
525
568
  return;
526
569
  }
527
570
 
528
- if (npmPlugins.length === 0 && mktPlugins.length === 0) {
571
+ if (npmPlugins.length === 0 && mktPlugins.length === 0 && skcBundles.length === 0) {
529
572
  console.log(chalk.dim("No plugins installed"));
530
573
  console.log(chalk.dim(`\nInstall plugins with: ${APP_NAME} plugin install <package>`));
531
574
  return;
@@ -567,6 +610,26 @@ async function handleList(manager: PluginManager, flags: { json?: boolean }): Pr
567
610
  console.log(` ${plugin.id} (${version})${scopeLabel}${shadowLabel}`);
568
611
  }
569
612
  }
613
+
614
+ if (skcBundles.length > 0) {
615
+ if (npmPlugins.length > 0 || mktPlugins.length > 0) console.log();
616
+ console.log(chalk.bold("SKC Plugin Bundles:\n"));
617
+ for (const plugin of skcBundles) {
618
+ const status = plugin.enabled ? chalk.green(theme.status.enabled) : chalk.dim(theme.status.disabled);
619
+ const scopeLabel = chalk.dim(` (${plugin.scope})`);
620
+ const disabledCount = plugin.disabledSurfaceIds.length;
621
+ const quarantineCount = plugin.quarantine?.length ?? 0;
622
+ const detail = [
623
+ disabledCount > 0 ? `${disabledCount} disabled` : null,
624
+ quarantineCount > 0 ? `${quarantineCount} quarantined` : null,
625
+ ]
626
+ .filter((v): v is string => Boolean(v))
627
+ .join(", ");
628
+ console.log(
629
+ `${status} ${plugin.name}@${plugin.version}${scopeLabel}${detail ? chalk.dim(` — ${detail}`) : ""}`,
630
+ );
631
+ }
632
+ }
570
633
  }
571
634
 
572
635
  async function handleLink(manager: PluginManager, paths: string[], flags: { json?: boolean }): Promise<void> {
package/src/cli.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
3
  /**
4
- * CLI entry point — registers all commands explicitly and delegates to the
4
+ * CLI entry point — registers all commands explicitly and delegates to the
5
5
  * lightweight CLI runner from pi-utils.
6
6
  */
7
7
  import { Args, type CliConfig, Command, type CommandEntry, Flags, run } from "@sayknow-cli/utils/cli";
@@ -22,7 +22,7 @@ process.title = APP_NAME;
22
22
  const rootHelpFlags = ["--help", "-h", "help"];
23
23
  const versionFlags = ["--version", "-v"];
24
24
 
25
- const commands: CommandEntry[] = [
25
+ export const commands: CommandEntry[] = [
26
26
  { name: "codex-native-hook", load: () => import("./commands/codex-native-hook").then(m => m.default) },
27
27
  { name: "state", load: () => import("./commands/state").then(m => m.default) },
28
28
  { name: "setup", load: () => import("./commands/setup").then(m => m.default) },
@@ -39,6 +39,7 @@ const commands: CommandEntry[] = [
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
41
  { name: "mcp-serve", load: () => import("./commands/mcp-serve").then(m => m.default) },
42
+ { name: "mcp", load: () => import("./commands/mcp").then(m => m.default) },
42
43
  {
43
44
  name: "contribute-pr",
44
45
  aliases: ["contribution-prep"],
@@ -48,6 +49,7 @@ const commands: CommandEntry[] = [
48
49
  { name: "migrate", load: () => import("./commands/migrate").then(m => m.default) },
49
50
  { name: "rlm", load: () => import("./commands/rlm").then(m => m.default) },
50
51
  { name: "update", load: () => import("./commands/update").then(m => m.default) },
52
+ { name: "plugin", load: () => import("./commands/plugin").then(m => m.default) },
51
53
  { name: "launch", load: () => import("./commands/launch").then(m => m.default) },
52
54
  ];
53
55
 
@@ -62,10 +64,7 @@ async function showHelp(config: CliConfig): Promise<void> {
62
64
  }
63
65
 
64
66
  async function installRuntimeGlobals(): Promise<void> {
65
- const [{ installH2Fetch }, { procmgr }] = await Promise.all([
66
- import("@sayknow-cli/ai"),
67
- import("@sayknow-cli/utils"),
68
- ]);
67
+ const { installH2Fetch } = await import("@sayknow-cli/ai/utils/h2-fetch");
69
68
  // Activate HTTP/2 for all `fetch()` calls (provider streams, OAuth, model
70
69
  // discovery, web tools). Bun's HTTP/2 client is gated on a startup flag we
71
70
  // can't toggle from JS, so we patch globalThis.fetch to pass
@@ -75,8 +74,25 @@ async function installRuntimeGlobals(): Promise<void> {
75
74
 
76
75
  // Strip macOS malloc-stack-logging env vars before any subprocess is spawned.
77
76
  // Otherwise every child bun process (subagents, plugin installs, ptree spawns,
78
- // etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
79
- procmgr.scrubProcessEnv();
77
+ // etc.) prints a `MallocStackLogging: can't turn off …` warning to stderr.
78
+ delete process.env.MallocStackLogging;
79
+ delete process.env.MallocStackLoggingNoCompact;
80
+ }
81
+
82
+ function hasRootFastFlag(argv: string[], flags: readonly string[]): boolean {
83
+ for (const arg of argv) {
84
+ if (isSubcommand(arg)) return false;
85
+ if (flags.includes(arg)) return true;
86
+ }
87
+ return false;
88
+ }
89
+
90
+ function hasRootHelpFlag(argv: string[]): boolean {
91
+ return hasRootFastFlag(argv, rootHelpFlags);
92
+ }
93
+
94
+ function hasRootVersionFlag(argv: string[]): boolean {
95
+ return hasRootFastFlag(argv, versionFlags);
80
96
  }
81
97
 
82
98
  class RootHelpCommand extends Command {
@@ -165,7 +181,7 @@ function isSubcommand(first: string | undefined): boolean {
165
181
  *
166
182
  * Purpose: catch the silent worker-load regressions that hit compiled
167
183
  * binaries (issues #1011 and #1027). Neither `--version` nor
168
- * `stats --summary` actually spawns a Worker on a fresh install — the
184
+ * `stats --summary` actually spawns a Worker on a fresh install — the
169
185
  * sync path early-returns when no session files exist. This probe is the
170
186
  * minimal end-to-end test that proves `new Worker(...)` resolves and the
171
187
  * bundled worker module evaluates successfully. Wired into
@@ -196,7 +212,7 @@ export async function runCli(argv: string[]): Promise<void> {
196
212
  await runSmokeTest();
197
213
  return;
198
214
  }
199
- if (rootHelpFlags.includes(argv[0] ?? "")) {
215
+ if (hasRootHelpFlag(argv)) {
200
216
  const { renderRootHelp } = await import("@sayknow-cli/utils/cli");
201
217
  const { getExtraHelpText } = await import("./cli/fast-help");
202
218
  renderRootHelp({ bin: APP_NAME, version: VERSION, commands: new Map([["launch", RootHelpCommand]]) });
@@ -206,7 +222,7 @@ export async function runCli(argv: string[]): Promise<void> {
206
222
  }
207
223
  return;
208
224
  }
209
- if (versionFlags.includes(argv[0] ?? "")) {
225
+ if (hasRootVersionFlag(argv)) {
210
226
  process.stdout.write(`${APP_NAME}/${VERSION}\n`);
211
227
  return;
212
228
  }
@@ -223,4 +239,6 @@ export async function runCli(argv: string[]): Promise<void> {
223
239
  return run({ bin: APP_NAME, version: VERSION, argv: runArgv, commands, help: showHelp });
224
240
  }
225
241
 
226
- await runCli(process.argv.slice(2));
242
+ if (import.meta.main) {
243
+ await runCli(process.argv.slice(2));
244
+ }
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Direct MCP server registration for standalone SKC.
3
+ */
4
+ import { Args, Command, Flags } from "@sayknow-cli/utils/cli";
5
+ import { type MCPAction, type MCPCommandArgs, runMCPCommand } from "../cli/mcp-cli";
6
+
7
+ const ACTIONS: MCPAction[] = ["add", "list", "remove"];
8
+
9
+ export default class MCP extends Command {
10
+ static description = "Register standalone MCP servers explicitly in SKC config";
11
+ static delegateHelp = true;
12
+
13
+ static examples = [
14
+ "skc mcp add context7 npx -y @upstash/context7-mcp",
15
+ "skc mcp add docs --type http --url https://example.test/mcp --header Authorization=Bearer_TOKEN",
16
+ "skc mcp list --json",
17
+ "skc mcp remove context7",
18
+ ];
19
+
20
+ static args = {
21
+ action: Args.string({ description: "MCP action", required: false, options: ACTIONS }),
22
+ name: Args.string({ description: "Server name", required: false }),
23
+ commandArgs: Args.string({
24
+ description: "Command/URL and trailing args for add",
25
+ required: false,
26
+ multiple: true,
27
+ }),
28
+ };
29
+
30
+ static flags = {
31
+ project: Flags.boolean({ description: "Write/read project scope (./.skc/mcp.json) instead of user scope" }),
32
+ force: Flags.boolean({ description: "Overwrite an existing server during add", default: false }),
33
+ json: Flags.boolean({
34
+ char: "j",
35
+ description: "Emit machine-readable JSON with sensitive values redacted",
36
+ default: false,
37
+ }),
38
+ type: Flags.string({ description: "Server transport type", options: ["stdio", "http", "sse"] }),
39
+ command: Flags.string({ description: "Stdio server command for add" }),
40
+ url: Flags.string({ description: "HTTP/SSE server URL for add" }),
41
+ arg: Flags.string({ description: "Argument passed to a stdio server (repeatable)", multiple: true }),
42
+ env: Flags.string({
43
+ description: "Environment variable for stdio server as KEY=VALUE (repeatable)",
44
+ multiple: true,
45
+ }),
46
+ header: Flags.string({
47
+ description: "HTTP/SSE header as KEY=VALUE (repeatable; redacted in output)",
48
+ multiple: true,
49
+ }),
50
+ cwd: Flags.string({ description: "Working directory for stdio server" }),
51
+ timeout: Flags.integer({ description: "Connection timeout in milliseconds" }),
52
+ };
53
+
54
+ async run(): Promise<void> {
55
+ if (this.argv.includes("--help") || this.argv.includes("-h")) {
56
+ this.printHelp();
57
+ return;
58
+ }
59
+
60
+ const { args, flags } = await this.parse(MCP);
61
+ const action = (args.action ?? "list") as MCPAction;
62
+ const cmd: MCPCommandArgs = {
63
+ action,
64
+ name: args.name,
65
+ commandArgs: args.commandArgs,
66
+ flags: {
67
+ project: flags.project,
68
+ force: flags.force,
69
+ json: flags.json,
70
+ type: flags.type as MCPCommandArgs["flags"]["type"],
71
+ command: flags.command,
72
+ url: flags.url,
73
+ arg: flags.arg,
74
+ env: flags.env,
75
+ header: flags.header,
76
+ cwd: flags.cwd,
77
+ timeout: flags.timeout,
78
+ },
79
+ };
80
+ await runMCPCommand(cmd);
81
+ }
82
+
83
+ private printHelp(): void {
84
+ process.stdout.write(`Register standalone MCP servers explicitly in SKC config
85
+
86
+ USAGE
87
+ $ skc mcp [add|list|remove] [NAME] [COMMAND_OR_URL] [ARGS...] [FLAGS]
88
+
89
+ COMMANDS
90
+ add Add an explicit user-provided MCP server definition
91
+ list List registered servers with env/header/auth values redacted
92
+ remove Remove a registered server and print the removed definition redacted
93
+
94
+ FLAGS
95
+ --project Use project scope (./.skc/mcp.json) instead of user scope
96
+ --force Overwrite an existing server during add
97
+ -j, --json Emit machine-readable JSON with sensitive values redacted
98
+ --type=<value> stdio | http | sse (default: stdio, or http when --url is set)
99
+ --command=<value> Stdio server command for add
100
+ --url=<value> HTTP/SSE server URL for add
101
+ --arg=<value> Stdio server argument (repeatable)
102
+ --env=<value> Stdio env var as KEY=VALUE (repeatable; redacted in output)
103
+ --header=<value> HTTP/SSE header as KEY=VALUE (repeatable; redacted in output)
104
+ --cwd=<value> Working directory for stdio server
105
+ --timeout=<int> Connection timeout in milliseconds
106
+
107
+ EXAMPLES
108
+ $ skc mcp add context7 npx -y @upstash/context7-mcp
109
+ $ skc mcp add docs --type http --url https://example.test/mcp --header Authorization=Bearer_TOKEN
110
+ $ skc mcp list --json
111
+ $ skc mcp remove context7
112
+
113
+ SECURITY
114
+ This command writes only the server definition supplied on this invocation. It does not import or inherit Claude Code, Codex, OpenCode, or other live MCP configs. Public output redacts env, header, auth, and OAuth credential values.
115
+ `);
116
+ }
117
+ }
@@ -49,6 +49,8 @@ export default class Plugin extends Command {
49
49
  description: 'Install scope: "user" (default) or "project"',
50
50
  options: ["user", "project"],
51
51
  }),
52
+ user: Flags.boolean({ description: "Install SKC plugin bundle into the user root" }),
53
+ project: Flags.boolean({ description: "Install SKC plugin bundle into the project root" }),
52
54
  };
53
55
 
54
56
  async run(): Promise<void> {
@@ -69,6 +71,8 @@ export default class Plugin extends Command {
69
71
  disable: flags.disable,
70
72
  set: flags.set,
71
73
  scope: flags.scope as "user" | "project" | undefined,
74
+ user: flags.user,
75
+ project: flags.project,
72
76
  },
73
77
  };
74
78
 
@@ -2,6 +2,7 @@ import { Args, Command, Flags } from "@sayknow-cli/utils/cli";
2
2
  import {
3
3
  attachSkcTmuxSession,
4
4
  createSkcTmuxSession,
5
+ forceCloseSkcTmuxSession,
5
6
  listSkcTmuxSessions,
6
7
  removeSkcTmuxSession,
7
8
  statusSkcTmuxSession,
@@ -60,6 +61,12 @@ export default class Session extends Command {
60
61
 
61
62
  static flags = {
62
63
  json: Flags.boolean({ char: "j", description: "Emit machine-readable JSON", default: false }),
64
+ "session-id": Flags.string({
65
+ description: "Expected @skc-session-id tag for force-close (defense-in-depth match)",
66
+ }),
67
+ "state-file": Flags.string({
68
+ description: "Expected @skc-session-state-file tag for force-close (defense-in-depth match)",
69
+ }),
63
70
  };
64
71
 
65
72
  static examples = [
@@ -68,6 +75,7 @@ export default class Session extends Command {
68
75
  "skc session status <session>",
69
76
  "skc session attach <session>",
70
77
  "skc session remove <session>",
78
+ "skc session force-close <session> --session-id <id>",
71
79
  ];
72
80
 
73
81
  async run(): Promise<void> {
@@ -136,6 +144,16 @@ export default class Session extends Command {
136
144
  return;
137
145
  }
138
146
 
147
+ if (action === "force-close" || action === "force-remove") {
148
+ const closed = forceCloseSkcTmuxSession(sessionName, process.env, flags["session-id"], flags["state-file"]);
149
+ if (json) {
150
+ writeJson({ ok: true, session: sessionJson(closed) });
151
+ return;
152
+ }
153
+ writeText([`force-closed: ${closed.name}`]);
154
+ return;
155
+ }
156
+
139
157
  if (action === "attach") {
140
158
  attachSkcTmuxSession(sessionName);
141
159
  return;
@@ -117,8 +117,8 @@ export const KEYBINDINGS = {
117
117
  description: "Open external editor",
118
118
  },
119
119
  "app.message.followUp": {
120
- defaultKeys: "ctrl+enter",
121
- description: "Send follow-up message",
120
+ defaultKeys: [],
121
+ description: "Send follow-up message (no default; Ctrl+Enter inserts a newline)",
122
122
  },
123
123
  "app.message.queue": {
124
124
  defaultKeys: "alt+enter",