@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/index.js
CHANGED
|
@@ -7,16 +7,24 @@ export { allowAnyMcpTool } from "./core/mcpPermissions.js";
|
|
|
7
7
|
export { createPromptLoader } from "./core/promptTemplate.js";
|
|
8
8
|
export { resolveClaudeAuth } from "./core/claudeAuth.js";
|
|
9
9
|
export { ensureClaudeAuth } from "./tui/claudeAuth.js";
|
|
10
|
-
export { createFriendlyToolLabel, truncate, truncatePath, } from "./core/toolLabels.js";
|
|
10
|
+
export { createFriendlyToolLabel, truncate, truncatePath, withToolLabels, withToolPhrases, } from "./core/toolLabels.js";
|
|
11
11
|
export { buildSessionOptions, createInputQueue, createDeferred, createModeControl, togglePlanMode } from "./core/session.js";
|
|
12
12
|
export { runQuery } from "./core/runner.js";
|
|
13
|
-
export { knowledgePluginRoot, knowledgePromptSection } from "./
|
|
13
|
+
export { knowledgePluginRoot, knowledgePromptSection } from "./extensions/knowledge/prompt.js";
|
|
14
|
+
export { awareness } from "./extensions/awareness/index.js";
|
|
15
|
+
export { sources } from "./extensions/sources/index.js";
|
|
16
|
+
export { knowledge } from "./extensions/knowledge/index.js";
|
|
17
|
+
export { memory } from "./extensions/memory/index.js";
|
|
18
|
+
export { readExtensionManifest, } from "./core/extensions.js";
|
|
19
|
+
export { addExtension, removeExtension, setExtensionEnabled, listInstalled, readExternalManifest, } from "./core/externalExtensions.js";
|
|
20
|
+
export { addMarketplace, findPlugin, inspectMarketplace, installFromMarketplace, listMarketplaces, readMarketplaceManifest, removeMarketplace, resolvePluginSource, updateMarketplace, } from "./core/marketplaces.js";
|
|
21
|
+
export { runExtensionCommand } from "./tui/extensionCommand.js";
|
|
14
22
|
export { agentKitVersion } from "./core/version.js";
|
|
15
|
-
export { BUILT_IN_PAGE_TYPES, isSlug, parseId, linkedIds, } from "./
|
|
16
|
-
export { createFileKnowledgeStore } from "./
|
|
17
|
-
export { createKnowledgeServer } from "./
|
|
23
|
+
export { BUILT_IN_PAGE_TYPES, isSlug, parseId, linkedIds, } from "./extensions/knowledge/knowledgeStore.js";
|
|
24
|
+
export { createFileKnowledgeStore } from "./extensions/knowledge/fileKnowledgeStore.js";
|
|
25
|
+
export { createKnowledgeServer } from "./extensions/knowledge/tools.js";
|
|
18
26
|
export { resolveLanguage, detectLanguage, SUPPORTED_LANGUAGES, } from "./core/language.js";
|
|
19
|
-
export { setLanguage, getLanguage, messagesFor } from "./core/messages/index.js";
|
|
27
|
+
export { setLanguage, switchLanguage, getLanguage, messagesFor } from "./core/messages/index.js";
|
|
20
28
|
export { createRunStore, createRunFolder, listRuns, readConversation, } from "./core/runs.js";
|
|
21
29
|
// And the two functions an agent needs for tools of its own (an in-process MCP server,
|
|
22
30
|
// returned from AgentSpec.buildMcpServers()), so it doesn't need the SDK for those either.
|
|
@@ -36,8 +44,9 @@ export { terminalInteractionPort, setSharedReadline, getSharedReadline } from ".
|
|
|
36
44
|
setInteractionPort(terminalInteractionPort);
|
|
37
45
|
export { createHumanApprovalServer, DEFAULT_HUMAN_APPROVAL_TEXTS } from "./core/tools/humanApproval.js";
|
|
38
46
|
export { createManualLoginServer } from "./core/tools/manualLogin.js";
|
|
39
|
-
export { createSaveToSourcesServer, sourcesPromptSection } from "./
|
|
47
|
+
export { createSaveToSourcesServer, sourcesPromptSection } from "./extensions/sources/tools.js";
|
|
40
48
|
export { runChatTui } from "./tui/chatTui.js";
|
|
49
|
+
export { createChatController, } from "./chat/chatController.js";
|
|
41
50
|
export { createConsoleRenderer } from "./tui/consoleRenderer.js";
|
|
42
51
|
export { runChatInk } from "./tui/ink/runChatInk.js";
|
|
43
52
|
export { createProgressView } from "./tui/ink/progressView.js";
|
package/dist/tui/chatTui.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import type { ModeControl } from "../core/session.js";
|
|
3
|
+
import type { AgentEvent } from "../core/runner.js";
|
|
4
4
|
import { type Theme } from "./theme.js";
|
|
5
|
-
import {
|
|
5
|
+
import type { ToolLabels } from "../core/toolLabels.js";
|
|
6
|
+
import { type SessionOpener } from "../chat/runs.js";
|
|
7
|
+
export { capHistory, loadHistory, saveHistory, slashCommandToken } from "../chat/history.js";
|
|
6
8
|
/** Options of the chats (`runChatTui()`, and `runChatInk()` through `InkChatOptions`). */
|
|
7
9
|
export interface ChatTuiOptions {
|
|
8
10
|
/**
|
|
@@ -80,6 +82,12 @@ export interface ChatTuiOptions {
|
|
|
80
82
|
* returns is used instead.
|
|
81
83
|
*/
|
|
82
84
|
modeControl?: ModeControl;
|
|
85
|
+
/**
|
|
86
|
+
* How the chat shows the extensions' tools (`buildSessionOptions()` returns them as
|
|
87
|
+
* `toolLabels`), before `formatAction`. With a session opener, the ones it returns are used
|
|
88
|
+
* instead.
|
|
89
|
+
*/
|
|
90
|
+
toolLabels?: ToolLabels;
|
|
83
91
|
/**
|
|
84
92
|
* Colors for the kit's roles (see `Theme`), on top of the kit's defaults: only the roles
|
|
85
93
|
* given change, e.g. `{ toolResult: "yellow", selection: "#00ff00" }`. One theme per
|
|
@@ -87,22 +95,6 @@ export interface ChatTuiOptions {
|
|
|
87
95
|
*/
|
|
88
96
|
theme?: Partial<Theme>;
|
|
89
97
|
}
|
|
90
|
-
interface HistoryEntry {
|
|
91
|
-
text: string;
|
|
92
|
-
timestamp: string;
|
|
93
|
-
}
|
|
94
|
-
/** Keeps only the most recent `limit` entries (oldest-first order in, oldest-first out). */
|
|
95
|
-
export declare function capHistory(entries: readonly HistoryEntry[], limit: number): HistoryEntry[];
|
|
96
|
-
/** Oldest-first. Missing file reads as empty — nothing to load yet is the normal case. */
|
|
97
|
-
export declare function loadHistory(filePath: string): Promise<HistoryEntry[]>;
|
|
98
|
-
/** Overwrites the whole file (not appended) so a prior prune (oldest entries dropped) sticks. */
|
|
99
|
-
export declare function saveHistory(filePath: string, entries: readonly HistoryEntry[]): Promise<void>;
|
|
100
|
-
/**
|
|
101
|
-
* The command name a typed line would invoke (e.g. "chiste" for both "/chiste" and
|
|
102
|
-
* "/chiste con salsa"), or `null` for a line that isn't a slash command at all (doesn't
|
|
103
|
-
* start with "/") or is just a bare "/" with nothing after it.
|
|
104
|
-
*/
|
|
105
|
-
export declare function slashCommandToken(line: string): string | null;
|
|
106
98
|
/**
|
|
107
99
|
* Reads `events` one turn at a time — up to and including a `turn-end` — via manual
|
|
108
100
|
* `.next()` calls, calling `onEvent` for each. Deliberately never uses
|
|
@@ -139,4 +131,3 @@ export declare function drainTurn(events: AsyncIterator<AgentEvent>, onEvent: (e
|
|
|
139
131
|
* interface itself before returning.
|
|
140
132
|
*/
|
|
141
133
|
export declare function runChatTui(options: Options | SessionOpener, tuiOptions?: ChatTuiOptions): Promise<void>;
|
|
142
|
-
export {};
|
package/dist/tui/chatTui.js
CHANGED
|
@@ -1,66 +1,17 @@
|
|
|
1
1
|
import readline from "node:readline/promises";
|
|
2
2
|
import { stdin, stdout } from "node:process";
|
|
3
|
-
import { createWriteStream } from "node:fs";
|
|
4
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
5
|
-
import path from "node:path";
|
|
6
|
-
import { createInputQueue, togglePlanMode } from "../core/session.js";
|
|
7
|
-
import { runQuery } from "../core/runner.js";
|
|
8
|
-
import { listRuns, readConversation } from "../core/runs.js";
|
|
9
3
|
import { isSharedQuestionActive, setSharedReadline } from "./terminalInteraction.js";
|
|
10
4
|
import * as ui from "./ui.js";
|
|
11
5
|
import { t } from "../core/messages/index.js";
|
|
12
6
|
import { applyLanguage } from "./language.js";
|
|
13
7
|
import { applyTheme } from "./theme.js";
|
|
14
8
|
import { createConsoleRenderer } from "./consoleRenderer.js";
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
// sequences (cursor movement, OSC, ...) this kit never emits.
|
|
19
|
-
// eslint-disable-next-line no-control-regex -- \x1b is the ESC byte SGR sequences start with, not an accident
|
|
20
|
-
const ANSI_SGR = /\x1b\[[0-9;]*m/g;
|
|
21
|
-
function stripAnsi(text) {
|
|
22
|
-
return text.replace(ANSI_SGR, "");
|
|
23
|
-
}
|
|
9
|
+
import { runDate } from "../chat/runs.js";
|
|
10
|
+
import { createChatController } from "../chat/chatController.js";
|
|
11
|
+
export { capHistory, loadHistory, saveHistory, slashCommandToken } from "../chat/history.js";
|
|
24
12
|
const DEFAULT_PROMPT_LABEL = "\n> ";
|
|
25
13
|
const DEFAULT_EXIT_COMMANDS = ["/exit", "/quit"];
|
|
26
14
|
const DEFAULT_HISTORY_LIMIT = 100;
|
|
27
|
-
/** Keeps only the most recent `limit` entries (oldest-first order in, oldest-first out). */
|
|
28
|
-
export function capHistory(entries, limit) {
|
|
29
|
-
return entries.length > limit ? entries.slice(-limit) : entries.slice();
|
|
30
|
-
}
|
|
31
|
-
/** Oldest-first. Missing file reads as empty — nothing to load yet is the normal case. */
|
|
32
|
-
export async function loadHistory(filePath) {
|
|
33
|
-
let raw;
|
|
34
|
-
try {
|
|
35
|
-
raw = await readFile(filePath, "utf8");
|
|
36
|
-
}
|
|
37
|
-
catch (error) {
|
|
38
|
-
if (error.code === "ENOENT")
|
|
39
|
-
return [];
|
|
40
|
-
throw error;
|
|
41
|
-
}
|
|
42
|
-
return raw
|
|
43
|
-
.split("\n")
|
|
44
|
-
.filter((line) => line.trim().length > 0)
|
|
45
|
-
.map((line) => JSON.parse(line));
|
|
46
|
-
}
|
|
47
|
-
/** Overwrites the whole file (not appended) so a prior prune (oldest entries dropped) sticks. */
|
|
48
|
-
export async function saveHistory(filePath, entries) {
|
|
49
|
-
await mkdir(path.dirname(filePath), { recursive: true });
|
|
50
|
-
const body = entries.map((entry) => JSON.stringify(entry)).join("\n");
|
|
51
|
-
await writeFile(filePath, entries.length > 0 ? `${body}\n` : "", "utf8");
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* The command name a typed line would invoke (e.g. "chiste" for both "/chiste" and
|
|
55
|
-
* "/chiste con salsa"), or `null` for a line that isn't a slash command at all (doesn't
|
|
56
|
-
* start with "/") or is just a bare "/" with nothing after it.
|
|
57
|
-
*/
|
|
58
|
-
export function slashCommandToken(line) {
|
|
59
|
-
if (!line.startsWith("/"))
|
|
60
|
-
return null;
|
|
61
|
-
const token = line.slice(1).split(/\s/, 1)[0];
|
|
62
|
-
return token ? token : null;
|
|
63
|
-
}
|
|
64
15
|
/**
|
|
65
16
|
* Reads `events` one turn at a time — up to and including a `turn-end` — via manual
|
|
66
17
|
* `.next()` calls, calling `onEvent` for each. Deliberately never uses
|
|
@@ -108,65 +59,48 @@ export async function drainTurn(events, onEvent) {
|
|
|
108
59
|
export async function runChatTui(options, tuiOptions = {}) {
|
|
109
60
|
applyLanguage(tuiOptions.language);
|
|
110
61
|
applyTheme(tuiOptions.theme);
|
|
111
|
-
const exitCommands = new Set((tuiOptions.exitCommands ?? DEFAULT_EXIT_COMMANDS).map((c) => c.toLowerCase()));
|
|
112
62
|
const promptLabel = tuiOptions.promptLabel ?? DEFAULT_PROMPT_LABEL;
|
|
113
|
-
const opener = typeof options === "function" ? options : null;
|
|
114
|
-
const runsDir = opener ? tuiOptions.runsDir : undefined;
|
|
115
|
-
if (opener && !runsDir)
|
|
116
|
-
throw new Error("runChatTui(): a session opener needs `runsDir`.");
|
|
117
|
-
// The session in use: with runs, the run's folder, rebuilt on /resume.
|
|
118
|
-
let current = opener && runsDir ? await openSession(opener, await firstRun(runsDir)) : { options: options, run: null };
|
|
119
|
-
const modeControl = () => current.modeControl ?? tuiOptions.modeControl;
|
|
120
|
-
let queue = createInputQueue({ modeControl: modeControl() });
|
|
121
|
-
let run = runQuery(queue.iterable, current.options);
|
|
122
|
-
let events = run.events[Symbol.asyncIterator]();
|
|
123
63
|
const historyLimit = tuiOptions.historyLimit ?? DEFAULT_HISTORY_LIMIT;
|
|
124
|
-
|
|
64
|
+
// The readline interface and its writer, set below once the history it starts with is loaded.
|
|
65
|
+
const late = {};
|
|
66
|
+
const controller = await createChatController(options, {
|
|
67
|
+
exitCommands: tuiOptions.exitCommands ?? DEFAULT_EXIT_COMMANDS,
|
|
68
|
+
initialPrompt: tuiOptions.initialPrompt,
|
|
69
|
+
sessionLogPath: tuiOptions.sessionLogPath,
|
|
70
|
+
promptLabel,
|
|
71
|
+
agentLabel: tuiOptions.agentLabel,
|
|
72
|
+
historyPath: tuiOptions.historyPath,
|
|
73
|
+
historyLimit,
|
|
74
|
+
runsDir: tuiOptions.runsDir,
|
|
75
|
+
modeControl: tuiOptions.modeControl,
|
|
76
|
+
toolLabels: tuiOptions.toolLabels,
|
|
77
|
+
formatAction: tuiOptions.formatAction,
|
|
78
|
+
// Nothing here shows them.
|
|
79
|
+
promptSuggestions: false,
|
|
80
|
+
// /resume: a numbered list, and a number typed.
|
|
81
|
+
async pick(title, choices) {
|
|
82
|
+
late.writeLine(ui.heading(title));
|
|
83
|
+
choices.forEach((choice) => late.writeLine(` ${choice.label}`));
|
|
84
|
+
const answer = (await late.rl.question(t().resumeQuestion)).trim();
|
|
85
|
+
return choices[Number(answer) - 1]?.value ?? null;
|
|
86
|
+
},
|
|
87
|
+
});
|
|
125
88
|
const rl = readline.createInterface({
|
|
126
89
|
input: stdin,
|
|
127
90
|
output: stdout,
|
|
128
|
-
// readline wants most-recent-first (↑ shows
|
|
129
|
-
|
|
130
|
-
history: historyEntries.map((entry) => entry.text).reverse(),
|
|
91
|
+
// readline wants most-recent-first (↑ shows the newest entry first); the history is oldest-first.
|
|
92
|
+
history: [...controller.getState().history].reverse(),
|
|
131
93
|
historySize: historyLimit,
|
|
132
94
|
});
|
|
133
95
|
setSharedReadline(rl);
|
|
134
|
-
// With runs, the session log is the run's own; it moves with /resume.
|
|
135
|
-
let sessionLog = null;
|
|
136
|
-
function openLog() {
|
|
137
|
-
sessionLog?.end();
|
|
138
|
-
const file = current.run ? path.join(current.run.dir, "session.log") : tuiOptions.sessionLogPath;
|
|
139
|
-
sessionLog = file ? createWriteStream(file, { flags: "a" }) : null;
|
|
140
|
-
}
|
|
141
|
-
openLog();
|
|
142
|
-
// Mirrors to the log file only — for text already visible on the terminal some other
|
|
143
|
-
// way (readline's own prompt + the human's local echo), so it isn't printed twice.
|
|
144
|
-
function mirror(text) {
|
|
145
|
-
sessionLog?.write(stripAnsi(text));
|
|
146
|
-
}
|
|
147
|
-
// Prints to the console *and* mirrors to the log file — the normal case for anything
|
|
148
|
-
// this loop itself puts on screen (streamed reply text, action/error lines, ...).
|
|
149
|
-
// Tracks whether the terminal cursor currently sits at column 0 of a fresh row — true
|
|
150
|
-
// right after any write ending in "\n" (writeLine's normal case), false right after a
|
|
151
|
-
// streamed text delta that doesn't (the common case: deltas rarely end mid-sentence on a
|
|
152
|
-
// newline). Confirmed empirically (against a working chat loop that
|
|
153
|
-
// ends its streamed text block with an explicit newline): even with `prevRows` reset to 0 above, if the
|
|
154
|
-
// cursor is left *mid-row* when the next prompt redraws, `[kRefreshLine]`'s `cursorTo(0)`
|
|
155
|
-
// moves it to column 0 of that *same* row — the row still holding the tail of what we
|
|
156
|
-
// just wrote — and the following `clearScreenDown` wipes the row from there on, eating
|
|
157
|
-
// whatever was on it (for a short reply, potentially the whole thing, since "that row" is
|
|
158
|
-
// its only row). Ending every turn's output on a fresh blank row (see below, after
|
|
159
|
-
// `drainTurn()`) sidesteps this entirely: `clearScreenDown` then only ever clears blank
|
|
160
|
-
// space, on any row.
|
|
161
96
|
// Every write goes through the shared console renderer (./consoleRenderer.ts), which
|
|
162
97
|
// tracks whether the cursor sits at the start of a line and never emits a blank line
|
|
163
|
-
// between consecutive action lines.
|
|
98
|
+
// between consecutive action lines. The session log is the controller's.
|
|
164
99
|
const renderer = createConsoleRenderer({
|
|
165
|
-
formatAction:
|
|
100
|
+
formatAction: controller.formatAction,
|
|
166
101
|
agentLabel: tuiOptions.agentLabel,
|
|
167
102
|
output: (text) => void stdout.write(text),
|
|
168
|
-
onWrite: (
|
|
169
|
-
mirror(text);
|
|
103
|
+
onWrite: () => {
|
|
170
104
|
// node:readline's `Interface` tracks, on itself, how many terminal rows its own last
|
|
171
105
|
// rendered prompt+line took (`prevRows`), so that the *next* time it redraws (the next
|
|
172
106
|
// `rl.question()` for the following turn, but — confirmed empirically — also any
|
|
@@ -188,43 +122,45 @@ export async function runChatTui(options, tuiOptions = {}) {
|
|
|
188
122
|
},
|
|
189
123
|
});
|
|
190
124
|
const { writeLine } = renderer;
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
125
|
+
Object.assign(late, { rl, writeLine });
|
|
126
|
+
const toned = (text, tone) => (tone === "plain" ? text : tone === "dim" ? ui.dim(text) : tone === "warn" ? ui.warn(text) : ui.error(text));
|
|
127
|
+
controller.onEvent((event) => {
|
|
128
|
+
switch (event.type) {
|
|
129
|
+
case "agent":
|
|
130
|
+
if (event.event.type !== "prompt-suggestion")
|
|
131
|
+
renderer.render(event.event);
|
|
132
|
+
return;
|
|
133
|
+
case "notice":
|
|
134
|
+
writeLine(toned(event.text, event.tone));
|
|
135
|
+
return;
|
|
136
|
+
case "turn-start":
|
|
137
|
+
renderer.startTurn();
|
|
138
|
+
return;
|
|
139
|
+
case "turn-end":
|
|
140
|
+
// Every turn ends on a fresh row, so readline's redraw only ever clears blank space.
|
|
141
|
+
renderer.endLine();
|
|
142
|
+
return;
|
|
143
|
+
case "conversation":
|
|
144
|
+
for (const message of event.messages) {
|
|
145
|
+
// Printed, not logged: the run's session log already has it.
|
|
146
|
+
stdout.write(message.role === "user" ? `${promptLabel}${message.text}\n` : `\n${tuiOptions.agentLabel ? `${tuiOptions.agentLabel} ` : ""}${message.text}\n`);
|
|
147
|
+
}
|
|
148
|
+
writeLine(ui.dim(`\n${t().resumed(runDate(event.updatedAt))}`));
|
|
149
|
+
return;
|
|
150
|
+
case "mode":
|
|
151
|
+
// The mode changed (/plan, or the agent leaving plan mode with present_plan): say so.
|
|
152
|
+
writeLine(ui.dim(`⏵⏵ ${t().mode(event.mode)}`));
|
|
153
|
+
return;
|
|
154
|
+
case "user":
|
|
155
|
+
case "session":
|
|
156
|
+
return; // readline already echoed the line; nothing to draw for a reopened session
|
|
208
157
|
}
|
|
209
|
-
|
|
210
|
-
}
|
|
211
|
-
let turnInFlight = false;
|
|
212
|
-
let turnInterrupted = false;
|
|
213
|
-
function interruptTurn() {
|
|
214
|
-
if (turnInterrupted)
|
|
215
|
-
return;
|
|
216
|
-
turnInterrupted = true;
|
|
217
|
-
void run.interrupt();
|
|
218
|
-
writeLine(ui.warn(t().interrupted));
|
|
219
|
-
}
|
|
158
|
+
});
|
|
220
159
|
rl.on("SIGINT", () => {
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
else {
|
|
225
|
-
queue.end();
|
|
160
|
+
if (controller.getState().busy)
|
|
161
|
+
controller.interrupt();
|
|
162
|
+
else
|
|
226
163
|
rl.close();
|
|
227
|
-
}
|
|
228
164
|
});
|
|
229
165
|
// Esc interrupts the turn in flight too, like Ctrl+C — but, unlike it, never ends the
|
|
230
166
|
// chat, and does nothing while a human-in-the-loop checkpoint is asking on this same
|
|
@@ -232,121 +168,30 @@ export async function runChatTui(options, tuiOptions = {}) {
|
|
|
232
168
|
// already emits "keypress" on stdin, and only reports name "escape" for a lone Esc, not
|
|
233
169
|
// for the Esc-prefixed sequences of arrow/function keys.
|
|
234
170
|
const onKeypress = (_chunk, key) => {
|
|
235
|
-
if (key?.name === "escape" &&
|
|
236
|
-
|
|
171
|
+
if (key?.name === "escape" && controller.getState().busy && !isSharedQuestionActive())
|
|
172
|
+
controller.interrupt();
|
|
237
173
|
};
|
|
238
174
|
stdin.on("keypress", onKeypress);
|
|
239
|
-
/** Prints the run's earlier conversation (when it has one) and says it was resumed. */
|
|
240
|
-
async function showConversation(folder) {
|
|
241
|
-
if (!folder.sessionId)
|
|
242
|
-
return;
|
|
243
|
-
for (const message of await readConversation(folder.dir)) {
|
|
244
|
-
// Printed, not logged: the run's session log already has it.
|
|
245
|
-
stdout.write(message.role === "user" ? `${promptLabel}${message.text}\n` : `\n${tuiOptions.agentLabel ? `${tuiOptions.agentLabel} ` : ""}${message.text}\n`);
|
|
246
|
-
}
|
|
247
|
-
const [summary] = (await listRuns(path.dirname(folder.dir))).filter((r) => path.resolve(r.dir) === path.resolve(folder.dir));
|
|
248
|
-
writeLine(ui.dim(`\n${t().resumed(runDate(summary?.updatedAt ?? new Date()))}`));
|
|
249
|
-
}
|
|
250
|
-
/** /resume: lists the runs, asks for one and switches the session to it. */
|
|
251
|
-
async function resume() {
|
|
252
|
-
if (!opener || !runsDir)
|
|
253
|
-
return;
|
|
254
|
-
const runs = await listRuns(runsDir);
|
|
255
|
-
if (runs.length === 0) {
|
|
256
|
-
writeLine(ui.dim(t().noEarlierRuns));
|
|
257
|
-
return;
|
|
258
|
-
}
|
|
259
|
-
writeLine(ui.heading(t().resumeTitle));
|
|
260
|
-
runs.forEach((summary, i) => writeLine(` ${i + 1}. ${runLabel(summary, current.run ?? undefined)}`));
|
|
261
|
-
const answer = (await rl.question(t().resumeQuestion)).trim();
|
|
262
|
-
const summary = runs[Number(answer) - 1];
|
|
263
|
-
if (!answer || !summary || (current.run && path.resolve(summary.dir) === path.resolve(current.run.dir)))
|
|
264
|
-
return;
|
|
265
|
-
queue.end();
|
|
266
|
-
run.close();
|
|
267
|
-
current = await openSession(opener, await runFolderOf(summary));
|
|
268
|
-
openLog();
|
|
269
|
-
queue = createInputQueue({ modeControl: modeControl() });
|
|
270
|
-
run = runQuery(queue.iterable, current.options);
|
|
271
|
-
events = run.events[Symbol.asyncIterator]();
|
|
272
|
-
watchMode();
|
|
273
|
-
knownCommandTokens = null;
|
|
274
|
-
await showConversation(current.run);
|
|
275
|
-
}
|
|
276
175
|
if (tuiOptions.welcomeMessage)
|
|
277
|
-
|
|
176
|
+
controller.notice(tuiOptions.welcomeMessage, "plain");
|
|
278
177
|
try {
|
|
279
|
-
|
|
280
|
-
await showConversation(current.run);
|
|
281
|
-
if (tuiOptions.initialPrompt && !current.run?.sessionId) {
|
|
282
|
-
mirror(`${promptLabel}${tuiOptions.initialPrompt}\n`);
|
|
283
|
-
queue.push(tuiOptions.initialPrompt);
|
|
284
|
-
turnInFlight = true;
|
|
285
|
-
turnInterrupted = false;
|
|
286
|
-
renderer.startTurn();
|
|
287
|
-
await drainTurn(events, renderer.render);
|
|
288
|
-
turnInFlight = false;
|
|
289
|
-
renderer.endLine();
|
|
290
|
-
}
|
|
178
|
+
await controller.start();
|
|
291
179
|
while (true) {
|
|
292
180
|
let line;
|
|
293
181
|
try {
|
|
294
|
-
line =
|
|
182
|
+
line = await rl.question(promptLabel);
|
|
295
183
|
}
|
|
296
184
|
catch {
|
|
297
185
|
break; // the interface closed underneath us (SIGINT/EOF while idle)
|
|
298
186
|
}
|
|
299
|
-
if (
|
|
300
|
-
continue;
|
|
301
|
-
mirror(`${promptLabel}${line}\n`);
|
|
302
|
-
if (tuiOptions.historyPath) {
|
|
303
|
-
historyEntries = capHistory([...historyEntries, { text: line, timestamp: new Date().toISOString() }], historyLimit);
|
|
304
|
-
await saveHistory(tuiOptions.historyPath, historyEntries);
|
|
305
|
-
}
|
|
306
|
-
if (exitCommands.has(line.toLowerCase()))
|
|
187
|
+
if ((await controller.send(line)) === "exit")
|
|
307
188
|
break;
|
|
308
|
-
if (runsDir && line.toLowerCase() === "/resume") {
|
|
309
|
-
await resume();
|
|
310
|
-
continue;
|
|
311
|
-
}
|
|
312
|
-
// Plan mode on and off (the Ink chat also has Shift+Tab); the line says where it is now.
|
|
313
|
-
if (line.toLowerCase() === "/plan") {
|
|
314
|
-
const control = modeControl();
|
|
315
|
-
const next = control ? togglePlanMode(control) : null;
|
|
316
|
-
// The new mode's line comes from watchMode(); a control without subscribe() says it here.
|
|
317
|
-
if (!next)
|
|
318
|
-
writeLine(ui.dim(t().modeLocked(control?.mode)));
|
|
319
|
-
else if (!control?.subscribe)
|
|
320
|
-
writeLine(ui.dim(`⏵⏵ ${t().mode(next)}`));
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
// A line that *looks* like a slash command but matches nothing registered (a typo,
|
|
324
|
-
// an unnamespaced plugin command, ...) would otherwise reach the model as literal
|
|
325
|
-
// text with no special handling — confirmed empirically (captain-whiskers'
|
|
326
|
-
// /chiste before it was renamed /captain-whiskers:chiste) that this can look
|
|
327
|
-
// exactly like nothing happened at all. Caught here, before the turn even starts,
|
|
328
|
-
// so it's an immediate, unambiguous message instead of a guess at what the model's
|
|
329
|
-
// silence on unrecognized input might mean.
|
|
330
|
-
const commandToken = slashCommandToken(line);
|
|
331
|
-
if (commandToken && !(await isKnownSlashCommand(commandToken))) {
|
|
332
|
-
writeLine(ui.warn(t().unknownCommand(commandToken)));
|
|
333
|
-
continue;
|
|
334
|
-
}
|
|
335
|
-
queue.push(line);
|
|
336
|
-
turnInFlight = true;
|
|
337
|
-
turnInterrupted = false;
|
|
338
|
-
renderer.startTurn();
|
|
339
|
-
await drainTurn(events, renderer.render);
|
|
340
|
-
turnInFlight = false;
|
|
341
|
-
renderer.endLine();
|
|
342
189
|
}
|
|
343
190
|
}
|
|
344
191
|
finally {
|
|
345
192
|
stdin.off("keypress", onKeypress);
|
|
346
|
-
|
|
347
|
-
run.close();
|
|
193
|
+
controller.close();
|
|
348
194
|
setSharedReadline(null);
|
|
349
195
|
rl.close();
|
|
350
|
-
sessionLog?.end();
|
|
351
196
|
}
|
|
352
197
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type ExtensionDirs } from "../core/externalExtensions.js";
|
|
2
|
+
export { chatExtensionsCommand, describe } from "../chat/extensions.js";
|
|
3
|
+
/**
|
|
4
|
+
* The `extension` command an agent exposes in its own binary (#37), e.g. `captain extension add
|
|
5
|
+
* ./jokebook --project`: installs, removes, enables, disables and lists the extensions in its two
|
|
6
|
+
* scopes. Returns `false` when `argv` isn't this command (so the agent goes on), and sets
|
|
7
|
+
* `process.exitCode` to 1 on an error.
|
|
8
|
+
*
|
|
9
|
+
* - `extension list`
|
|
10
|
+
* - `extension info <name>`: its metadata, what it offers and where its README is
|
|
11
|
+
* - `extension add <folder | git URL[#ref] | plugin[@marketplace]> [--project] [--path <subfolder>] [--yes]`
|
|
12
|
+
* - `extension remove|enable|disable <name> [--project | --agent]`
|
|
13
|
+
* - `extension search [words]`: what the known marketplaces offer
|
|
14
|
+
* - `extension marketplace add <folder | git URL[#ref] | owner/repo> [--yes]`, `list`, `update [name]`, `remove <name>`
|
|
15
|
+
*
|
|
16
|
+
* Without a scope, `add` installs into the agent's (the project's when the agent has no scope of
|
|
17
|
+
* its own); the others act where the extension is, the project's first. The marketplaces live in
|
|
18
|
+
* the same folder. `official` is the agent's own marketplace (a folder or git URL): known without
|
|
19
|
+
* asking, and installed from without a confirmation. Any other is added after a warning and
|
|
20
|
+
* typing its name, and installing from it asks first; `--yes` answers for the person (a script).
|
|
21
|
+
*/
|
|
22
|
+
export declare function runExtensionCommand(argv: readonly string[], options: {
|
|
23
|
+
dirs: ExtensionDirs;
|
|
24
|
+
command?: string;
|
|
25
|
+
write?: (line: string) => void;
|
|
26
|
+
/** The agent's official marketplace: a folder or a git URL (`#ref`). */
|
|
27
|
+
official?: string;
|
|
28
|
+
/** Asks the person (a confirmation); by default on the terminal, and nothing without one. */
|
|
29
|
+
ask?: (question: string) => Promise<string>;
|
|
30
|
+
}): Promise<boolean>;
|