@falkenslab/agent-kit 0.18.0 → 0.20.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 +24 -0
- package/dist/chat/chatController.d.ts +267 -0
- package/dist/chat/chatController.js +536 -0
- package/dist/chat/extensions.d.ts +18 -0
- package/dist/chat/extensions.js +44 -0
- package/dist/chat/history.d.ts +17 -0
- package/dist/chat/history.js +39 -0
- package/dist/{tui → chat}/runs.d.ts +11 -1
- package/dist/{tui → chat}/runs.js +1 -1
- package/dist/core/agentSpec.d.ts +28 -50
- package/dist/core/extensions.d.ts +128 -0
- package/dist/core/extensions.js +87 -0
- package/dist/core/externalExtensions.d.ts +143 -0
- package/dist/core/externalExtensions.js +368 -0
- package/dist/core/hooks/planGate.d.ts +2 -0
- package/dist/core/hooks/planGate.js +1 -10
- package/dist/core/hooks/stepGate.d.ts +1 -1
- package/dist/core/hooks/stepGate.js +3 -3
- package/dist/core/liveExtensions.d.ts +51 -0
- package/dist/core/liveExtensions.js +70 -0
- package/dist/core/marketplaces.d.ts +118 -0
- package/dist/core/marketplaces.js +242 -0
- package/dist/core/messages/de.js +12 -32
- package/dist/core/messages/en.d.ts +15 -16
- package/dist/core/messages/en.js +12 -32
- package/dist/core/messages/es.js +12 -32
- package/dist/core/messages/fr.js +12 -32
- package/dist/core/messages/index.d.ts +6 -0
- package/dist/core/messages/index.js +12 -1
- package/dist/core/packaged.d.ts +10 -0
- package/dist/core/packaged.js +38 -0
- package/dist/core/pluginAgents.d.ts +20 -0
- package/dist/core/pluginAgents.js +76 -0
- package/dist/core/pluginDownloads.d.ts +14 -0
- package/dist/core/pluginDownloads.js +141 -0
- package/dist/core/runner.js +21 -9
- package/dist/core/session.d.ts +38 -6
- package/dist/core/session.js +200 -99
- package/dist/core/sessionFacts.d.ts +85 -0
- package/dist/core/sessionFacts.js +49 -0
- package/dist/core/toolLabels.d.ts +24 -6
- package/dist/core/toolLabels.js +24 -39
- package/dist/extensions/awareness/index.d.ts +22 -0
- package/dist/extensions/awareness/index.js +42 -0
- package/dist/extensions/awareness/labels.d.ts +3 -0
- package/dist/extensions/awareness/labels.js +12 -0
- package/dist/extensions/awareness/tools.d.ts +5 -0
- package/dist/extensions/awareness/tools.js +54 -0
- package/dist/extensions/knowledge/index.d.ts +22 -0
- package/dist/extensions/knowledge/index.js +38 -0
- package/dist/{core → extensions/knowledge}/knowledgeStore.d.ts +2 -1
- package/dist/extensions/knowledge/labels.d.ts +3 -0
- package/dist/extensions/knowledge/labels.js +119 -0
- package/dist/{core/knowledge.d.ts → extensions/knowledge/prompt.d.ts} +4 -4
- package/dist/{core/knowledge.js → extensions/knowledge/prompt.js} +10 -7
- package/dist/{core/tools/knowledgeTools.d.ts → extensions/knowledge/tools.d.ts} +1 -1
- package/dist/{core/tools/knowledgeTools.js → extensions/knowledge/tools.js} +2 -2
- package/dist/extensions/memory/index.d.ts +21 -0
- package/dist/extensions/memory/index.js +59 -0
- package/dist/extensions/memory/labels.d.ts +3 -0
- package/dist/extensions/memory/labels.js +58 -0
- package/dist/extensions/memory/memoryStore.d.ts +53 -0
- package/dist/extensions/memory/memoryStore.js +88 -0
- package/dist/extensions/memory/tools.d.ts +9 -0
- package/dist/extensions/memory/tools.js +82 -0
- package/dist/extensions/sources/index.d.ts +21 -0
- package/dist/extensions/sources/index.js +42 -0
- package/dist/extensions/sources/labels.d.ts +3 -0
- package/dist/extensions/sources/labels.js +82 -0
- package/dist/{core/tools/saveToSources.d.ts → extensions/sources/tools.d.ts} +1 -1
- package/dist/{core/tools/saveToSources.js → extensions/sources/tools.js} +5 -5
- package/dist/index.d.ts +19 -9
- package/dist/index.js +16 -7
- package/dist/tui/chatTui.d.ts +11 -20
- package/dist/tui/chatTui.js +75 -230
- package/dist/tui/extensionCommand.d.ts +30 -0
- package/dist/tui/extensionCommand.js +257 -0
- package/dist/tui/ink/runChatInk.d.ts +4 -3
- package/dist/tui/ink/runChatInk.js +101 -217
- package/extensions/awareness/.claude-plugin/plugin.json +10 -0
- package/{assets/agent-help → extensions/awareness/skills/help}/SKILL.md +4 -3
- package/{assets/knowledge-plugin → extensions/knowledge}/.claude-plugin/plugin.json +7 -1
- package/extensions/memory/.claude-plugin/plugin.json +10 -0
- package/extensions/memory/commands/forget.md +8 -0
- package/extensions/memory/commands/list.md +5 -0
- package/extensions/sources/.claude-plugin/plugin.json +10 -0
- package/package.json +3 -2
- package/dist/core/agentHelp.d.ts +0 -30
- package/dist/core/agentHelp.js +0 -55
- /package/dist/{core → extensions/knowledge}/fileKnowledgeStore.d.ts +0 -0
- /package/dist/{core → extensions/knowledge}/fileKnowledgeStore.js +0 -0
- /package/dist/{core → extensions/knowledge}/knowledgeStore.js +0 -0
- /package/dist/{core → extensions/sources}/extractText.d.ts +0 -0
- /package/dist/{core → extensions/sources}/extractText.js +0 -0
- /package/dist/{core → extensions/sources}/sources.d.ts +0 -0
- /package/dist/{core → extensions/sources}/sources.js +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/commands/ingest.md +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/commands/lint.md +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/commands/query.md +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-ingest/SKILL.md +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-lint/SKILL.md +0 -0
- /package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-query/SKILL.md +0 -0
package/dist/core/messages/fr.js
CHANGED
|
@@ -22,6 +22,18 @@ export const fr = {
|
|
|
22
22
|
resumeHint: "↑/↓ choisir · Entrée reprendre · Échap annuler",
|
|
23
23
|
resumeQuestion: "Numéro de la conversation à reprendre (Entrée pour annuler) : ",
|
|
24
24
|
noEarlierRuns: "(aucune conversation précédente à reprendre)",
|
|
25
|
+
commandResume: "Choisir une conversation précédente pour la reprendre",
|
|
26
|
+
commandPlan: "Passer en mode plan, ou revenir",
|
|
27
|
+
commandExtensions: "Ce qui tourne, ce qui est éteint et pourquoi ; en activer ou désactiver une",
|
|
28
|
+
extensionsRunning: (names) => `En service : ${names}`,
|
|
29
|
+
extensionsNone: "(aucune extension)",
|
|
30
|
+
extensionOff: (name, reason) => `${name} est désactivée : ${reason}`,
|
|
31
|
+
extensionsInstalled: "Installées :",
|
|
32
|
+
extensionSwitched: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"}, dans cette même session)`,
|
|
33
|
+
extensionToggled: (name, enabled) => `(${name} ${enabled ? "activée" : "désactivée"} : la session se rouvre, avec la même conversation)`,
|
|
34
|
+
extensionNotInstalled: (name) => `(${name} n'est pas installée : /extensions les liste)`,
|
|
35
|
+
extensionsUsage: "(usage : /extensions, /extensions enable <nom>, /extensions disable <nom>)",
|
|
36
|
+
extensionsCantReopen: "(ce chat ne peut pas rouvrir sa session : activez-la ou désactivez-la depuis le terminal, puis recommencez)",
|
|
25
37
|
currentRun: "(celle-ci)",
|
|
26
38
|
resumed: (date) => `(conversation du ${date} reprise)`,
|
|
27
39
|
suggestionKey: "tab",
|
|
@@ -91,22 +103,6 @@ export const fr = {
|
|
|
91
103
|
Task: ["a lancé {n} sous-agent", "a lancé {n} sous-agents"],
|
|
92
104
|
mcp__time__current_time: ["a consulté l'heure", "a consulté l'heure {n} fois"],
|
|
93
105
|
mcp__time__date_math: ["a calculé des dates", "a calculé des dates {n} fois"],
|
|
94
|
-
mcp__sourceFiles__list_sources: ["a examiné les sources", "a examiné les sources {n} fois"],
|
|
95
|
-
mcp__sourceFiles__extract_text: ["a lu {n} document", "a lu {n} documents"],
|
|
96
|
-
mcp__sourceFiles__save_to_sources: ["a enregistré {n} source", "a enregistré {n} sources"],
|
|
97
|
-
mcp__sourceFiles__download_to_sources: ["a téléchargé {n} source", "a téléchargé {n} sources"],
|
|
98
|
-
mcp__sourceFiles__request_file: ["a demandé {n} fichier", "a demandé {n} fichiers"],
|
|
99
|
-
mcp__sourceFiles__retire_source: ["a retiré {n} source", "a retiré {n} sources"],
|
|
100
|
-
mcp__knowledge__knowledge_index: ["a lu l'index", "a lu l'index {n} fois"],
|
|
101
|
-
mcp__knowledge__knowledge_search: ["a cherché dans la base", "a cherché dans la base {n} fois"],
|
|
102
|
-
mcp__knowledge__knowledge_read: ["a lu {n} page", "a lu {n} pages"],
|
|
103
|
-
mcp__knowledge__knowledge_create: ["a créé {n} page", "a créé {n} pages"],
|
|
104
|
-
mcp__knowledge__knowledge_edit: ["a modifié {n} page", "a modifié {n} pages"],
|
|
105
|
-
mcp__knowledge__knowledge_rewrite: ["a réécrit {n} page", "a réécrit {n} pages"],
|
|
106
|
-
mcp__knowledge__knowledge_supersede: ["a remplacé {n} page", "a remplacé {n} pages"],
|
|
107
|
-
mcp__knowledge__knowledge_retire: ["a retiré {n} page", "a retiré {n} pages"],
|
|
108
|
-
mcp__knowledge__knowledge_log: ["a mis à jour le journal", "a mis à jour le journal {n} fois"],
|
|
109
|
-
mcp__knowledge__knowledge_check: ["a vérifié la base", "a vérifié la base {n} fois"],
|
|
110
106
|
mcp__approvals__ask_human: ["a posé {n} question", "a posé {n} questions"],
|
|
111
107
|
mcp__approvals__present_plan: ["a présenté le plan", "a présenté le plan {n} fois"],
|
|
112
108
|
},
|
|
@@ -120,7 +116,6 @@ export const fr = {
|
|
|
120
116
|
labels: {
|
|
121
117
|
askingApproval: (summary) => `Demande d'approbation humaine : ${summary}`,
|
|
122
118
|
waitingManual: "En attente d'une intervention manuelle",
|
|
123
|
-
savingToSources: (destination) => `Enregistrement de ${destination} dans les sources`,
|
|
124
119
|
checkingTime: "Consultation de la date et de l'heure",
|
|
125
120
|
calculatingDates: "Calcul de dates",
|
|
126
121
|
reading: (file) => `Lecture de ${file}`,
|
|
@@ -139,21 +134,6 @@ export const fr = {
|
|
|
139
134
|
applyingSkill: (skill) => `Application de la skill "${skill}"`,
|
|
140
135
|
aSkill: "une skill",
|
|
141
136
|
updatingTasks: "Mise à jour de la liste des tâches",
|
|
142
|
-
listingSources: "Examen des sources",
|
|
143
|
-
extractingText: (source) => `Lecture de ${source}`,
|
|
144
|
-
downloading: (url) => `Téléchargement de ${url} dans les sources`,
|
|
145
|
-
requestingFile: (description) => `Demande d'un fichier : ${description}`,
|
|
146
|
-
retiringSource: (source) => `Retrait de la source ${source}`,
|
|
147
|
-
knowledgeIndex: "Lecture de l'index de la base de connaissances",
|
|
148
|
-
knowledgeSearch: (query) => `Recherche de "${query}" dans la base de connaissances`,
|
|
149
|
-
knowledgeRead: (page) => `Lecture de ${page}`,
|
|
150
|
-
knowledgeCreate: (page) => `Création de ${page}`,
|
|
151
|
-
knowledgeEdit: (page) => `Modification de ${page}`,
|
|
152
|
-
knowledgeRewrite: (page) => `Réécriture de ${page}`,
|
|
153
|
-
knowledgeSupersede: (page) => `${page} marquée comme remplacée`,
|
|
154
|
-
knowledgeRetire: (page) => `Retrait de ${page}`,
|
|
155
|
-
knowledgeLog: "Mise à jour du journal de la base de connaissances",
|
|
156
|
-
knowledgeCheck: "Vérification de la base de connaissances",
|
|
157
137
|
askingHuman: (question) => `Question : ${question}`,
|
|
158
138
|
presentingPlan: "Présentation du plan",
|
|
159
139
|
},
|
|
@@ -17,6 +17,12 @@ export declare function getLanguage(): Language;
|
|
|
17
17
|
* once per process.
|
|
18
18
|
*/
|
|
19
19
|
export declare function chooseLanguage(option?: string): Language;
|
|
20
|
+
/**
|
|
21
|
+
* Switches the kit's language on purpose, e.g. the person picked another in a chat (#47): from
|
|
22
|
+
* then on it wins over `--language` and `config.language`, until switched again. The session
|
|
23
|
+
* says so to the model when it opens next (the controller's `setLanguage()` reopens it).
|
|
24
|
+
*/
|
|
25
|
+
export declare function switchLanguage(language: Language): void;
|
|
20
26
|
/** Warnings about unsupported language codes not shown yet (English, one line each). */
|
|
21
27
|
export declare function takeLanguageWarnings(): string[];
|
|
22
28
|
/** The kit's texts in the current language. */
|
|
@@ -23,6 +23,8 @@ export function withEnglishFallback(own) {
|
|
|
23
23
|
let current = "en";
|
|
24
24
|
let messages = en;
|
|
25
25
|
let chosen = false;
|
|
26
|
+
// A language switched to on purpose (switchLanguage()): kept over --language and options.
|
|
27
|
+
let switched = false;
|
|
26
28
|
let pendingWarnings = [];
|
|
27
29
|
const warned = new Set();
|
|
28
30
|
/** Sets the language of the kit's texts directly (a host that resolved it itself, tests). */
|
|
@@ -43,7 +45,7 @@ export function getLanguage() {
|
|
|
43
45
|
* once per process.
|
|
44
46
|
*/
|
|
45
47
|
export function chooseLanguage(option) {
|
|
46
|
-
if (chosen && option === undefined)
|
|
48
|
+
if (switched || (chosen && option === undefined))
|
|
47
49
|
return current;
|
|
48
50
|
const resolved = detectLanguage(option);
|
|
49
51
|
for (const warning of resolved.warnings) {
|
|
@@ -55,6 +57,15 @@ export function chooseLanguage(option) {
|
|
|
55
57
|
setLanguage(resolved.language);
|
|
56
58
|
return current;
|
|
57
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Switches the kit's language on purpose, e.g. the person picked another in a chat (#47): from
|
|
62
|
+
* then on it wins over `--language` and `config.language`, until switched again. The session
|
|
63
|
+
* says so to the model when it opens next (the controller's `setLanguage()` reopens it).
|
|
64
|
+
*/
|
|
65
|
+
export function switchLanguage(language) {
|
|
66
|
+
setLanguage(language);
|
|
67
|
+
switched = true;
|
|
68
|
+
}
|
|
58
69
|
/** Warnings about unsupported language codes not shown yet (English, one line each). */
|
|
59
70
|
export function takeLanguageWarnings() {
|
|
60
71
|
const taken = pendingWarnings;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** A path inside `…/app.asar/…`, as the unpacked copy next to it (`…/app.asar.unpacked/…`); any other path as it is. */
|
|
2
|
+
export declare function outsideArchive(file: string): string;
|
|
3
|
+
/**
|
|
4
|
+
* The SDK's CLI binary unpacked next to the archive, when the kit runs packaged: the SDK would
|
|
5
|
+
* resolve it inside `app.asar`, where it can't be executed, and its first turn would hang with
|
|
6
|
+
* no error (confirmed empirically). Looked up as the SDK does (`@anthropic-ai/claude-agent-sdk-
|
|
7
|
+
* <platform>-<arch>`, either libc on Linux), with `resolve` replaceable for tests. `undefined`
|
|
8
|
+
* when not packaged, or when the build didn't unpack it: then the SDK finds its own.
|
|
9
|
+
*/
|
|
10
|
+
export declare function unpackedClaudeExecutable(resolve?: (request: string) => string, platform?: NodeJS.Platform, arch?: string): string | undefined;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
/**
|
|
4
|
+
* Running packaged in an archive (Electron's `app.asar`, ADR-026, #42). Electron reads files
|
|
5
|
+
* inside the archive for its own process, but another process can't: not the Claude Code CLI
|
|
6
|
+
* reading a plugin, not the binary itself. So every path the kit hands to another process goes
|
|
7
|
+
* through `outsideArchive()`, which points it at the copy the build unpacked (`asarUnpack`).
|
|
8
|
+
*/
|
|
9
|
+
const ARCHIVE = /([\\/])app\.asar([\\/])/;
|
|
10
|
+
/** A path inside `…/app.asar/…`, as the unpacked copy next to it (`…/app.asar.unpacked/…`); any other path as it is. */
|
|
11
|
+
export function outsideArchive(file) {
|
|
12
|
+
return file.replace(ARCHIVE, "$1app.asar.unpacked$2");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The SDK's CLI binary unpacked next to the archive, when the kit runs packaged: the SDK would
|
|
16
|
+
* resolve it inside `app.asar`, where it can't be executed, and its first turn would hang with
|
|
17
|
+
* no error (confirmed empirically). Looked up as the SDK does (`@anthropic-ai/claude-agent-sdk-
|
|
18
|
+
* <platform>-<arch>`, either libc on Linux), with `resolve` replaceable for tests. `undefined`
|
|
19
|
+
* when not packaged, or when the build didn't unpack it: then the SDK finds its own.
|
|
20
|
+
*/
|
|
21
|
+
export function unpackedClaudeExecutable(resolve = createRequire(import.meta.url).resolve, platform = process.platform, arch = process.arch) {
|
|
22
|
+
const packages = platform === "linux" ? [`linux-${arch}`, `linux-${arch}-musl`] : [`${platform}-${arch}`];
|
|
23
|
+
for (const suffix of packages) {
|
|
24
|
+
let file;
|
|
25
|
+
try {
|
|
26
|
+
file = resolve(`@anthropic-ai/claude-agent-sdk-${suffix}/claude${platform === "win32" ? ".exe" : ""}`);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (!ARCHIVE.test(file))
|
|
32
|
+
return undefined;
|
|
33
|
+
const unpacked = outsideArchive(file);
|
|
34
|
+
if (existsSync(unpacked))
|
|
35
|
+
return unpacked;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AgentDefinition } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
/**
|
|
3
|
+
* A plugin's subagents (`agents/<file>.md`, ADR-025), read so the session registers them as it
|
|
4
|
+
* does an agent's own from `buildSubagents()`: in the type gate's allow-list, in the `Agent` and
|
|
5
|
+
* `Bash` decision, and with the reply-language line. The SDK loads them itself as
|
|
6
|
+
* `<plugin>:<name>`, the name from the frontmatter, not the file (confirmed empirically); a
|
|
7
|
+
* definition under that same key in `options.agents` replaces the plugin's (also confirmed), which
|
|
8
|
+
* is how the kit adds the line.
|
|
9
|
+
*/
|
|
10
|
+
/** A markdown file's frontmatter: `key: value`, with `[a, b]`, `a, b` or a YAML list for lists. */
|
|
11
|
+
export declare function frontmatter(text: string): {
|
|
12
|
+
fields: Record<string, string | string[]>;
|
|
13
|
+
body: string;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* The subagents of the plugins in `roots`, by the name the SDK gives them (`<plugin>:<name>`):
|
|
17
|
+
* `description` and the body as the prompt, `tools` (none listed: it inherits the session's),
|
|
18
|
+
* `model` and `maxTurns` when given.
|
|
19
|
+
*/
|
|
20
|
+
export declare function pluginAgents(roots: readonly string[]): Promise<Record<string, AgentDefinition>>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { readdir, readFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* A plugin's subagents (`agents/<file>.md`, ADR-025), read so the session registers them as it
|
|
5
|
+
* does an agent's own from `buildSubagents()`: in the type gate's allow-list, in the `Agent` and
|
|
6
|
+
* `Bash` decision, and with the reply-language line. The SDK loads them itself as
|
|
7
|
+
* `<plugin>:<name>`, the name from the frontmatter, not the file (confirmed empirically); a
|
|
8
|
+
* definition under that same key in `options.agents` replaces the plugin's (also confirmed), which
|
|
9
|
+
* is how the kit adds the line.
|
|
10
|
+
*/
|
|
11
|
+
/** A markdown file's frontmatter: `key: value`, with `[a, b]`, `a, b` or a YAML list for lists. */
|
|
12
|
+
export function frontmatter(text) {
|
|
13
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(text);
|
|
14
|
+
if (!match)
|
|
15
|
+
return { fields: {}, body: text };
|
|
16
|
+
const fields = {};
|
|
17
|
+
const lines = match[1].split(/\r?\n/);
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const kv = /^([A-Za-z_][\w-]*):\s*(.*)$/.exec(lines[i]);
|
|
20
|
+
if (!kv)
|
|
21
|
+
continue;
|
|
22
|
+
const [, key, raw] = kv;
|
|
23
|
+
if (raw === "") {
|
|
24
|
+
const items = [];
|
|
25
|
+
while (i + 1 < lines.length && /^\s+-\s/.test(lines[i + 1]))
|
|
26
|
+
items.push(lines[++i].replace(/^\s+-\s*/, ""));
|
|
27
|
+
fields[key] = items.map(unquote);
|
|
28
|
+
}
|
|
29
|
+
else if (/^\[.*\]$/.test(raw)) {
|
|
30
|
+
fields[key] = raw.slice(1, -1).split(",").map(unquote).filter(Boolean);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
fields[key] = unquote(raw);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return { fields, body: text.slice(match[0].length).trim() };
|
|
37
|
+
}
|
|
38
|
+
const unquote = (value) => value.trim().replace(/^["']|["']$/g, "");
|
|
39
|
+
/** A field as a list: a list as is, `a, b` split on commas. */
|
|
40
|
+
function list(value) {
|
|
41
|
+
if (value === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
return Array.isArray(value) ? value : value.split(",").map(unquote).filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The subagents of the plugins in `roots`, by the name the SDK gives them (`<plugin>:<name>`):
|
|
47
|
+
* `description` and the body as the prompt, `tools` (none listed: it inherits the session's),
|
|
48
|
+
* `model` and `maxTurns` when given.
|
|
49
|
+
*/
|
|
50
|
+
export async function pluginAgents(roots) {
|
|
51
|
+
const agents = {};
|
|
52
|
+
for (const root of roots) {
|
|
53
|
+
let plugin;
|
|
54
|
+
try {
|
|
55
|
+
plugin = JSON.parse(await readFile(path.join(root, ".claude-plugin", "plugin.json"), "utf8")).name;
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const files = await readdir(path.join(root, "agents")).catch(() => []);
|
|
61
|
+
for (const file of files.filter((name) => name.endsWith(".md"))) {
|
|
62
|
+
const { fields, body } = frontmatter(await readFile(path.join(root, "agents", file), "utf8"));
|
|
63
|
+
const name = typeof fields.name === "string" && fields.name ? fields.name : file.replace(/\.md$/, "");
|
|
64
|
+
const tools = list(fields.tools);
|
|
65
|
+
const maxTurns = typeof fields.maxTurns === "string" ? Number(fields.maxTurns) : undefined;
|
|
66
|
+
agents[`${plugin}:${name}`] = {
|
|
67
|
+
description: typeof fields.description === "string" ? fields.description : "",
|
|
68
|
+
prompt: body,
|
|
69
|
+
...(tools ? { tools } : {}),
|
|
70
|
+
...(typeof fields.model === "string" ? { model: fields.model } : {}),
|
|
71
|
+
...(maxTurns && Number.isFinite(maxTurns) ? { maxTurns } : {}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return agents;
|
|
76
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** The regular files of a tar archive, by path (ustar, with pax and GNU long names). */
|
|
2
|
+
export declare function untar(tar: Uint8Array): Map<string, Uint8Array>;
|
|
3
|
+
/** The highest of `versions` that `wanted` asks for: an exact version, or a range (`^`, `~`, comparators, `*`). */
|
|
4
|
+
export declare function pickVersion(versions: readonly string[], wanted: string): string | undefined;
|
|
5
|
+
/** An npm package into a new temporary folder: `{ dir, version }`. */
|
|
6
|
+
export declare function fetchNpmPlugin(name: string, wanted?: string, registry?: string): Promise<{
|
|
7
|
+
dir: string;
|
|
8
|
+
version: string;
|
|
9
|
+
}>;
|
|
10
|
+
/** A zip archive into a new temporary folder; the plugin is its root, or its only folder. */
|
|
11
|
+
export declare function fetchArchivePlugin(url: string, sha256?: string): Promise<{
|
|
12
|
+
dir: string;
|
|
13
|
+
root: string;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { inKitRange } from "./externalExtensions.js";
|
|
6
|
+
async function fflate() {
|
|
7
|
+
try {
|
|
8
|
+
return (await import("fflate"));
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
throw new Error('Installing from an npm or archive source needs the optional "fflate" library: npm install fflate', { cause: error });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
async function download(url) {
|
|
15
|
+
if (!url.startsWith("https://"))
|
|
16
|
+
throw new Error(`Only https downloads: ${url}.`);
|
|
17
|
+
const response = await fetch(url);
|
|
18
|
+
if (!response.ok)
|
|
19
|
+
throw new Error(`${url} answered ${response.status}.`);
|
|
20
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
21
|
+
}
|
|
22
|
+
/** Files into `dir`, each at its relative path; one that would land outside it is refused. */
|
|
23
|
+
async function writeFiles(dir, files) {
|
|
24
|
+
const root = path.resolve(dir);
|
|
25
|
+
for (const [name, data] of files) {
|
|
26
|
+
const target = path.resolve(root, name);
|
|
27
|
+
if (!target.startsWith(`${root}${path.sep}`) || name.split(/[\\/]/).includes(".."))
|
|
28
|
+
throw new Error(`The download has a file outside its folder: ${name}.`);
|
|
29
|
+
await mkdir(path.dirname(target), { recursive: true });
|
|
30
|
+
await writeFile(target, data);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** The regular files of a tar archive, by path (ustar, with pax and GNU long names). */
|
|
34
|
+
export function untar(tar) {
|
|
35
|
+
const files = new Map();
|
|
36
|
+
const text = (from, length) => new TextDecoder().decode(tar.subarray(from, from + length)).replace(/\0.*$/s, "");
|
|
37
|
+
let offset = 0;
|
|
38
|
+
let longName;
|
|
39
|
+
while (offset + 512 <= tar.length) {
|
|
40
|
+
const name = text(offset, 100);
|
|
41
|
+
if (!name)
|
|
42
|
+
break; // the two empty blocks at the end
|
|
43
|
+
const size = parseInt(text(offset + 124, 12).trim() || "0", 8);
|
|
44
|
+
const type = text(offset + 156, 1);
|
|
45
|
+
const prefix = text(offset + 345, 155);
|
|
46
|
+
const body = tar.subarray(offset + 512, offset + 512 + size);
|
|
47
|
+
if (type === "x") {
|
|
48
|
+
longName = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(new TextDecoder().decode(body))?.[1] ?? longName;
|
|
49
|
+
}
|
|
50
|
+
else if (type === "L") {
|
|
51
|
+
longName = new TextDecoder().decode(body).replace(/\0.*$/s, "");
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
if (type === "0" || type === "" || type === "7")
|
|
55
|
+
files.set(longName ?? (prefix ? `${prefix}/${name}` : name), body);
|
|
56
|
+
longName = undefined;
|
|
57
|
+
}
|
|
58
|
+
offset += 512 + Math.ceil(size / 512) * 512;
|
|
59
|
+
}
|
|
60
|
+
return files;
|
|
61
|
+
}
|
|
62
|
+
/** The highest of `versions` that `wanted` asks for: an exact version, or a range (`^`, `~`, comparators, `*`). */
|
|
63
|
+
export function pickVersion(versions, wanted) {
|
|
64
|
+
const range = wanted.trim();
|
|
65
|
+
if (versions.includes(range))
|
|
66
|
+
return range;
|
|
67
|
+
const comparators = range.split(/\s+/).flatMap((part) => {
|
|
68
|
+
if (part === "*" || part === "x" || part === "")
|
|
69
|
+
return [];
|
|
70
|
+
const caret = /^\^(\d+)\.(\d+)\.(\d+)$/.exec(part);
|
|
71
|
+
if (caret) {
|
|
72
|
+
const [major, minor, patch] = caret.slice(1).map(Number);
|
|
73
|
+
const upper = major > 0 ? `${major + 1}.0.0` : minor > 0 ? `0.${minor + 1}.0` : `0.0.${patch + 1}`;
|
|
74
|
+
return [`>=${major}.${minor}.${patch}`, `<${upper}`];
|
|
75
|
+
}
|
|
76
|
+
const tilde = /^~(\d+)\.(\d+)\.(\d+)$/.exec(part);
|
|
77
|
+
if (tilde)
|
|
78
|
+
return [`>=${tilde[1]}.${tilde[2]}.${tilde[3]}`, `<${tilde[1]}.${Number(tilde[2]) + 1}.0`];
|
|
79
|
+
return [part];
|
|
80
|
+
});
|
|
81
|
+
const stable = versions.filter((version) => /^\d+\.\d+\.\d+$/.test(version));
|
|
82
|
+
const matching = stable.filter((version) => !comparators.length || inKitRange(version, comparators.join(" ")));
|
|
83
|
+
const order = (version) => version.split(".").map(Number);
|
|
84
|
+
return matching.sort((a, b) => {
|
|
85
|
+
const [x, y] = [order(a), order(b)];
|
|
86
|
+
for (let i = 0; i < 3; i++)
|
|
87
|
+
if (x[i] !== y[i])
|
|
88
|
+
return x[i] - y[i];
|
|
89
|
+
return 0;
|
|
90
|
+
}).at(-1);
|
|
91
|
+
}
|
|
92
|
+
/** An npm package into a new temporary folder: `{ dir, version }`. */
|
|
93
|
+
export async function fetchNpmPlugin(name, wanted = "latest", registry = "https://registry.npmjs.org") {
|
|
94
|
+
if (!/^(@[a-z0-9][\w.-]*\/)?[a-z0-9][\w.-]*$/i.test(name))
|
|
95
|
+
throw new Error(`"${name}" isn't an npm package name.`);
|
|
96
|
+
const response = await fetch(`${registry.replace(/\/+$/, "")}/${name.replace("/", "%2f")}`, { headers: { accept: "application/json" } });
|
|
97
|
+
if (!response.ok)
|
|
98
|
+
throw new Error(`The registry has no package ${name} (${response.status}).`);
|
|
99
|
+
const metadata = (await response.json());
|
|
100
|
+
const version = metadata["dist-tags"]?.[wanted] ?? pickVersion(Object.keys(metadata.versions ?? {}), wanted);
|
|
101
|
+
const dist = version ? metadata.versions?.[version]?.dist : undefined;
|
|
102
|
+
if (!version || !dist?.tarball)
|
|
103
|
+
throw new Error(`${name} has no version ${wanted}.`);
|
|
104
|
+
const tarball = await download(dist.tarball);
|
|
105
|
+
const sri = /^sha512-(.+)$/.exec(dist.integrity ?? "");
|
|
106
|
+
if (!sri)
|
|
107
|
+
throw new Error(`${name}@${version} has no sha512 integrity to check it against.`);
|
|
108
|
+
if (createHash("sha512").update(tarball).digest("base64") !== sri[1])
|
|
109
|
+
throw new Error(`${name}@${version}'s download doesn't match its integrity.`);
|
|
110
|
+
const { gunzipSync } = await fflate();
|
|
111
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "agent-kit-npm-"));
|
|
112
|
+
try {
|
|
113
|
+
// npm puts a package's files under package/.
|
|
114
|
+
const files = [...untar(gunzipSync(tarball))].flatMap(([file, data]) => (file.startsWith("package/") ? [[file.slice("package/".length), data]] : []));
|
|
115
|
+
await writeFiles(dir, files);
|
|
116
|
+
return { dir, version };
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
await rm(dir, { recursive: true, force: true });
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
/** A zip archive into a new temporary folder; the plugin is its root, or its only folder. */
|
|
124
|
+
export async function fetchArchivePlugin(url, sha256) {
|
|
125
|
+
const zip = await download(url);
|
|
126
|
+
if (sha256 && createHash("sha256").update(zip).digest("hex") !== sha256.toLowerCase())
|
|
127
|
+
throw new Error(`${url} doesn't match its sha256.`);
|
|
128
|
+
const { unzipSync } = await fflate();
|
|
129
|
+
const files = Object.entries(unzipSync(zip)).filter(([name]) => !name.endsWith("/"));
|
|
130
|
+
const dir = await mkdtemp(path.join(os.tmpdir(), "agent-kit-archive-"));
|
|
131
|
+
try {
|
|
132
|
+
await writeFiles(dir, files);
|
|
133
|
+
const tops = new Set(files.map(([name]) => name.split("/")[0]));
|
|
134
|
+
const only = tops.size === 1 && files.every(([name]) => name.includes("/")) ? [...tops][0] : "";
|
|
135
|
+
return { dir, root: path.join(dir, only) };
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
await rm(dir, { recursive: true, force: true });
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
141
|
+
}
|
package/dist/core/runner.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
+
import { sessionViewOf } from "./sessionFacts.js";
|
|
2
3
|
/**
|
|
3
4
|
* A turn's errors without the CLI's internal diagnostics: an interrupted turn ends with
|
|
4
5
|
* one like "[ede_diagnostic] result_type=user ... stop_reason=tool_use" (seen on Esc),
|
|
@@ -27,11 +28,21 @@ export function toolResultText(content) {
|
|
|
27
28
|
*/
|
|
28
29
|
export function runQuery(prompt, options) {
|
|
29
30
|
const result = query({ prompt, options });
|
|
31
|
+
// What the session is, for an extension that asks (sessionFacts.ts): the controls now, the
|
|
32
|
+
// tools and skills when the session starts.
|
|
33
|
+
const view = sessionViewOf(options);
|
|
34
|
+
view?.attach({
|
|
35
|
+
supportedCommands: () => result.supportedCommands(),
|
|
36
|
+
contextUsage: () => contextUsage(),
|
|
37
|
+
toggleMcpServer: (server, enabled) => result.toggleMcpServer(server, enabled),
|
|
38
|
+
reloadPlugins: () => result.reloadPlugins(),
|
|
39
|
+
});
|
|
30
40
|
// The main agent's tool calls by id, to name the results that come back for them.
|
|
31
41
|
const toolNames = new Map();
|
|
32
42
|
async function* generateEvents() {
|
|
33
43
|
for await (const message of result) {
|
|
34
44
|
if (message.type === "system" && message.subtype === "init") {
|
|
45
|
+
view?.attach({ tools: message.tools, skills: message.skills });
|
|
35
46
|
const failedServers = message.mcp_servers.filter((s) => s.status === "failed").map((s) => s.name);
|
|
36
47
|
if (failedServers.length > 0)
|
|
37
48
|
yield { type: "mcp-error", failedServers };
|
|
@@ -103,14 +114,15 @@ export function runQuery(prompt, options) {
|
|
|
103
114
|
interrupt: () => result.interrupt(),
|
|
104
115
|
close: () => result.close(),
|
|
105
116
|
supportedCommands: () => result.supportedCommands(),
|
|
106
|
-
contextUsage
|
|
107
|
-
try {
|
|
108
|
-
const usage = await result.getContextUsage();
|
|
109
|
-
return { percentage: usage.percentage, totalTokens: usage.totalTokens, maxTokens: usage.maxTokens };
|
|
110
|
-
}
|
|
111
|
-
catch {
|
|
112
|
-
return null;
|
|
113
|
-
}
|
|
114
|
-
},
|
|
117
|
+
contextUsage,
|
|
115
118
|
};
|
|
119
|
+
async function contextUsage() {
|
|
120
|
+
try {
|
|
121
|
+
const usage = await result.getContextUsage();
|
|
122
|
+
return { percentage: usage.percentage, totalTokens: usage.totalTokens, maxTokens: usage.maxTokens };
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
116
128
|
}
|
package/dist/core/session.d.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import type { Options, SDKUserMessage } from "@anthropic-ai/claude-agent-sdk";
|
|
2
2
|
import { type TranscriptLogger } from "./hooks/transcriptLogger.js";
|
|
3
3
|
import { type ModeControl } from "./modeControl.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type ExtensionDirs, type InstalledExtension } from "./externalExtensions.js";
|
|
5
|
+
import type { ToolLabels } from "./toolLabels.js";
|
|
5
6
|
import type { AgentSpec, BaseSessionConfig } from "./agentSpec.js";
|
|
6
7
|
import { type Language } from "./language.js";
|
|
7
8
|
import { type RunFolder } from "./runs.js";
|
|
9
|
+
import { type ExtensionSwitch } from "./liveExtensions.js";
|
|
8
10
|
/**
|
|
9
11
|
* Builds the `options` object passed to the Agent SDK's `query()` — everything about
|
|
10
12
|
* *how* the agent runs a session (system prompt, tools, MCP servers, hooks), independent
|
|
@@ -12,9 +14,10 @@ import { type RunFolder } from "./runs.js";
|
|
|
12
14
|
* one-shot run or an `AsyncIterable` for a multi-turn chat).
|
|
13
15
|
*
|
|
14
16
|
* Everything actually *about the domain* (which system prompt, which MCP servers besides
|
|
15
|
-
* the generic human-in-the-loop
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* the generic human-in-the-loop ones below, which plugin roots, which subagents) is behind
|
|
18
|
+
* `spec` (see agentSpec.ts), and the kit's optional parts (the knowledge base, the sources
|
|
19
|
+
* folder) are extensions whose contributions this function puts together (extensions.ts,
|
|
20
|
+
* ADR-025): it names none of them.
|
|
18
21
|
*
|
|
19
22
|
* The per-mode behavioral differences (whether the approval/manual-intervention tools
|
|
20
23
|
* exist, whether the interactive step gate or the plan gate decides) all fall out of
|
|
@@ -33,16 +36,45 @@ export declare function buildSessionOptions<TConfig extends BaseSessionConfig>(c
|
|
|
33
36
|
transcriptPath: string;
|
|
34
37
|
modeControl: ModeControl;
|
|
35
38
|
language: Language;
|
|
36
|
-
/**
|
|
37
|
-
|
|
39
|
+
/** How the chat shows the extensions' tools (`runChatInk()` takes them from a session opener). */
|
|
40
|
+
toolLabels: ToolLabels;
|
|
41
|
+
/** The installed extensions and what this session runs with, for `/extensions` (#37). */
|
|
42
|
+
extensions: ExtensionsStatus;
|
|
43
|
+
/** What each active extension hands the host (its contribution's `api`), by its name: the sources' `addSource`, the knowledge base's `knowledgeStore`. */
|
|
44
|
+
apis: Record<string, Record<string, unknown>>;
|
|
45
|
+
/** Turns an installed extension off or on in the running session; false when that takes opening it again (#49). */
|
|
46
|
+
switchExtension: ExtensionSwitch;
|
|
38
47
|
}>;
|
|
39
48
|
export { createModeControl, togglePlanMode, type ModeControl } from "./modeControl.js";
|
|
49
|
+
/** The extensions of a session: where they're installed, what's installed, what runs and what can't (with why). */
|
|
50
|
+
export interface ExtensionsStatus {
|
|
51
|
+
dirs: ExtensionDirs;
|
|
52
|
+
installed: InstalledExtension[];
|
|
53
|
+
/** The names of the extensions this session runs with, the kit's and the agent's too. */
|
|
54
|
+
active: string[];
|
|
55
|
+
/** What each active one is, from its manifest: for a view that lists them. */
|
|
56
|
+
about: Record<string, {
|
|
57
|
+
description: string;
|
|
58
|
+
provides: string[];
|
|
59
|
+
}>;
|
|
60
|
+
inactive: {
|
|
61
|
+
name: string;
|
|
62
|
+
reason: string;
|
|
63
|
+
}[];
|
|
64
|
+
}
|
|
40
65
|
/**
|
|
41
66
|
* The CLI's folder for a project, where it keeps each session's transcript and large tool
|
|
42
67
|
* results (`<session>/tool-results/`): the project's path with every character but letters
|
|
43
68
|
* and digits as "-" (confirmed empirically).
|
|
44
69
|
*/
|
|
45
70
|
export declare function claudeProjectDir(projectDir: string): string;
|
|
71
|
+
/**
|
|
72
|
+
* The SDK's `permissions.deny` rules for paths no file tool may read or change, each one and
|
|
73
|
+
* whatever is inside it. An absolute path is written `//` and POSIX: `C:\Users\x` is
|
|
74
|
+
* `//c/Users/x` (confirmed empirically on Windows; on POSIX `/home/x` is `//home/x`). `Read`
|
|
75
|
+
* rules reach the tools that read, `Edit` rules those that write.
|
|
76
|
+
*/
|
|
77
|
+
export declare function denyRules(paths: readonly string[]): string[];
|
|
46
78
|
/**
|
|
47
79
|
* The skills of the plugins in `roots`, as the SDK names them: `<plugin>:<folder>`, the
|
|
48
80
|
* plugin's name from its `.claude-plugin/plugin.json` and the skill's folder under `skills/`,
|