@falkenslab/agent-kit 0.17.0 → 0.19.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.
Files changed (111) hide show
  1. package/assets/extension-launcher.mjs +24 -0
  2. package/dist/chat/chatController.d.ts +264 -0
  3. package/dist/chat/chatController.js +510 -0
  4. package/dist/chat/extensions.d.ts +14 -0
  5. package/dist/chat/extensions.js +41 -0
  6. package/dist/chat/history.d.ts +17 -0
  7. package/dist/chat/history.js +39 -0
  8. package/dist/{tui → chat}/runs.d.ts +8 -1
  9. package/dist/{tui → chat}/runs.js +1 -1
  10. package/dist/core/agentSpec.d.ts +41 -27
  11. package/dist/core/extensions.d.ts +119 -0
  12. package/dist/core/extensions.js +97 -0
  13. package/dist/core/externalExtensions.d.ts +131 -0
  14. package/dist/core/externalExtensions.js +340 -0
  15. package/dist/core/hooks/fileScopeGate.d.ts +17 -2
  16. package/dist/core/hooks/fileScopeGate.js +45 -10
  17. package/dist/core/hooks/planGate.d.ts +2 -0
  18. package/dist/core/hooks/planGate.js +1 -10
  19. package/dist/core/hooks/stepGate.d.ts +1 -1
  20. package/dist/core/hooks/stepGate.js +3 -3
  21. package/dist/core/language.js +1 -1
  22. package/dist/core/messages/de.js +12 -33
  23. package/dist/core/messages/en.d.ts +13 -16
  24. package/dist/core/messages/en.js +12 -33
  25. package/dist/core/messages/es.js +12 -33
  26. package/dist/core/messages/fr.js +12 -33
  27. package/dist/core/messages/index.d.ts +6 -0
  28. package/dist/core/messages/index.js +12 -1
  29. package/dist/core/packaged.d.ts +10 -0
  30. package/dist/core/packaged.js +38 -0
  31. package/dist/core/pluginAgents.d.ts +20 -0
  32. package/dist/core/pluginAgents.js +76 -0
  33. package/dist/core/runner.js +16 -9
  34. package/dist/core/session.d.ts +42 -5
  35. package/dist/core/session.js +203 -99
  36. package/dist/core/sessionFacts.d.ts +80 -0
  37. package/dist/core/sessionFacts.js +48 -0
  38. package/dist/core/toolLabels.d.ts +24 -6
  39. package/dist/core/toolLabels.js +24 -39
  40. package/dist/core/tools/humanApproval.js +3 -3
  41. package/dist/extensions/awareness/index.d.ts +13 -0
  42. package/dist/extensions/awareness/index.js +39 -0
  43. package/dist/extensions/awareness/labels.d.ts +3 -0
  44. package/dist/extensions/awareness/labels.js +12 -0
  45. package/dist/extensions/awareness/tools.d.ts +5 -0
  46. package/dist/extensions/awareness/tools.js +54 -0
  47. package/dist/{core → extensions/knowledge}/fileKnowledgeStore.d.ts +1 -3
  48. package/dist/{core → extensions/knowledge}/fileKnowledgeStore.js +23 -29
  49. package/dist/extensions/knowledge/index.d.ts +8 -0
  50. package/dist/extensions/knowledge/index.js +35 -0
  51. package/dist/{core → extensions/knowledge}/knowledgeStore.d.ts +5 -9
  52. package/dist/{core → extensions/knowledge}/knowledgeStore.js +16 -2
  53. package/dist/extensions/knowledge/labels.d.ts +3 -0
  54. package/dist/extensions/knowledge/labels.js +119 -0
  55. package/dist/extensions/knowledge/prompt.d.ts +28 -0
  56. package/dist/extensions/knowledge/prompt.js +59 -0
  57. package/dist/{core/tools/knowledgeTools.d.ts → extensions/knowledge/tools.d.ts} +7 -7
  58. package/dist/{core/tools/knowledgeTools.js → extensions/knowledge/tools.js} +20 -55
  59. package/dist/extensions/memory/index.d.ts +12 -0
  60. package/dist/extensions/memory/index.js +56 -0
  61. package/dist/extensions/memory/labels.d.ts +3 -0
  62. package/dist/extensions/memory/labels.js +58 -0
  63. package/dist/extensions/memory/memoryStore.d.ts +53 -0
  64. package/dist/extensions/memory/memoryStore.js +88 -0
  65. package/dist/extensions/memory/tools.d.ts +9 -0
  66. package/dist/extensions/memory/tools.js +82 -0
  67. package/dist/extensions/sources/index.d.ts +9 -0
  68. package/dist/extensions/sources/index.js +40 -0
  69. package/dist/extensions/sources/labels.d.ts +3 -0
  70. package/dist/extensions/sources/labels.js +82 -0
  71. package/dist/{core → extensions/sources}/sources.d.ts +24 -31
  72. package/dist/{core → extensions/sources}/sources.js +45 -108
  73. package/dist/{core/tools/saveToSources.d.ts → extensions/sources/tools.d.ts} +7 -3
  74. package/dist/{core/tools/saveToSources.js → extensions/sources/tools.js} +22 -17
  75. package/dist/index.d.ts +14 -9
  76. package/dist/index.js +11 -7
  77. package/dist/tui/chatTui.d.ts +11 -20
  78. package/dist/tui/chatTui.js +75 -230
  79. package/dist/tui/extensionCommand.d.ts +21 -0
  80. package/dist/tui/extensionCommand.js +133 -0
  81. package/dist/tui/ink/runChatInk.d.ts +4 -3
  82. package/dist/tui/ink/runChatInk.js +101 -217
  83. package/extensions/awareness/.claude-plugin/plugin.json +10 -0
  84. package/{assets/agent-help → extensions/awareness/skills/help}/SKILL.md +4 -3
  85. package/{assets/knowledge-plugin → extensions/knowledge}/.claude-plugin/plugin.json +7 -1
  86. package/extensions/knowledge/commands/ingest.md +7 -0
  87. package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-ingest/SKILL.md +8 -3
  88. package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-lint/SKILL.md +12 -5
  89. package/extensions/memory/.claude-plugin/plugin.json +10 -0
  90. package/extensions/memory/commands/forget.md +8 -0
  91. package/extensions/memory/commands/list.md +5 -0
  92. package/extensions/sources/.claude-plugin/plugin.json +10 -0
  93. package/package.json +3 -2
  94. package/assets/knowledge-plugin/commands/ingest.md +0 -7
  95. package/assets/knowledge-plugin/commands/query.md +0 -5
  96. package/assets/knowledge-plugin-files/.claude-plugin/plugin.json +0 -4
  97. package/assets/knowledge-plugin-files/commands/ingest.md +0 -7
  98. package/assets/knowledge-plugin-files/commands/lint.md +0 -5
  99. package/assets/knowledge-plugin-files/skills/knowledge-ingest/SKILL.md +0 -46
  100. package/assets/knowledge-plugin-files/skills/knowledge-lint/SKILL.md +0 -26
  101. package/assets/knowledge-plugin-files/skills/knowledge-pages/SKILL.md +0 -137
  102. package/assets/knowledge-plugin-files/skills/knowledge-query/SKILL.md +0 -14
  103. package/dist/core/agentHelp.d.ts +0 -30
  104. package/dist/core/agentHelp.js +0 -55
  105. package/dist/core/knowledge.d.ts +0 -25
  106. package/dist/core/knowledge.js +0 -84
  107. /package/dist/{core → extensions/sources}/extractText.d.ts +0 -0
  108. /package/dist/{core → extensions/sources}/extractText.js +0 -0
  109. /package/{assets/knowledge-plugin → extensions/knowledge}/commands/lint.md +0 -0
  110. /package/{assets/knowledge-plugin-files → extensions/knowledge}/commands/query.md +0 -0
  111. /package/{assets/knowledge-plugin → extensions/knowledge}/skills/knowledge-query/SKILL.md +0 -0
@@ -1,35 +1,31 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { createWriteStream } from "node:fs";
3
- import path from "node:path";
4
2
  import { stdin, stdout } from "node:process";
5
3
  import { useSyncExternalStore } from "react";
6
4
  import { Box, render, Text, useInput } from "ink";
7
5
  import { Select } from "@inkjs/ui";
8
6
  import { getInteractionPort, setInteractionPort } from "../../core/interaction.js";
9
- import { createInputQueue, togglePlanMode } from "../../core/session.js";
10
- import { runQuery } from "../../core/runner.js";
11
- import { listRuns, readConversation } from "../../core/runs.js";
12
- import { capHistory, loadHistory, runChatTui, saveHistory, slashCommandToken } from "../chatTui.js";
7
+ import { runChatTui } from "../chatTui.js";
8
+ import { createChatController } from "../../chat/chatController.js";
13
9
  import * as ui from "../ui.js";
14
10
  import { t } from "../../core/messages/index.js";
15
11
  import { applyLanguage } from "../language.js";
16
12
  import { applyTheme, inkColor } from "../theme.js";
17
13
  import { KitTheme } from "./inkTheme.js";
18
- import { firstRun, openSession, runDate, runFolderOf, runLabel } from "../runs.js";
14
+ import { runDate } from "../../chat/runs.js";
19
15
  import { createInkInteraction } from "./inkInteraction.js";
20
16
  import { PromptInput } from "./PromptInput.js";
21
17
  import { createSessionModel, liveWidth } from "./sessionModel.js";
22
18
  import { SessionView } from "./SessionView.js";
23
- import { fitWidth, stripAnsi } from "./lineBuffer.js";
19
+ import { fitWidth } from "./lineBuffer.js";
24
20
  import { clipboardSequence, enterFullscreen } from "./fullscreen.js";
25
21
  import { createCursorController, CursorContext } from "./terminalCursor.js";
26
22
  import { createTerminalStatus, focusFromReport } from "./terminalStatus.js";
27
23
  import { listProjectFiles } from "./fileMentions.js";
28
24
  import { headerLines } from "./header.js";
29
25
  const DEFAULT_PROMPT_LABEL = "\n> ";
30
- /** The chat's own commands, never sent to the model (the agent-help skill lists them; a test keeps both in step). */
26
+ /** The chat's own commands, never sent to the model (the awareness extension's help skill lists them; a test keeps both in step). */
31
27
  export const DEFAULT_EXIT_COMMANDS = ["/exit", "/quit"];
32
- export const LOCAL_COMMANDS = { copy: "/copy", resume: "/resume", plan: "/plan" };
28
+ export const LOCAL_COMMANDS = { copy: "/copy", resume: "/resume", plan: "/plan", extensions: "/extensions" };
33
29
  const DEFAULT_HISTORY_LIMIT = 100;
34
30
  // The prompt's frame: a border and one column of padding on each side.
35
31
  const PROMPT_FRAME_COLUMNS = 4;
@@ -120,11 +116,12 @@ function PickerPanel({ picker }) {
120
116
  const width = liveWidth(columns) - PROMPT_FRAME_COLUMNS;
121
117
  return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: inkColor("border"), paddingX: 1, children: [_jsx(Text, { bold: true, children: picker.title }), _jsx(Select, { options: picker.options.map((option) => ({ ...option, label: fitWidth(option.label, Math.max(10, width - 2)) })), visibleOptionCount: Math.min(8, picker.options.length), onChange: (value) => picker.resolve(value) }), _jsx(Text, { dimColor: true, children: t().resumeHint })] }));
122
118
  }
123
- function ChatApp({ model, interaction, input, promptLabel, renderApproval, mode, fullscreen, header, onFocusChange, modeControl: currentModeControl, onInterrupt }) {
119
+ function ChatApp({ model, interaction, input, promptLabel, renderApproval, mode, fullscreen, header, onFocusChange, controller }) {
124
120
  const chat = useSyncExternalStore(input.subscribe, input.getSnapshot);
125
121
  const session = useSyncExternalStore(model.subscribe, model.getSnapshot);
126
122
  const checkpoint = useSyncExternalStore(interaction.subscribe, interaction.getSnapshot);
127
- const modeControl = currentModeControl();
123
+ const control = useSyncExternalStore(controller.subscribe, controller.getState);
124
+ const onInterrupt = () => controller.interrupt();
128
125
  // Ctrl+C stops what is running (a checkpoint, then the turn) and leaves only when idle;
129
126
  // Esc interrupts the turn but never answers a checkpoint or leaves.
130
127
  useInput((text, key) => {
@@ -139,13 +136,7 @@ function ChatApp({ model, interaction, input, promptLabel, renderApproval, mode,
139
136
  }
140
137
  if (key.shift && key.tab) {
141
138
  // Shift+Tab: the next mode this session allows.
142
- if (!modeControl || modeControl.switchable.length < 2) {
143
- model.note(ui.dim(t().modeLocked(modeControl?.mode ?? mode)), "notice");
144
- return;
145
- }
146
- const next = modeControl.switchable[(modeControl.switchable.indexOf(modeControl.mode) + 1) % modeControl.switchable.length];
147
- modeControl.set(next);
148
- input.setMode(next);
139
+ controller.cycleMode();
149
140
  return;
150
141
  }
151
142
  if (key.ctrl && text === "c" && chat.picker) {
@@ -164,7 +155,7 @@ function ChatApp({ model, interaction, input, promptLabel, renderApproval, mode,
164
155
  onInterrupt();
165
156
  }
166
157
  });
167
- return (_jsx(SessionView, { model: model, interaction: interaction, renderApproval: renderApproval, mode: chat.mode ?? mode, modeSwitchable: (modeControl?.switchable.length ?? 0) > 1, closed: chat.closed, fullscreen: fullscreen, header: header, onCopy: fullscreen ? (text) => void stdout.write(clipboardSequence(text)) : undefined, children: _jsxs(Box, { flexDirection: "column", children: [chat.queued.map((line, index) => (_jsx(Text, { wrap: "truncate-end", children: ui.dim(` ${t().queued(line)}`) }, index))), chat.picker ? (_jsx(PickerPanel, { picker: chat.picker })) : (_jsx(Box, { borderStyle: "round", borderColor: inkColor("border"), paddingX: 1, children: _jsx(PromptInput, { label: promptLabel.replace(/^\n+/, ""), history: chat.history, commands: chat.commands, inset: PROMPT_FRAME_COLUMNS, suggestion: chat.suggestion, onSubmit: (line) => input.submit(line), onRecallQueued: () => input.unqueueLast(), files: chat.files, onExit: () => input.submit(null) }) }))] }) }));
158
+ return (_jsx(SessionView, { model: model, interaction: interaction, renderApproval: renderApproval, mode: control.mode ?? mode, modeSwitchable: control.switchableModes.length > 1, closed: chat.closed, fullscreen: fullscreen, header: header, onCopy: fullscreen ? (text) => void stdout.write(clipboardSequence(text)) : undefined, children: _jsxs(Box, { flexDirection: "column", children: [chat.queued.map((line, index) => (_jsx(Text, { wrap: "truncate-end", children: ui.dim(` ${t().queued(line)}`) }, index))), chat.picker ? (_jsx(PickerPanel, { picker: chat.picker })) : (_jsx(Box, { borderStyle: "round", borderColor: inkColor("border"), paddingX: 1, children: _jsx(PromptInput, { label: promptLabel.replace(/^\n+/, ""), history: chat.history, commands: chat.commands, inset: PROMPT_FRAME_COLUMNS, suggestion: chat.suggestion, onSubmit: (line) => input.submit(line), onRecallQueued: () => input.unqueueLast(), files: chat.files, onExit: () => input.submit(null) }) }))] }) }));
168
159
  }
169
160
  /**
170
161
  * The Ink counterpart of `runChatTui()`, with the same options, session log and history
@@ -188,166 +179,107 @@ export async function runChatInk(options, tuiOptions = {}) {
188
179
  const languageWarnings = [];
189
180
  applyLanguage(tuiOptions.language, (line) => languageWarnings.push(line));
190
181
  applyTheme(tuiOptions.theme);
191
- const opener = typeof options === "function" ? options : null;
192
- const runsDir = opener ? tuiOptions.runsDir : undefined;
193
- if (opener && !runsDir)
194
- throw new Error("runChatInk(): a session opener needs `runsDir`.");
195
- const exitCommands = new Set((tuiOptions.exitCommands ?? DEFAULT_EXIT_COMMANDS).map((c) => c.toLowerCase()));
196
182
  const promptLabel = tuiOptions.promptLabel ?? DEFAULT_PROMPT_LABEL;
197
183
  const userLine = (text) => `${promptLabel.replace(/^\n+/, "")}${text}`;
198
- const historyLimit = tuiOptions.historyLimit ?? DEFAULT_HISTORY_LIMIT;
199
- let historyEntries = tuiOptions.historyPath ? capHistory(await loadHistory(tuiOptions.historyPath), historyLimit) : [];
200
- // The session in use: with runs, the run's folder, rebuilt on /resume.
201
- let current = opener && runsDir ? await openSession(opener, await firstRun(runsDir)) : { options: options, run: null };
202
- const modeControl = () => current.modeControl ?? tuiOptions.modeControl;
203
- // With runs, the session log is the run's own; it moves with /resume.
204
- let sessionLog = null;
205
- function openLog() {
206
- sessionLog?.end();
207
- const file = current.run ? path.join(current.run.dir, "session.log") : tuiOptions.sessionLogPath;
208
- sessionLog = file ? createWriteStream(file, { flags: "a" }) : null;
209
- }
210
- openLog();
211
- const mirror = (text) => void sessionLog?.write(stripAnsi(text));
212
- const model = createSessionModel({
184
+ const input = createChatInput();
185
+ // Set once the terminal integration is up: /copy needs it.
186
+ let status = null;
187
+ const controller = await createChatController(options, {
188
+ exitCommands: tuiOptions.exitCommands ?? DEFAULT_EXIT_COMMANDS,
189
+ initialPrompt: tuiOptions.initialPrompt,
190
+ sessionLogPath: tuiOptions.sessionLogPath,
191
+ promptLabel,
192
+ agentLabel: tuiOptions.agentLabel,
193
+ historyPath: tuiOptions.historyPath,
194
+ historyLimit: tuiOptions.historyLimit ?? DEFAULT_HISTORY_LIMIT,
195
+ runsDir: tuiOptions.runsDir,
196
+ modeControl: tuiOptions.modeControl,
197
+ toolLabels: tuiOptions.toolLabels,
213
198
  formatAction: tuiOptions.formatAction,
214
199
  toolPhrase: tuiOptions.toolPhrase,
200
+ promptSuggestions: tuiOptions.promptSuggestions ?? true,
201
+ // The /resume list in place of the prompt.
202
+ pick: (title, choices) => input.pick(title, choices),
203
+ // A local command, never sent to the model: the last reply to the clipboard (OSC 52).
204
+ ...(tuiOptions.terminalIntegration === false
205
+ ? {}
206
+ : {
207
+ commands: {
208
+ copy: () => {
209
+ const reply = controller.lastReply();
210
+ if (reply)
211
+ stdout.write(clipboardSequence(reply));
212
+ model.writeLine(ui.dim(reply ? t().copiedReply(reply.length) : t().nothingToCopy));
213
+ },
214
+ },
215
+ }),
216
+ });
217
+ // The screen, drawn from the controller's events; the session log is the controller's.
218
+ const model = createSessionModel({
219
+ formatAction: controller.formatAction,
220
+ toolPhrase: controller.toolPhrase,
215
221
  toolDetail: tuiOptions.toolDetail,
216
222
  formatResult: tuiOptions.formatResult,
217
223
  agentLabel: tuiOptions.agentLabel,
218
- onWrite: mirror,
219
224
  width: () => liveWidth(stdout.columns),
220
225
  });
221
226
  const interaction = createInkInteraction((text) => model.note(text));
222
- const input = createChatInput();
223
227
  if (tuiOptions.firstPromptSuggestion)
224
228
  input.setSuggestion(tuiOptions.firstPromptSuggestion);
225
- void listProjectFiles(current.options.cwd ?? process.cwd()).then((files) => input.setFiles(files));
226
- let turnInterrupted = false;
227
- function interruptTurn() {
228
- if (turnInterrupted)
229
- return;
230
- turnInterrupted = true;
231
- void run.interrupt();
232
- model.writeLine(ui.warn(t().interrupted));
233
- }
234
- // One reader per session instead of drainTurn()'s one per turn: the prompt suggestion
235
- // arrives after its turn's turn-end, while the human is already at the prompt, and MCP
236
- // errors arrive before the first turn. Still manual .next() calls, never a for-await that
237
- // could be broken out of (ADR-010). A reader left behind by /resume stops reporting.
238
- let turnDone = null;
239
- let readError = null;
240
- function finishTurn() {
241
- const done = turnDone;
242
- turnDone = null;
243
- done?.();
244
- }
245
- let queue = createInputQueue();
246
- let run;
247
- let knownCommands;
248
- let generation = 0;
249
- let unsubscribeMode;
250
- function connect() {
251
- const own = ++generation;
252
- queue = createInputQueue({ modeControl: modeControl() });
253
- run = runQuery(queue.iterable, { ...current.options, promptSuggestions: tuiOptions.promptSuggestions ?? true });
254
- const events = run.events[Symbol.asyncIterator]();
255
- input.setMode(modeControl()?.mode ?? null);
256
- // The mode can also change from inside the session (the agent leaving plan mode with
257
- // present_plan): the status bar follows it.
258
- unsubscribeMode?.();
259
- unsubscribeMode = modeControl()?.subscribe?.((mode) => input.setMode(mode));
260
- // Every registered command name and alias, built-ins included, for completion and for
261
- // catching an unknown "/command" before it reaches the model as plain text (see chatTui.ts).
262
- knownCommands = run.supportedCommands().then((commands) => new Set(commands.flatMap((c) => [c.name, ...(c.aliases ?? [])])), () => null);
263
- void knownCommands.then((names) => {
264
- const exits = [...exitCommands].filter((c) => c.startsWith("/")).map((c) => c.slice(1));
265
- const local = [
266
- ...(tuiOptions.terminalIntegration === false ? [] : ["copy"]),
267
- ...(runsDir ? ["resume"] : []),
268
- ...(modeControl()?.switchable.includes("plan") ? ["plan"] : []),
269
- ];
270
- input.setCommands([...new Set([...(names ?? []), ...exits, ...local])].sort());
271
- });
272
- void (async () => {
273
- try {
274
- while (true) {
275
- const { value, done } = await events.next();
276
- if (own !== generation || done)
277
- break;
278
- if (value.type === "prompt-suggestion") {
279
- input.setSuggestion(value.suggestion);
280
- continue;
281
- }
282
- model.render(value);
283
- if (value.type === "turn-end")
284
- finishTurn();
285
- }
286
- }
287
- catch (error) {
288
- if (own === generation)
289
- readError = error;
290
- }
291
- if (own === generation)
292
- finishTurn();
293
- })();
294
- }
295
- connect();
296
- async function runTurn(line) {
297
- input.setSuggestion(null);
298
- status?.turnStarted();
299
- turnInterrupted = false;
300
- model.separate();
301
- model.startTurn();
302
- const finished = new Promise((resolve) => (turnDone = resolve));
303
- queue.push(line);
304
- await finished;
305
- model.endTurn();
306
- status?.turnEnded();
307
- void run.contextUsage().then((usage) => usage && model.setContextPercent(usage.percentage));
308
- if (readError)
309
- throw readError;
310
- }
311
- /** Draws the run's earlier conversation (when it has one) and says it was resumed. */
312
- async function showConversation(folder, fromResume) {
313
- if (!folder.sessionId)
314
- return;
315
- const messages = await readConversation(folder.dir);
316
- const [summary] = (await listRuns(path.dirname(folder.dir))).filter((r) => path.resolve(r.dir) === path.resolve(folder.dir));
317
- // In full screen the history is redrawn from the start; inline, the terminal's
318
- // scrollback can't be taken back, so the conversation follows what's already there.
319
- if (fromResume && tuiOptions.fullscreen)
320
- model.reset();
321
- model.replay(messages, userLine);
322
- model.writeLine(ui.dim(t().resumed(runDate(summary?.updatedAt ?? new Date()))));
323
- }
324
- /** /resume: picks one of the runs and switches the session to it. */
325
- async function resume() {
326
- if (!opener || !runsDir)
327
- return;
328
- const runs = await listRuns(runsDir);
329
- if (runs.length === 0) {
330
- model.writeLine(ui.dim(t().noEarlierRuns));
331
- return;
229
+ const refreshFiles = () => void listProjectFiles(controller.cwd()).then((files) => input.setFiles(files));
230
+ refreshFiles();
231
+ const toned = (text, tone) => (tone === "plain" ? text : tone === "dim" ? ui.dim(text) : tone === "warn" ? ui.warn(text) : ui.error(text));
232
+ controller.onEvent((event) => {
233
+ switch (event.type) {
234
+ case "agent":
235
+ if (event.event.type === "prompt-suggestion")
236
+ input.setSuggestion(event.event.suggestion);
237
+ else
238
+ model.render(event.event);
239
+ return;
240
+ case "user":
241
+ model.note(userLine(event.text), "user");
242
+ return;
243
+ case "notice":
244
+ model.writeLine(toned(event.text, event.tone));
245
+ return;
246
+ case "turn-start":
247
+ input.setSuggestion(null);
248
+ status?.turnStarted();
249
+ model.separate();
250
+ model.startTurn();
251
+ return;
252
+ case "turn-end":
253
+ model.endTurn();
254
+ status?.turnEnded();
255
+ return;
256
+ case "conversation":
257
+ // In full screen the history is redrawn from the start; inline, the terminal's
258
+ // scrollback can't be taken back, so the conversation follows what's already there.
259
+ if (event.fromResume && tuiOptions.fullscreen)
260
+ model.reset();
261
+ model.replay(event.messages, userLine);
262
+ model.writeLine(ui.dim(t().resumed(runDate(event.updatedAt))));
263
+ return;
264
+ case "session":
265
+ refreshFiles();
266
+ return;
267
+ case "mode":
268
+ return; // the status bar follows the state
332
269
  }
333
- const chosen = await input.pick(t().resumeTitle, runs.map((summary, i) => ({ label: `${i + 1}. ${runLabel(summary, current.run ?? undefined)}`, value: summary.dir })));
334
- const summary = runs.find((r) => r.dir === chosen);
335
- if (!summary || (current.run && path.resolve(summary.dir) === path.resolve(current.run.dir)))
336
- return;
337
- queue.end();
338
- run.close();
339
- current = await openSession(opener, await runFolderOf(summary));
340
- openLog();
341
- connect();
342
- await showConversation(current.run, true);
343
- }
270
+ });
271
+ controller.subscribe((state) => {
272
+ input.setCommands(state.commands);
273
+ if (state.contextPercent !== model.getSnapshot().contextPercent)
274
+ model.setContextPercent(state.contextPercent);
275
+ });
344
276
  const previousPort = getInteractionPort();
345
277
  setInteractionPort(interaction.port);
346
278
  const restoreTerminal = tuiOptions.fullscreen ? enterFullscreen(stdout) : null;
347
279
  const cursor = tuiOptions.fullscreen ? createCursorController(stdout) : null;
348
- const status = tuiOptions.terminalIntegration === false ? null : createTerminalStatus((text) => void stdout.write(text), tuiOptions.header?.title ?? "agent");
280
+ status = tuiOptions.terminalIntegration === false ? null : createTerminalStatus((text) => void stdout.write(text), tuiOptions.header?.title ?? "agent");
349
281
  status?.start();
350
- const app = render(_jsx(CursorContext.Provider, { value: cursor, children: _jsx(KitTheme, { children: _jsx(ChatApp, { model: model, interaction: interaction, input: input, promptLabel: promptLabel, renderApproval: tuiOptions.renderApproval, mode: tuiOptions.mode, fullscreen: tuiOptions.fullscreen, header: tuiOptions.header && tuiOptions.fullscreen ? headerLines(tuiOptions.header) : undefined, onFocusChange: status ? (focused) => status.setFocused(focused) : undefined, modeControl: modeControl, onInterrupt: interruptTurn }) }) }),
282
+ const app = render(_jsx(CursorContext.Provider, { value: cursor, children: _jsx(KitTheme, { children: _jsx(ChatApp, { model: model, interaction: interaction, input: input, promptLabel: promptLabel, renderApproval: tuiOptions.renderApproval, mode: tuiOptions.mode, fullscreen: tuiOptions.fullscreen, header: tuiOptions.header && tuiOptions.fullscreen ? headerLines(tuiOptions.header) : undefined, onFocusChange: status ? (focused) => status?.setFocused(focused) : undefined, controller: controller }) }) }),
351
283
  // In full screen Ink writes through the cursor controller, which places the terminal's cursor.
352
284
  { exitOnCtrlC: false, ...(cursor ? { stdout: cursor.stream } : {}) });
353
285
  try {
@@ -355,71 +287,24 @@ export async function runChatInk(options, tuiOptions = {}) {
355
287
  if (tuiOptions.header && !tuiOptions.fullscreen)
356
288
  model.note(headerLines(tuiOptions.header).join("\n"));
357
289
  for (const warning of languageWarnings)
358
- model.writeLine(ui.warn(warning));
290
+ controller.notice(warning, "warn");
359
291
  if (tuiOptions.welcomeMessage)
360
- model.writeLine(tuiOptions.welcomeMessage);
361
- if (current.run)
362
- await showConversation(current.run, false);
363
- if (tuiOptions.initialPrompt && !current.run?.sessionId) {
364
- // Logged but not shown, like runChatTui(): it is the agent's cue, not something typed.
365
- mirror(`${promptLabel}${tuiOptions.initialPrompt}\n`);
366
- await runTurn(tuiOptions.initialPrompt);
367
- }
292
+ controller.notice(tuiOptions.welcomeMessage, "plain");
293
+ await controller.start();
368
294
  while (true) {
369
295
  // The blank line before the prompt is in place while typing, so Enter doesn't push
370
296
  // everything down a line: promptLabel's own leading newlines only reach the log.
371
297
  model.separate();
372
- const raw = await input.next(historyEntries.map((entry) => entry.text));
373
- if (raw === null)
298
+ const line = await input.next(controller.getState().history);
299
+ if (line === null)
374
300
  break;
375
- const line = raw.trim();
376
- if (!line)
377
- continue;
378
- mirror(`${promptLabel}${line}\n`);
379
- model.note(userLine(line), "user");
380
- if (tuiOptions.historyPath) {
381
- historyEntries = capHistory([...historyEntries, { text: line, timestamp: new Date().toISOString() }], historyLimit);
382
- await saveHistory(tuiOptions.historyPath, historyEntries);
383
- }
384
- if (exitCommands.has(line.toLowerCase()))
301
+ if ((await controller.send(line)) === "exit")
385
302
  break;
386
- // A local command, never sent to the model: the last reply to the clipboard (OSC 52).
387
- if (status && line.toLowerCase() === LOCAL_COMMANDS.copy) {
388
- const reply = model.lastReply();
389
- if (reply)
390
- stdout.write(clipboardSequence(reply));
391
- model.writeLine(ui.dim(reply ? t().copiedReply(reply.length) : t().nothingToCopy));
392
- continue;
393
- }
394
- if (runsDir && line.toLowerCase() === LOCAL_COMMANDS.resume) {
395
- await resume();
396
- continue;
397
- }
398
- // A local command too: into plan mode, or back to the mode it was entered from.
399
- if (line.toLowerCase() === LOCAL_COMMANDS.plan) {
400
- const control = modeControl();
401
- const next = control ? togglePlanMode(control) : null;
402
- if (next)
403
- input.setMode(next);
404
- else
405
- model.writeLine(ui.dim(t().modeLocked(control?.mode ?? tuiOptions.mode)));
406
- continue;
407
- }
408
- const commandToken = slashCommandToken(line);
409
- if (commandToken) {
410
- const names = await knownCommands;
411
- if (names && !names.has(commandToken)) {
412
- model.writeLine(ui.warn(t().unknownCommand(commandToken)));
413
- continue;
414
- }
415
- }
416
- await runTurn(line);
417
303
  }
418
304
  }
419
305
  finally {
420
306
  setInteractionPort(previousPort);
421
- queue.end();
422
- run.close();
307
+ controller.close();
423
308
  input.close();
424
309
  // One more frame with only the history, so the prompt and status bar don't stay behind.
425
310
  await new Promise((resolve) => setTimeout(resolve, 0));
@@ -427,6 +312,5 @@ export async function runChatInk(options, tuiOptions = {}) {
427
312
  await app.waitUntilExit();
428
313
  status?.stop();
429
314
  restoreTerminal?.();
430
- sessionLog?.end();
431
315
  }
432
316
  }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "awareness",
3
+ "description": "You know who you are and what you are right now (your mode, extensions, tools, subagents, commands, context) through about_me, and how your chat is used through the help skill.",
4
+ "agent-kit": {
5
+ "provides": [
6
+ "self-awareness"
7
+ ],
8
+ "requires": []
9
+ }
10
+ }
@@ -1,11 +1,11 @@
1
1
  ---
2
- name: agent-help
3
- description: How to use this agent - its chat's modes, keys and slash commands, resuming an earlier conversation, approvals and questions, the command line, and the agent's own commands and configuration. Load it whenever the person asks how to do something with you, what a key or command does, or what you can do.
2
+ name: help
3
+ description: How to use this agent's chat - its modes, keys and slash commands, resuming an earlier conversation, approvals and questions, the command line. Load it whenever the person asks how to do something with you, or what a key or command does.
4
4
  ---
5
5
 
6
6
  # Helping the person use you
7
7
 
8
- Answer from this page only: the chat's part below is the same for every agent built on agent-kit, and the "This session" and "This agent" sections at the end are about you. If something isn't covered here, say you don't know rather than inventing a key, command or option. Answer in the person's language, briefly, with the exact key or command; keys and commands are typed as written here, whatever the language.
8
+ This page is the chat's part, the same for every agent built on agent-kit. What you are right now (your mode, the commands this session has, your extensions) comes from `about_me`, and your own domain from `about_me` with `part: "guide"`. If something is in none of them, say you don't know rather than inventing a key, command or option. Answer in the language you reply in, briefly, with the exact key or command; keys and commands are typed as written here, whatever the language.
9
9
 
10
10
  ## Modes
11
11
 
@@ -45,6 +45,7 @@ Messages typed while the agent works are queued and sent when it finishes.
45
45
  - `/exit` or `/quit`: leave the chat (an agent can name its own exit commands instead).
46
46
  - `/copy`: copy the last reply to the clipboard.
47
47
  - `/resume`: pick an earlier conversation to go on with (`↑`/`↓`, `Enter`, `Esc` to cancel), where the agent keeps its conversations.
48
+ - `/extensions`: what the session runs with, what's off and why, and the extensions installed; `/extensions enable <name>` or `/extensions disable <name>` turns an installed one on or off and reopens the session, keeping the conversation.
48
49
  - `/plan`: plan mode on or off (see Modes).
49
50
 
50
51
  Typing `/` and `Tab` lists every command, the agent's own included (named `/plugin:command`). An unknown `/command` isn't sent.
@@ -1,4 +1,10 @@
1
1
  {
2
2
  "name": "knowledge",
3
- "description": "Built-in knowledge base workflows (an LLM wiki) over the kit's knowledge_* tools: ingesting sources, querying the knowledge base and health-checking it."
3
+ "description": "Built-in knowledge base workflows (an LLM wiki) over the kit's knowledge_* tools: ingesting sources, querying the knowledge base and health-checking it.",
4
+ "agent-kit": {
5
+ "provides": [
6
+ "knowledge-base"
7
+ ],
8
+ "requires": []
9
+ }
4
10
  }
@@ -0,0 +1,7 @@
1
+ ---
2
+ description: Ingest material into the knowledge base right now - the files or topic given, or everything in sources/ not ingested yet
3
+ ---
4
+
5
+ Apply the `knowledge-ingest` skill right now to: $ARGUMENTS
6
+
7
+ If nothing is given above, ingest every original in `list_sources` that no summary is about, and update the summaries whose original's `changedAt` is after their `ingested` (both ISO 8601 in UTC: the later one sorts after as text). When you're done, summarize which pages you created and updated, and anything you couldn't read.
@@ -9,7 +9,12 @@ A source is ingested once, and from then on the knowledge base holds what it tau
9
9
 
10
10
  ## 1. Check it isn't already ingested
11
11
 
12
- `list_sources` gives each original's status and summary pages: *new* needs ingesting; *ingested* has a page; *changed* changed after its ingest (update its summary). For a document from elsewhere, `knowledge_search` its title or URL.
12
+ The sources and the knowledge base each keep their own data; you match them:
13
+
14
+ - `list_sources` gives each original with `changedAt`, when its content last changed.
15
+ - `knowledge_index` gives each summary with `file`, the original it's about, and `ingested`, when it was written.
16
+
17
+ An original with no summary needs ingesting. One whose `changedAt` is after its summary's `ingested` changed since: update its summary (`knowledge_rewrite`, which sets `ingested` again). Both dates are ISO 8601 in UTC, so the later one sorts after as text; use `date_math` if in doubt. For a document from elsewhere, `knowledge_search` its title or URL.
13
18
 
14
19
  ## 2. Read it for real
15
20
 
@@ -17,7 +22,7 @@ Read the whole source, not its first page (a long PDF in parts: `list_sources` g
17
22
 
18
23
  ## 3. Write the summary page
19
24
 
20
- `knowledge_create` with type `summary` (call it once without content to get the template), and the field `file` (the original, relative to sources/) or `url`. Key points quote literally where exactness matters. Create the new concept and entity pages it feeds in the same call (`also`): they can link to each other.
25
+ `knowledge_create` with type `summary` (call it once without content to get the template), and the field `file` (the original, exactly as `list_sources` names it) or `url`; `ingested` is set for you. Key points quote literally where exactness matters. Create the new concept and entity pages it feeds in the same call (`also`): they can link to each other.
21
26
 
22
27
  ## 4. Update concepts and entities
23
28
 
@@ -33,4 +38,4 @@ If the source contradicts a page, record both versions with attribution in both
33
38
 
34
39
  ## An original that was wrong or replaced
35
40
 
36
- `retire_source` (the person approves it) moves it aside and marks its summary retired (wrong) or superseded (replaced). Then `knowledge_check` lists the passages that cite it: after a wrong one, correct or remove what it taught (`knowledge_edit`, or `knowledge_retire` for a page that was only about it); after a replaced one, ingest the new original and re-ground those passages on it.
41
+ `retire_source` (it asks the person itself) moves it aside; `list_sources` shows it as retired, with why and what replaced it. The knowledge base is then yours to update: find its summaries (`knowledge_index`, by `file`) and the passages that cite it (`knowledge_search` its file name). After a wrong one, `knowledge_retire` its summary (it asks the person itself: don't ask them first) and correct or remove what it taught (`knowledge_edit`, or `knowledge_retire` for a page that was only about it). After a replaced one, ingest the new original, `knowledge_supersede` the old summary by the new one, and re-ground those passages on it.
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: knowledge-lint
3
- description: Health-check the knowledge base - broken links, orphan pages, links to retired pages, missing or changed originals, duplicated concepts, mentioned-but-missing concepts, contradictions, gaps - fix what's mechanical and report the rest.
3
+ description: Health-check the knowledge base - broken links, orphan pages, links to retired pages, summaries behind their originals, duplicated concepts, mentioned-but-missing concepts, contradictions, gaps - fix what's mechanical and report the rest.
4
4
  ---
5
5
 
6
6
  # Linting the knowledge base
@@ -9,14 +9,21 @@ A knowledge base maintained across sessions drifts. Lint it when asked and after
9
9
 
10
10
  ## 1. The mechanical checks, in one call
11
11
 
12
- `knowledge_check` returns broken links, orphan pages, links to retired pages, summaries whose original is gone, originals that are new, changed or missing, and the passages that cite a retired original. The index and backlinks are kept by the store: there's nothing to fix there.
12
+ `knowledge_check` returns broken links, orphan pages and links to retired pages. The index and backlinks are kept by the store: there's nothing to fix there.
13
13
 
14
14
  - **Broken links**: fix them with `knowledge_edit` when the right target is obvious (a typo, a page under a close name: `knowledge_search`); otherwise report them.
15
15
  - **Orphans**: link them from the pages where they belong, or report why they don't belong anywhere.
16
- - **Links to retired pages, passages citing a retired original**: correct or re-ground them.
17
- - **New or changed originals**: report them as what to ingest or update next (or do it, if asked).
16
+ - **Links to retired pages**: correct or re-ground them.
18
17
 
19
- ## 2. The checks that need judgement
18
+ ## 2. The knowledge base against its originals
19
+
20
+ When there's a sources folder, match `list_sources` with `knowledge_index`:
21
+
22
+ - **Not ingested**: present originals no summary is about. Report them as what to ingest next (or do it, if asked).
23
+ - **Behind**: summaries whose original's `changedAt` is after their `ingested`, (ISO 8601 in UTC: the later one sorts after as text). Report them as what to update.
24
+ - **Retired or missing originals**: summaries still active whose original is retired (`knowledge_retire` them if it was wrong, which asks the person itself, or `knowledge_supersede` them if it was replaced) or missing (report them); and the passages that cite them (`knowledge_search` the file name).
25
+
26
+ ## 3. The checks that need judgement
20
27
 
21
28
  Use `knowledge_index` and `knowledge_search`; read only the pages a check needs.
22
29
 
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "memory",
3
+ "description": "Your memory of the person you work for, across all their projects: who they are and how they want things done, kept only from what they tell you, through its tools: recall, remember and forget.",
4
+ "agent-kit": {
5
+ "provides": [
6
+ "person-memory"
7
+ ],
8
+ "requires": []
9
+ }
10
+ }
@@ -0,0 +1,8 @@
1
+ ---
2
+ description: Forget an entry of what the agent remembers about you
3
+ argument-hint: <entry>
4
+ ---
5
+
6
+ The person wants you to forget this from your memory of them: $ARGUMENTS
7
+
8
+ Find the entry with `recall` (by name, or the one whose description matches) and `forget` it, then say which one you forgot. If nothing is given above, or several entries match, show them and ask which. Stop applying what it said from now on.
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Show what the agent remembers about you, across all your projects
3
+ ---
4
+
5
+ Call `recall`, without a name, right now and show the person everything you remember about them, grouped by type (who they are, how they want things done), one short line per entry with its name. If it's empty, say so. End by telling them `/memory:forget <entry>` forgets an entry, or that they can just tell you what's wrong.
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "sources",
3
+ "description": "The sources folder: originals kept as obtained, listed, downloaded, asked for and retired through the kit's sources tools.",
4
+ "agent-kit": {
5
+ "provides": [
6
+ "sources"
7
+ ],
8
+ "requires": []
9
+ }
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falkenslab/agent-kit",
3
- "version": "0.17.0",
3
+ "version": "0.19.0",
4
4
  "description": "Library for building an agent on top of the Claude Agent SDK",
5
5
  "author": "Francisco Vargas Ruiz",
6
6
  "license": "MIT",
@@ -32,7 +32,8 @@
32
32
  "types": "./dist/index.d.ts",
33
33
  "files": [
34
34
  "dist",
35
- "assets"
35
+ "assets",
36
+ "extensions"
36
37
  ],
37
38
  "scripts": {
38
39
  "build": "tsc",
@@ -1,7 +0,0 @@
1
- ---
2
- description: Ingest material into the knowledge base right now - the files or topic given, or everything in sources/ not ingested yet
3
- ---
4
-
5
- Apply the `knowledge-ingest` skill right now to: $ARGUMENTS
6
-
7
- If nothing is given above, ingest every original `list_sources` reports as new, and update the summaries of the ones it reports as changed. When you're done, summarize which pages you created and updated, and anything you couldn't read.
@@ -1,5 +0,0 @@
1
- ---
2
- description: Answer a question from the knowledge base, and file the answer back if it is worth keeping
3
- ---
4
-
5
- Apply the `knowledge-query` skill right now to answer: $ARGUMENTS
@@ -1,4 +0,0 @@
1
- {
2
- "name": "knowledge",
3
- "description": "Built-in knowledge base workflows (an LLM wiki): page templates, ingesting sources, querying the knowledge base and health-checking it."
4
- }