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.
- package/LICENSE +21 -0
- package/README.md +434 -0
- package/dist/agent/budget.js +25 -0
- package/dist/agent/compactor.js +78 -0
- package/dist/agent/contextWarning.js +6 -0
- package/dist/agent/killSwitch.js +111 -0
- package/dist/agent/loop.js +543 -0
- package/dist/agent/memory.js +145 -0
- package/dist/agent/plugins.js +408 -0
- package/dist/agent/skills.js +210 -0
- package/dist/agent/skillsCli.js +51 -0
- package/dist/agent/systemPrompt.js +122 -0
- package/dist/agent/tokens.js +55 -0
- package/dist/agent/toolExecutor.js +331 -0
- package/dist/agent/workflow.js +496 -0
- package/dist/agent/worktree.js +116 -0
- package/dist/atomicWrite.js +109 -0
- package/dist/audit/audit.js +230 -0
- package/dist/audit/cli.js +135 -0
- package/dist/commands/custom.js +69 -0
- package/dist/commands/mcpCommand.js +433 -0
- package/dist/commands/pluginsCommand.js +204 -0
- package/dist/commands/registry.js +739 -0
- package/dist/config/config.js +214 -0
- package/dist/config/debug.js +19 -0
- package/dist/config/models.js +82 -0
- package/dist/config/retention.js +24 -0
- package/dist/config/winAcl.js +38 -0
- package/dist/crash.js +102 -0
- package/dist/engine.js +113 -0
- package/dist/git/git.js +46 -0
- package/dist/headless.js +274 -0
- package/dist/hooks/hooks.js +122 -0
- package/dist/index.js +670 -0
- package/dist/lsp/client.js +413 -0
- package/dist/lsp/manager.js +0 -0
- package/dist/lsp/registry.js +62 -0
- package/dist/mcp/callback.js +141 -0
- package/dist/mcp/client.js +945 -0
- package/dist/mcp/login.js +117 -0
- package/dist/mcp/oauth.js +345 -0
- package/dist/mcp/servers.js +114 -0
- package/dist/mcp/spawnWin.js +86 -0
- package/dist/mcp/tokens.js +97 -0
- package/dist/mcp/transport.js +295 -0
- package/dist/net/urlSafety.js +158 -0
- package/dist/permissions/danger.js +96 -0
- package/dist/permissions/permissions.js +62 -0
- package/dist/permissions/rules.js +69 -0
- package/dist/plugins/discover.js +108 -0
- package/dist/plugins/mcp.js +80 -0
- package/dist/provider/client.js +460 -0
- package/dist/provider/switchyardClient.js +46 -0
- package/dist/provider/switchyardSidecar.js +245 -0
- package/dist/provider/textToolCalls.js +140 -0
- package/dist/repomap/repoMap.js +168 -0
- package/dist/repomap/symbols.js +190 -0
- package/dist/session/store.js +328 -0
- package/dist/shell/background.js +131 -0
- package/dist/shell/sandbox.js +396 -0
- package/dist/telemetry/metrics.js +172 -0
- package/dist/telemetry/otlp.js +128 -0
- package/dist/telemetry/tracer.js +240 -0
- package/dist/tools/askUser.js +64 -0
- package/dist/tools/bg.js +59 -0
- package/dist/tools/common.js +158 -0
- package/dist/tools/deepResearch.js +126 -0
- package/dist/tools/diff.js +70 -0
- package/dist/tools/document/docx.js +44 -0
- package/dist/tools/document/pdf.js +190 -0
- package/dist/tools/document/pptx.js +148 -0
- package/dist/tools/document/types.js +26 -0
- package/dist/tools/document/xlsx.js +87 -0
- package/dist/tools/document.js +389 -0
- package/dist/tools/edit.js +56 -0
- package/dist/tools/fetchUrl.js +218 -0
- package/dist/tools/fuzzyMatch.js +74 -0
- package/dist/tools/glob.js +34 -0
- package/dist/tools/grep.js +85 -0
- package/dist/tools/ignore.js +28 -0
- package/dist/tools/index.js +66 -0
- package/dist/tools/ls.js +27 -0
- package/dist/tools/lsp.js +239 -0
- package/dist/tools/notebook.js +215 -0
- package/dist/tools/read.js +32 -0
- package/dist/tools/repoMap.js +26 -0
- package/dist/tools/secretScan.js +143 -0
- package/dist/tools/shell.js +134 -0
- package/dist/tools/skills.js +69 -0
- package/dist/tools/subagent.js +57 -0
- package/dist/tools/tasks.js +43 -0
- package/dist/tools/webSearch.js +97 -0
- package/dist/tools/write.js +47 -0
- package/dist/tools/writeAgent.js +72 -0
- package/dist/trust/aiDisclosure.js +45 -0
- package/dist/trust/mcpTrust.js +111 -0
- package/dist/trust/trust.js +231 -0
- package/dist/types.js +1 -0
- package/dist/ui/AiDisclosurePrompt.js +23 -0
- package/dist/ui/App.js +449 -0
- package/dist/ui/Banner.js +71 -0
- package/dist/ui/ElicitationPrompt.js +46 -0
- package/dist/ui/Markdown.js +148 -0
- package/dist/ui/McpTrustPrompt.js +42 -0
- package/dist/ui/ModelPicker.js +19 -0
- package/dist/ui/PermissionPrompt.js +27 -0
- package/dist/ui/SelectList.js +24 -0
- package/dist/ui/Spinner.js +12 -0
- package/dist/ui/StatusLine.js +11 -0
- package/dist/ui/TranscriptItem.js +13 -0
- package/dist/ui/TrustPrompt.js +12 -0
- package/dist/ui/highlight.js +137 -0
- package/dist/ui/inline.js +241 -0
- package/dist/ui/mermaid.js +98 -0
- package/dist/ui/table.js +148 -0
- package/dist/ui/toolOutputPreview.js +34 -0
- package/dist/ui/useAgent.js +534 -0
- package/dist/ui/useKillSwitch.js +65 -0
- package/dist/ui/useSessionResume.js +39 -0
- package/dist/ui/useUsageBudget.js +149 -0
- package/dist/ui/viewport.js +71 -0
- package/dist/undo/undo.js +293 -0
- package/dist/version.js +4 -0
- package/package.json +122 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,670 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { render } from "ink";
|
|
6
|
+
import { Agent } from "./agent/loop.js";
|
|
7
|
+
import { CONFIG_DIR, legacyGlobalModel, loadConfig, loadDotEnv, resolveProvider, } from "./config/config.js";
|
|
8
|
+
import { DEFAULT_MODEL, contextWindowFor } from "./config/models.js";
|
|
9
|
+
import { PermissionManager } from "./permissions/permissions.js";
|
|
10
|
+
import { loadRules } from "./permissions/rules.js";
|
|
11
|
+
import { ProviderClient } from "./provider/client.js";
|
|
12
|
+
import { createSwitchyardClient } from "./provider/switchyardClient.js";
|
|
13
|
+
import { SWITCHYARD_ROUTE_ID, resolveEffectiveModel, staleSwitchyardModelWarning, } from "./provider/switchyardSidecar.js";
|
|
14
|
+
import { SessionStore } from "./session/store.js";
|
|
15
|
+
import { AuditLog } from "./audit/audit.js";
|
|
16
|
+
import { runAuditCli } from "./audit/cli.js";
|
|
17
|
+
import { runSkillsCli } from "./agent/skillsCli.js";
|
|
18
|
+
import { createTracer, cleanupOldTelemetry } from "./telemetry/tracer.js";
|
|
19
|
+
import { createMeter } from "./telemetry/metrics.js";
|
|
20
|
+
import { retentionDaysFor } from "./config/retention.js";
|
|
21
|
+
import { backgroundManager } from "./shell/background.js";
|
|
22
|
+
import { defaultSandboxMode, sandboxAvailable, sandboxUnavailableReason } from "./shell/sandbox.js";
|
|
23
|
+
import { lspManager } from "./lsp/manager.js";
|
|
24
|
+
import { ALL_TOOLS, READONLY_TOOLS } from "./tools/index.js";
|
|
25
|
+
import { UndoStack } from "./undo/undo.js";
|
|
26
|
+
import { App } from "./ui/App.js";
|
|
27
|
+
import { TrustPrompt } from "./ui/TrustPrompt.js";
|
|
28
|
+
import { AiDisclosurePrompt } from "./ui/AiDisclosurePrompt.js";
|
|
29
|
+
import { McpTrustPrompt } from "./ui/McpTrustPrompt.js";
|
|
30
|
+
import { loadHooks, HookRunner } from "./hooks/hooks.js";
|
|
31
|
+
import { loadMcpTools, shutdownMcp, } from "./mcp/client.js";
|
|
32
|
+
import { loadProjectMcpServers, mergeMcpServers } from "./mcp/servers.js";
|
|
33
|
+
import { loadCustomCommands } from "./commands/custom.js";
|
|
34
|
+
import { pluginsDir, scanPlugins, userPluginsDir } from "./plugins/discover.js";
|
|
35
|
+
import { loadPluginMcpServers } from "./plugins/mcp.js";
|
|
36
|
+
import { describeGatedContent, gatedContentHash, isTrusted, saveTrust } from "./trust/trust.js";
|
|
37
|
+
import { isAiDisclosureShown, markAiDisclosureShown } from "./trust/aiDisclosure.js";
|
|
38
|
+
import { partitionByTrust, serverFingerprint, trustServer } from "./trust/mcpTrust.js";
|
|
39
|
+
import { runHeadless } from "./headless.js";
|
|
40
|
+
import { installCrashHandlers } from "./crash.js";
|
|
41
|
+
import { createWorktree, commitWorktree, worktreeDiffStat, removeWorktree, isGitRepo, } from "./agent/worktree.js";
|
|
42
|
+
import { VERSION } from "./version.js";
|
|
43
|
+
const USAGE = `kritya — a lean, provider-agnostic terminal coding agent
|
|
44
|
+
|
|
45
|
+
Usage: kritya [directory] [options]
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
-c, --continue resume the most recent session for this directory
|
|
49
|
+
-r, --resume pick a past session for this directory from a list
|
|
50
|
+
-m, --model <id> model ID to use (any model your provider offers)
|
|
51
|
+
-p, --provider <n> provider: nvidia (default), openai, openrouter, groq,
|
|
52
|
+
deepseek, mistral, together, ollama, or a custom one
|
|
53
|
+
-h, --help show this help
|
|
54
|
+
-v, --version show version
|
|
55
|
+
|
|
56
|
+
Headless / CI mode (no terminal UI, exits with 0 on success / 1 on failure):
|
|
57
|
+
--prompt <text> run this one prompt to completion, then exit
|
|
58
|
+
--output <fmt> text (default) or json — a single JSON object on stdout
|
|
59
|
+
with {success, result, error, toolCalls, usage, durationMs, model}
|
|
60
|
+
--allow-all auto-approve tool calls that would otherwise prompt
|
|
61
|
+
(destructive commands are still always denied — there's
|
|
62
|
+
no terminal to confirm them)
|
|
63
|
+
--trust trust the workspace's own .kritya/settings.json allow
|
|
64
|
+
rules, hooks, .env, and custom commands (off by default,
|
|
65
|
+
since CI often checks out untrusted branches/PRs)
|
|
66
|
+
--timeout <seconds> hard wall-clock cap for the whole run (default 1800)
|
|
67
|
+
--non-interactive accepted for compatibility; implied by --prompt
|
|
68
|
+
|
|
69
|
+
Inspect the local audit log:
|
|
70
|
+
kritya audit --list | --verify [file] | --show [file]
|
|
71
|
+
|
|
72
|
+
List and validate skills:
|
|
73
|
+
kritya skills [dir] [--json] [--validate]
|
|
74
|
+
|
|
75
|
+
Setup:
|
|
76
|
+
1. Get an API key at https://build.nvidia.com (free credits available)
|
|
77
|
+
2. export NVIDIA_API_KEY=nvapi-... (Linux/macOS)
|
|
78
|
+
setx NVIDIA_API_KEY nvapi-... (Windows)
|
|
79
|
+
3. cd your-project && kritya .
|
|
80
|
+
|
|
81
|
+
Config file: ~/.kritya/config.json { "apiKey", "model", "customModels": [{"id"}] }`;
|
|
82
|
+
function parseArgs(argv) {
|
|
83
|
+
const args = {
|
|
84
|
+
dir: ".",
|
|
85
|
+
continue: false,
|
|
86
|
+
resume: false,
|
|
87
|
+
model: "",
|
|
88
|
+
provider: "",
|
|
89
|
+
help: false,
|
|
90
|
+
version: false,
|
|
91
|
+
prompt: "",
|
|
92
|
+
output: "text",
|
|
93
|
+
allowAll: false,
|
|
94
|
+
trust: false,
|
|
95
|
+
timeoutSeconds: 1800,
|
|
96
|
+
};
|
|
97
|
+
for (let i = 0; i < argv.length; i++) {
|
|
98
|
+
const a = argv[i];
|
|
99
|
+
if (a === "-c" || a === "--continue")
|
|
100
|
+
args.continue = true;
|
|
101
|
+
else if (a === "-r" || a === "--resume")
|
|
102
|
+
args.resume = true;
|
|
103
|
+
else if (a === "-m" || a === "--model")
|
|
104
|
+
args.model = argv[++i] ?? "";
|
|
105
|
+
else if (a === "-p" || a === "--provider")
|
|
106
|
+
args.provider = argv[++i] ?? "";
|
|
107
|
+
else if (a === "-h" || a === "--help")
|
|
108
|
+
args.help = true;
|
|
109
|
+
else if (a === "-v" || a === "--version")
|
|
110
|
+
args.version = true;
|
|
111
|
+
else if (a === "--prompt")
|
|
112
|
+
args.prompt = argv[++i] ?? "";
|
|
113
|
+
else if (a === "--output") {
|
|
114
|
+
const v = argv[++i] ?? "";
|
|
115
|
+
if (v !== "text" && v !== "json") {
|
|
116
|
+
console.error(`--output must be "text" or "json", got "${v}"`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
args.output = v;
|
|
120
|
+
}
|
|
121
|
+
else if (a === "--allow-all")
|
|
122
|
+
args.allowAll = true;
|
|
123
|
+
else if (a === "--trust")
|
|
124
|
+
args.trust = true;
|
|
125
|
+
else if (a === "--timeout")
|
|
126
|
+
args.timeoutSeconds = Number(argv[++i]) || args.timeoutSeconds;
|
|
127
|
+
else if (a === "--non-interactive") {
|
|
128
|
+
// implied by --prompt; accepted so scripts can pass it explicitly
|
|
129
|
+
}
|
|
130
|
+
else if (!a.startsWith("-"))
|
|
131
|
+
args.dir = a;
|
|
132
|
+
else {
|
|
133
|
+
console.error(`Unknown option: ${a}\n\n${USAGE}`);
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return args;
|
|
138
|
+
}
|
|
139
|
+
// `kritya audit ...` is a standalone inspection subcommand, not a session — it
|
|
140
|
+
// never touches a workspace directory, so it's dispatched before the normal
|
|
141
|
+
// directory-based argv parsing below even looks at it.
|
|
142
|
+
if (process.argv[2] === "audit") {
|
|
143
|
+
process.exit(runAuditCli(process.argv.slice(3)));
|
|
144
|
+
}
|
|
145
|
+
// `kritya skills` is likewise a standalone inspection subcommand, dispatched
|
|
146
|
+
// the same way and for the same reason.
|
|
147
|
+
if (process.argv[2] === "skills") {
|
|
148
|
+
process.exit(runSkillsCli(process.argv.slice(3)));
|
|
149
|
+
}
|
|
150
|
+
const args = parseArgs(process.argv.slice(2));
|
|
151
|
+
if (args.help) {
|
|
152
|
+
console.log(USAGE);
|
|
153
|
+
process.exit(0);
|
|
154
|
+
}
|
|
155
|
+
if (args.version) {
|
|
156
|
+
console.log(VERSION);
|
|
157
|
+
process.exit(0);
|
|
158
|
+
}
|
|
159
|
+
const workspace = path.resolve(args.dir);
|
|
160
|
+
if (!fs.existsSync(workspace) || !fs.statSync(workspace).isDirectory()) {
|
|
161
|
+
console.error(`Not a directory: ${workspace}`);
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
// Headless mode: run one prompt to completion with no terminal UI at all, and
|
|
165
|
+
// no TTY requirement — this is what makes `kritya --prompt "..."` usable from
|
|
166
|
+
// CI/scripts. Everything else in this file (the TTY check, the Ink app) is
|
|
167
|
+
// only for the interactive path, so it's skipped entirely here.
|
|
168
|
+
if (args.prompt) {
|
|
169
|
+
runHeadless({
|
|
170
|
+
dir: args.dir,
|
|
171
|
+
prompt: args.prompt,
|
|
172
|
+
provider: args.provider,
|
|
173
|
+
model: args.model,
|
|
174
|
+
continue: args.continue,
|
|
175
|
+
output: args.output,
|
|
176
|
+
allowAll: args.allowAll,
|
|
177
|
+
trust: args.trust,
|
|
178
|
+
timeoutSeconds: args.timeoutSeconds,
|
|
179
|
+
}).then((code) => process.exit(code));
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
runInteractive();
|
|
183
|
+
}
|
|
184
|
+
function runInteractive() {
|
|
185
|
+
// Only the user's own global .env (~/.kritya/.env) is unconditionally
|
|
186
|
+
// trusted. The workspace's .env can be authored by whoever's repo this is,
|
|
187
|
+
// so loading it is gated behind the workspace trust prompt below (see
|
|
188
|
+
// resolveWorkspaceTrust).
|
|
189
|
+
loadDotEnv([path.join(CONFIG_DIR, ".env")]);
|
|
190
|
+
if (!process.stdin.isTTY) {
|
|
191
|
+
console.error("kritya is interactive and requires a TTY.");
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
void main();
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Whether the workspace's .kritya/settings.json `allow` rules, `hooks`,
|
|
198
|
+
* `.env` file, and `.kritya/commands/*.md` may take effect. Prompts (via a
|
|
199
|
+
* standalone Ink screen) only when that content actually exists and hasn't
|
|
200
|
+
* already been trusted.
|
|
201
|
+
*/
|
|
202
|
+
async function resolveWorkspaceTrust() {
|
|
203
|
+
const hash = gatedContentHash(workspace);
|
|
204
|
+
if (!hash)
|
|
205
|
+
return true;
|
|
206
|
+
if (isTrusted(workspace, hash))
|
|
207
|
+
return true;
|
|
208
|
+
const preview = describeGatedContent(workspace);
|
|
209
|
+
return new Promise((resolve) => {
|
|
210
|
+
const instance = render(_jsx(TrustPrompt, { workspace: workspace, preview: preview, onDecision: (trust) => {
|
|
211
|
+
if (trust)
|
|
212
|
+
saveTrust(workspace, hash);
|
|
213
|
+
instance.unmount();
|
|
214
|
+
resolve(trust);
|
|
215
|
+
} }));
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Whether each server named in the workspace's .mcp.json may be loaded.
|
|
220
|
+
* Workspace trust above only gates the file as a whole; this adds a second,
|
|
221
|
+
* per-server gate so a later edit to .mcp.json (e.g. a new server added by a
|
|
222
|
+
* `git pull` or a malicious PR branch) doesn't silently inherit trust from
|
|
223
|
+
* servers the user already reviewed. Already-approved servers (by
|
|
224
|
+
* fingerprint, across all workspaces) pass straight through.
|
|
225
|
+
*
|
|
226
|
+
* Pending servers are decided one at a time: a batch approval makes refusing
|
|
227
|
+
* one server cost you the others, which is how people end up approving things
|
|
228
|
+
* they wouldn't have alone.
|
|
229
|
+
*/
|
|
230
|
+
async function resolveMcpServerTrust(projectMcp) {
|
|
231
|
+
if (!projectMcp)
|
|
232
|
+
return undefined;
|
|
233
|
+
const { trusted, pending } = partitionByTrust(projectMcp);
|
|
234
|
+
if (Object.keys(pending).length === 0)
|
|
235
|
+
return trusted;
|
|
236
|
+
return new Promise((resolve) => {
|
|
237
|
+
const instance = render(_jsx(McpTrustPrompt, { servers: pending, onComplete: (approved) => {
|
|
238
|
+
const loaded = { ...trusted };
|
|
239
|
+
for (const name of approved) {
|
|
240
|
+
trustServer(name, serverFingerprint(pending[name]));
|
|
241
|
+
loaded[name] = pending[name];
|
|
242
|
+
}
|
|
243
|
+
instance.unmount();
|
|
244
|
+
resolve(loaded);
|
|
245
|
+
} }));
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Shown once per workspace, on the interactive path only. Persisted in
|
|
250
|
+
* ~/.kritya/ai-disclosure.json, keyed by resolved workspace path — see
|
|
251
|
+
* isAiDisclosureShown/markAiDisclosureShown.
|
|
252
|
+
*/
|
|
253
|
+
async function showAiDisclosureNotice() {
|
|
254
|
+
return new Promise((resolve) => {
|
|
255
|
+
const instance = render(_jsx(AiDisclosurePrompt, { onDismiss: () => {
|
|
256
|
+
instance.clear();
|
|
257
|
+
instance.unmount();
|
|
258
|
+
resolve();
|
|
259
|
+
} }));
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
async function main() {
|
|
263
|
+
const config = loadConfig();
|
|
264
|
+
if (!isAiDisclosureShown(workspace)) {
|
|
265
|
+
await showAiDisclosureNotice();
|
|
266
|
+
markAiDisclosureShown(workspace);
|
|
267
|
+
}
|
|
268
|
+
const trustWorkspace = await resolveWorkspaceTrust();
|
|
269
|
+
if (trustWorkspace) {
|
|
270
|
+
// Only the workspace's own .env — it's part of the trust-gated content.
|
|
271
|
+
// The launch directory's .env is deliberately NOT loaded: it can belong to
|
|
272
|
+
// an unrelated (possibly cloned) repo and is invisible to the trust hash.
|
|
273
|
+
loadDotEnv([path.join(workspace, ".env")]);
|
|
274
|
+
}
|
|
275
|
+
const provider = resolveProvider(config, args.provider || undefined);
|
|
276
|
+
const apiKey = provider.apiKey;
|
|
277
|
+
if (!apiKey) {
|
|
278
|
+
const hint = provider.name === "nvidia"
|
|
279
|
+
? `Get one at https://build.nvidia.com, then one of:\n` +
|
|
280
|
+
` put NVIDIA_API_KEY=nvapi-... in a .env file (workspace or ~/.kritya/.env)\n` +
|
|
281
|
+
` export NVIDIA_API_KEY=nvapi-...\n` +
|
|
282
|
+
` add "apiKey" to ~/.kritya/config.json`
|
|
283
|
+
: `Set the API key for provider "${provider.name}" via its env var or a .env file,\n` +
|
|
284
|
+
` or add it under providers.${provider.name}.apiKey in ~/.kritya/config.json`;
|
|
285
|
+
console.error(`No API key found for provider "${provider.name}".\n\n${hint}`);
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
const sandboxMode = config.sandboxExec ?? defaultSandboxMode();
|
|
289
|
+
if (sandboxMode !== "off" && !sandboxAvailable()) {
|
|
290
|
+
console.error(`⚠ sandboxExec is "${sandboxMode}" but ${sandboxUnavailableReason()}. ` +
|
|
291
|
+
`Shell commands will run WITHOUT sandbox isolation this session — each one will ` +
|
|
292
|
+
`say so in its output.`);
|
|
293
|
+
}
|
|
294
|
+
const providerDefaultModel = config.providers?.[provider.name]?.model;
|
|
295
|
+
const modelRef = {
|
|
296
|
+
current: resolveEffectiveModel(provider.name, [args.model, providerDefaultModel, legacyGlobalModel(config, provider.name)], provider.name === "switchyard" ? SWITCHYARD_ROUTE_ID : DEFAULT_MODEL),
|
|
297
|
+
};
|
|
298
|
+
const staleModelWarning = staleSwitchyardModelWarning(provider.name, modelRef.current, args.model);
|
|
299
|
+
if (staleModelWarning)
|
|
300
|
+
console.error(staleModelWarning);
|
|
301
|
+
const providerRef = { current: provider.name };
|
|
302
|
+
// Mutable so /provider can swap the active client mid-session (fallback
|
|
303
|
+
// when a provider keeps timing out or rate-limiting — see
|
|
304
|
+
// RetryExhaustedError) without losing the conversation. runReadOnlyAgent /
|
|
305
|
+
// runWriteAgent below read this variable at call time, so subagents spawned
|
|
306
|
+
// after a switch pick up the new provider too.
|
|
307
|
+
const sampling = {
|
|
308
|
+
temperature: provider.temperature,
|
|
309
|
+
topP: provider.topP,
|
|
310
|
+
maxTokens: provider.maxTokens,
|
|
311
|
+
};
|
|
312
|
+
let client = provider.name === "switchyard"
|
|
313
|
+
? await createSwitchyardClient(apiKey, sampling)
|
|
314
|
+
: new ProviderClient(apiKey, provider.baseUrl, sampling);
|
|
315
|
+
const session = new SessionStore(workspace);
|
|
316
|
+
// Shared by the main agent and every subagent it spawns, so a write
|
|
317
|
+
// subagent's commits and a read-only subagent's tool calls land in the same
|
|
318
|
+
// audit trail and trace tree as the turn that spawned them — an agent that
|
|
319
|
+
// edits the repo should never do so off the record.
|
|
320
|
+
const sessionAudit = AuditLog.forSession(session.id, config.audit);
|
|
321
|
+
const sessionTracer = createTracer(session.id, config.otel);
|
|
322
|
+
const sessionMeter = createMeter(session.id, config.otel);
|
|
323
|
+
const initialHistory = args.continue ? (SessionStore.loadLatest(workspace) ?? []) : [];
|
|
324
|
+
const initialTasks = args.continue ? SessionStore.loadLatestTasks(workspace) : [];
|
|
325
|
+
session.start(initialHistory);
|
|
326
|
+
const resumeSessions = args.resume ? SessionStore.listSessions(workspace) : [];
|
|
327
|
+
// Filled in once the app is mounted, below; the crash handler needs a way to
|
|
328
|
+
// tear the UI down and is installed before there is a UI to tear down.
|
|
329
|
+
const ui = {};
|
|
330
|
+
const cleanup = () => {
|
|
331
|
+
backgroundManager.killAll();
|
|
332
|
+
lspManager.disposeAll();
|
|
333
|
+
shutdownMcp();
|
|
334
|
+
undoStack.closeAll();
|
|
335
|
+
sessionMeter.flush();
|
|
336
|
+
sessionMeter.stop();
|
|
337
|
+
};
|
|
338
|
+
// Node ignores async work started inside an "exit" handler, so this last-
|
|
339
|
+
// resort fallback path stays on the synchronous, fire-and-forget flush()
|
|
340
|
+
// inside cleanup() — it's best-effort only, not guaranteed to land.
|
|
341
|
+
process.on("exit", cleanup);
|
|
342
|
+
// Same reasoning as the signal handlers below, for the other way the process
|
|
343
|
+
// can die without firing "exit": an error nothing caught. Also hands the
|
|
344
|
+
// terminal back — Ink leaves it in raw mode with the cursor hidden.
|
|
345
|
+
installCrashHandlers({
|
|
346
|
+
cleanup,
|
|
347
|
+
restoreTerminal: true,
|
|
348
|
+
unmountUi: () => ui.instance?.unmount(),
|
|
349
|
+
details: () => {
|
|
350
|
+
const file = session.path;
|
|
351
|
+
return file
|
|
352
|
+
? ["", `The conversation was saved to ${file}`, `Resume it with: kritya -c ${workspace}`]
|
|
353
|
+
: [];
|
|
354
|
+
},
|
|
355
|
+
});
|
|
356
|
+
// Default signal handling terminates WITHOUT firing "exit", which would
|
|
357
|
+
// orphan background dev servers and MCP children (e.g. when the terminal
|
|
358
|
+
// window closes → SIGHUP). Clean up, then exit with the conventional code.
|
|
359
|
+
// Give the final metrics export a real chance to land first (capped so a
|
|
360
|
+
// hung collector can't stall shutdown) — cleanup()'s own sessionMeter.flush()
|
|
361
|
+
// is fire-and-forget and would otherwise usually be discarded by the
|
|
362
|
+
// process.exit() that follows it.
|
|
363
|
+
for (const [sig, code] of [
|
|
364
|
+
["SIGTERM", 143],
|
|
365
|
+
["SIGHUP", 129],
|
|
366
|
+
]) {
|
|
367
|
+
process.on(sig, () => {
|
|
368
|
+
void (async () => {
|
|
369
|
+
await Promise.race([
|
|
370
|
+
sessionMeter.flushAndWait(),
|
|
371
|
+
new Promise((resolve) => setTimeout(resolve, 2000).unref()),
|
|
372
|
+
]);
|
|
373
|
+
cleanup();
|
|
374
|
+
process.exit(code);
|
|
375
|
+
})();
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
// Best-effort retention: session transcripts, audit logs, and telemetry can
|
|
379
|
+
// all carry secrets that passed through tool output, so none of them
|
|
380
|
+
// accumulate forever by default. retentionDaysFor honors config.json's
|
|
381
|
+
// retentionDays / KRITYA_RETENTION_DAYS; 0 or negative disables this.
|
|
382
|
+
const retentionDays = retentionDaysFor(config);
|
|
383
|
+
SessionStore.cleanupOldSessions(retentionDays);
|
|
384
|
+
AuditLog.cleanupOld(retentionDays);
|
|
385
|
+
cleanupOldTelemetry(retentionDays);
|
|
386
|
+
const undoStack = new UndoStack();
|
|
387
|
+
const uiBridge = {
|
|
388
|
+
onTasksUpdate: (_tasks) => { },
|
|
389
|
+
onExternalEdit: (_relPath) => { },
|
|
390
|
+
};
|
|
391
|
+
undoStack.onExternalChange = (relPath) => uiBridge.onExternalEdit?.(relPath);
|
|
392
|
+
// MCP servers (if any) contribute extra tools; loading is resilient.
|
|
393
|
+
// Project-level .mcp.json is part of the workspace trust gate: it launches
|
|
394
|
+
// processes / contacts endpoints with the user's credentials, so it only
|
|
395
|
+
// takes effect once the workspace is trusted.
|
|
396
|
+
const projectMcp = trustWorkspace ? loadProjectMcpServers(workspace) : undefined;
|
|
397
|
+
const approvedProjectMcp = await resolveMcpServerTrust(projectMcp);
|
|
398
|
+
// Agent Plugins (.kritya/plugins/, ~/.kritya/plugins/). The workspace's own
|
|
399
|
+
// plugins/ folder is part of the same trust gate as .mcp.json above -- a
|
|
400
|
+
// plugin dropped into a cloned repo is just as capable of declaring a
|
|
401
|
+
// server that runs on the user's behalf. User-global plugins are always
|
|
402
|
+
// discovered, same as ~/.kritya/config.json.
|
|
403
|
+
const plugins = scanPlugins(trustWorkspace ? [pluginsDir(workspace), userPluginsDir()] : [userPluginsDir()]);
|
|
404
|
+
const { servers: pluginMcp } = loadPluginMcpServers(plugins);
|
|
405
|
+
const approvedPluginMcp = await resolveMcpServerTrust(Object.keys(pluginMcp).length ? pluginMcp : undefined);
|
|
406
|
+
// Filled in once <App> mounts (below) — sampling requests only ever arrive
|
|
407
|
+
// after that, so the callback can safely read it lazily at call time (same
|
|
408
|
+
// pattern as modelRef/providerRef above).
|
|
409
|
+
const permissionRef = {};
|
|
410
|
+
const samplingApprovedServers = new Set();
|
|
411
|
+
const onSampling = async (server, req) => {
|
|
412
|
+
if (!samplingApprovedServers.has(server)) {
|
|
413
|
+
if (!permissionRef.current)
|
|
414
|
+
return { ok: false, reason: "sampling approval unavailable" };
|
|
415
|
+
const decision = await permissionRef.current(`mcp:sampling:${server}`, `Server "${server}" wants to use your model to generate text.`);
|
|
416
|
+
if (decision === "always")
|
|
417
|
+
samplingApprovedServers.add(server);
|
|
418
|
+
else if (decision !== "yes")
|
|
419
|
+
return { ok: false, reason: "user declined" };
|
|
420
|
+
}
|
|
421
|
+
try {
|
|
422
|
+
const result = await client.complete(modelRef.current, [
|
|
423
|
+
...(req.systemPrompt ? [{ role: "system", content: req.systemPrompt }] : []),
|
|
424
|
+
...req.messages.map((m) => ({ role: m.role, content: m.content })),
|
|
425
|
+
], req.maxTokens);
|
|
426
|
+
return {
|
|
427
|
+
ok: true,
|
|
428
|
+
role: "assistant",
|
|
429
|
+
content: result.text,
|
|
430
|
+
model: result.model,
|
|
431
|
+
stopReason: result.stopReason,
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
catch (err) {
|
|
435
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
436
|
+
}
|
|
437
|
+
};
|
|
438
|
+
const elicitationRef = {};
|
|
439
|
+
const onElicitation = async (server, message, fields) => {
|
|
440
|
+
if (!elicitationRef.current)
|
|
441
|
+
return { action: "cancel" };
|
|
442
|
+
return elicitationRef.current(`[MCP: ${server}] ${message}`, fields);
|
|
443
|
+
};
|
|
444
|
+
// Same prompt UI as MCP elicitation, but for the agent's own ask_user tool —
|
|
445
|
+
// no "[MCP: server]" prefix, since the question is the agent's, not a
|
|
446
|
+
// third-party server's.
|
|
447
|
+
const onAskUser = async (message, fields) => {
|
|
448
|
+
if (!elicitationRef.current)
|
|
449
|
+
return { action: "cancel" };
|
|
450
|
+
return elicitationRef.current(message, fields);
|
|
451
|
+
};
|
|
452
|
+
const mcpTools = await loadMcpTools(mergeMcpServers(config.mcpServers, approvedProjectMcp, approvedPluginMcp), { tracer: sessionTracer, audit: sessionAudit, workspace, onSampling, onElicitation });
|
|
453
|
+
const tools = [...ALL_TOOLS, ...mcpTools];
|
|
454
|
+
// Subagents never get spawn_agent/spawn_write_agent themselves — otherwise a
|
|
455
|
+
// subagent could spawn more subagents unboundedly (fork-bomb-style resource
|
|
456
|
+
// exhaustion) with no human in the loop to notice.
|
|
457
|
+
const nonRecursive = (list) => list.filter((t) => t.name !== "spawn_agent" && t.name !== "spawn_write_agent");
|
|
458
|
+
const readOnlySubTools = [...READONLY_TOOLS, ...mcpTools.filter((t) => !t.requiresPermission)];
|
|
459
|
+
const writeSubTools = [
|
|
460
|
+
...nonRecursive(ALL_TOOLS),
|
|
461
|
+
...mcpTools.filter((t) => !t.requiresPermission),
|
|
462
|
+
];
|
|
463
|
+
// Hard caps so a runaway or hung subagent can't stall the session or burn
|
|
464
|
+
// unbounded API/compute: a wall-clock timeout per subagent, and a bound on
|
|
465
|
+
// how many run at once.
|
|
466
|
+
const SUBAGENT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
467
|
+
const SUBAGENT_CONCURRENCY = 3;
|
|
468
|
+
function silentHandlers(onFinalText, requestPermission) {
|
|
469
|
+
return {
|
|
470
|
+
onTextDelta: () => { },
|
|
471
|
+
onReasoningDelta: () => { },
|
|
472
|
+
onAssistantText: onFinalText,
|
|
473
|
+
onToolStart: () => { },
|
|
474
|
+
onToolEnd: () => { },
|
|
475
|
+
requestPermission,
|
|
476
|
+
onUsage: () => { },
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
async function runReadOnlyAgent(task, signal) {
|
|
480
|
+
const sub = new Agent(client, () => modelRef.current, readOnlySubTools, { workspace, sandboxMode, trustWorkspace }, new PermissionManager(), new SessionStore(workspace, true), []);
|
|
481
|
+
sub.maxSteps = 15;
|
|
482
|
+
sub.audit = sessionAudit;
|
|
483
|
+
sub.tracer = sessionTracer;
|
|
484
|
+
sub.meter = sessionMeter;
|
|
485
|
+
// Share the parent's kill switch: a subagent with its own would keep
|
|
486
|
+
// running after the user stopped the session.
|
|
487
|
+
sub.kill = agent.kill;
|
|
488
|
+
sub.spanParent = agent.turnSpan;
|
|
489
|
+
sub.spanAttributes = { "kritya.subagent": true, "kritya.subagent_task": task.slice(0, 120) };
|
|
490
|
+
let finalText = "";
|
|
491
|
+
await sub.runTurn(task,
|
|
492
|
+
// read-only tools never require permission, so this is never invoked
|
|
493
|
+
silentHandlers((t) => (finalText = t), async () => "no"), signal);
|
|
494
|
+
return { task, write: false, summary: finalText.trim() || "(subagent returned no findings)" };
|
|
495
|
+
}
|
|
496
|
+
async function runWriteAgent(task, signal) {
|
|
497
|
+
if (!isGitRepo(workspace)) {
|
|
498
|
+
return {
|
|
499
|
+
task,
|
|
500
|
+
write: true,
|
|
501
|
+
summary: "",
|
|
502
|
+
error: "the workspace is not a git repository, so an isolated worktree could not be created",
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
const wt = createWorktree(workspace);
|
|
506
|
+
if (!wt) {
|
|
507
|
+
sessionAudit?.logTool({
|
|
508
|
+
tool: "subagent_worktree",
|
|
509
|
+
summary: `worktree creation failed for task: ${task.slice(0, 72)}`,
|
|
510
|
+
outcome: "error",
|
|
511
|
+
});
|
|
512
|
+
return { task, write: true, summary: "", error: "failed to create an isolated git worktree" };
|
|
513
|
+
}
|
|
514
|
+
sessionAudit?.logTool({
|
|
515
|
+
tool: "subagent_worktree",
|
|
516
|
+
summary: `created branch "${wt.branch}" for task: ${task.slice(0, 72)}`,
|
|
517
|
+
outcome: "ok",
|
|
518
|
+
});
|
|
519
|
+
let finalText = "";
|
|
520
|
+
try {
|
|
521
|
+
// Auto-allow ordinary writes/edits/shell (no human is watching this run),
|
|
522
|
+
// but a destructive command still forces `warning` via classifyDanger in
|
|
523
|
+
// the agent loop regardless of the allowlist — fail-safe deny it there,
|
|
524
|
+
// since there's no one to confirm it and letting it run unattended would
|
|
525
|
+
// be unsafe even inside an isolated worktree (it still has real shell/
|
|
526
|
+
// network access).
|
|
527
|
+
const sub = new Agent(client, () => modelRef.current, writeSubTools, { workspace: wt.dir, sandboxMode, trustWorkspace }, new PermissionManager({ allow: ["write_file", "edit_file", "shell(*)"], deny: [] }), new SessionStore(wt.dir, true), []);
|
|
528
|
+
sub.maxSteps = 30;
|
|
529
|
+
sub.audit = sessionAudit;
|
|
530
|
+
sub.tracer = sessionTracer;
|
|
531
|
+
sub.meter = sessionMeter;
|
|
532
|
+
sub.kill = agent.kill; // see runReadOnlyAgent
|
|
533
|
+
sub.spanParent = agent.turnSpan;
|
|
534
|
+
sub.spanAttributes = {
|
|
535
|
+
"kritya.subagent": true,
|
|
536
|
+
"kritya.subagent_write": true,
|
|
537
|
+
"kritya.subagent_task": task.slice(0, 120),
|
|
538
|
+
"kritya.subagent_branch": wt.branch,
|
|
539
|
+
};
|
|
540
|
+
await sub.runTurn(task, silentHandlers((t) => (finalText = t), async (_name, _summary, _diff, warning) => (warning ? "no" : "yes")), signal);
|
|
541
|
+
}
|
|
542
|
+
catch (err) {
|
|
543
|
+
if (!finalText)
|
|
544
|
+
finalText = `(subagent stopped: ${err instanceof Error ? err.message : String(err)})`;
|
|
545
|
+
}
|
|
546
|
+
const commitState = commitWorktree(wt, `kritya subagent: ${task.slice(0, 72)}`);
|
|
547
|
+
sessionAudit?.logTool({
|
|
548
|
+
tool: "subagent_worktree",
|
|
549
|
+
summary: `branch "${wt.branch}": commit ${commitState}`,
|
|
550
|
+
outcome: commitState === "failed" ? "error" : "ok",
|
|
551
|
+
});
|
|
552
|
+
if (commitState === "clean") {
|
|
553
|
+
const cleaned = removeWorktree(workspace, wt, true);
|
|
554
|
+
const summary = finalText.trim() || "(no changes made)";
|
|
555
|
+
// Surface this rather than silently leave an empty orphaned branch: a
|
|
556
|
+
// failed `git branch -D` (e.g. a transient ref lock) shouldn't look
|
|
557
|
+
// identical to a subagent that genuinely made no changes.
|
|
558
|
+
return cleaned
|
|
559
|
+
? { task, write: true, summary }
|
|
560
|
+
: {
|
|
561
|
+
task,
|
|
562
|
+
write: true,
|
|
563
|
+
summary,
|
|
564
|
+
error: `made no changes, but its empty scratch branch "${wt.branch}" could not be ` +
|
|
565
|
+
`auto-deleted (a transient git lock) — safe to remove manually with ` +
|
|
566
|
+
`\`git branch -D ${wt.branch}\``,
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
if (commitState === "failed") {
|
|
570
|
+
// Don't discard the worktree: the subagent's edits are real work, even
|
|
571
|
+
// if a commit hook rejected them. Leave it on disk for manual recovery.
|
|
572
|
+
return {
|
|
573
|
+
task,
|
|
574
|
+
write: true,
|
|
575
|
+
summary: finalText.trim(),
|
|
576
|
+
error: `changes could not be committed (a commit hook may have rejected them) — left uncommitted at ${wt.dir}`,
|
|
577
|
+
};
|
|
578
|
+
}
|
|
579
|
+
const diffstat = worktreeDiffStat(workspace, wt);
|
|
580
|
+
removeWorktree(workspace, wt, false);
|
|
581
|
+
return {
|
|
582
|
+
task,
|
|
583
|
+
write: true,
|
|
584
|
+
branch: wt.branch,
|
|
585
|
+
summary: `${finalText.trim() || "(no summary)"}${diffstat ? `\n\n${diffstat}` : ""}`,
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
async function runOneAgent(spec, parentSignal) {
|
|
589
|
+
const controller = new AbortController();
|
|
590
|
+
const onParentAbort = () => controller.abort();
|
|
591
|
+
parentSignal?.addEventListener("abort", onParentAbort);
|
|
592
|
+
const timer = setTimeout(() => controller.abort(), SUBAGENT_TIMEOUT_MS);
|
|
593
|
+
try {
|
|
594
|
+
return spec.write
|
|
595
|
+
? await runWriteAgent(spec.task, controller.signal)
|
|
596
|
+
: await runReadOnlyAgent(spec.task, controller.signal);
|
|
597
|
+
}
|
|
598
|
+
catch (err) {
|
|
599
|
+
return {
|
|
600
|
+
task: spec.task,
|
|
601
|
+
write: Boolean(spec.write),
|
|
602
|
+
summary: "",
|
|
603
|
+
error: err instanceof Error ? err.message : String(err),
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
finally {
|
|
607
|
+
clearTimeout(timer);
|
|
608
|
+
parentSignal?.removeEventListener("abort", onParentAbort);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
// Runs subagents concurrently, capped at SUBAGENT_CONCURRENCY at a time, so
|
|
612
|
+
// a burst of parallel tasks can't exhaust API rate limits or system resources.
|
|
613
|
+
const spawnAgents = async (specs, signal) => {
|
|
614
|
+
// Don't stand up worktrees and API calls for work that the shared kill
|
|
615
|
+
// switch would abort on its first step anyway.
|
|
616
|
+
if (agent.kill.active) {
|
|
617
|
+
return specs.map((s) => ({
|
|
618
|
+
task: s.task,
|
|
619
|
+
write: Boolean(s.write),
|
|
620
|
+
summary: "",
|
|
621
|
+
error: "not started — the kill switch is active",
|
|
622
|
+
}));
|
|
623
|
+
}
|
|
624
|
+
const results = new Array(specs.length);
|
|
625
|
+
let next = 0;
|
|
626
|
+
const workers = Array.from({ length: Math.min(SUBAGENT_CONCURRENCY, specs.length) }, async () => {
|
|
627
|
+
while (next < specs.length) {
|
|
628
|
+
const i = next++;
|
|
629
|
+
results[i] = await runOneAgent(specs[i], signal);
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
await Promise.all(workers);
|
|
633
|
+
return results;
|
|
634
|
+
};
|
|
635
|
+
const agent = new Agent(client, () => modelRef.current, tools, {
|
|
636
|
+
workspace,
|
|
637
|
+
sandboxMode,
|
|
638
|
+
trustWorkspace,
|
|
639
|
+
undo: undoStack,
|
|
640
|
+
onTasksUpdate: (t) => {
|
|
641
|
+
uiBridge.onTasksUpdate(t);
|
|
642
|
+
session.saveTasks(t);
|
|
643
|
+
},
|
|
644
|
+
requestElicitation: onAskUser,
|
|
645
|
+
spawnAgents,
|
|
646
|
+
}, new PermissionManager(loadRules(workspace, trustWorkspace)), session, initialHistory);
|
|
647
|
+
agent.contextWindow = contextWindowFor(modelRef.current, config);
|
|
648
|
+
if (config.maxSteps && config.maxSteps > 0)
|
|
649
|
+
agent.maxSteps = config.maxSteps;
|
|
650
|
+
// Not gated on > 0: 0 is a meaningful setting here ("no cap"), unlike maxSteps.
|
|
651
|
+
if (config.toolTimeoutSeconds !== undefined) {
|
|
652
|
+
agent.toolTimeoutMs = config.toolTimeoutSeconds * 1000;
|
|
653
|
+
}
|
|
654
|
+
agent.hooks = new HookRunner(loadHooks(workspace, trustWorkspace), workspace);
|
|
655
|
+
agent.audit = sessionAudit;
|
|
656
|
+
agent.tracer = sessionTracer;
|
|
657
|
+
agent.meter = sessionMeter;
|
|
658
|
+
agent.hooks.tracer = sessionTracer;
|
|
659
|
+
// Only the main interactive agent distills durable facts into KRITYA.md on
|
|
660
|
+
// compaction — subagents (read-only or write) never do, even though they
|
|
661
|
+
// run the same Agent class and can also trigger auto-compaction.
|
|
662
|
+
agent.autoMemory = true;
|
|
663
|
+
ui.instance = render(_jsx(App, { agent: agent, workspace: workspace, modelRef: modelRef, providerRef: providerRef, config: config, resumedCount: initialHistory.length, initialTasks: initialTasks, undoStack: undoStack, uiBridge: uiBridge, resumeSessions: resumeSessions.length ? resumeSessions : undefined, customCommands: loadCustomCommands(workspace, trustWorkspace, plugins), mcpToolCount: mcpTools.length, onSwitchClient: (newClient) => {
|
|
664
|
+
client = newClient;
|
|
665
|
+
}, onRequestPermissionReady: (fn) => {
|
|
666
|
+
permissionRef.current = fn;
|
|
667
|
+
}, onRequestElicitationReady: (fn) => {
|
|
668
|
+
elicitationRef.current = fn;
|
|
669
|
+
} }));
|
|
670
|
+
}
|