@falkenslab/agent-kit 0.10.1

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 (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +97 -0
  3. package/assets/knowledge-plugin/.claude-plugin/plugin.json +4 -0
  4. package/assets/knowledge-plugin/commands/ingest.md +7 -0
  5. package/assets/knowledge-plugin/commands/lint.md +5 -0
  6. package/assets/knowledge-plugin/commands/query.md +5 -0
  7. package/assets/knowledge-plugin/skills/knowledge-ingest/SKILL.md +42 -0
  8. package/assets/knowledge-plugin/skills/knowledge-lint/SKILL.md +26 -0
  9. package/assets/knowledge-plugin/skills/knowledge-pages/SKILL.md +137 -0
  10. package/assets/knowledge-plugin/skills/knowledge-query/SKILL.md +14 -0
  11. package/dist/core/agentSpec.d.ts +132 -0
  12. package/dist/core/agentSpec.js +1 -0
  13. package/dist/core/claudeAuth.d.ts +24 -0
  14. package/dist/core/claudeAuth.js +27 -0
  15. package/dist/core/hooks/fileScopeGate.d.ts +33 -0
  16. package/dist/core/hooks/fileScopeGate.js +68 -0
  17. package/dist/core/hooks/humanInput.d.ts +15 -0
  18. package/dist/core/hooks/humanInput.js +52 -0
  19. package/dist/core/hooks/stepGate.d.ts +11 -0
  20. package/dist/core/hooks/stepGate.js +47 -0
  21. package/dist/core/hooks/subagentBashGate.d.ts +28 -0
  22. package/dist/core/hooks/subagentBashGate.js +40 -0
  23. package/dist/core/hooks/subagentForegroundGate.d.ts +18 -0
  24. package/dist/core/hooks/subagentForegroundGate.js +34 -0
  25. package/dist/core/hooks/subagentTypeGate.d.ts +16 -0
  26. package/dist/core/hooks/subagentTypeGate.js +33 -0
  27. package/dist/core/hooks/transcriptLogger.d.ts +14 -0
  28. package/dist/core/hooks/transcriptLogger.js +66 -0
  29. package/dist/core/interaction.d.ts +29 -0
  30. package/dist/core/interaction.js +7 -0
  31. package/dist/core/knowledge.d.ts +15 -0
  32. package/dist/core/knowledge.js +54 -0
  33. package/dist/core/mcpPermissions.d.ts +23 -0
  34. package/dist/core/mcpPermissions.js +47 -0
  35. package/dist/core/promptTemplate.d.ts +6 -0
  36. package/dist/core/promptTemplate.js +20 -0
  37. package/dist/core/runner.d.ts +137 -0
  38. package/dist/core/runner.js +115 -0
  39. package/dist/core/session.d.ts +63 -0
  40. package/dist/core/session.js +222 -0
  41. package/dist/core/toolLabels.d.ts +23 -0
  42. package/dist/core/toolLabels.js +114 -0
  43. package/dist/core/tools/humanApproval.d.ts +14 -0
  44. package/dist/core/tools/humanApproval.js +37 -0
  45. package/dist/core/tools/manualLogin.d.ts +16 -0
  46. package/dist/core/tools/manualLogin.js +27 -0
  47. package/dist/core/tools/saveToSources.d.ts +10 -0
  48. package/dist/core/tools/saveToSources.js +105 -0
  49. package/dist/index.d.ts +31 -0
  50. package/dist/index.js +32 -0
  51. package/dist/tui/chatTui.d.ts +105 -0
  52. package/dist/tui/chatTui.js +265 -0
  53. package/dist/tui/claudeAuth.d.ts +19 -0
  54. package/dist/tui/claudeAuth.js +96 -0
  55. package/dist/tui/consoleRenderer.d.ts +32 -0
  56. package/dist/tui/consoleRenderer.js +72 -0
  57. package/dist/tui/ink/PromptInput.d.ts +40 -0
  58. package/dist/tui/ink/PromptInput.js +333 -0
  59. package/dist/tui/ink/SessionView.d.ts +42 -0
  60. package/dist/tui/ink/SessionView.js +223 -0
  61. package/dist/tui/ink/fileMentions.d.ts +13 -0
  62. package/dist/tui/ink/fileMentions.js +50 -0
  63. package/dist/tui/ink/fullscreen.d.ts +79 -0
  64. package/dist/tui/ink/fullscreen.js +175 -0
  65. package/dist/tui/ink/header.d.ts +17 -0
  66. package/dist/tui/ink/header.js +24 -0
  67. package/dist/tui/ink/inkInteraction.d.ts +22 -0
  68. package/dist/tui/ink/inkInteraction.js +64 -0
  69. package/dist/tui/ink/lineBuffer.d.ts +28 -0
  70. package/dist/tui/ink/lineBuffer.js +87 -0
  71. package/dist/tui/ink/markdown.d.ts +12 -0
  72. package/dist/tui/ink/markdown.js +148 -0
  73. package/dist/tui/ink/progressView.d.ts +27 -0
  74. package/dist/tui/ink/progressView.js +65 -0
  75. package/dist/tui/ink/promptEditing.d.ts +54 -0
  76. package/dist/tui/ink/promptEditing.js +123 -0
  77. package/dist/tui/ink/runChatInk.d.ts +103 -0
  78. package/dist/tui/ink/runChatInk.js +298 -0
  79. package/dist/tui/ink/sessionModel.d.ts +85 -0
  80. package/dist/tui/ink/sessionModel.js +262 -0
  81. package/dist/tui/ink/terminalCursor.d.ts +32 -0
  82. package/dist/tui/ink/terminalCursor.js +68 -0
  83. package/dist/tui/ink/terminalStatus.d.ts +20 -0
  84. package/dist/tui/ink/terminalStatus.js +55 -0
  85. package/dist/tui/ink/toolGroup.d.ts +36 -0
  86. package/dist/tui/ink/toolGroup.js +77 -0
  87. package/dist/tui/ink/wizard.d.ts +62 -0
  88. package/dist/tui/ink/wizard.js +167 -0
  89. package/dist/tui/promptErrors.d.ts +2 -0
  90. package/dist/tui/promptErrors.js +4 -0
  91. package/dist/tui/terminalInteraction.d.ts +18 -0
  92. package/dist/tui/terminalInteraction.js +85 -0
  93. package/dist/tui/ui.d.ts +21 -0
  94. package/dist/tui/ui.js +25 -0
  95. package/package.json +75 -0
@@ -0,0 +1,265 @@
1
+ import readline from "node:readline/promises";
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 } from "../core/session.js";
7
+ import { runQuery } from "../core/runner.js";
8
+ import { isSharedQuestionActive, setSharedReadline } from "./terminalInteraction.js";
9
+ import * as ui from "./ui.js";
10
+ import { createConsoleRenderer } from "./consoleRenderer.js";
11
+ // Only strips picocolors' own SGR sequences (`\x1b[<codes>m`) — the only kind this file
12
+ // ever writes to the console — not a general-purpose ANSI stripper for arbitrary escape
13
+ // sequences (cursor movement, OSC, ...) this kit never emits.
14
+ // eslint-disable-next-line no-control-regex -- \x1b is the ESC byte SGR sequences start with, not an accident
15
+ const ANSI_SGR = /\x1b\[[0-9;]*m/g;
16
+ function stripAnsi(text) {
17
+ return text.replace(ANSI_SGR, "");
18
+ }
19
+ const DEFAULT_PROMPT_LABEL = "\n> ";
20
+ const DEFAULT_EXIT_COMMANDS = ["/exit", "/quit"];
21
+ const DEFAULT_HISTORY_LIMIT = 100;
22
+ /** Keeps only the most recent `limit` entries (oldest-first order in, oldest-first out). */
23
+ export function capHistory(entries, limit) {
24
+ return entries.length > limit ? entries.slice(-limit) : entries.slice();
25
+ }
26
+ /** Oldest-first. Missing file reads as empty — nothing to load yet is the normal case. */
27
+ export async function loadHistory(filePath) {
28
+ let raw;
29
+ try {
30
+ raw = await readFile(filePath, "utf8");
31
+ }
32
+ catch (error) {
33
+ if (error.code === "ENOENT")
34
+ return [];
35
+ throw error;
36
+ }
37
+ return raw
38
+ .split("\n")
39
+ .filter((line) => line.trim().length > 0)
40
+ .map((line) => JSON.parse(line));
41
+ }
42
+ /** Overwrites the whole file (not appended) so a prior prune (oldest entries dropped) sticks. */
43
+ export async function saveHistory(filePath, entries) {
44
+ await mkdir(path.dirname(filePath), { recursive: true });
45
+ const body = entries.map((entry) => JSON.stringify(entry)).join("\n");
46
+ await writeFile(filePath, entries.length > 0 ? `${body}\n` : "", "utf8");
47
+ }
48
+ /**
49
+ * The command name a typed line would invoke (e.g. "chiste" for both "/chiste" and
50
+ * "/chiste con salsa"), or `null` for a line that isn't a slash command at all (doesn't
51
+ * start with "/") or is just a bare "/" with nothing after it.
52
+ */
53
+ export function slashCommandToken(line) {
54
+ if (!line.startsWith("/"))
55
+ return null;
56
+ const token = line.slice(1).split(/\s/, 1)[0];
57
+ return token ? token : null;
58
+ }
59
+ /**
60
+ * Reads `events` one turn at a time — up to and including a `turn-end` — via manual
61
+ * `.next()` calls, calling `onEvent` for each. Deliberately never uses
62
+ * `for await...of` + `break`: `run.events` (see runner.ts) is one long-lived generator
63
+ * spanning the *whole* multi-turn session, not one per turn, and breaking out of a
64
+ * `for-await-of` loop calls the generator's `.return()`, permanently closing it — the next
65
+ * turn's drain would then see `done: true` immediately and silently lose the rest of the
66
+ * conversation. Exported on its own (not part of the package's public API — see index.ts)
67
+ * so this exact behavior has a direct regression test.
68
+ */
69
+ export async function drainTurn(events, onEvent) {
70
+ while (true) {
71
+ const { value, done } = await events.next();
72
+ if (done)
73
+ return;
74
+ onEvent(value);
75
+ if (value.type === "turn-end")
76
+ return;
77
+ }
78
+ }
79
+ /**
80
+ * A ready-to-run interactive terminal chat loop, assembled from this kit's own chat
81
+ * primitives: `createInputQueue()` feeds typed lines into a multi-turn `query()` session,
82
+ * `runQuery()`'s normalized event stream is rendered to the console (streamed text,
83
+ * friendly action labels via `createFriendlyToolLabel()`, turn failures), and this loop's
84
+ * own `readline.Interface` is registered via `setSharedReadline()` so any human-in-the-loop
85
+ * checkpoint the session hits (the interactive-mode step gate, or the guided-mode approval/
86
+ * manual-login tools — see terminalInteraction.ts) prompts on the same interface instead of a
87
+ * second one fighting it for stdin's raw mode.
88
+ *
89
+ * Takes the SDK `Options` produced by `buildSessionOptions()` rather than an `AgentSpec`
90
+ * directly, so it stays reusable for an `Options` assembled any other way:
91
+ *
92
+ * const { options } = await buildSessionOptions(config, runDir, spec);
93
+ * await runChatTui(options, { welcomeMessage: "Ready. Type /exit to quit." });
94
+ *
95
+ * Resolves once the chat ends (an exit command, Ctrl+C/Ctrl+D at an idle prompt) — this
96
+ * function owns the whole session lifecycle, closing the underlying query and readline
97
+ * interface itself before returning.
98
+ */
99
+ export async function runChatTui(options, tuiOptions = {}) {
100
+ const exitCommands = new Set((tuiOptions.exitCommands ?? DEFAULT_EXIT_COMMANDS).map((c) => c.toLowerCase()));
101
+ const promptLabel = tuiOptions.promptLabel ?? DEFAULT_PROMPT_LABEL;
102
+ const queue = createInputQueue();
103
+ const run = runQuery(queue.iterable, options);
104
+ const events = run.events[Symbol.asyncIterator]();
105
+ const historyLimit = tuiOptions.historyLimit ?? DEFAULT_HISTORY_LIMIT;
106
+ let historyEntries = tuiOptions.historyPath ? capHistory(await loadHistory(tuiOptions.historyPath), historyLimit) : [];
107
+ const rl = readline.createInterface({
108
+ input: stdin,
109
+ output: stdout,
110
+ // readline wants most-recent-first (↑ shows historyEntries' last/newest entry first);
111
+ // this file is stored oldest-first (append-friendly, reads top-to-bottom like a log).
112
+ history: historyEntries.map((entry) => entry.text).reverse(),
113
+ historySize: historyLimit,
114
+ });
115
+ setSharedReadline(rl);
116
+ const sessionLog = tuiOptions.sessionLogPath ? createWriteStream(tuiOptions.sessionLogPath, { flags: "a" }) : null;
117
+ // Mirrors to the log file only — for text already visible on the terminal some other
118
+ // way (readline's own prompt + the human's local echo), so it isn't printed twice.
119
+ function mirror(text) {
120
+ sessionLog?.write(stripAnsi(text));
121
+ }
122
+ // Prints to the console *and* mirrors to the log file — the normal case for anything
123
+ // this loop itself puts on screen (streamed reply text, action/error lines, ...).
124
+ // Tracks whether the terminal cursor currently sits at column 0 of a fresh row — true
125
+ // right after any write ending in "\n" (writeLine's normal case), false right after a
126
+ // streamed text delta that doesn't (the common case: deltas rarely end mid-sentence on a
127
+ // newline). Confirmed empirically (against a working chat loop that
128
+ // ends its streamed text block with an explicit newline): even with `prevRows` reset to 0 above, if the
129
+ // cursor is left *mid-row* when the next prompt redraws, `[kRefreshLine]`'s `cursorTo(0)`
130
+ // moves it to column 0 of that *same* row — the row still holding the tail of what we
131
+ // just wrote — and the following `clearScreenDown` wipes the row from there on, eating
132
+ // whatever was on it (for a short reply, potentially the whole thing, since "that row" is
133
+ // its only row). Ending every turn's output on a fresh blank row (see below, after
134
+ // `drainTurn()`) sidesteps this entirely: `clearScreenDown` then only ever clears blank
135
+ // space, on any row.
136
+ // Every write goes through the shared console renderer (./consoleRenderer.ts), which
137
+ // tracks whether the cursor sits at the start of a line and never emits a blank line
138
+ // between consecutive action lines.
139
+ const renderer = createConsoleRenderer({
140
+ formatAction: tuiOptions.formatAction,
141
+ agentLabel: tuiOptions.agentLabel,
142
+ output: (text) => void stdout.write(text),
143
+ onWrite: (text) => {
144
+ mirror(text);
145
+ // node:readline's `Interface` tracks, on itself, how many terminal rows its own last
146
+ // rendered prompt+line took (`prevRows`), so that the *next* time it redraws (the next
147
+ // `rl.question()` for the following turn, but — confirmed empirically — also any
148
+ // ordinary keystroke typed ahead while a long reply is still streaming here, since the
149
+ // interface keeps listening to stdin the whole time, not just while a `question()` is
150
+ // pending) it knows how far to move the cursor up before clearing. Every write this
151
+ // function makes happens outside readline entirely, so that bookkeeping goes stale the
152
+ // moment we print anything — left uncorrected, that next redraw moves the cursor up by
153
+ // the stale (small/zero) row count from wherever it *actually* is now (the bottom of
154
+ // everything just written) and clears from there down, visibly eating the tail of a
155
+ // short reply (or, for a long one the user started typing over), a chunk out of its
156
+ // middle. `prevRows` isn't part of readline's public/typed API, but it *is* a plain,
157
+ // externally-settable instance property at runtime (not a real JS `#private` field) —
158
+ // resetting it to 0 after every write of our own keeps that bookkeeping honest right up
159
+ // to the moment readline's own logic (ours or a stray keystroke's) needs it next, so
160
+ // the cursor-up step is always a no-op and the clear only ever touches blank space
161
+ // below the real cursor.
162
+ rl.prevRows = 0;
163
+ },
164
+ });
165
+ const { writeLine } = renderer;
166
+ // Cached lazily (not fetched until the first "/..." line, and only once — the SDK docs
167
+ // don't promise this list changes mid-session, and re-fetching per line would add a
168
+ // round-trip to the CLI subprocess for every turn): every registered command name/alias,
169
+ // built-ins (`/compact`, ...) included, so `/exit`-style local ones never false-flag.
170
+ let knownCommandTokens = null;
171
+ async function isKnownSlashCommand(token) {
172
+ if (!knownCommandTokens) {
173
+ const commands = await run.supportedCommands();
174
+ knownCommandTokens = new Set(commands.flatMap((c) => [c.name, ...(c.aliases ?? [])]));
175
+ }
176
+ return knownCommandTokens.has(token);
177
+ }
178
+ let turnInFlight = false;
179
+ let turnInterrupted = false;
180
+ function interruptTurn() {
181
+ if (turnInterrupted)
182
+ return;
183
+ turnInterrupted = true;
184
+ void run.interrupt();
185
+ writeLine(ui.warn("(interrupted)"));
186
+ }
187
+ rl.on("SIGINT", () => {
188
+ if (turnInFlight) {
189
+ interruptTurn();
190
+ }
191
+ else {
192
+ queue.end();
193
+ rl.close();
194
+ }
195
+ });
196
+ // Esc interrupts the turn in flight too, like Ctrl+C — but, unlike it, never ends the
197
+ // chat, and does nothing while a human-in-the-loop checkpoint is asking on this same
198
+ // interface (Esc there is just a keystroke on the question). The readline interface
199
+ // already emits "keypress" on stdin, and only reports name "escape" for a lone Esc, not
200
+ // for the Esc-prefixed sequences of arrow/function keys.
201
+ const onKeypress = (_chunk, key) => {
202
+ if (key?.name === "escape" && turnInFlight && !isSharedQuestionActive())
203
+ interruptTurn();
204
+ };
205
+ stdin.on("keypress", onKeypress);
206
+ if (tuiOptions.welcomeMessage)
207
+ writeLine(tuiOptions.welcomeMessage);
208
+ try {
209
+ if (tuiOptions.initialPrompt) {
210
+ mirror(`${promptLabel}${tuiOptions.initialPrompt}\n`);
211
+ queue.push(tuiOptions.initialPrompt);
212
+ turnInFlight = true;
213
+ turnInterrupted = false;
214
+ renderer.startTurn();
215
+ await drainTurn(events, renderer.render);
216
+ turnInFlight = false;
217
+ renderer.endLine();
218
+ }
219
+ while (true) {
220
+ let line;
221
+ try {
222
+ line = (await rl.question(promptLabel)).trim();
223
+ }
224
+ catch {
225
+ break; // the interface closed underneath us (SIGINT/EOF while idle)
226
+ }
227
+ if (!line)
228
+ continue;
229
+ mirror(`${promptLabel}${line}\n`);
230
+ if (tuiOptions.historyPath) {
231
+ historyEntries = capHistory([...historyEntries, { text: line, timestamp: new Date().toISOString() }], historyLimit);
232
+ await saveHistory(tuiOptions.historyPath, historyEntries);
233
+ }
234
+ if (exitCommands.has(line.toLowerCase()))
235
+ break;
236
+ // A line that *looks* like a slash command but matches nothing registered (a typo,
237
+ // an unnamespaced plugin command, ...) would otherwise reach the model as literal
238
+ // text with no special handling — confirmed empirically (captain-whiskers'
239
+ // /chiste before it was renamed /captain-whiskers:chiste) that this can look
240
+ // exactly like nothing happened at all. Caught here, before the turn even starts,
241
+ // so it's an immediate, unambiguous message instead of a guess at what the model's
242
+ // silence on unrecognized input might mean.
243
+ const commandToken = slashCommandToken(line);
244
+ if (commandToken && !(await isKnownSlashCommand(commandToken))) {
245
+ writeLine(ui.warn(`Unknown command: /${commandToken}`));
246
+ continue;
247
+ }
248
+ queue.push(line);
249
+ turnInFlight = true;
250
+ turnInterrupted = false;
251
+ renderer.startTurn();
252
+ await drainTurn(events, renderer.render);
253
+ turnInFlight = false;
254
+ renderer.endLine();
255
+ }
256
+ }
257
+ finally {
258
+ stdin.off("keypress", onKeypress);
259
+ queue.end();
260
+ run.close();
261
+ setSharedReadline(null);
262
+ rl.close();
263
+ sessionLog?.end();
264
+ }
265
+ }
@@ -0,0 +1,19 @@
1
+ import { type ClaudeAuthConfig } from "../core/claudeAuth.js";
2
+ /**
3
+ * The terminal-facing counterpart to `resolveClaudeAuth()` (core/claudeAuth.ts), which only
4
+ * *looks up* a token. This one also gets you one: if `resolveClaudeAuth(config)` comes back
5
+ * empty, it prints why, offers to run "claude setup-token" interactively, and sets the
6
+ * result on `process.env.CLAUDE_CODE_OAUTH_TOKEN` for this run. Exits the process if the
7
+ * user declines or cancels — there's no agent to run without a token.
8
+ *
9
+ * Persisting a freshly-generated token across runs is *not* this function's job — unlike an
10
+ * earlier version, this kit owns no config-file storage of its own (see git history). When
11
+ * the interactive flow does generate one, it's returned so the caller can save it wherever
12
+ * it wants (a file, an OS keychain, ...) and pass it back in as `config.claudeCodeOAuthToken`
13
+ * next time; returns `undefined` when auth was already resolved (an env var, or the `config`
14
+ * passed in) and nothing new was generated.
15
+ *
16
+ * Only makes sense with a real terminal in front of a human (a `runWizard()` confirm, colored console
17
+ * output), which is why it lives here rather than in the non-interactive core.
18
+ */
19
+ export declare function ensureClaudeAuth(config?: ClaudeAuthConfig): Promise<string | undefined>;
@@ -0,0 +1,96 @@
1
+ import { spawn } from "node:child_process";
2
+ import { resolveClaudeAuth } from "../core/claudeAuth.js";
3
+ import { isExitPromptError } from "./promptErrors.js";
4
+ import * as ui from "./ui.js";
5
+ import { runWizard } from "./ink/wizard.js";
6
+ // The real format of "claude setup-token" tokens ("sk-ant-oat01-...", seen in the
7
+ // installed @anthropic-ai/claude-code binary) — used to extract the token from its output.
8
+ const TOKEN_PATTERN = /sk-ant-oat\d{2}-[A-Za-z0-9_-]+/;
9
+ /**
10
+ * Launches "claude setup-token" as an interactive child process: it opens a browser and
11
+ * asks the user to authenticate, with instructions it prints itself. stdin/stderr are
12
+ * inherited as-is so the interaction (pasting the authorization code, etc.) is identical
13
+ * to running the command by hand; stdout is duplicated live and also buffered to extract
14
+ * the token it prints at the end ("Your OAuth token (...): sk-ant-oat01-...").
15
+ */
16
+ function runSetupToken() {
17
+ return new Promise((resolve, reject) => {
18
+ // On Windows npx is npx.cmd, which only runs through a shell. The command is passed as
19
+ // one fixed string there (nothing user-supplied in it): an args array with
20
+ // `shell: true` triggers Node's DEP0190 warning, since args are concatenated unescaped.
21
+ const stdio = ["inherit", "pipe", "inherit"];
22
+ const child = process.platform === "win32"
23
+ ? spawn("npx @anthropic-ai/claude-code setup-token", { stdio, shell: true })
24
+ : spawn("npx", ["@anthropic-ai/claude-code", "setup-token"], { stdio });
25
+ let output = "";
26
+ child.stdout.on("data", (chunk) => {
27
+ const text = chunk.toString("utf-8");
28
+ output += text;
29
+ process.stdout.write(text);
30
+ });
31
+ child.on("error", reject);
32
+ child.on("exit", (code) => {
33
+ if (code !== 0) {
34
+ reject(new Error(`"claude setup-token" exited with code ${code}.`));
35
+ return;
36
+ }
37
+ const match = output.match(TOKEN_PATTERN);
38
+ if (!match) {
39
+ reject(new Error('"claude setup-token" exited without errors, but no token was found in its ' +
40
+ "output (sk-ant-oat...). Copy it by hand into CLAUDE_CODE_OAUTH_TOKEN if you see it above."));
41
+ return;
42
+ }
43
+ resolve(match[0]);
44
+ });
45
+ });
46
+ }
47
+ /**
48
+ * The terminal-facing counterpart to `resolveClaudeAuth()` (core/claudeAuth.ts), which only
49
+ * *looks up* a token. This one also gets you one: if `resolveClaudeAuth(config)` comes back
50
+ * empty, it prints why, offers to run "claude setup-token" interactively, and sets the
51
+ * result on `process.env.CLAUDE_CODE_OAUTH_TOKEN` for this run. Exits the process if the
52
+ * user declines or cancels — there's no agent to run without a token.
53
+ *
54
+ * Persisting a freshly-generated token across runs is *not* this function's job — unlike an
55
+ * earlier version, this kit owns no config-file storage of its own (see git history). When
56
+ * the interactive flow does generate one, it's returned so the caller can save it wherever
57
+ * it wants (a file, an OS keychain, ...) and pass it back in as `config.claudeCodeOAuthToken`
58
+ * next time; returns `undefined` when auth was already resolved (an env var, or the `config`
59
+ * passed in) and nothing new was generated.
60
+ *
61
+ * Only makes sense with a real terminal in front of a human (a `runWizard()` confirm, colored console
62
+ * output), which is why it lives here rather than in the non-interactive core.
63
+ */
64
+ export async function ensureClaudeAuth(config = {}) {
65
+ if (resolveClaudeAuth(config))
66
+ return undefined;
67
+ console.log(ui.warn("\nNo Claude authentication token was found"));
68
+ console.log(ui.dim("(neither the CLAUDE_CODE_OAUTH_TOKEN environment variable, nor one passed in)."));
69
+ try {
70
+ const { generate } = await runWizard([
71
+ {
72
+ type: "confirm",
73
+ name: "generate",
74
+ message: 'Generate one now with "claude setup-token" (requires a Claude Pro/Max subscription)?',
75
+ default: true,
76
+ },
77
+ ]);
78
+ if (!generate) {
79
+ console.log(ui.error("\nThe agent can't start without a token. Set CLAUDE_CODE_OAUTH_TOKEN by hand " +
80
+ "(or run this again and accept generating it) and try again."));
81
+ process.exit(0);
82
+ }
83
+ }
84
+ catch (error) {
85
+ if (isExitPromptError(error))
86
+ process.exit(0);
87
+ throw error;
88
+ }
89
+ console.log(ui.heading("\n=== Generating a Claude authentication token ==="));
90
+ console.log(ui.dim("A browser will open to log in; follow the instructions the command itself prints " +
91
+ "below.\n"));
92
+ const token = await runSetupToken();
93
+ process.env.CLAUDE_CODE_OAUTH_TOKEN = token;
94
+ console.log(ui.success("\nToken generated for this run. Save it yourself (e.g. CLAUDE_CODE_OAUTH_TOKEN in your shell profile) to skip this next time.\n"));
95
+ return token;
96
+ }
@@ -0,0 +1,32 @@
1
+ import type { AgentEvent } from "../core/runner.js";
2
+ export interface ConsoleRendererOptions {
3
+ /** Turns a tool call into its console label; defaults to `createFriendlyToolLabel()`. */
4
+ formatAction?: (toolName: string, toolInput: unknown) => string;
5
+ /** Printed (already styled) before the first text of each turn, e.g. "my-agent>". */
6
+ agentLabel?: string;
7
+ /** Where the text goes; defaults to `process.stdout`. */
8
+ output?: (text: string) => void;
9
+ /** Called after every write with the same text, e.g. to mirror it into a log file. */
10
+ onWrite?: (text: string) => void;
11
+ }
12
+ export interface ConsoleRenderer {
13
+ /** Prints one normalized event from `runQuery()`. */
14
+ render(event: AgentEvent): void;
15
+ write(text: string): void;
16
+ /** Writes `text` on a line of its own: after ending the current line if needed. */
17
+ writeLine(text: string): void;
18
+ /** Ends the current line, if the cursor isn't already at the start of one. */
19
+ endLine(): void;
20
+ /** Marks the start of a new turn, so the agent label is printed again before its text. */
21
+ startTurn(): void;
22
+ /** Whether the cursor sits at the start of a line (after anything ending in "\n"). */
23
+ readonly atLineStart: boolean;
24
+ }
25
+ /**
26
+ * The console rendering of `runQuery()`'s event stream, shared by `runChatTui()` and by any
27
+ * one-shot run that prints the same events. Streamed text rarely ends in a newline, so
28
+ * every other line (actions, notices, errors) starts by ending the current line only if
29
+ * it isn't already ended — never with a fixed "\n", which leaves a blank line between
30
+ * consecutive actions.
31
+ */
32
+ export declare function createConsoleRenderer(options?: ConsoleRendererOptions): ConsoleRenderer;
@@ -0,0 +1,72 @@
1
+ import { stdout } from "node:process";
2
+ import { createFriendlyToolLabel } from "../core/toolLabels.js";
3
+ import * as ui from "./ui.js";
4
+ /**
5
+ * The console rendering of `runQuery()`'s event stream, shared by `runChatTui()` and by any
6
+ * one-shot run that prints the same events. Streamed text rarely ends in a newline, so
7
+ * every other line (actions, notices, errors) starts by ending the current line only if
8
+ * it isn't already ended — never with a fixed "\n", which leaves a blank line between
9
+ * consecutive actions.
10
+ */
11
+ export function createConsoleRenderer(options = {}) {
12
+ const formatAction = options.formatAction ?? createFriendlyToolLabel();
13
+ const output = options.output ?? ((text) => void stdout.write(text));
14
+ let atLineStart = true;
15
+ let labelPrinted = false;
16
+ function write(text) {
17
+ if (text.length === 0)
18
+ return;
19
+ output(text);
20
+ options.onWrite?.(text);
21
+ atLineStart = text.endsWith("\n");
22
+ }
23
+ function endLine() {
24
+ if (!atLineStart)
25
+ write("\n");
26
+ }
27
+ function writeLine(text) {
28
+ endLine();
29
+ write(`${text}\n`);
30
+ }
31
+ function render(event) {
32
+ switch (event.type) {
33
+ case "text":
34
+ if (options.agentLabel && !labelPrinted) {
35
+ endLine();
36
+ write(`${options.agentLabel} `);
37
+ labelPrinted = true;
38
+ }
39
+ write(ui.agent(event.text));
40
+ return;
41
+ case "action":
42
+ writeLine(ui.action(`[action] ${formatAction(event.toolName, event.input)}`));
43
+ return;
44
+ case "subagent-action":
45
+ case "tool-result":
46
+ case "prompt-suggestion":
47
+ return;
48
+ case "mcp-error":
49
+ writeLine(ui.warn(`Some MCP servers failed to connect: ${event.failedServers.join(", ")}`));
50
+ return;
51
+ case "info":
52
+ writeLine((event.level === "warning" ? ui.warn : ui.dim)(`(${event.text})`));
53
+ return;
54
+ case "turn-end":
55
+ if (event.failed && event.errorText)
56
+ writeLine(ui.error(event.errorText));
57
+ return;
58
+ }
59
+ }
60
+ return {
61
+ render,
62
+ write,
63
+ writeLine,
64
+ endLine,
65
+ startTurn: () => {
66
+ labelPrinted = false;
67
+ },
68
+ get atLineStart() {
69
+ return atLineStart;
70
+ },
71
+ };
72
+ }
@@ -0,0 +1,40 @@
1
+ /** Commands (names without "/") a partial "/name" line could still become. */
2
+ export declare function matchingCommands(value: string, commands: readonly string[]): string[];
3
+ /** Tab: completes a unique match (plus a space for its arguments), else the common prefix. */
4
+ export declare function completeCommand(value: string, commands: readonly string[]): string;
5
+ /**
6
+ * The slice [start, end) of a `length`-character line shown in `room` columns, keeping the
7
+ * cursor in view: like the live area, the prompt must never reach the terminal's edge (see
8
+ * lineBuffer.ts), so a long line scrolls sideways, with "…" on each cut side.
9
+ */
10
+ export declare function visibleWindow(length: number, cursor: number, room: number): [number, number];
11
+ export interface PromptInputProps {
12
+ label: string;
13
+ /** Columns taken around the prompt (a frame), so a long line scrolls before reaching the edge. */
14
+ inset?: number;
15
+ /** A suggested prompt, shown dim in the empty prompt; Tab takes it. */
16
+ suggestion?: string | null;
17
+ /** Earlier lines, oldest first, for ↑/↓ and Ctrl+R. */
18
+ history: readonly string[];
19
+ /** Slash command names (without "/") for suggestions and Tab completion. */
20
+ commands: readonly string[];
21
+ /** Gets the text as sent: pasted blocks expanded. */
22
+ onSubmit(line: string): void;
23
+ /** Ctrl+D on an empty line. */
24
+ onExit(): void;
25
+ /** ↑ on an empty prompt: takes the last queued line back (see runChatInk.tsx), or null if none. */
26
+ onRecallQueued?: () => string | null;
27
+ /** Project files for `@` mentions (paths relative to the session's folder). */
28
+ files?: readonly string[];
29
+ }
30
+ /** The shortcuts panel, opened with "?" on an empty prompt. */
31
+ export declare const SHORTCUTS: string[];
32
+ /**
33
+ * The chat's prompt: history (↑/↓), "/command" completion and suggestions (Tab), pasted
34
+ * blocks folded into a token, multi-line input (`\` + Enter or Ctrl+J), word and line
35
+ * shortcuts (Ctrl+W/K/U, Ctrl+←/→, Home/End, Ctrl+A/E), reverse history search (Ctrl+R)
36
+ * and taking a queued line back (↑ on an empty prompt). Written here rather than taken from
37
+ * @inkjs/ui because its TextInput is uncontrolled. Backspace also arrives as `delete` on
38
+ * Windows terminals, so both erase backwards.
39
+ */
40
+ export declare function PromptInput({ label, inset, suggestion, history, commands, onSubmit, onExit, onRecallQueued, files }: PromptInputProps): import("react").JSX.Element;