@falkenslab/agent-kit 0.21.0 → 0.22.0
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/assets/extension-launcher.mjs +28 -5
- package/dist/chat/chatController.d.ts +59 -0
- package/dist/chat/chatController.js +209 -2
- package/dist/chat/extensions.d.ts +30 -2
- package/dist/chat/extensions.js +62 -3
- package/dist/chat/runs.d.ts +3 -1
- package/dist/chat/runs.js +1 -1
- package/dist/core/agentSpec.d.ts +11 -0
- package/dist/core/elicitation.d.ts +54 -0
- package/dist/core/elicitation.js +120 -0
- package/dist/core/extensionCatalog.d.ts +94 -0
- package/dist/core/extensionCatalog.js +228 -0
- package/dist/core/extensionOptions.d.ts +56 -0
- package/dist/core/extensionOptions.js +139 -0
- package/dist/core/extensions.d.ts +7 -1
- package/dist/core/extensions.js +2 -1
- package/dist/core/externalExtensions.d.ts +83 -8
- package/dist/core/externalExtensions.js +233 -30
- package/dist/core/hooks/planGate.d.ts +2 -0
- package/dist/core/hooks/planGate.js +3 -0
- package/dist/core/interaction.d.ts +7 -0
- package/dist/core/liveExtensions.d.ts +11 -0
- package/dist/core/liveExtensions.js +22 -0
- package/dist/core/marketplaces.d.ts +6 -2
- package/dist/core/marketplaces.js +6 -4
- package/dist/core/messages/de.js +33 -1
- package/dist/core/messages/en.d.ts +36 -0
- package/dist/core/messages/en.js +33 -1
- package/dist/core/messages/es.js +33 -1
- package/dist/core/messages/fr.js +33 -1
- package/dist/core/npxPackages.d.ts +48 -0
- package/dist/core/npxPackages.js +121 -0
- package/dist/core/pluginDownloads.d.ts +2 -1
- package/dist/core/pluginDownloads.js +2 -2
- package/dist/core/readOnlyHints.d.ts +13 -0
- package/dist/core/readOnlyHints.js +26 -0
- package/dist/core/runner.js +2 -0
- package/dist/core/session.d.ts +13 -1
- package/dist/core/session.js +31 -7
- package/dist/core/sessionFacts.d.ts +5 -1
- package/dist/core/workspaceTrust.d.ts +12 -0
- package/dist/core/workspaceTrust.js +60 -0
- package/dist/index.d.ts +7 -1
- package/dist/index.js +5 -1
- package/dist/tui/extensionCommand.d.ts +10 -2
- package/dist/tui/extensionCommand.js +182 -33
- package/extensions/awareness/.claude-plugin/plugin.json +13 -5
- package/extensions/knowledge/.claude-plugin/plugin.json +13 -5
- package/extensions/memory/.claude-plugin/plugin.json +13 -5
- package/extensions/sources/.claude-plugin/plugin.json +13 -5
- package/package.json +7 -5
- package/schemas/agent-kit-metadata.schema.json +62 -0
- package/schemas/marketplace-catalog.schema.json +64 -0
|
@@ -9,6 +9,9 @@ import { getLanguage } from "./messages/index.js";
|
|
|
9
9
|
import { truncate } from "./toolLabels.js";
|
|
10
10
|
import { agentKitVersion } from "./version.js";
|
|
11
11
|
import { outsideArchive } from "./packaged.js";
|
|
12
|
+
import { extensionSecretsPath, missingOptions, optionsNamed, parseOptionValue, parseUserConfig, readOptionValues, removeSecrets, storeOptionValues } from "./extensionOptions.js";
|
|
13
|
+
import { isTrusted } from "./workspaceTrust.js";
|
|
14
|
+
import { cachedPackageDir, cachedPackageProblem, cachePackage, npxServer, packageCacheDir, packageSpec } from "./npxPackages.js";
|
|
12
15
|
export const LOCK_FILE = "extensions.lock.json";
|
|
13
16
|
/**
|
|
14
17
|
* An installed extension's data folder: `${CLAUDE_PLUGIN_DATA}` in its servers' arguments and
|
|
@@ -19,6 +22,39 @@ export const LOCK_FILE = "extensions.lock.json";
|
|
|
19
22
|
export function extensionDataDir(scopeDir, name) {
|
|
20
23
|
return path.join(scopeDir, ".data", name);
|
|
21
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* Where a scope keeps what's the person's, not the workspace's: its extensions' data and secrets
|
|
27
|
+
* (#57). The agent's scope, its own folder; a project's, a folder of its own in the agent's
|
|
28
|
+
* (`<agent scope>/.projects/<project>-<hash>/`), so nothing private travels with a project. An
|
|
29
|
+
* agent without a scope of its own keeps it in the project's.
|
|
30
|
+
*/
|
|
31
|
+
export function scopePrivateDir(dirs, scope) {
|
|
32
|
+
const scopeDir = dirs[scope];
|
|
33
|
+
if (!scopeDir)
|
|
34
|
+
return undefined;
|
|
35
|
+
if (scope === "agent" || !dirs.agent)
|
|
36
|
+
return scopeDir;
|
|
37
|
+
const resolved = path.resolve(scopeDir);
|
|
38
|
+
const key = createHash("sha256").update(resolved.toLowerCase()).digest("hex").slice(0, 12);
|
|
39
|
+
return path.join(dirs.agent, ".projects", `${path.basename(path.dirname(resolved)) || "project"}-${key}`);
|
|
40
|
+
}
|
|
41
|
+
/** The agent's store of extensions' code, by content (#57): where a project's scope keeps its extensions' code. */
|
|
42
|
+
export function codeStoreDir(dirs) {
|
|
43
|
+
return dirs.agent ? path.join(dirs.agent, ".store") : undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Where a stored extension's code is: by its name and its files' hash. */
|
|
46
|
+
export function storedCodeDir(store, name, sha256) {
|
|
47
|
+
return path.join(store, name, sha256.slice(0, 16));
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* How an install into `scope` is done: its npx packages into the agent's cache, and a project's
|
|
51
|
+
* code into the agent's store when the agent has a scope of its own.
|
|
52
|
+
*/
|
|
53
|
+
export function installTarget(dirs, scope) {
|
|
54
|
+
const packages = packageCacheDir(dirs);
|
|
55
|
+
const store = scope === "project" ? codeStoreDir(dirs) : undefined;
|
|
56
|
+
return { ...(packages ? { packages } : {}), ...(store ? { store } : {}) };
|
|
57
|
+
}
|
|
22
58
|
/** A scope's lock: its extensions by name. */
|
|
23
59
|
export async function readLock(scopeDir) {
|
|
24
60
|
try {
|
|
@@ -48,7 +84,9 @@ export async function readExternalManifest(dir) {
|
|
|
48
84
|
const name = raw.name;
|
|
49
85
|
if (typeof name !== "string" || !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(name))
|
|
50
86
|
throw new Error(`${file}: "name" must be kebab-case.`);
|
|
51
|
-
|
|
87
|
+
// The kit's data, where Claude Code keeps a plugin's own (`metadata`, which it doesn't read): a
|
|
88
|
+
// top-level key would fail `claude plugin validate --strict` (#55).
|
|
89
|
+
const kit = (raw.metadata?.["agent-kit"] ?? {});
|
|
52
90
|
const text = (key) => (typeof raw[key] === "string" ? { [key]: raw[key] } : {});
|
|
53
91
|
const author = raw.author;
|
|
54
92
|
// `repository` may be a URL or `{ url }`, as in package.json.
|
|
@@ -69,11 +107,21 @@ export async function readExternalManifest(dir) {
|
|
|
69
107
|
...(kit.kit ? { kit: kit.kit } : {}),
|
|
70
108
|
needs: Array.isArray(kit.needs) ? kit.needs.filter((program) => typeof program === "string") : [],
|
|
71
109
|
servers: await pluginServers(dir, raw),
|
|
72
|
-
readOnlyTools: kit.readOnlyTools
|
|
110
|
+
...(Array.isArray(kit.readOnlyTools) ? { readOnlyTools: kit.readOnlyTools.filter((tool) => typeof tool === "string") } : {}),
|
|
73
111
|
labels: kit.labels ?? {},
|
|
74
112
|
...(kit.help ? { help: kit.help } : {}),
|
|
113
|
+
userConfig: parseUserConfig(raw.userConfig),
|
|
114
|
+
...catalogLook(kit.catalog),
|
|
75
115
|
};
|
|
76
116
|
}
|
|
117
|
+
/** A manifest's `catalog` (#59): only its `logo` and `banner`, when they're paths. */
|
|
118
|
+
function catalogLook(raw) {
|
|
119
|
+
if (!raw || typeof raw !== "object")
|
|
120
|
+
return {};
|
|
121
|
+
const { logo, banner } = raw;
|
|
122
|
+
const look = { ...(typeof logo === "string" ? { logo } : {}), ...(typeof banner === "string" ? { banner } : {}) };
|
|
123
|
+
return Object.keys(look).length ? { catalog: look } : {};
|
|
124
|
+
}
|
|
77
125
|
/**
|
|
78
126
|
* A plugin's MCP servers, where Claude Code looks for them: `mcpServers` in `plugin.json` (inline,
|
|
79
127
|
* or a path to a JSON file), else `.mcp.json` at its root, with or without the `mcpServers` key.
|
|
@@ -113,26 +161,50 @@ function expand(value, folders) {
|
|
|
113
161
|
return value.replace(/\$\{(\w+)\}/g, (_, name) => name === "CLAUDE_PLUGIN_ROOT" ? folders.root : name === "CLAUDE_PLUGIN_DATA" ? folders.data : (process.env[name] ?? ""));
|
|
114
162
|
}
|
|
115
163
|
/**
|
|
116
|
-
* Why one of an extension's servers can't run through the launcher (
|
|
117
|
-
*
|
|
164
|
+
* Why one of an extension's servers can't run through the launcher (Node, its script first,
|
|
165
|
+
* inside the extension; or npx with a package at an exact version, from the cache), or how the
|
|
166
|
+
* kit starts it.
|
|
118
167
|
*/
|
|
119
|
-
function serverLaunch(name, server, folders) {
|
|
168
|
+
function serverLaunch(name, server, folders, packages = {}) {
|
|
120
169
|
const { root } = folders;
|
|
170
|
+
// Its folders as variables too, as Claude Code gives them to a plugin's servers.
|
|
171
|
+
const declared = Object.fromEntries(Object.entries(server.env ?? {}).map(([key, value]) => [key, expand(String(value), folders)]));
|
|
172
|
+
const env = { CLAUDE_PLUGIN_ROOT: root, CLAUDE_PLUGIN_DATA: folders.data, ...declared };
|
|
173
|
+
const npx = npxServer(server);
|
|
174
|
+
if (typeof npx === "string")
|
|
175
|
+
return `${npx} (its server "${name}")`;
|
|
176
|
+
if (npx) {
|
|
177
|
+
const locked = packages.locked?.[packageSpec(npx)];
|
|
178
|
+
if (!packages.cache || !locked)
|
|
179
|
+
return `runs ${packageSpec(npx)}, which wasn't fetched when it was installed: install it again`;
|
|
180
|
+
const entry = path.join(cachedPackageDir(packages.cache, npx.name, npx.version), locked.bin);
|
|
181
|
+
return { entry, args: npx.args.map((arg) => expand(arg, folders)), env };
|
|
182
|
+
}
|
|
121
183
|
if (!/^node(\.exe)?$/i.test(path.basename(server.command)))
|
|
122
|
-
return `runs its server "${name}" with "${server.command}": only Node servers can be installed`;
|
|
184
|
+
return `runs its server "${name}" with "${server.command}": only Node servers, or npx with a package's exact version, can be installed`;
|
|
123
185
|
const [script, ...args] = (server.args ?? []).map((arg) => expand(arg, folders));
|
|
124
186
|
if (!script)
|
|
125
187
|
return `gives its server "${name}" no script to run`;
|
|
126
188
|
const entry = path.resolve(root, script);
|
|
127
189
|
if (path.relative(root, entry).startsWith(".."))
|
|
128
190
|
return `runs its server "${name}" from outside its folder (${script})`;
|
|
129
|
-
|
|
130
|
-
const declared = Object.fromEntries(Object.entries(server.env ?? {}).map(([key, value]) => [key, expand(String(value), folders)]));
|
|
131
|
-
return { entry, args, env: { CLAUDE_PLUGIN_ROOT: root, CLAUDE_PLUGIN_DATA: folders.data, ...declared } };
|
|
191
|
+
return { entry, args, env };
|
|
132
192
|
}
|
|
133
|
-
/**
|
|
134
|
-
|
|
193
|
+
/**
|
|
194
|
+
* Why an extension's servers can't run, or nothing: each Node with its script there, or npx with
|
|
195
|
+
* a package at an exact version (in the cache, once `packages` is given: when it loads).
|
|
196
|
+
*/
|
|
197
|
+
async function serversProblem(manifest, root, packages) {
|
|
135
198
|
for (const [name, server] of Object.entries(manifest.servers)) {
|
|
199
|
+
const npx = npxServer(server);
|
|
200
|
+
if (typeof npx === "string")
|
|
201
|
+
return `${npx} (its server "${name}")`;
|
|
202
|
+
if (npx) {
|
|
203
|
+
const problem = packages && (await cachedPackageProblem(packages.cache, npx, packages.locked?.[packageSpec(npx)]));
|
|
204
|
+
if (problem)
|
|
205
|
+
return problem;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
136
208
|
const launch = serverLaunch(name, server, { root, data: "" });
|
|
137
209
|
if (typeof launch === "string")
|
|
138
210
|
return launch;
|
|
@@ -141,7 +213,28 @@ async function serversProblem(manifest, root) {
|
|
|
141
213
|
}
|
|
142
214
|
return undefined;
|
|
143
215
|
}
|
|
144
|
-
/**
|
|
216
|
+
/** Fetches the packages an extension's npx servers run into the cache (#54), as its lock records them. */
|
|
217
|
+
async function fetchServerPackages(manifest, cache) {
|
|
218
|
+
const packages = {};
|
|
219
|
+
for (const server of Object.values(manifest.servers)) {
|
|
220
|
+
const npx = npxServer(server);
|
|
221
|
+
if (!npx || typeof npx === "string")
|
|
222
|
+
continue;
|
|
223
|
+
packages[packageSpec(npx)] = await cachePackage(cache, npx.name, npx.version);
|
|
224
|
+
}
|
|
225
|
+
return packages;
|
|
226
|
+
}
|
|
227
|
+
/** A text file's bytes with CRLF as LF (latin1 maps each byte to itself); a binary one's (a NUL byte in it) as they are. */
|
|
228
|
+
function withoutCarriageReturns(bytes) {
|
|
229
|
+
if (bytes.includes(0) || !bytes.includes(13))
|
|
230
|
+
return bytes;
|
|
231
|
+
return Buffer.from(bytes.toString("latin1").replace(/\r\n/g, "\n"), "latin1");
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* SHA-256 over an extension's files (their paths and contents, in order), `.git` aside. A text
|
|
235
|
+
* file's line endings don't count (CRLF is taken as LF): git checks the same commit out with CRLF
|
|
236
|
+
* on one computer and LF on another, and a workspace's lock must match on both (#57).
|
|
237
|
+
*/
|
|
145
238
|
export async function hashExtension(dir) {
|
|
146
239
|
const hash = createHash("sha256");
|
|
147
240
|
async function walk(relative) {
|
|
@@ -151,7 +244,7 @@ export async function hashExtension(dir) {
|
|
|
151
244
|
if (entry.isDirectory())
|
|
152
245
|
await walk(child);
|
|
153
246
|
else if (entry.isFile())
|
|
154
|
-
hash.update(`${child}\0`).update(await readFile(path.join(dir, child))).update("\0");
|
|
247
|
+
hash.update(`${child}\0`).update(withoutCarriageReturns(await readFile(path.join(dir, child)))).update("\0");
|
|
155
248
|
}
|
|
156
249
|
}
|
|
157
250
|
await walk("");
|
|
@@ -199,10 +292,12 @@ export async function listInstalled(dirs) {
|
|
|
199
292
|
const scopeDir = dirs[scope];
|
|
200
293
|
if (!scopeDir)
|
|
201
294
|
continue;
|
|
295
|
+
const privateDir = scopePrivateDir(dirs, scope);
|
|
296
|
+
const store = codeStoreDir(dirs);
|
|
202
297
|
for (const [name, lock] of Object.entries(await readLock(scopeDir))) {
|
|
203
|
-
const dir = lock.linked ? lock.source : path.join(scopeDir, name);
|
|
298
|
+
const dir = lock.linked ? lock.source : lock.stored && store ? storedCodeDir(store, name, lock.sha256) : path.join(scopeDir, name);
|
|
204
299
|
const manifest = await readExternalManifest(dir).catch(() => undefined);
|
|
205
|
-
installed.push({ name, scope, scopeDir, dir, dataDir: extensionDataDir(
|
|
300
|
+
installed.push({ name, scope, scopeDir, dir, dataDir: extensionDataDir(privateDir, name), privateDir, lock, shadowed: installed.some((other) => other.name === name), ...(manifest ? { manifest } : {}) });
|
|
206
301
|
}
|
|
207
302
|
}
|
|
208
303
|
return installed;
|
|
@@ -225,7 +320,7 @@ function manifestToolLabels(manifest) {
|
|
|
225
320
|
}));
|
|
226
321
|
}
|
|
227
322
|
/** The extension an installed one makes: its servers started through the launcher, its rules from its manifest. */
|
|
228
|
-
function externalExtension(installed, manifest) {
|
|
323
|
+
function externalExtension(installed, manifest, cache) {
|
|
229
324
|
return {
|
|
230
325
|
name: manifest.name,
|
|
231
326
|
plugin: installed.dir,
|
|
@@ -236,38 +331,73 @@ function externalExtension(installed, manifest) {
|
|
|
236
331
|
if (Object.keys(manifest.servers).length)
|
|
237
332
|
await mkdir(installed.dataDir, { recursive: true });
|
|
238
333
|
for (const [name, server] of Object.entries(manifest.servers)) {
|
|
239
|
-
const launch = serverLaunch(name, server, { root: installed.dir, data: installed.dataDir });
|
|
334
|
+
const launch = serverLaunch(name, server, { root: installed.dir, data: installed.dataDir }, { cache, locked: installed.lock.packages });
|
|
240
335
|
if (typeof launch === "string")
|
|
241
336
|
continue; // checked when it loaded
|
|
242
337
|
// Its `env` reaches it (the SDK merges it with the agent's); the launcher removes the rest.
|
|
243
338
|
// Inside Electron, process.execPath is the app itself: ELECTRON_RUN_AS_NODE makes it run
|
|
244
339
|
// the launcher as Node (the launcher removes it, with the rest, once it runs).
|
|
245
340
|
const env = process.versions.electron ? { ...launch.env, ELECTRON_RUN_AS_NODE: "1" } : launch.env;
|
|
246
|
-
|
|
341
|
+
// The launcher's own spec: the variables it keeps, and where its options' values are, read
|
|
342
|
+
// when it starts the server, so a changed value reaches a server started again (#55).
|
|
343
|
+
const spec = {
|
|
344
|
+
keep: Object.keys(launch.env),
|
|
345
|
+
...(Object.keys(manifest.userConfig).length
|
|
346
|
+
? { options: { lock: path.join(installed.scopeDir, LOCK_FILE), secrets: extensionSecretsPath(installed.privateDir, installed.name), name: installed.name, defaults: optionDefaults(manifest.userConfig) } }
|
|
347
|
+
: {}),
|
|
348
|
+
};
|
|
349
|
+
servers[name] = { type: "stdio", command: process.execPath, args: [extensionLauncherPath(), launch.entry, JSON.stringify(spec), ...launch.args], env };
|
|
247
350
|
}
|
|
248
351
|
return {
|
|
249
352
|
mcpServers: servers,
|
|
250
|
-
|
|
353
|
+
// Its manifest's list, or its servers' own word: the server is the code the kit trusts
|
|
354
|
+
// least, so its hints only fill the list's absence (ADR-025).
|
|
355
|
+
...(manifest.readOnlyTools
|
|
356
|
+
? { readOnlyTools: Object.keys(manifest.servers).flatMap((server) => manifest.readOnlyTools.map((tool) => `mcp__${server}__${tool}`)) }
|
|
357
|
+
: { readOnlyTools: [], readOnlyHintServers: Object.keys(servers) }),
|
|
251
358
|
toolLabels: manifestToolLabels(manifest),
|
|
252
359
|
...(manifest.help ? { helpLines: [manifest.help] } : {}),
|
|
253
360
|
};
|
|
254
361
|
},
|
|
255
362
|
};
|
|
256
363
|
}
|
|
364
|
+
/** Its options' defaults, for the launcher. */
|
|
365
|
+
function optionDefaults(declared) {
|
|
366
|
+
return Object.fromEntries(Object.entries(declared).flatMap(([key, option]) => (option.default === undefined ? [] : [[key, option.default]])));
|
|
367
|
+
}
|
|
368
|
+
/** Why its options keep it from running (a server naming one it doesn't declare, a required one without a value), or nothing. */
|
|
369
|
+
function optionsProblem(manifest, values) {
|
|
370
|
+
const named = optionsNamed(Object.values(manifest.servers).flatMap((server) => [server.command, ...(server.args ?? []), ...Object.values(server.env ?? {}).map(String)]));
|
|
371
|
+
const undeclared = named.filter((key) => !manifest.userConfig[key]);
|
|
372
|
+
if (undeclared.length)
|
|
373
|
+
return `names ${undeclared.map((key) => "${user_config." + key + "}").join(", ")}, which its userConfig doesn't declare`;
|
|
374
|
+
const missing = missingOptions(manifest.userConfig, values);
|
|
375
|
+
if (missing.length)
|
|
376
|
+
return `needs ${missing.map((key) => `${key} (${manifest.userConfig[key].title})`).join(", ")} set: \`extension config ${manifest.name} ${missing[0]}=<value>\``;
|
|
377
|
+
return undefined;
|
|
378
|
+
}
|
|
257
379
|
/** The installed extensions a session runs with, and those it can't (with why). Disabled ones are neither. */
|
|
258
380
|
export async function loadExternalExtensions(dirs) {
|
|
259
381
|
const extensions = [];
|
|
260
382
|
const off = [];
|
|
383
|
+
const untrusted = [];
|
|
384
|
+
const cache = packageCacheDir(dirs);
|
|
261
385
|
for (const installed of await listInstalled(dirs)) {
|
|
262
386
|
if (installed.shadowed || !installed.lock.enabled)
|
|
263
387
|
continue;
|
|
264
|
-
|
|
388
|
+
// A workspace's own: only once the person trusts it there (#57); the agent's never asks.
|
|
389
|
+
if (installed.scope === "project" && dirs.agent && !(await isTrusted(dirs.agent, installed.scopeDir, installed.name, installed.lock))) {
|
|
390
|
+
untrusted.push(installed);
|
|
391
|
+
off.push({ name: installed.name, reason: "comes with this workspace and isn't trusted here yet: `extension trust`, or the chat asks" });
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
const reason = await problem(installed, cache);
|
|
265
395
|
if (typeof reason === "string")
|
|
266
396
|
off.push({ name: installed.name, reason });
|
|
267
397
|
else
|
|
268
|
-
extensions.push(externalExtension(installed, reason));
|
|
398
|
+
extensions.push(externalExtension(installed, reason, cache));
|
|
269
399
|
}
|
|
270
|
-
return { extensions, off };
|
|
400
|
+
return { extensions, off, untrusted };
|
|
271
401
|
}
|
|
272
402
|
/** Why an installed extension can't load, or its manifest when it can. */
|
|
273
403
|
/** Whether a program is on the `PATH`, as a shell would find it (with `PATHEXT`'s extensions on Windows). */
|
|
@@ -283,9 +413,11 @@ export async function onPath(program, env = process.env) {
|
|
|
283
413
|
}
|
|
284
414
|
return false;
|
|
285
415
|
}
|
|
286
|
-
async function problem(installed) {
|
|
416
|
+
async function problem(installed, cache) {
|
|
287
417
|
let manifest;
|
|
288
418
|
try {
|
|
419
|
+
if (installed.lock.stored && !(await stat(installed.dir).catch(() => null))?.isDirectory())
|
|
420
|
+
return "isn't on this computer yet (its workspace says which version it uses): `extension restore` gets it";
|
|
289
421
|
if (!installed.lock.linked && (await hashExtension(installed.dir)) !== installed.lock.sha256)
|
|
290
422
|
return "has files that changed since it was installed: install it again to trust them";
|
|
291
423
|
manifest = await readExternalManifest(installed.dir);
|
|
@@ -303,7 +435,10 @@ async function problem(installed) {
|
|
|
303
435
|
missing.push(program);
|
|
304
436
|
if (missing.length)
|
|
305
437
|
return `needs ${missing.join(", ")}, which ${missing.length === 1 ? "isn't" : "aren't"} on this computer (on the PATH)`;
|
|
306
|
-
|
|
438
|
+
const options = optionsProblem(manifest, await readOptionValues(installed.privateDir, installed.name, manifest.userConfig, installed.lock.options));
|
|
439
|
+
if (options)
|
|
440
|
+
return options;
|
|
441
|
+
return (await serversProblem(manifest, installed.dir, { cache, locked: installed.lock.packages })) ?? manifest;
|
|
307
442
|
}
|
|
308
443
|
const exec = promisify(execFile);
|
|
309
444
|
/** Whether `source` names a git repository rather than a folder. */
|
|
@@ -316,7 +451,9 @@ export function isGitSource(source) {
|
|
|
316
451
|
* manifest, and locks it, enabled, with the marketplace it came from when it did (`origin`, for a
|
|
317
452
|
* temporary folder, is what the lock says it came from). An extension with the same name in that
|
|
318
453
|
* scope is replaced; its data folder stays. With `link`, a folder is registered where it is
|
|
319
|
-
* instead, to develop it: not copied, and its files not checked.
|
|
454
|
+
* instead, to develop it: not copied, and its files not checked. The packages its npx servers
|
|
455
|
+
* run are fetched into `packages` (#54), the agent's package cache (`packageCacheDir()`; by
|
|
456
|
+
* default the scope's).
|
|
320
457
|
*/
|
|
321
458
|
export async function addExtension(source, scopeDir, options = {}) {
|
|
322
459
|
if (options.link && isGitSource(source))
|
|
@@ -347,21 +484,41 @@ export async function addExtension(source, scopeDir, options = {}) {
|
|
|
347
484
|
// Replacing what's installed there would delete the source itself.
|
|
348
485
|
if (path.resolve(from) === path.resolve(target))
|
|
349
486
|
throw new Error(`${from} is where ${manifest.name} is installed: install it from another folder.`);
|
|
487
|
+
const packages = await fetchServerPackages(manifest, options.packages ?? path.join(scopeDir, ".npm"));
|
|
488
|
+
const locked = Object.keys(packages).length ? { packages } : {};
|
|
350
489
|
const lock = await readLock(scopeDir);
|
|
351
490
|
const replaced = Boolean(lock[manifest.name]);
|
|
491
|
+
// Reinstalled, it keeps the values it had for the options it still declares, as it keeps its data.
|
|
492
|
+
const kept = Object.fromEntries(Object.entries(lock[manifest.name]?.options ?? {}).filter(([key]) => manifest.userConfig[key] && !manifest.userConfig[key].sensitive));
|
|
493
|
+
if (Object.keys(kept).length)
|
|
494
|
+
locked.options = kept;
|
|
352
495
|
await rm(target, { recursive: true, force: true });
|
|
353
496
|
await mkdir(scopeDir, { recursive: true });
|
|
497
|
+
const origin = options.origin ?? (isGitSource(source) ? source : from);
|
|
498
|
+
const provenance = { ...(commit ? { commit } : {}), ...(options.subdir ? { subdir: options.subdir } : {}), ...(options.marketplace ? { marketplace: options.marketplace } : {}) };
|
|
499
|
+
if (options.store && !options.link) {
|
|
500
|
+
// By content, in the agent's store: two workspaces with the same version share one copy.
|
|
501
|
+
const sha256 = await hashExtension(from);
|
|
502
|
+
const stored = storedCodeDir(options.store, manifest.name, sha256);
|
|
503
|
+
if (!(await stat(stored).catch(() => null))?.isDirectory() || (await hashExtension(stored)) !== sha256) {
|
|
504
|
+
await rm(stored, { recursive: true, force: true });
|
|
505
|
+
await cp(from, stored, { recursive: true, filter: (file) => path.basename(file) !== ".git" });
|
|
506
|
+
}
|
|
507
|
+
lock[manifest.name] = { source: origin, ...provenance, sha256, stored: true, ...locked, enabled: true, installed: new Date().toISOString() };
|
|
508
|
+
await writeLock(scopeDir, lock);
|
|
509
|
+
return { name: manifest.name, replaced, ...(commit ? { commit } : {}) };
|
|
510
|
+
}
|
|
354
511
|
if (options.link) {
|
|
355
|
-
lock[manifest.name] = { source: from, linked: true, sha256: "", enabled: true, installed: new Date().toISOString() };
|
|
512
|
+
lock[manifest.name] = { source: from, linked: true, sha256: "", ...locked, enabled: true, installed: new Date().toISOString() };
|
|
356
513
|
await writeLock(scopeDir, lock);
|
|
357
514
|
return { name: manifest.name, replaced };
|
|
358
515
|
}
|
|
359
516
|
await cp(from, target, { recursive: true, filter: (file) => path.basename(file) !== ".git" });
|
|
360
517
|
lock[manifest.name] = {
|
|
361
|
-
source:
|
|
362
|
-
...
|
|
363
|
-
...(options.marketplace ? { marketplace: options.marketplace } : {}),
|
|
518
|
+
source: origin,
|
|
519
|
+
...provenance,
|
|
364
520
|
sha256: await hashExtension(target),
|
|
521
|
+
...locked,
|
|
365
522
|
enabled: true,
|
|
366
523
|
installed: new Date().toISOString(),
|
|
367
524
|
};
|
|
@@ -378,13 +535,16 @@ export async function addExtension(source, scopeDir, options = {}) {
|
|
|
378
535
|
* where it is), and its data folder with `deleteData`; false when it wasn't there.
|
|
379
536
|
*/
|
|
380
537
|
export async function removeExtension(scopeDir, name, options = {}) {
|
|
538
|
+
const privateDir = options.privateDir ?? scopeDir;
|
|
381
539
|
const lock = await readLock(scopeDir);
|
|
382
540
|
if (!lock[name])
|
|
383
541
|
return false;
|
|
384
542
|
delete lock[name];
|
|
385
543
|
await rm(path.join(scopeDir, name), { recursive: true, force: true });
|
|
544
|
+
// A stored one's code stays in the store: another workspace may use the same version.
|
|
386
545
|
if (options.deleteData)
|
|
387
|
-
await rm(extensionDataDir(
|
|
546
|
+
await rm(extensionDataDir(privateDir, name), { recursive: true, force: true });
|
|
547
|
+
await removeSecrets(privateDir, name);
|
|
388
548
|
await writeLock(scopeDir, lock);
|
|
389
549
|
return true;
|
|
390
550
|
}
|
|
@@ -409,3 +569,46 @@ export async function setExtensionEnabled(scopeDir, name, enabled) {
|
|
|
409
569
|
await writeLock(scopeDir, lock);
|
|
410
570
|
return true;
|
|
411
571
|
}
|
|
572
|
+
/**
|
|
573
|
+
* Sets an installed extension's options (#55), each from what the person typed (`undefined`
|
|
574
|
+
* clears it), checked against its `userConfig`: sensitive ones into the scope's secrets, the rest
|
|
575
|
+
* into its lock. A running session's servers take them when they start again.
|
|
576
|
+
*/
|
|
577
|
+
export async function setExtensionOptions(where, name, typed) {
|
|
578
|
+
// A scope's folder (the agent's, whose code and secrets are in it), or an installed one, which knows where they are.
|
|
579
|
+
const scopeDir = typeof where === "string" ? where : where.scopeDir;
|
|
580
|
+
const privateDir = typeof where === "string" ? where : where.privateDir;
|
|
581
|
+
const lock = await readLock(scopeDir);
|
|
582
|
+
const entry = lock[name];
|
|
583
|
+
if (!entry)
|
|
584
|
+
throw new Error(`${name} isn't installed in ${scopeDir}.`);
|
|
585
|
+
const manifest = (typeof where === "string" ? undefined : where.manifest) ?? (await readExternalManifest(entry.linked ? entry.source : path.join(scopeDir, name)));
|
|
586
|
+
const changes = {};
|
|
587
|
+
for (const [key, text] of Object.entries(typed)) {
|
|
588
|
+
const option = manifest.userConfig[key];
|
|
589
|
+
if (!option)
|
|
590
|
+
throw new Error(`${name} has no option "${key}"${Object.keys(manifest.userConfig).length ? `: it has ${Object.keys(manifest.userConfig).join(", ")}` : ""}.`);
|
|
591
|
+
try {
|
|
592
|
+
changes[key] = text === undefined ? undefined : parseOptionValue(option, text);
|
|
593
|
+
}
|
|
594
|
+
catch (error) {
|
|
595
|
+
throw new Error(`${key}: ${error instanceof Error ? error.message : String(error)}.`, { cause: error });
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
const options = await storeOptionValues(privateDir, name, manifest.userConfig, entry.options ?? {}, changes);
|
|
599
|
+
lock[name] = { ...entry, options };
|
|
600
|
+
if (!Object.keys(options).length)
|
|
601
|
+
delete lock[name].options;
|
|
602
|
+
await writeLock(scopeDir, lock);
|
|
603
|
+
}
|
|
604
|
+
/** An installed extension's options, as they are now. */
|
|
605
|
+
export async function extensionOptionStates(installed) {
|
|
606
|
+
const declared = installed.manifest?.userConfig ?? {};
|
|
607
|
+
const values = await readOptionValues(installed.privateDir, installed.name, declared, installed.lock.options);
|
|
608
|
+
const secrets = await readOptionValues(installed.privateDir, installed.name, Object.fromEntries(Object.entries(declared).map(([key, option]) => [key, { ...option, default: undefined }])), installed.lock.options);
|
|
609
|
+
return Object.entries(declared).map(([key, option]) => {
|
|
610
|
+
const value = values[key];
|
|
611
|
+
const own = secrets[key];
|
|
612
|
+
return { key, option, ...(value !== undefined && !option.sensitive ? { value } : {}), set: value !== undefined && value !== "", isDefault: value !== undefined && own === undefined };
|
|
613
|
+
});
|
|
614
|
+
}
|
|
@@ -9,6 +9,8 @@ export interface PlanScope {
|
|
|
9
9
|
isReadOnlyTool?(toolName: string, input: Record<string, unknown>): boolean;
|
|
10
10
|
/** The enabled extensions' MCP tools that only read (`mcp__<server>__<tool>`). */
|
|
11
11
|
readOnlyTools?: readonly string[];
|
|
12
|
+
/** Whether an MCP tool's own server marks it `readOnlyHint`, for the extensions whose manifest lists none (readOnlyHints.ts). */
|
|
13
|
+
isReadOnlyHinted?(toolName: string): Promise<boolean>;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* The reason to deny `toolName` with `input` in plan mode, or `undefined` to let it through.
|
|
@@ -41,6 +41,9 @@ export function createPlanGate(scope, isActive) {
|
|
|
41
41
|
const reason = checkPlanScope(scope, pre.tool_name, (pre.tool_input ?? {}));
|
|
42
42
|
if (!reason)
|
|
43
43
|
return {};
|
|
44
|
+
// Asked last, and only for an MCP tool nothing else let through: it's a call to the session.
|
|
45
|
+
if (pre.tool_name.startsWith("mcp__") && (await scope.isReadOnlyHinted?.(pre.tool_name)))
|
|
46
|
+
return {};
|
|
44
47
|
return {
|
|
45
48
|
hookSpecificOutput: {
|
|
46
49
|
hookEventName: pre.hook_event_name,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { FormAnswer, FormRequest } from "./elicitation.js";
|
|
1
2
|
/** What a checkpoint shows the person: a title, detail lines and, in a plain terminal, the question. */
|
|
2
3
|
export interface ApprovalPrompt {
|
|
3
4
|
title: string;
|
|
@@ -43,6 +44,12 @@ export interface InteractionPort {
|
|
|
43
44
|
* `askDecision()`, with the options numbered in the prompt.
|
|
44
45
|
*/
|
|
45
46
|
askChoice?(prompt: ApprovalPrompt, choice: ChoiceSettings, signal: AbortSignal): Promise<string>;
|
|
47
|
+
/**
|
|
48
|
+
* A form an MCP server asks for (elicitation, #58), whole: its fields, and the person's
|
|
49
|
+
* values, or that they declined or cancelled. Optional: without it the kit asks field by
|
|
50
|
+
* field through the other methods. Never written to a log: the answer is the server's.
|
|
51
|
+
*/
|
|
52
|
+
askForm?(form: FormRequest, signal: AbortSignal): Promise<FormAnswer>;
|
|
46
53
|
/** A one-way message for the person. */
|
|
47
54
|
notify(message: string): void;
|
|
48
55
|
}
|
|
@@ -63,3 +63,14 @@ export interface LiveTargets {
|
|
|
63
63
|
export type ExtensionSwitch = (name: string, enabled: boolean) => Promise<boolean>;
|
|
64
64
|
/** The switch for a session's installed extensions. */
|
|
65
65
|
export declare function createExtensionSwitch(extensions: readonly LiveExtension[], targets: LiveTargets): ExtensionSwitch;
|
|
66
|
+
/**
|
|
67
|
+
* Restarts `name`'s MCP servers in the running session, so they take its options as they are now
|
|
68
|
+
* (#55): the launcher reads them when it starts a server. `reconnectMcpServer()` ends a server's
|
|
69
|
+
* process and starts a new one with the session's config (confirmed empirically; `setMcpServers()`
|
|
70
|
+
* replaced every server of the session and left the old process running). False when it can't
|
|
71
|
+
* be done here (not running in this session, or the session hasn't started): the next session
|
|
72
|
+
* takes them.
|
|
73
|
+
*/
|
|
74
|
+
export type ExtensionRestart = (name: string) => Promise<boolean>;
|
|
75
|
+
/** The restart for a session's installed extensions. */
|
|
76
|
+
export declare function createExtensionRestart(extensions: readonly LiveExtension[], targets: LiveTargets): ExtensionRestart;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { cp, lstat, mkdir, readdir, rm, symlink, unlink } from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { hashExtension, listInstalled, readLock } from "./externalExtensions.js";
|
|
4
|
+
import { cachedPackagesIntact, packageCacheDir } from "./npxPackages.js";
|
|
4
5
|
/**
|
|
5
6
|
* Turning an installed extension off and on in a running session (#49), without reopening it.
|
|
6
7
|
* Only one that was running when the session opened: its MCP servers are toggled
|
|
@@ -68,6 +69,9 @@ export function createExtensionSwitch(extensions, targets) {
|
|
|
68
69
|
const lock = (await readLock(extension.scopeDir))[name];
|
|
69
70
|
if (!lock || (!lock.linked && lock.sha256 !== (await hashExtension(extension.installedDir))))
|
|
70
71
|
return false;
|
|
72
|
+
// The packages its npx servers run (#54), checked whether it's linked or not.
|
|
73
|
+
if (!(await cachedPackagesIntact(packageCacheDir(targets.status.dirs), lock.packages)))
|
|
74
|
+
return false;
|
|
71
75
|
await mountPlugin(extension.installedDir, extension.pluginMount);
|
|
72
76
|
}
|
|
73
77
|
else {
|
|
@@ -85,6 +89,24 @@ export function createExtensionSwitch(extensions, targets) {
|
|
|
85
89
|
return true;
|
|
86
90
|
};
|
|
87
91
|
}
|
|
92
|
+
/** The restart for a session's installed extensions. */
|
|
93
|
+
export function createExtensionRestart(extensions, targets) {
|
|
94
|
+
return async (name) => {
|
|
95
|
+
const extension = extensions.find((candidate) => candidate.name === name);
|
|
96
|
+
const reconnect = targets.controls().reconnectMcpServer;
|
|
97
|
+
if (!extension || !reconnect || !extension.servers.length)
|
|
98
|
+
return false;
|
|
99
|
+
try {
|
|
100
|
+
for (const server of extension.servers)
|
|
101
|
+
await reconnect(server);
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
// Turned off in the session: it starts with them when it's turned on again.
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
|
88
110
|
/** The kit's lists, as the extension is now. */
|
|
89
111
|
function follow(extension, enabled, targets) {
|
|
90
112
|
const without = (list, items) => {
|
|
@@ -108,9 +108,13 @@ export type ResolvedSource = {
|
|
|
108
108
|
export declare function resolvePluginSource(marketplace: KnownMarketplace, plugin: MarketplacePlugin): ResolvedSource;
|
|
109
109
|
/**
|
|
110
110
|
* Installs `<plugin>@<marketplace>` (or a plugin only one known marketplace offers) into a scope,
|
|
111
|
-
* locked with where it came from
|
|
111
|
+
* locked with where it came from (the packages its npx servers run into `options.packages`, #54).
|
|
112
|
+
* Its name in the agent is its `plugin.json`'s, as for any extension.
|
|
112
113
|
*/
|
|
113
|
-
export declare function installFromMarketplace(dir: string, spec: string, scopeDir: string
|
|
114
|
+
export declare function installFromMarketplace(dir: string, spec: string, scopeDir: string, options?: {
|
|
115
|
+
packages?: string;
|
|
116
|
+
store?: string;
|
|
117
|
+
}): Promise<{
|
|
114
118
|
name: string;
|
|
115
119
|
replaced: boolean;
|
|
116
120
|
commit?: string;
|
|
@@ -208,9 +208,10 @@ export function resolvePluginSource(marketplace, plugin) {
|
|
|
208
208
|
}
|
|
209
209
|
/**
|
|
210
210
|
* Installs `<plugin>@<marketplace>` (or a plugin only one known marketplace offers) into a scope,
|
|
211
|
-
* locked with where it came from
|
|
211
|
+
* locked with where it came from (the packages its npx servers run into `options.packages`, #54).
|
|
212
|
+
* Its name in the agent is its `plugin.json`'s, as for any extension.
|
|
212
213
|
*/
|
|
213
|
-
export async function installFromMarketplace(dir, spec, scopeDir) {
|
|
214
|
+
export async function installFromMarketplace(dir, spec, scopeDir, options = {}) {
|
|
214
215
|
const { marketplace, plugin } = await findPlugin(dir, spec);
|
|
215
216
|
const resolved = resolvePluginSource(marketplace, plugin);
|
|
216
217
|
const from = `${plugin.name}@${marketplace.name}`;
|
|
@@ -218,7 +219,7 @@ export async function installFromMarketplace(dir, spec, scopeDir) {
|
|
|
218
219
|
const { name, version, registry } = resolved.npm;
|
|
219
220
|
const fetched = await fetchNpmPlugin(name, version, registry);
|
|
220
221
|
try {
|
|
221
|
-
return { ...(await addExtension(fetched.dir, scopeDir, { marketplace: from, origin: `npm:${name}@${fetched.version}` })), marketplace: marketplace.name };
|
|
222
|
+
return { ...(await addExtension(fetched.dir, scopeDir, { ...options, marketplace: from, origin: `npm:${name}@${fetched.version}` })), marketplace: marketplace.name };
|
|
222
223
|
}
|
|
223
224
|
finally {
|
|
224
225
|
await rm(fetched.dir, { recursive: true, force: true });
|
|
@@ -227,13 +228,14 @@ export async function installFromMarketplace(dir, spec, scopeDir) {
|
|
|
227
228
|
if ("archive" in resolved) {
|
|
228
229
|
const fetched = await fetchArchivePlugin(resolved.archive.url, resolved.archive.sha256);
|
|
229
230
|
try {
|
|
230
|
-
return { ...(await addExtension(fetched.root, scopeDir, { marketplace: from, origin: resolved.archive.url })), marketplace: marketplace.name };
|
|
231
|
+
return { ...(await addExtension(fetched.root, scopeDir, { ...options, marketplace: from, origin: resolved.archive.url })), marketplace: marketplace.name };
|
|
231
232
|
}
|
|
232
233
|
finally {
|
|
233
234
|
await rm(fetched.dir, { recursive: true, force: true });
|
|
234
235
|
}
|
|
235
236
|
}
|
|
236
237
|
const result = await addExtension(resolved.source, scopeDir, {
|
|
238
|
+
...options,
|
|
237
239
|
...(resolved.subdir ? { subdir: resolved.subdir } : {}),
|
|
238
240
|
...(resolved.sha ? { sha: resolved.sha } : {}),
|
|
239
241
|
marketplace: from,
|
package/dist/core/messages/de.js
CHANGED
|
@@ -32,8 +32,40 @@ export const de = {
|
|
|
32
32
|
extensionSwitched: (name, enabled) => `(${name} ${enabled ? "aktiviert" : "deaktiviert"}, in derselben Sitzung)`,
|
|
33
33
|
extensionToggled: (name, enabled) => `(${name} ${enabled ? "aktiviert" : "deaktiviert"}: die Sitzung wird mit derselben Unterhaltung neu geöffnet)`,
|
|
34
34
|
extensionNotInstalled: (name) => `(${name} ist nicht installiert: /extensions listet sie auf)`,
|
|
35
|
-
extensionsUsage: "(Verwendung: /extensions,
|
|
35
|
+
extensionsUsage: "(Verwendung: /extensions, enable|disable <Name>, config <Name> [Schlüssel=Wert…], search [Wörter], install <Name>, update [Name], remove <Name> [--delete-data], reload, trust)",
|
|
36
36
|
extensionsCantReopen: "(dieser Chat kann seine Sitzung nicht neu öffnen: im Terminal aktivieren oder deaktivieren und neu starten)",
|
|
37
|
+
extensionOptions: (name) => `Optionen von ${name}:`,
|
|
38
|
+
extensionNoOptions: (name) => `(${name} hat keine Optionen)`,
|
|
39
|
+
optionNotSet: "(nicht gesetzt)",
|
|
40
|
+
optionSecretSet: "(gesetzt, verborgen)",
|
|
41
|
+
optionDefault: "(Standard)",
|
|
42
|
+
optionRequired: "erforderlich",
|
|
43
|
+
optionSensitiveInChat: (key, name) => `(${key} ist ein Geheimnis: was im Chat getippt wird, bleibt in seinem Verlauf, also im Terminal setzen: extension config ${name} ${key})`,
|
|
44
|
+
extensionOptionsSaved: (name) => `(Optionen von ${name} gespeichert: sie gelten, wenn sein Server neu startet)`,
|
|
45
|
+
extensionRestarted: (name) => `(Optionen von ${name} gespeichert, und sein Server damit neu gestartet, in derselben Sitzung)`,
|
|
46
|
+
extensionReconfiguredReopen: (name) => `(Optionen von ${name} gespeichert: die Sitzung wird neu geöffnet, mit demselben Gespräch)`,
|
|
47
|
+
extensionsNoMatch: "(keine Erweiterung passt: /extensions search mit anderen Wörtern, oder ohne, um alle zu sehen)",
|
|
48
|
+
extensionIsInstalled: "(installiert)",
|
|
49
|
+
extensionUpdateAvailable: (installed) => `(installiert: ${installed}, Update verfügbar)`,
|
|
50
|
+
extensionInstalled: (name) => `(${name} installiert: in einem neuen Gespräch da, oder sofort mit /extensions reload, ohne dieses zu verlieren)`,
|
|
51
|
+
extensionUpdated: (name, from, to) => `(${name} aktualisiert, ${from} → ${to}: in einem neuen Gespräch da, oder sofort mit /extensions reload)`,
|
|
52
|
+
extensionsUpToDate: "(alle Erweiterungen aus einem Marketplace sind aktuell)",
|
|
53
|
+
extensionRemoved: (name, withData) => `(${name} deinstalliert${withData ? ", mit seinen Daten" : "; seine Daten bleiben für eine Neuinstallation"})`,
|
|
54
|
+
extensionConfirm: (spec, marketplace) => `(${spec} kommt aus ${marketplace}, nicht aus dem Marketplace dieses Agenten: es liefe auf diesem Computer mit deinen Rechten. /extensions install ${spec} --yes installiert es)`,
|
|
55
|
+
extensionsBusy: "(nach diesem Zug: der Agent arbeitet)",
|
|
56
|
+
extensionsReloaded: "(die Sitzung wird mit den jetzt installierten Erweiterungen neu geöffnet, mit demselben Gespräch)",
|
|
57
|
+
elicitTitle: (server) => `${server} fragt dich`,
|
|
58
|
+
elicitQuestion: "Antworten? (Enter/y: antworten, n: ablehnen, q: abbrechen) ",
|
|
59
|
+
elicitAgain: "Das ist kein gültiger Wert: versuch es nochmal, oder lass es leer.",
|
|
60
|
+
elicitNotANumber: (text) => `„${text}“ ist keine Zahl`,
|
|
61
|
+
elicitOutOfRange: (minimum, maximum) => `zwischen ${minimum ?? "…"} und ${maximum ?? "…"}`,
|
|
62
|
+
elicitUrlTitle: (server) => `${server} bittet dich, eine Seite zu öffnen`,
|
|
63
|
+
elicitUrlQuestion: "Öffne sie selbst, dann Enter, wenn du fertig bist (n: ablehnen) ",
|
|
64
|
+
trustTitle: "Dieser Arbeitsbereich bringt Erweiterungen mit",
|
|
65
|
+
trustBody: "Sie führen Code auf diesem Computer mit deinen Rechten aus. Vertraue ihnen nur, wenn du der Herkunft dieses Arbeitsbereichs vertraust:",
|
|
66
|
+
trustQuestion: "Ihnen vertrauen und sie ausführen? (y/N) ",
|
|
67
|
+
trustDeclined: "(sie bleiben aus: /extensions trust vertraut ihnen später)",
|
|
68
|
+
trustGiven: (names) => `(${names}: in diesem Arbeitsbereich vertraut, so wie sie sind; eine andere Version fragt erneut)`,
|
|
37
69
|
currentRun: "(dieses)",
|
|
38
70
|
resumed: (date) => `(Gespräch vom ${date} fortgesetzt)`,
|
|
39
71
|
suggestionKey: "Tab",
|