@sayknow-cli/coding-agent 0.3.0 → 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 (233) 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/notify-cli.d.ts +2 -0
  4. package/dist/types/cli/plugin-cli.d.ts +2 -0
  5. package/dist/types/cli.d.ts +6 -0
  6. package/dist/types/commands/mcp.d.ts +70 -0
  7. package/dist/types/commands/plugin.d.ts +6 -0
  8. package/dist/types/commands/session.d.ts +6 -0
  9. package/dist/types/config/keybindings.d.ts +2 -2
  10. package/dist/types/config/model-profile-activation.d.ts +8 -1
  11. package/dist/types/config/settings-schema.d.ts +39 -2
  12. package/dist/types/deep-interview/plaintext-gate-guard.d.ts +11 -0
  13. package/dist/types/defaults/skc-defaults.d.ts +2 -13
  14. package/dist/types/extensibility/shared-events.d.ts +1 -0
  15. package/dist/types/extensibility/skc-plugins/compiler.d.ts +19 -0
  16. package/dist/types/extensibility/skc-plugins/constrained-hooks.d.ts +29 -0
  17. package/dist/types/extensibility/skc-plugins/index.d.ts +9 -0
  18. package/dist/types/extensibility/skc-plugins/injection.d.ts +9 -0
  19. package/dist/types/extensibility/skc-plugins/installer.d.ts +13 -0
  20. package/dist/types/extensibility/skc-plugins/mcp-policy.d.ts +26 -0
  21. package/dist/types/extensibility/skc-plugins/observability.d.ts +27 -0
  22. package/dist/types/extensibility/skc-plugins/prompt-appendix.d.ts +16 -0
  23. package/dist/types/extensibility/skc-plugins/registry.d.ts +32 -0
  24. package/dist/types/extensibility/skc-plugins/runtime-adapters.d.ts +64 -0
  25. package/dist/types/extensibility/skc-plugins/session-validation.d.ts +42 -0
  26. package/dist/types/extensibility/skc-plugins/types.d.ts +158 -2
  27. package/dist/types/extensibility/skc-plugins/validation.d.ts +8 -1
  28. package/dist/types/lsp/types.d.ts +2 -0
  29. package/dist/types/main.d.ts +2 -0
  30. package/dist/types/modes/components/custom-editor.d.ts +1 -1
  31. package/dist/types/modes/components/model-selector.d.ts +8 -0
  32. package/dist/types/modes/components/status-line/git-utils.d.ts +6 -0
  33. package/dist/types/modes/theme/defaults/index.d.ts +99 -0
  34. package/dist/types/notifications/attachment-registry.d.ts +17 -0
  35. package/dist/types/notifications/chat-adapters.d.ts +9 -0
  36. package/dist/types/notifications/config.d.ts +9 -1
  37. package/dist/types/notifications/engine.d.ts +59 -0
  38. package/dist/types/notifications/html-format.d.ts +11 -0
  39. package/dist/types/notifications/index.d.ts +149 -1
  40. package/dist/types/notifications/lifecycle-commands.d.ts +60 -0
  41. package/dist/types/notifications/lifecycle-control-runtime.d.ts +98 -0
  42. package/dist/types/notifications/lifecycle-orchestrator.d.ts +144 -0
  43. package/dist/types/notifications/managed-daemon.d.ts +48 -0
  44. package/dist/types/notifications/operator-runtime.d.ts +52 -0
  45. package/dist/types/notifications/rate-limit-pool.d.ts +2 -0
  46. package/dist/types/notifications/recent-activity.d.ts +35 -0
  47. package/dist/types/notifications/telegram-daemon.d.ts +133 -16
  48. package/dist/types/notifications/telegram-reference.d.ts +3 -1
  49. package/dist/types/notifications/threaded-inbound.d.ts +19 -0
  50. package/dist/types/notifications/threaded-render.d.ts +6 -1
  51. package/dist/types/notifications/topic-registry.d.ts +12 -9
  52. package/dist/types/runtime-mcp/types.d.ts +7 -0
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +16 -4
  55. package/dist/types/session/blob-store.d.ts +25 -0
  56. package/dist/types/session/session-manager.d.ts +57 -0
  57. package/dist/types/skc-runtime/launch-tmux.d.ts +1 -0
  58. package/dist/types/skc-runtime/psmux-detect.d.ts +78 -0
  59. package/dist/types/skc-runtime/ralplan-runtime.d.ts +1 -1
  60. package/dist/types/skc-runtime/team-runtime.d.ts +2 -0
  61. package/dist/types/skc-runtime/tmux-common.d.ts +30 -2
  62. package/dist/types/skc-runtime/tmux-sessions.d.ts +18 -0
  63. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +6 -0
  64. package/dist/types/system-prompt.d.ts +2 -0
  65. package/dist/types/task/executor.d.ts +9 -1
  66. package/dist/types/tools/composer-bash-policy.d.ts +14 -0
  67. package/dist/types/tools/fetch.d.ts +23 -0
  68. package/dist/types/tools/index.d.ts +4 -1
  69. package/dist/types/tools/telegram-send.d.ts +32 -0
  70. package/dist/types/utils/changelog.d.ts +1 -0
  71. package/dist/types/web/insane/bridge.d.ts +103 -0
  72. package/dist/types/web/insane/url-guard.d.ts +25 -0
  73. package/dist/types/web/scrapers/types.d.ts +5 -0
  74. package/dist/types/web/scrapers/utils.d.ts +7 -1
  75. package/dist/types/web/search/provider.d.ts +18 -1
  76. package/dist/types/web/search/providers/insane.d.ts +53 -0
  77. package/dist/types/web/search/providers/text-citations.d.ts +23 -0
  78. package/dist/types/web/search/types.d.ts +12 -4
  79. package/package.json +14 -10
  80. package/scripts/g004-tmux-smoke.ts +100 -0
  81. package/scripts/g005-daemon-smoke.ts +180 -0
  82. package/scripts/g011-daemon-path-smoke.ts +153 -0
  83. package/scripts/verify-insane-vendor.ts +132 -0
  84. package/src/cli/args.ts +1 -1
  85. package/src/cli/fast-help.ts +30 -2
  86. package/src/cli/mcp-cli.ts +272 -0
  87. package/src/cli/notify-cli.ts +152 -5
  88. package/src/cli/plugin-cli.ts +66 -3
  89. package/src/cli.ts +30 -12
  90. package/src/commands/mcp.ts +117 -0
  91. package/src/commands/plugin.ts +4 -0
  92. package/src/commands/session.ts +18 -0
  93. package/src/commands/team.ts +1 -1
  94. package/src/config/keybindings.ts +2 -2
  95. package/src/config/model-profile-activation.ts +55 -7
  96. package/src/config/settings-schema.ts +30 -1
  97. package/src/deep-interview/plaintext-gate-guard.ts +94 -0
  98. package/src/defaults/skc/extensions/grok-cli-vendor/biome.json +1 -1
  99. package/src/defaults/skc/skills/deep-interview/SKILL.md +7 -6
  100. package/src/defaults/skc/skills/ralplan/SKILL.md +11 -4
  101. package/src/defaults/skc/skills/team/SKILL.md +5 -3
  102. package/src/defaults/skc/skills/ultragoal/SKILL.md +41 -13
  103. package/src/defaults/skc-defaults.ts +2 -27
  104. package/src/export/html/index.ts +2 -2
  105. package/src/extensibility/extensions/runner.ts +1 -0
  106. package/src/extensibility/shared-events.ts +1 -0
  107. package/src/extensibility/skc-plugins/compiler.ts +351 -0
  108. package/src/extensibility/skc-plugins/constrained-hooks.ts +170 -0
  109. package/src/extensibility/skc-plugins/index.ts +9 -0
  110. package/src/extensibility/skc-plugins/injection.ts +109 -0
  111. package/src/extensibility/skc-plugins/installer.ts +434 -0
  112. package/src/extensibility/skc-plugins/loader.ts +3 -1
  113. package/src/extensibility/skc-plugins/mcp-policy.ts +239 -0
  114. package/src/extensibility/skc-plugins/observability.ts +84 -0
  115. package/src/extensibility/skc-plugins/paths.ts +1 -1
  116. package/src/extensibility/skc-plugins/prompt-appendix.ts +109 -0
  117. package/src/extensibility/skc-plugins/registry.ts +180 -0
  118. package/src/extensibility/skc-plugins/runtime-adapters.ts +234 -0
  119. package/src/extensibility/skc-plugins/schema.ts +250 -20
  120. package/src/extensibility/skc-plugins/session-validation.ts +147 -0
  121. package/src/extensibility/skc-plugins/types.ts +199 -3
  122. package/src/extensibility/skc-plugins/validation.ts +80 -0
  123. package/src/extensibility/skills.ts +15 -0
  124. package/src/hooks/skill-state.ts +57 -0
  125. package/src/internal-urls/docs-index.generated.ts +17 -13
  126. package/src/lsp/config.ts +16 -3
  127. package/src/lsp/defaults.json +7 -0
  128. package/src/lsp/types.ts +2 -0
  129. package/src/main.ts +14 -3
  130. package/src/modes/bridge/bridge-mode.ts +11 -0
  131. package/src/modes/components/assistant-message.ts +49 -1
  132. package/src/modes/components/custom-editor.ts +2 -0
  133. package/src/modes/components/footer.ts +2 -3
  134. package/src/modes/components/hook-editor.ts +1 -1
  135. package/src/modes/components/hook-selector.ts +67 -43
  136. package/src/modes/components/model-selector.ts +56 -11
  137. package/src/modes/components/status-line/git-utils.ts +25 -0
  138. package/src/modes/components/status-line.ts +10 -11
  139. package/src/modes/components/welcome.ts +2 -3
  140. package/src/modes/controllers/event-controller.ts +15 -0
  141. package/src/modes/controllers/extension-ui-controller.ts +0 -27
  142. package/src/modes/controllers/selector-controller.ts +53 -11
  143. package/src/modes/interactive-mode.ts +50 -3
  144. package/src/modes/shared/agent-wire/scopes.ts +1 -1
  145. package/src/modes/theme/defaults/gruvbox-dark.json +99 -0
  146. package/src/modes/theme/defaults/index.ts +2 -0
  147. package/src/modes/utils/context-usage.ts +2 -2
  148. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  149. package/src/notifications/attachment-registry.ts +23 -0
  150. package/src/notifications/chat-adapters.ts +147 -0
  151. package/src/notifications/config.ts +23 -2
  152. package/src/notifications/engine.ts +100 -0
  153. package/src/notifications/html-format.ts +38 -0
  154. package/src/notifications/index.ts +417 -45
  155. package/src/notifications/lifecycle-commands.ts +238 -0
  156. package/src/notifications/lifecycle-control-runtime.ts +405 -0
  157. package/src/notifications/lifecycle-orchestrator.ts +358 -0
  158. package/src/notifications/managed-daemon.ts +163 -0
  159. package/src/notifications/operator-runtime.ts +171 -0
  160. package/src/notifications/rate-limit-pool.ts +19 -0
  161. package/src/notifications/recent-activity.ts +132 -0
  162. package/src/notifications/telegram-daemon.ts +984 -242
  163. package/src/notifications/telegram-reference.ts +25 -7
  164. package/src/notifications/threaded-inbound.ts +60 -4
  165. package/src/notifications/threaded-render.ts +20 -2
  166. package/src/notifications/topic-registry.ts +23 -9
  167. package/src/prompts/agents/executor.md +2 -2
  168. package/src/runtime-mcp/transports/stdio.ts +38 -4
  169. package/src/runtime-mcp/types.ts +7 -0
  170. package/src/sdk.ts +157 -10
  171. package/src/session/agent-session.ts +248 -125
  172. package/src/session/blob-store.ts +196 -8
  173. package/src/session/session-manager.ts +762 -12
  174. package/src/skc-runtime/launch-tmux.ts +85 -22
  175. package/src/skc-runtime/ledger-event-renderer.ts +1 -0
  176. package/src/skc-runtime/psmux-detect.ts +239 -0
  177. package/src/skc-runtime/ralplan-runtime.ts +2 -2
  178. package/src/skc-runtime/team-runtime.ts +56 -23
  179. package/src/skc-runtime/tmux-common.ts +88 -4
  180. package/src/skc-runtime/tmux-sessions.ts +111 -9
  181. package/src/skc-runtime/ultragoal-guard.ts +25 -8
  182. package/src/skc-runtime/ultragoal-runtime.ts +75 -15
  183. package/src/skc-runtime/workflow-manifest.generated.json +29 -0
  184. package/src/skc-runtime/workflow-manifest.ts +7 -2
  185. package/src/slash-commands/builtin-registry.ts +23 -3
  186. package/src/slash-commands/helpers/fast-status-report.ts +13 -3
  187. package/src/slash-commands/helpers/parse.ts +2 -1
  188. package/src/system-prompt.ts +9 -0
  189. package/src/task/executor.ts +31 -7
  190. package/src/task/index.ts +2 -0
  191. package/src/tools/ask.ts +5 -1
  192. package/src/tools/bash.ts +9 -0
  193. package/src/tools/composer-bash-policy.ts +96 -0
  194. package/src/tools/fetch.ts +94 -1
  195. package/src/tools/index.ts +6 -1
  196. package/src/tools/telegram-send.ts +137 -0
  197. package/src/utils/changelog.ts +8 -0
  198. package/src/web/insane/bridge.ts +350 -0
  199. package/src/web/insane/url-guard.ts +159 -0
  200. package/src/web/scrapers/types.ts +143 -45
  201. package/src/web/scrapers/utils.ts +70 -19
  202. package/src/web/search/provider.ts +77 -18
  203. package/src/web/search/providers/anthropic.ts +70 -3
  204. package/src/web/search/providers/codex.ts +1 -119
  205. package/src/web/search/providers/gemini.ts +99 -0
  206. package/src/web/search/providers/insane.ts +551 -0
  207. package/src/web/search/providers/openai-compatible.ts +66 -32
  208. package/src/web/search/providers/text-citations.ts +111 -0
  209. package/src/web/search/types.ts +13 -2
  210. package/vendor/insane-search/LICENSE +21 -0
  211. package/vendor/insane-search/MANIFEST.json +24 -0
  212. package/vendor/insane-search/engine/__init__.py +23 -0
  213. package/vendor/insane-search/engine/__main__.py +128 -0
  214. package/vendor/insane-search/engine/bias_check.py +183 -0
  215. package/vendor/insane-search/engine/executor.py +254 -0
  216. package/vendor/insane-search/engine/fetch_chain.py +725 -0
  217. package/vendor/insane-search/engine/learning.py +175 -0
  218. package/vendor/insane-search/engine/phase0.py +214 -0
  219. package/vendor/insane-search/engine/safety.py +91 -0
  220. package/vendor/insane-search/engine/templates/package.json +11 -0
  221. package/vendor/insane-search/engine/templates/playwright_mobile_chrome.js +188 -0
  222. package/vendor/insane-search/engine/templates/playwright_real_chrome.js +243 -0
  223. package/vendor/insane-search/engine/tests/test_hardening.py +57 -0
  224. package/vendor/insane-search/engine/tests/test_smoke.py +152 -0
  225. package/vendor/insane-search/engine/tests/test_u1.py +200 -0
  226. package/vendor/insane-search/engine/tests/test_u4.py +131 -0
  227. package/vendor/insane-search/engine/tests/test_u5.py +163 -0
  228. package/vendor/insane-search/engine/tests/test_u7.py +124 -0
  229. package/vendor/insane-search/engine/transport.py +211 -0
  230. package/vendor/insane-search/engine/url_transforms.py +98 -0
  231. package/vendor/insane-search/engine/validators.py +331 -0
  232. package/vendor/insane-search/engine/waf_detector.py +214 -0
  233. package/vendor/insane-search/engine/waf_profiles.yaml +162 -0
@@ -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
+ }
@@ -29,6 +29,8 @@ export interface NotifyCommandDeps {
29
29
  pollIntervalMs?: number;
30
30
  setupChatId?: string;
31
31
  setupRedact?: boolean;
32
+ setupInteractive?: boolean;
33
+ threadedModePrompt?: (message: string) => Promise<string>;
32
34
  }
33
35
 
34
36
  interface TelegramApiResponse<T> {
@@ -47,6 +49,18 @@ interface TelegramUpdate {
47
49
  };
48
50
  }
49
51
 
52
+ interface TelegramUser {
53
+ id: number;
54
+ is_bot?: boolean;
55
+ first_name?: string;
56
+ username?: string;
57
+ has_topics_enabled?: boolean;
58
+ allows_users_to_create_topics?: boolean;
59
+ }
60
+
61
+ type ThreadedModeState = "enabled" | "disabled" | "unknown";
62
+ type ThreadedModeFinalLabel = "verified" | "unverified" | "unknown";
63
+
50
64
  const DEFAULT_API_BASE = "https://api.telegram.org";
51
65
  const DEFAULT_POLL_TIMEOUT_MS = 60_000;
52
66
  const DEFAULT_POLL_INTERVAL_MS = 1_000;
@@ -120,7 +134,11 @@ async function runSetup(deps: NotifyCommandDeps): Promise<void> {
120
134
  throw new Error("Telegram bot token is required.");
121
135
  }
122
136
 
123
- await callTelegram(fetchImpl, apiBase, token, "getMe", {});
137
+ const user = await getMe(fetchImpl, apiBase, token);
138
+ const threadedState = await verifyThreadedMode(fetchImpl, apiBase, token, user, {
139
+ interactive: resolveSetupInteractive(deps),
140
+ prompt: deps.threadedModePrompt ?? promptForThreadedMode,
141
+ });
124
142
  process.stdout.write(
125
143
  "Token validated. Message your bot now from the private Telegram chat to pair notifications.\n",
126
144
  );
@@ -145,7 +163,9 @@ async function runSetup(deps: NotifyCommandDeps): Promise<void> {
145
163
  if (deps.setupRedact) settings.set("notifications.redact", true);
146
164
  await settings.flush();
147
165
 
148
- process.stdout.write(`Notifications enabled. botToken=${maskToken(token)} chatId=${chatId}\n`);
166
+ process.stdout.write(
167
+ `Notifications enabled. botToken=${maskToken(token)} chatId=${chatId} threaded=${threadedLabel(threadedState)}\n`,
168
+ );
149
169
  }
150
170
 
151
171
  async function promptForToken(): Promise<string> {
@@ -160,14 +180,135 @@ async function promptForToken(): Promise<string> {
160
180
  }
161
181
  }
162
182
 
183
+ const THREADED_ENABLED_SUCCESS =
184
+ "Telegram Threaded Mode capability verified for this bot. SKC will request a private-chat topic per session; if Telegram ever refuses topic creation, notifications fall back to this flat chat with a one-time nudge.\n";
185
+
186
+ const THREADED_MISSING_WARNING =
187
+ "Warning: Telegram getMe did not include has_topics_enabled, so SKC cannot verify private-chat Threaded Mode capability for this bot. Setup will continue; update Telegram/Bot API support or re-run setup if per-session topics fail.\n";
188
+
189
+ const THREADED_NONINTERACTIVE_WARNING =
190
+ "Warning: Telegram Threaded Mode capability is OFF for this bot. Setup will be saved because this run is non-interactive, but per-session Telegram delivery may fail closed until the bot owner enables Threaded Mode in @BotFather. SKC cannot enable it through the Bot API.\n";
191
+
192
+ const THREADED_DISABLED_GUIDANCE =
193
+ "Telegram Threaded Mode is OFF for this bot. SKC needs Telegram private-chat topics so each session can use its own thread.\n" +
194
+ "SKC cannot enable this through the Bot API. Open @BotFather, select this bot, enable Threaded Mode / forum topics for private chats, then return here.\n" +
195
+ "Telegram may require an additional Stars purchase fee for private-chat topics.\n";
196
+
197
+ const THREADED_DISABLED_PROMPT =
198
+ "Press Enter after enabling Threaded Mode, or type skip to finish setup with a warning: ";
199
+
200
+ const THREADED_STILL_OFF = "Telegram still reports Threaded Mode OFF for this bot.\n";
201
+
202
+ const THREADED_RETRY_PROMPT = "Press Enter to check again, or type skip to finish setup with a warning: ";
203
+
204
+ const THREADED_SKIP_WARNING =
205
+ "Warning: continuing without verified Telegram Threaded Mode capability. Setup will be saved, but per-session Telegram delivery may fail closed until Threaded Mode is enabled in BotFather.\n";
206
+
207
+ const THREADED_INVALID_INPUT = "Type Enter to retry or skip to continue with a warning.\n";
208
+
209
+ const THREADED_RETRY_INPUTS = new Set(["", "y", "yes", "r", "retry"]);
210
+ const THREADED_SKIP_INPUTS = new Set(["s", "skip", "n", "no"]);
211
+
212
+ function isTelegramUser(value: unknown): value is TelegramUser {
213
+ return Boolean(value) && typeof value === "object" && typeof (value as { id?: unknown }).id === "number";
214
+ }
215
+
216
+ async function getMe(fetchImpl: typeof fetch, apiBase: string, token: string): Promise<TelegramUser> {
217
+ const user = await callTelegram<unknown>(fetchImpl, apiBase, token, "getMe", {});
218
+ if (!isTelegramUser(user)) {
219
+ throw new Error("Telegram getMe returned invalid Telegram response: missing valid User result.");
220
+ }
221
+ return user;
222
+ }
223
+
224
+ function threadedModeState(user: TelegramUser): ThreadedModeState {
225
+ if (user.has_topics_enabled === true) return "enabled";
226
+ if (user.has_topics_enabled === false) return "disabled";
227
+ return "unknown";
228
+ }
229
+
230
+ function threadedLabel(state: ThreadedModeState): ThreadedModeFinalLabel {
231
+ if (state === "enabled") return "verified";
232
+ if (state === "disabled") return "unverified";
233
+ return "unknown";
234
+ }
235
+
236
+ function resolveSetupInteractive(deps: NotifyCommandDeps): boolean {
237
+ if (deps.setupInteractive !== undefined) return deps.setupInteractive;
238
+ return Boolean(process.stdin.isTTY) && !deps.setupChatId?.trim();
239
+ }
240
+
241
+ async function promptForThreadedMode(message: string): Promise<string> {
242
+ if (!process.stdin.isTTY) return "skip";
243
+ const rl = createInterface({ input: process.stdin, output: process.stdout, terminal: true });
244
+ try {
245
+ return (await rl.question(message)).trim();
246
+ } finally {
247
+ rl.close();
248
+ }
249
+ }
250
+
251
+ async function verifyThreadedMode(
252
+ fetchImpl: typeof fetch,
253
+ apiBase: string,
254
+ token: string,
255
+ initialUser: TelegramUser,
256
+ opts: { interactive: boolean; prompt: (message: string) => Promise<string> },
257
+ ): Promise<ThreadedModeState> {
258
+ const classify = (user: TelegramUser): ThreadedModeState | undefined => {
259
+ const state = threadedModeState(user);
260
+ if (state === "enabled") {
261
+ process.stdout.write(THREADED_ENABLED_SUCCESS);
262
+ return "enabled";
263
+ }
264
+ if (state === "unknown") {
265
+ process.stdout.write(THREADED_MISSING_WARNING);
266
+ return "unknown";
267
+ }
268
+ return undefined;
269
+ };
270
+
271
+ const initial = classify(initialUser);
272
+ if (initial) return initial;
273
+
274
+ if (!opts.interactive) {
275
+ process.stdout.write(THREADED_NONINTERACTIVE_WARNING);
276
+ return "disabled";
277
+ }
278
+
279
+ process.stdout.write(THREADED_DISABLED_GUIDANCE);
280
+ let firstPrompt = true;
281
+ for (;;) {
282
+ const answer = (await opts.prompt(firstPrompt ? THREADED_DISABLED_PROMPT : THREADED_RETRY_PROMPT))
283
+ .trim()
284
+ .toLowerCase();
285
+ firstPrompt = false;
286
+ if (THREADED_SKIP_INPUTS.has(answer)) {
287
+ process.stdout.write(THREADED_SKIP_WARNING);
288
+ return "disabled";
289
+ }
290
+ if (!THREADED_RETRY_INPUTS.has(answer)) {
291
+ process.stdout.write(THREADED_INVALID_INPUT);
292
+ continue;
293
+ }
294
+ const resolved = classify(await getMe(fetchImpl, apiBase, token));
295
+ if (resolved) return resolved;
296
+ process.stdout.write(THREADED_STILL_OFF);
297
+ }
298
+ }
299
+
163
300
  async function runStatus(deps: NotifyCommandDeps): Promise<void> {
164
301
  const settings = await getSettings(deps);
165
302
  const cfg = getNotificationConfig(settings);
166
303
  process.stdout.write(
167
304
  `${chalk.bold("Notifications")}\n` +
168
305
  ` enabled: ${cfg.enabled}\n` +
169
- ` botToken: ${maskToken(cfg.botToken)}\n` +
170
- ` chatId: ${cfg.chatId ?? "(unset)"}\n` +
306
+ ` telegram.botToken: ${maskToken(cfg.botToken)}\n` +
307
+ ` telegram.chatId: ${cfg.chatId ?? "(unset)"}\n` +
308
+ ` discord.botToken: ${maskToken(cfg.discord.botToken)}\n` +
309
+ ` discord.channelId: ${cfg.discord.channelId ?? "(unset)"}\n` +
310
+ ` slack.botToken: ${maskToken(cfg.slack.botToken)}\n` +
311
+ ` slack.channelId: ${cfg.slack.channelId ?? "(unset)"}\n` +
171
312
  ` redact: ${cfg.redact}\n`,
172
313
  );
173
314
  }
@@ -263,12 +404,18 @@ ${chalk.bold("Usage:")}
263
404
  ${APP_NAME} notify status
264
405
 
265
406
  ${chalk.bold("Subcommands:")}
266
- setup Pair a Telegram bot token with a private chat
407
+ setup Pair a Telegram bot token with a private chat and verify Threaded Mode capability
267
408
  status Show notification configuration without secrets
268
409
 
269
410
  ${chalk.bold("Examples:")}
270
411
  ${APP_NAME} notify setup
271
412
  ${APP_NAME} notify setup --token <botToken> --chat-id <chatId> [--redact]
272
413
  ${APP_NAME} notify status
414
+
415
+ ${chalk.bold("Threaded Mode:")}
416
+ SKC uses Telegram private-chat topics for per-session threads. Setup verifies the bot
417
+ capability via getMe.has_topics_enabled. If it is off, enable Threaded Mode in @BotFather;
418
+ bots cannot toggle it through the Bot API. If Telegram refuses topic creation at runtime,
419
+ SKC delivers flat to the paired private chat and nudges you to enable Threaded Mode.
273
420
  `);
274
421
  }
@@ -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> {