kritya 0.8.2-beta

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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +434 -0
  3. package/dist/agent/budget.js +25 -0
  4. package/dist/agent/compactor.js +78 -0
  5. package/dist/agent/contextWarning.js +6 -0
  6. package/dist/agent/killSwitch.js +111 -0
  7. package/dist/agent/loop.js +543 -0
  8. package/dist/agent/memory.js +145 -0
  9. package/dist/agent/plugins.js +408 -0
  10. package/dist/agent/skills.js +210 -0
  11. package/dist/agent/skillsCli.js +51 -0
  12. package/dist/agent/systemPrompt.js +122 -0
  13. package/dist/agent/tokens.js +55 -0
  14. package/dist/agent/toolExecutor.js +331 -0
  15. package/dist/agent/workflow.js +496 -0
  16. package/dist/agent/worktree.js +116 -0
  17. package/dist/atomicWrite.js +109 -0
  18. package/dist/audit/audit.js +230 -0
  19. package/dist/audit/cli.js +135 -0
  20. package/dist/commands/custom.js +69 -0
  21. package/dist/commands/mcpCommand.js +433 -0
  22. package/dist/commands/pluginsCommand.js +204 -0
  23. package/dist/commands/registry.js +739 -0
  24. package/dist/config/config.js +214 -0
  25. package/dist/config/debug.js +19 -0
  26. package/dist/config/models.js +82 -0
  27. package/dist/config/retention.js +24 -0
  28. package/dist/config/winAcl.js +38 -0
  29. package/dist/crash.js +102 -0
  30. package/dist/engine.js +113 -0
  31. package/dist/git/git.js +46 -0
  32. package/dist/headless.js +274 -0
  33. package/dist/hooks/hooks.js +122 -0
  34. package/dist/index.js +670 -0
  35. package/dist/lsp/client.js +413 -0
  36. package/dist/lsp/manager.js +0 -0
  37. package/dist/lsp/registry.js +62 -0
  38. package/dist/mcp/callback.js +141 -0
  39. package/dist/mcp/client.js +945 -0
  40. package/dist/mcp/login.js +117 -0
  41. package/dist/mcp/oauth.js +345 -0
  42. package/dist/mcp/servers.js +114 -0
  43. package/dist/mcp/spawnWin.js +86 -0
  44. package/dist/mcp/tokens.js +97 -0
  45. package/dist/mcp/transport.js +295 -0
  46. package/dist/net/urlSafety.js +158 -0
  47. package/dist/permissions/danger.js +96 -0
  48. package/dist/permissions/permissions.js +62 -0
  49. package/dist/permissions/rules.js +69 -0
  50. package/dist/plugins/discover.js +108 -0
  51. package/dist/plugins/mcp.js +80 -0
  52. package/dist/provider/client.js +460 -0
  53. package/dist/provider/switchyardClient.js +46 -0
  54. package/dist/provider/switchyardSidecar.js +245 -0
  55. package/dist/provider/textToolCalls.js +140 -0
  56. package/dist/repomap/repoMap.js +168 -0
  57. package/dist/repomap/symbols.js +190 -0
  58. package/dist/session/store.js +328 -0
  59. package/dist/shell/background.js +131 -0
  60. package/dist/shell/sandbox.js +396 -0
  61. package/dist/telemetry/metrics.js +172 -0
  62. package/dist/telemetry/otlp.js +128 -0
  63. package/dist/telemetry/tracer.js +240 -0
  64. package/dist/tools/askUser.js +64 -0
  65. package/dist/tools/bg.js +59 -0
  66. package/dist/tools/common.js +158 -0
  67. package/dist/tools/deepResearch.js +126 -0
  68. package/dist/tools/diff.js +70 -0
  69. package/dist/tools/document/docx.js +44 -0
  70. package/dist/tools/document/pdf.js +190 -0
  71. package/dist/tools/document/pptx.js +148 -0
  72. package/dist/tools/document/types.js +26 -0
  73. package/dist/tools/document/xlsx.js +87 -0
  74. package/dist/tools/document.js +389 -0
  75. package/dist/tools/edit.js +56 -0
  76. package/dist/tools/fetchUrl.js +218 -0
  77. package/dist/tools/fuzzyMatch.js +74 -0
  78. package/dist/tools/glob.js +34 -0
  79. package/dist/tools/grep.js +85 -0
  80. package/dist/tools/ignore.js +28 -0
  81. package/dist/tools/index.js +66 -0
  82. package/dist/tools/ls.js +27 -0
  83. package/dist/tools/lsp.js +239 -0
  84. package/dist/tools/notebook.js +215 -0
  85. package/dist/tools/read.js +32 -0
  86. package/dist/tools/repoMap.js +26 -0
  87. package/dist/tools/secretScan.js +143 -0
  88. package/dist/tools/shell.js +134 -0
  89. package/dist/tools/skills.js +69 -0
  90. package/dist/tools/subagent.js +57 -0
  91. package/dist/tools/tasks.js +43 -0
  92. package/dist/tools/webSearch.js +97 -0
  93. package/dist/tools/write.js +47 -0
  94. package/dist/tools/writeAgent.js +72 -0
  95. package/dist/trust/aiDisclosure.js +45 -0
  96. package/dist/trust/mcpTrust.js +111 -0
  97. package/dist/trust/trust.js +231 -0
  98. package/dist/types.js +1 -0
  99. package/dist/ui/AiDisclosurePrompt.js +23 -0
  100. package/dist/ui/App.js +449 -0
  101. package/dist/ui/Banner.js +71 -0
  102. package/dist/ui/ElicitationPrompt.js +46 -0
  103. package/dist/ui/Markdown.js +148 -0
  104. package/dist/ui/McpTrustPrompt.js +42 -0
  105. package/dist/ui/ModelPicker.js +19 -0
  106. package/dist/ui/PermissionPrompt.js +27 -0
  107. package/dist/ui/SelectList.js +24 -0
  108. package/dist/ui/Spinner.js +12 -0
  109. package/dist/ui/StatusLine.js +11 -0
  110. package/dist/ui/TranscriptItem.js +13 -0
  111. package/dist/ui/TrustPrompt.js +12 -0
  112. package/dist/ui/highlight.js +137 -0
  113. package/dist/ui/inline.js +241 -0
  114. package/dist/ui/mermaid.js +98 -0
  115. package/dist/ui/table.js +148 -0
  116. package/dist/ui/toolOutputPreview.js +34 -0
  117. package/dist/ui/useAgent.js +534 -0
  118. package/dist/ui/useKillSwitch.js +65 -0
  119. package/dist/ui/useSessionResume.js +39 -0
  120. package/dist/ui/useUsageBudget.js +149 -0
  121. package/dist/ui/viewport.js +71 -0
  122. package/dist/undo/undo.js +293 -0
  123. package/dist/version.js +4 -0
  124. package/package.json +122 -0
@@ -0,0 +1,433 @@
1
+ import { assertSafeUrl, connectServer, disconnectServer, forgetStatus, mcpStatus, replaceStatus, isToolOf, } from "../mcp/client.js";
2
+ import { beginLogin, logout, pendingLogin } from "../mcp/login.js";
3
+ import { loadAuth } from "../mcp/tokens.js";
4
+ import { expandServerConfig, loadProjectMcpServers } from "../mcp/servers.js";
5
+ import { loadConfig, saveConfig } from "../config/config.js";
6
+ import { pluginsDir, scanPlugins, userPluginsDir } from "../plugins/discover.js";
7
+ import { loadPluginMcpServers } from "../plugins/mcp.js";
8
+ import { isServerTrusted, loadMcpAllowlist, revokeFingerprint, revokeServer, serverFingerprint, trustServer, } from "../trust/mcpTrust.js";
9
+ /** Find a server by name across both config sources, with where it came from. */
10
+ export function resolveServer(name, workspace) {
11
+ const global = loadConfig().mcpServers?.[name];
12
+ if (global)
13
+ return { name, cfg: expandServerConfig(global), provenance: "global" };
14
+ const project = loadProjectMcpServers(workspace)?.[name];
15
+ if (project)
16
+ return { name, cfg: expandServerConfig(project), provenance: "project" };
17
+ return undefined;
18
+ }
19
+ const USAGE = `Usage:
20
+ /mcp status of every configured server
21
+ /mcp add <name> <url> add a remote (HTTP) server to your config
22
+ /mcp add <name> -- <cmd...> add a local (stdio) server to your config
23
+ /mcp remove <name> remove a server from your config
24
+ /mcp login <name> sign in to a server via your browser
25
+ /mcp logout <name> revoke and delete a server's saved token
26
+ /mcp code <name> <code> finish a login by pasting the code (SSH/headless)
27
+ /mcp trust list the servers you've approved
28
+ /mcp trust revoke <name> withdraw approval, so it's asked about again`;
29
+ export async function runMcpCommand(ctx) {
30
+ const parts = ctx.arg.trim().split(/\s+/).filter(Boolean);
31
+ const sub = parts[0]?.toLowerCase();
32
+ switch (sub) {
33
+ case undefined:
34
+ return showStatus(ctx);
35
+ case "add":
36
+ return addServer(ctx, parts.slice(1));
37
+ case "remove":
38
+ case "rm":
39
+ return removeServer(ctx, parts[1]);
40
+ case "login":
41
+ return loginServer(ctx, parts[1], parts.slice(2));
42
+ case "logout":
43
+ return logoutServer(ctx, parts[1]);
44
+ case "code":
45
+ return submitCode(ctx, parts[1], parts.slice(2).join(" "));
46
+ case "trust":
47
+ return trustCommand(ctx, parts.slice(1));
48
+ default:
49
+ ctx.addItem({ kind: "info", text: `Unknown /mcp subcommand "${sub}".\n\n${USAGE}` });
50
+ }
51
+ }
52
+ function showStatus(ctx) {
53
+ const statuses = mcpStatus();
54
+ const plugins = scanPlugins([pluginsDir(ctx.workspace), userPluginsDir()]);
55
+ const { provenance } = loadPluginMcpServers(plugins, () => { });
56
+ if (statuses.length === 0) {
57
+ ctx.addItem({
58
+ kind: "info",
59
+ text: "No MCP servers configured.\n\n" +
60
+ "Add one with /mcp add <name> <url>, or by hand under mcpServers in\n" +
61
+ "~/.kritya/config.json or a .mcp.json at the workspace root:\n" +
62
+ ` { "mcpServers": { "linear": { "url": "https://mcp.linear.app/mcp" },\n` +
63
+ ` "files": { "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "."] } } }`,
64
+ });
65
+ return;
66
+ }
67
+ const lines = statuses.map((s) => {
68
+ const mark = s.ok ? "✔" : s.needsAuth ? "○" : "✘";
69
+ const plugin = provenance[s.name] ? ` (plugin: ${provenance[s.name]})` : "";
70
+ const head = `${mark} ${s.name} (${s.transport})${plugin} — ${s.target}`;
71
+ if (s.ok) {
72
+ const auth = s.transport === "http" && loadAuth(s.target) ? " · signed in" : "";
73
+ const hidden = s.hiddenTools ? ` · ${s.hiddenTools} hidden by config` : "";
74
+ const extras = [
75
+ s.prompts.length ? ` ${s.prompts.length} prompt(s): ${s.prompts.join(", ")}` : "",
76
+ s.resources.length
77
+ ? ` ${s.resources.length} resource(s): ${s.resources.join(", ")}`
78
+ : "",
79
+ ].filter(Boolean);
80
+ return [
81
+ `${head}${auth}${hidden}`,
82
+ ` ${s.tools.length} tool(s): ${s.tools.join(", ") || "(none)"}`,
83
+ ...extras,
84
+ ].join("\n");
85
+ }
86
+ return `${head}\n ${s.needsAuth ? s.error : `failed: ${s.error}`}`;
87
+ });
88
+ const anyAuth = statuses.some((s) => s.needsAuth);
89
+ const hint = anyAuth ? "\n\n○ = signed out. Run /mcp login <name> to connect it." : "";
90
+ ctx.addItem({ kind: "info", text: `MCP servers:\n${lines.join("\n")}${hint}\n\n${USAGE}` });
91
+ }
92
+ function addServer(ctx, args) {
93
+ const name = args[0];
94
+ const rest = args.slice(1);
95
+ if (!name || rest.length === 0) {
96
+ ctx.addItem({
97
+ kind: "info",
98
+ text: `Usage: /mcp add <name> <url> · /mcp add <name> -- <command> [args...]`,
99
+ });
100
+ return;
101
+ }
102
+ if (!/^[A-Za-z0-9_-]+$/.test(name)) {
103
+ ctx.addItem({
104
+ kind: "info",
105
+ text: `Invalid server name "${name}". Use letters, digits, "-" and "_" only — the name becomes part of each tool's name.`,
106
+ });
107
+ return;
108
+ }
109
+ let cfg;
110
+ if (rest[0] === "--") {
111
+ const [command, ...cmdArgs] = rest.slice(1);
112
+ if (!command) {
113
+ ctx.addItem({ kind: "info", text: "Usage: /mcp add <name> -- <command> [args...]" });
114
+ return;
115
+ }
116
+ cfg = { command, args: cmdArgs.length ? cmdArgs : undefined };
117
+ }
118
+ else {
119
+ const url = rest[0];
120
+ let parsed;
121
+ try {
122
+ parsed = new URL(url);
123
+ }
124
+ catch {
125
+ ctx.addItem({
126
+ kind: "info",
127
+ text: `"${url}" is not a valid URL. For a local server use: /mcp add ${name} -- <command> [args...]`,
128
+ });
129
+ return;
130
+ }
131
+ if (parsed.protocol !== "https:" &&
132
+ parsed.hostname !== "localhost" &&
133
+ parsed.hostname !== "127.0.0.1") {
134
+ ctx.addItem({
135
+ kind: "info",
136
+ text: `Refusing to add "${url}" over plain HTTP — an MCP session carries your credentials. Use https:// (localhost is exempt).`,
137
+ });
138
+ return;
139
+ }
140
+ cfg = { url };
141
+ }
142
+ const config = loadConfig();
143
+ const servers = { ...config.mcpServers, [name]: cfg };
144
+ saveConfig({ mcpServers: servers });
145
+ // Added by hand from this session, so it's the user's own decision — record
146
+ // the trust now rather than prompting about it on the next startup.
147
+ trustServer(name, serverFingerprint(cfg));
148
+ ctx.addItem({
149
+ kind: "info",
150
+ text: `Added "${name}" to ~/.kritya/config.json. Connecting…`,
151
+ });
152
+ void connectAndAttach(ctx, name, expandServerConfig(cfg));
153
+ }
154
+ async function removeServer(ctx, name) {
155
+ if (!name) {
156
+ ctx.addItem({ kind: "info", text: "Usage: /mcp remove <name>" });
157
+ return;
158
+ }
159
+ const config = loadConfig();
160
+ const cfg = config.mcpServers?.[name];
161
+ if (!cfg) {
162
+ const inProject = loadProjectMcpServers(ctx.workspace)?.[name];
163
+ ctx.addItem({
164
+ kind: "info",
165
+ text: inProject
166
+ ? `"${name}" comes from this workspace's .mcp.json, not your config — remove it from that file (it is checked into the repo).\n` +
167
+ `To stop it loading without touching the repo: /mcp trust revoke ${name}`
168
+ : `No server named "${name}" in ~/.kritya/config.json.`,
169
+ });
170
+ return;
171
+ }
172
+ const servers = { ...config.mcpServers };
173
+ delete servers[name];
174
+ saveConfig({ mcpServers: servers });
175
+ // A stored token for a server you just removed is a credential with no
176
+ // remaining purpose; revoke and delete it rather than leaving it behind.
177
+ let tokenNote = "";
178
+ if (cfg.url) {
179
+ const url = expandServerConfig(cfg).url;
180
+ const result = await logout(url);
181
+ if (result.hadToken) {
182
+ tokenNote = result.revoked
183
+ ? "\nIts saved token was revoked and deleted."
184
+ : "\nIts saved token was deleted locally (the server offers no revocation endpoint).";
185
+ }
186
+ }
187
+ // Leaving the approval behind would re-approve this exact config silently
188
+ // the next time any workspace declares it — the server is gone, so the
189
+ // standing permission should go with it.
190
+ const untrusted = revokeFingerprint(serverFingerprint(cfg));
191
+ disconnectServer(name);
192
+ // Withdraw before forgetting: forgetStatus releases the server's tool names,
193
+ // after which isToolOf can no longer identify them.
194
+ const removed = ctx.agent.removeTools(isToolOf(name));
195
+ forgetStatus(name);
196
+ ctx.addItem({
197
+ kind: "info",
198
+ text: `Removed "${name}" — ${removed} tool(s) withdrawn.${tokenNote}` +
199
+ (untrusted ? "\nIts approval was withdrawn too." : ""),
200
+ });
201
+ }
202
+ async function loginServer(ctx, name, flags) {
203
+ if (!name) {
204
+ ctx.addItem({ kind: "info", text: "Usage: /mcp login <name>" });
205
+ return;
206
+ }
207
+ const server = resolveServer(name, ctx.workspace);
208
+ if (!server) {
209
+ ctx.addItem({
210
+ kind: "info",
211
+ text: `No server named "${name}". See /mcp for the list, or add one with /mcp add <name> <url>.`,
212
+ });
213
+ return;
214
+ }
215
+ if (!server.cfg.url) {
216
+ ctx.addItem({
217
+ kind: "info",
218
+ text: `"${name}" is a local (stdio) server — it runs as your own user and has nothing to log in to.`,
219
+ });
220
+ return;
221
+ }
222
+ // A login never reaches connectServer, so the transport's own scheme check
223
+ // can't cover it — and this path is worse than a plain request, since it
224
+ // mints a durable token against the user's real account.
225
+ try {
226
+ assertSafeUrl(name, server.cfg.url);
227
+ }
228
+ catch (err) {
229
+ ctx.addItem({ kind: "info", text: err instanceof Error ? err.message : String(err) });
230
+ return;
231
+ }
232
+ const confirmed = flags.some((f) => f === "--yes" || f === "-y" || f === "confirm");
233
+ if (server.provenance === "project" && !confirmed) {
234
+ ctx.addItem({
235
+ kind: "info",
236
+ text: `"${name}" is declared by this workspace's .mcp.json, not by you.\n\n` +
237
+ ` ${server.cfg.url}\n\n` +
238
+ `Logging in opens your browser and grants this server a token for your\n` +
239
+ `real account there — it will keep working after this session ends.\n\n` +
240
+ `If you trust the repo, confirm with: /mcp login ${name} --yes`,
241
+ });
242
+ return;
243
+ }
244
+ // A project server that was never through the per-server trust gate (added by
245
+ // a pull after startup, say) must not reach a consent screen on --yes alone.
246
+ if (server.provenance === "project" && !isServerTrusted(serverFingerprint(server.cfg))) {
247
+ trustServer(name, serverFingerprint(server.cfg));
248
+ }
249
+ const status = mcpStatus().find((s) => s.name === name);
250
+ ctx.setActivity(`Signing in to ${name}…`);
251
+ try {
252
+ const login = await beginLogin({
253
+ serverName: name,
254
+ serverUrl: server.cfg.url,
255
+ resourceMetadataUrl: status?.authMetadataUrl,
256
+ });
257
+ ctx.addItem({
258
+ kind: "info",
259
+ text: login.browserOpened
260
+ ? `Opened your browser to sign in to "${name}".\n` +
261
+ `Approve the request there, then this session picks it up automatically.\n\n` +
262
+ `If the browser didn't open: ${login.authorizeUrl}`
263
+ : `No browser available here. Open this URL on any machine:\n\n` +
264
+ `${login.authorizeUrl}\n\n` +
265
+ `If the redirect can reach ${login.redirectUri} it completes by itself.\n` +
266
+ `Otherwise copy the "code" from the address bar and run:\n` +
267
+ ` /mcp code ${name} <code>`,
268
+ });
269
+ const auth = await login.completed;
270
+ ctx.addItem({
271
+ kind: "info",
272
+ text: `Signed in to "${name}"${auth.scope ? ` (scope: ${auth.scope})` : ""}. Connecting…`,
273
+ });
274
+ await connectAndAttach(ctx, name, server.cfg);
275
+ }
276
+ catch (err) {
277
+ ctx.addItem({
278
+ kind: "info",
279
+ text: `Login for "${name}" failed: ${err instanceof Error ? err.message : String(err)}`,
280
+ });
281
+ }
282
+ finally {
283
+ ctx.setActivity(null);
284
+ }
285
+ }
286
+ /**
287
+ * `/mcp trust` — inspect and withdraw per-server approvals.
288
+ *
289
+ * Trust is matched by fingerprint across every workspace, so an entry left
290
+ * behind for a server you've stopped using will silently approve it again the
291
+ * next time any repo declares the same config. Listing makes that visible;
292
+ * revoking is the only way to undo an approval short of editing the store.
293
+ */
294
+ function trustCommand(ctx, args) {
295
+ const sub = args[0]?.toLowerCase();
296
+ if (sub === undefined)
297
+ return listTrusted(ctx);
298
+ if (sub !== "revoke") {
299
+ ctx.addItem({ kind: "info", text: `Usage: /mcp trust · /mcp trust revoke <name>` });
300
+ return;
301
+ }
302
+ const name = args[1];
303
+ if (!name) {
304
+ ctx.addItem({ kind: "info", text: "Usage: /mcp trust revoke <name>" });
305
+ return;
306
+ }
307
+ const removed = revokeServer(name);
308
+ if (!removed.length) {
309
+ ctx.addItem({
310
+ kind: "info",
311
+ text: `"${name}" isn't in the trust list — nothing to revoke. See /mcp trust.`,
312
+ });
313
+ return;
314
+ }
315
+ // Revoking doesn't stop a server already running this session: it governs
316
+ // whether the next load asks again. Say so rather than implying it's dead.
317
+ const running = mcpStatus().find((s) => s.name === name && s.ok);
318
+ const note = running
319
+ ? `\n\nIt stays connected for this session — /mcp remove ${name} withdraws it now.`
320
+ : "";
321
+ ctx.addItem({
322
+ kind: "info",
323
+ text: `Revoked trust for "${name}" (${removed.length} entr${removed.length > 1 ? "ies" : "y"}).\n` +
324
+ `Any workspace declaring it will ask before loading it again.${note}`,
325
+ });
326
+ }
327
+ function listTrusted(ctx) {
328
+ const entries = loadMcpAllowlist();
329
+ if (!entries.length) {
330
+ ctx.addItem({
331
+ kind: "info",
332
+ text: "No MCP servers have been approved yet.\n\nServers you approve at startup are listed here.",
333
+ });
334
+ return;
335
+ }
336
+ const lines = entries
337
+ .slice()
338
+ .sort((a, b) => a.name.localeCompare(b.name))
339
+ .map((e) => {
340
+ const when = e.trustedAt.slice(0, 10);
341
+ return ` ${e.name} — approved ${when} (${e.fingerprint.slice(0, 12)})`;
342
+ });
343
+ ctx.addItem({
344
+ kind: "info",
345
+ text: `Approved MCP servers:\n${lines.join("\n")}\n\n` +
346
+ `Approval is matched by config fingerprint and applies in every workspace.\n` +
347
+ `Withdraw one with /mcp trust revoke <name>.`,
348
+ });
349
+ }
350
+ function submitCode(ctx, name, code) {
351
+ if (!name || !code) {
352
+ ctx.addItem({ kind: "info", text: "Usage: /mcp code <name> <code-or-redirect-url>" });
353
+ return;
354
+ }
355
+ const login = pendingLogin(name);
356
+ if (!login) {
357
+ ctx.addItem({
358
+ kind: "info",
359
+ text: `No login in progress for "${name}". Start one with /mcp login ${name}.`,
360
+ });
361
+ return;
362
+ }
363
+ login.submitCode(code);
364
+ ctx.addItem({ kind: "info", text: `Code accepted for "${name}" — exchanging it for a token…` });
365
+ }
366
+ async function logoutServer(ctx, name) {
367
+ if (!name) {
368
+ ctx.addItem({ kind: "info", text: "Usage: /mcp logout <name>" });
369
+ return;
370
+ }
371
+ const server = resolveServer(name, ctx.workspace);
372
+ if (!server?.cfg.url) {
373
+ ctx.addItem({
374
+ kind: "info",
375
+ text: `No remote server named "${name}" to log out of.`,
376
+ });
377
+ return;
378
+ }
379
+ const result = await logout(server.cfg.url);
380
+ if (!result.hadToken) {
381
+ ctx.addItem({ kind: "info", text: `No saved token for "${name}" — nothing to log out of.` });
382
+ return;
383
+ }
384
+ disconnectServer(name);
385
+ const removed = ctx.agent.removeTools(isToolOf(name));
386
+ replaceStatus({
387
+ name,
388
+ transport: "http",
389
+ target: server.cfg.url,
390
+ ok: false,
391
+ needsAuth: true,
392
+ error: `needs login — run /mcp login ${name}`,
393
+ tools: [],
394
+ prompts: [],
395
+ resources: [],
396
+ });
397
+ // Deleting a token locally is not the same as killing it, and only one of
398
+ // those is something the user can rely on. Say which happened.
399
+ ctx.addItem({
400
+ kind: "info",
401
+ text: result.revoked
402
+ ? `Logged out of "${name}" — token revoked with the server and deleted locally. ${removed} tool(s) withdrawn.`
403
+ : `Logged out of "${name}" — token deleted locally. ${removed} tool(s) withdrawn.\n` +
404
+ `⚠ This server offers no revocation endpoint, so the token stays valid on\n` +
405
+ ` their side until it expires. Remove kritya in that service's settings\n` +
406
+ ` if you need it dead now.`,
407
+ });
408
+ }
409
+ /** Connect (or reconnect) a server and hand its tools to the running agent. */
410
+ async function connectAndAttach(ctx, name, cfg) {
411
+ disconnectServer(name);
412
+ ctx.agent.removeTools(isToolOf(name));
413
+ const { tools, status } = await connectServer(name, cfg, {
414
+ tracer: ctx.agent.tracer,
415
+ audit: ctx.agent.audit,
416
+ workspace: ctx.workspace,
417
+ });
418
+ replaceStatus(status);
419
+ if (!status.ok) {
420
+ ctx.addItem({
421
+ kind: "info",
422
+ text: status.needsAuth
423
+ ? `"${name}" still needs a login — run /mcp login ${name}.`
424
+ : `"${name}" failed to connect: ${status.error}`,
425
+ });
426
+ return;
427
+ }
428
+ ctx.agent.addTools(tools);
429
+ ctx.addItem({
430
+ kind: "info",
431
+ text: `"${name}" connected — ${tools.length} tool(s) available now: ${status.tools.join(", ") || "(none)"}`,
432
+ });
433
+ }
@@ -0,0 +1,204 @@
1
+ import { scanAllPlugins, loadPluginMcpServers } from "../agent/plugins.js";
2
+ import { connectServer, mcpStatus, replaceStatus, disconnectServer, forgetStatus, isToolOf } from "../mcp/client.js";
3
+ /**
4
+ * The `/plugins` command family: list, show details, and manage plugins.
5
+ *
6
+ * Plugins are Agent Plugins packages that bundle skills and MCP servers
7
+ * into a portable format following the agent-plugins specification.
8
+ *
9
+ * @see https://agent-plugins.org/
10
+ */
11
+ const USAGE = `Usage:
12
+ /plugins list all discovered plugins
13
+ /plugins <name> show details for a specific plugin
14
+ /plugins <name> connect connect plugins MCP servers to kritya
15
+ /plugins <name> disconnect disconnect plugins MCP servers from kritya`;
16
+ export async function runPluginsCommand(ctx) {
17
+ const parts = ctx.arg.trim().split(/\s+/).filter(Boolean);
18
+ const sub = parts[0]?.toLowerCase();
19
+ switch (sub) {
20
+ case undefined:
21
+ return listPlugins(ctx);
22
+ case "connect":
23
+ return connectPlugin(ctx, parts[1]);
24
+ case "disconnect":
25
+ return disconnectPlugin(ctx, parts[1]);
26
+ default:
27
+ // If it's not a subcommand, treat it as a plugin name to show details
28
+ return showPlugin(ctx, sub);
29
+ }
30
+ }
31
+ function listPlugins(ctx) {
32
+ const { loaded: plugins } = scanAllPlugins(ctx.workspace);
33
+ if (!plugins.length) {
34
+ ctx.addItem({
35
+ kind: "info",
36
+ text: "No plugins found.\n\n" +
37
+ "Plugins are loaded from ~/.kritya/plugins (user-global) and .kritya/plugins (project).\n" +
38
+ "Create a plugin with a plugin.json manifest, skills/ directory, and optional mcp.json.",
39
+ });
40
+ return;
41
+ }
42
+ const lines = plugins.map((p) => {
43
+ const versionInfo = p.version ? ` v${p.version}` : "";
44
+ const desc = p.description ? ` - ${p.description}` : "";
45
+ return ` ${p.name}${versionInfo}${desc} (${p.id})`;
46
+ });
47
+ ctx.addItem({
48
+ kind: "info",
49
+ text: `Plugins:\n${lines.join("\n")}\n\n${USAGE}`,
50
+ });
51
+ }
52
+ async function showPlugin(ctx, pluginName) {
53
+ const { loaded: plugins } = scanAllPlugins(ctx.workspace);
54
+ const plugin = plugins.find((p) => p.name === pluginName || p.id === pluginName);
55
+ if (!plugin) {
56
+ ctx.addItem({
57
+ kind: "info",
58
+ text: `No plugin named "${pluginName}". Run /plugins to see available plugins.`,
59
+ });
60
+ return;
61
+ }
62
+ // Report skills - note that they are loaded from plugin.skillsDir
63
+ const skillsText = plugin.skillsDir
64
+ ? `Skills: Available from ${plugin.skillsDir}\n(Use /skills to list discovered skills including plugin skills)`
65
+ : "No skills directory";
66
+ // Load MCP servers from this plugin
67
+ const mcpServers = loadPluginMcpServers(plugin);
68
+ const mcpText = mcpServers
69
+ ? `MCP Servers (${Object.keys(mcpServers).length}):\n${Object.entries(mcpServers).map(([name, cfg]) => {
70
+ const target = typeof cfg.command === "string" ? `command: ${cfg.command}` : `url: ${cfg.url}`;
71
+ return ` - ${name}: ${target}`;
72
+ }).join("\n")}`
73
+ : "No MCP servers";
74
+ // Check if MCP servers are currently connected
75
+ const status = mcpStatus();
76
+ const connectedServers = status
77
+ .filter((s) => s.ok && Object.keys(mcpServers || {}).includes(s.name))
78
+ .map((s) => s.name);
79
+ const connectionState = connectedServers.length
80
+ ? `Connected servers: ${connectedServers.join(", ")}`
81
+ : "MCP servers not connected";
82
+ ctx.addItem({
83
+ kind: "info",
84
+ text: `Plugin: ${plugin.name} ${plugin.version ? `v${plugin.version}` : ""} (${plugin.id})
85
+ ${plugin.description ? `Description: ${plugin.description}` : ""}
86
+
87
+ ${skillsText}
88
+
89
+ ${mcpText}
90
+
91
+ ${connectionState}
92
+
93
+ ${USAGE}`,
94
+ });
95
+ }
96
+ async function connectPlugin(ctx, pluginName) {
97
+ if (!pluginName) {
98
+ ctx.addItem({ kind: "info", text: `Usage: /plugins <name> connect` });
99
+ return;
100
+ }
101
+ const { loaded: plugins } = scanAllPlugins(ctx.workspace);
102
+ const plugin = plugins.find((p) => p.name === pluginName || p.id === pluginName);
103
+ if (!plugin) {
104
+ ctx.addItem({
105
+ kind: "info",
106
+ text: `No plugin named "${pluginName}". Run /plugins to see available plugins.`,
107
+ });
108
+ return;
109
+ }
110
+ // Load MCP servers from this plugin
111
+ const mcpServers = loadPluginMcpServers(plugin);
112
+ if (!mcpServers || !Object.keys(mcpServers).length) {
113
+ ctx.addItem({
114
+ kind: "info",
115
+ text: `Plugin "${pluginName}" has no MCP servers to connect.`,
116
+ });
117
+ return;
118
+ }
119
+ // Connect each server with plugin-specific naming
120
+ const connected = [];
121
+ const failed = [];
122
+ for (const [name, cfg] of Object.entries(mcpServers)) {
123
+ // Namespace server name with plugin ID to avoid conflicts
124
+ const pluginNamespace = plugin.id.replace(/[^a-z0-9_-]/g, "_");
125
+ const serverName = `${pluginNamespace}:${name}`;
126
+ try {
127
+ const { tools, status } = await connectServer(serverName, cfg, {
128
+ tracer: ctx.agent.tracer,
129
+ audit: ctx.agent.audit,
130
+ workspace: ctx.workspace,
131
+ });
132
+ replaceStatus(status);
133
+ if (status.ok) {
134
+ ctx.agent.addTools(tools);
135
+ connected.push(serverName);
136
+ }
137
+ else {
138
+ failed.push({ name: serverName, error: status.error || "unknown error" });
139
+ }
140
+ }
141
+ catch (err) {
142
+ failed.push({ name: serverName, error: err instanceof Error ? err.message : String(err) });
143
+ }
144
+ }
145
+ let response = `Plugin "${pluginName}":\n`;
146
+ if (connected.length) {
147
+ response += `Connected ${connected.length} server(s): ${connected.join(", ")}\n`;
148
+ }
149
+ if (failed.length) {
150
+ response += `Failed to connect ${failed.length} server(s):\n`;
151
+ response += failed.map((f) => ` - ${f.name}: ${f.error}`).join("\n");
152
+ }
153
+ ctx.addItem({ kind: "info", text: response });
154
+ }
155
+ async function disconnectPlugin(ctx, pluginName) {
156
+ if (!pluginName) {
157
+ ctx.addItem({ kind: "info", text: `Usage: /plugins <name> disconnect` });
158
+ return;
159
+ }
160
+ const { loaded: plugins } = scanAllPlugins(ctx.workspace);
161
+ const plugin = plugins.find((p) => p.name === pluginName || p.id === pluginName);
162
+ if (!plugin) {
163
+ ctx.addItem({
164
+ kind: "info",
165
+ text: `No plugin named "${pluginName}". Run /plugins to see available plugins.`,
166
+ });
167
+ return;
168
+ }
169
+ // Load MCP servers from this plugin
170
+ const mcpServers = loadPluginMcpServers(plugin);
171
+ if (!mcpServers || !Object.keys(mcpServers).length) {
172
+ ctx.addItem({
173
+ kind: "info",
174
+ text: `Plugin "${pluginName}" has no MCP servers to disconnect.`,
175
+ });
176
+ return;
177
+ }
178
+ // Disconnect each server
179
+ const disconnected = [];
180
+ let removedTools = 0;
181
+ for (const [name] of Object.entries(mcpServers)) {
182
+ // Use the same namespace as connect
183
+ const pluginNamespace = plugin.id.replace(/[^a-z0-9_-]/g, "_");
184
+ const serverName = `${pluginNamespace}:${name}`;
185
+ disconnected.push(serverName);
186
+ // Get tools to remove
187
+ const status = mcpStatus().find((s) => s.name === serverName && s.ok);
188
+ if (status) {
189
+ removedTools += status.tools.length;
190
+ }
191
+ // Actually disconnect the server
192
+ disconnectServer(serverName);
193
+ // Remove tools from agent
194
+ ctx.agent.removeTools(isToolOf(serverName));
195
+ // Forget status so it doesn't remain visible
196
+ forgetStatus(serverName);
197
+ }
198
+ ctx.addItem({
199
+ kind: "info",
200
+ text: `Plugin "${pluginName}":\n` +
201
+ `Disconnected ${disconnected.length} server(s): ${disconnected.join(", ")}\n` +
202
+ `Removed ${removedTools} tool(s).`,
203
+ });
204
+ }