@falkenslab/agent-kit 0.30.1 → 0.31.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/README.md CHANGED
@@ -1,67 +1,48 @@
1
- <h1>
2
- <img src="https://raw.githubusercontent.com/falkenslab/agent-kit/main/docs/assets/logo-readme-any.svg" alt="agent-kit" height="64">
3
- </h1>
1
+ <div align="center">
4
2
 
5
- [![npm](https://img.shields.io/npm/v/@falkenslab/agent-kit?logo=npm)](https://www.npmjs.com/package/@falkenslab/agent-kit)
6
- [![npm downloads](https://img.shields.io/npm/dm/@falkenslab/agent-kit)](https://www.npmjs.com/package/@falkenslab/agent-kit)
7
- [![types](https://img.shields.io/npm/types/@falkenslab/agent-kit)](https://www.npmjs.com/package/@falkenslab/agent-kit)
8
- [![node](https://img.shields.io/node/v/@falkenslab/agent-kit)](https://nodejs.org/)
9
- [![license](https://img.shields.io/npm/l/@falkenslab/agent-kit)](LICENSE)
10
- [![docs](https://img.shields.io/badge/docs-falkenslab.github.io%2Fagent--kit-4453b8)](https://falkenslab.github.io/agent-kit/)
3
+ <img src="https://raw.githubusercontent.com/falkenslab/agent-kit/main/docs/static/img/logo.svg" alt="agent-kit logo: three blue building blocks and a coral one being set in place" width="112"/>
11
4
 
12
- **[📖 Documentation](https://falkenslab.github.io/agent-kit/)** · [📖 Documentación en español](https://falkenslab.github.io/agent-kit/es/)
13
-
14
- A foundation for building AI agents on top of the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). It isn't an agent itself: it solves once what every agent needs (human oversight, safety, memory and a good terminal interface), so each concrete agent only has to write what belongs to its own domain: what it knows how to do, with which tools, and how it talks.
15
-
16
- ## What an agent built with it can do
5
+ # agent-kit
17
6
 
18
- - **Work at different levels of oversight.** Autonomous, guided (it asks for permission before anything that can't be undone or that other people will see) or step by step (it asks before every action), and a plan mode where it only reads and plans until you let it act. Within the same conversation you can switch among guided, step by step and plan.
19
- - **Ask a person for help.** It asks for approvals, or stops so someone can do something by hand, such as signing in to a website. It can be answered from the keyboard or from another program driving it, for example a desktop app.
20
- - **Remember between sessions.** It keeps its own knowledge base, a small wiki of notes that grows with every session, and keeps the original documents you give it or that it downloads untouched. Each conversation stays in the project's folder, so you can pick it up again with `--continue` (the latest one) or `/resume` (any of them).
21
- - **Speak your language.** Its interface comes in English, Spanish, French or German, the system's by default or the one you ask for with `--language=fr`, and it answers in that language unless you write to it in another one.
22
- - **Delegate to subagents safely.** Only to the ones it declares, always waiting for their result, and without the main agent being able to slip past its own limits through them.
23
- - **Stay within limits.** It only reads and writes in the folders that belong to it, and never in protected files, such as one holding passwords.
24
- - **Use external tools.** MCP servers of its own or of the project, and skills and commands organized in plugins.
25
- - **Leave a trail.** It records every action and the whole conversation, with secrets hidden.
26
- - **Look the way you want.** The terminal's colors come from a theme by roles (the agent's replies, tool results, the focused option, borders…): pass only the ones you want to change, as a color name, a hex or a function.
27
- - **Chat in a terminal the way Claude Code does.** Full screen, with the reply written live and its markdown already formatted, the tools it uses shown in as much detail as the agent chooses (each call with its result, the calls alone, or one line per group; Ctrl+O unfolds them), panels for approvals, history and search, a suggestion for the next message, pasted blocks, file mentions with `@`, mouse selection (right-click to copy), and the agent's status in the tab and in the Windows taskbar.
28
- - **Also work without a terminal.** The same foundation runs inside a desktop app or a server, which bring their own interface.
7
+ **Build an AI agent on Claude that asks before it acts, remembers, and chats like Claude Code.**<br/>
8
+ You write what your agent knows and does; the kit brings oversight, safety, memory and the interface.
29
9
 
30
- ## Examples
10
+ [![npm](https://img.shields.io/npm/v/@falkenslab/agent-kit?color=cb3837&logo=npm)](https://www.npmjs.com/package/@falkenslab/agent-kit)
11
+ ![Node.js 20+](https://img.shields.io/badge/node-20%2B-339933?logo=node.js&logoColor=white)
12
+ ![Claude Agent SDK](https://img.shields.io/badge/Claude-Agent%20SDK-d97757?logo=anthropic&logoColor=white)
13
+ ![TypeScript](https://img.shields.io/badge/types-included-3178c6?logo=typescript&logoColor=white)
14
+ [![License: MIT](https://img.shields.io/badge/license-MIT-16a34a)](LICENSE)
31
15
 
32
- ### Captain Whiskers
33
-
34
- [Captain Whiskers](./examples/captain-whiskers) is a toy agent to see the kit in action: a retired pirate cat who tells jokes in the terminal, in your language. It starts full screen with its logo and has a small crew of subagents: one looks for new jokes on the web, another rates them before they're told, and a third tells the time with a tool of the captain's own. It lets you try the different levels of oversight and is the best starting point for an agent of your own. Its README explains how to run it.
16
+ **[📖 Documentation](https://falkenslab.github.io/agent-kit/)** · [📖 Documentación en español](https://falkenslab.github.io/agent-kit/es/)
35
17
 
36
- ![Captain Whiskers in Windows Terminal: his logo with the mode and the agent-kit version at the top, his crew at work in the middle, a task list under the spinner, and the framed prompt and status bar at the bottom](docs/assets/captain-whiskers.png)
18
+ </div>
37
19
 
38
- See [everything he can do](https://falkenslab.github.io/agent-kit/docs/next/examples/captain-whiskers#what-he-can-do), with screenshots.
20
+ agent-kit is a foundation for building AI agents on top of the [Claude Agent SDK](https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk). It isn't an agent itself: it solves once what every agent needs (levels of oversight, approvals, safe limits, memory across sessions, extensions, and a terminal chat as good as Claude Code's), so each agent writes only what belongs to its own domain: what it knows how to do, with which tools, and how it talks. A teacher's assistant for Moodle and a pirate cat who tells jokes are built the same way.
39
21
 
40
- ### miyagi
22
+ ## ✨ Why agent-kit
41
23
 
42
- [miyagi](https://github.com/falkenslab/miyagi) is a real assistant for Moodle teachers. It signs in with your account in a Chrome window and works as you would: it grades submissions, answers in the forum, creates or reviews content and sums up how the class is going. It asks for your permission before publishing anything students will see, and takes notes about the course to remember everything in the next session.
24
+ - 🎚️ **You choose how much it acts alone.** Autonomous, guided (it asks before what others will see or can't be undone), step by step, or a plan mode where it only reads and plans; switch among them in the same conversation.
25
+ - ✋ **It asks a person, and the kit makes sure.** Approvals, choices, a file, a sign-in by hand; an installed extension's tool that changes things waits for a yes whatever the model decided, and answers can last for the session or for good.
26
+ - 🧠 **It remembers.** A knowledge base that grows with every session, the original documents it learns from kept untouched, a memory of the person, and every conversation to pick up again with `--continue` or `/resume`.
27
+ - 🛡️ **It stays within limits.** Only its own folders, never protected files, subagents that can't slip past the main agent's limits, secrets hidden from the logs, remote servers allowed, asked about or denied.
28
+ - 🧩 **It grows with extensions.** Claude Code plugins with MCP servers, skills and commands, installed from a marketplace or a folder, with their own options, sign-in and a catalog in the chat; no Git needed.
29
+ - 🌐 **It speaks your language.** Its interface in English, Spanish, French or German, and it answers in that language unless you write in another.
30
+ - 🖥️ **A terminal chat like Claude Code's, or none at all.** Full screen, live markdown, folded tool calls, panels, history, themes; or the same agent inside a desktop app or a web page with its own interface.
43
31
 
44
- Oversight, memory, safety and the chat come from the kit; miyagi brings what it knows about Moodle and driving the browser.
32
+ ## 🚀 Quick start
45
33
 
46
- ## Installation
34
+ You need [Node.js](https://nodejs.org) 20 or later and a Claude account. In a project with `"type": "module"` in its `package.json`:
47
35
 
48
- ```
49
- npm install @falkenslab/agent-kit
36
+ ```bash
37
+ npm install @falkenslab/agent-kit tsx
50
38
  ```
51
39
 
52
- To try changes to the kit without publishing them, point to a local clone with `"file:../agent-kit"` in your `package.json` and build it with `npm run build` after each change.
53
-
54
- Requires Node.js 20 or later.
55
-
56
- ## A minimal agent
57
-
58
- An agent only describes what belongs to it (who it is, which MCP servers, skills and subagents it has) and picks a level of oversight; the kit sets up the session and the chat:
40
+ Save this as `agent.ts`: the only part that's your agent's is the spec at the top.
59
41
 
60
42
  ```ts
61
43
  import path from "node:path";
62
44
  import { buildSessionOptions, ensureClaudeAuth, runChatInk, type AgentSpec, type BaseSessionConfig } from "@falkenslab/agent-kit";
63
45
 
64
- // The only part specific to the agent: who it is and what it has.
65
46
  const spec: AgentSpec<BaseSessionConfig> = {
66
47
  buildSystemPrompt: () => "You are a pirate cat who tells jokes. Always answer in character.",
67
48
  buildMcpServers: () => ({}), // its own MCP servers, if any
@@ -70,42 +51,125 @@ const spec: AgentSpec<BaseSessionConfig> = {
70
51
  };
71
52
 
72
53
  await ensureClaudeAuth(); // offers to create a Claude token if it finds none
73
-
74
54
  const config: BaseSessionConfig = { mode: "guided", projectDir: process.cwd() };
75
55
 
76
- // Each run gets a folder of its own under .run/ (keep it out of git): its log, its
77
- // transcript and the conversation, to pick it up again with --continue or /resume.
56
+ // Each run gets a folder under .run/ (keep it out of git): its log, transcript and conversation.
78
57
  await runChatInk((run) => buildSessionOptions(config, run.dir, spec, { run }), {
79
58
  runsDir: path.resolve(".run"),
80
59
  header: { title: "My agent" },
81
60
  mode: config.mode, // Shift+Tab cycles through guided, step by step and plan
82
61
  fullscreen: true,
83
- theme: { toolResult: "gray", selection: "#ffb86c" }, // only the colors you want to change
84
62
  });
85
63
  ```
86
64
 
87
- Save it as `agent.ts` in a project with the kit and `tsx` installed and `"type": "module"` in its `package.json`, and run it with `npx tsx agent.ts`. Captain Whiskers is this same skeleton with more pieces: skills, subagents and its logo.
65
+ Run it with `npx tsx agent.ts`: a full-screen chat opens, in your system's language, and the agent asks before anything it shouldn't do alone. The [tutorial](https://falkenslab.github.io/agent-kit/docs/getting-started/first-agent) takes it from there.
66
+
67
+ ## 🧪 A taste of the code
68
+
69
+ Give it a memory and a folder of sources, with the kit's own extensions:
70
+
71
+ ```ts
72
+ import { knowledge, memory, sources } from "@falkenslab/agent-kit";
73
+
74
+ const spec: AgentSpec<BaseSessionConfig> = {
75
+ // …
76
+ extensions: [sources({ dir: "sources" }), knowledge({ dir: "knowledge" }), memory({ dir: "memory" })],
77
+ };
78
+ ```
79
+
80
+ Let it install extensions from your marketplace, and keep its data from leaving the computer without asking:
81
+
82
+ ```ts
83
+ const config: BaseSessionConfig = {
84
+ mode: "guided",
85
+ projectDir: process.cwd(),
86
+ extensionDirs: { agent: path.join(os.homedir(), ".my-agent", "extensions") },
87
+ officialMarketplace: "falkenslab/agent-extensions",
88
+ remoteServers: "ask",
89
+ };
90
+ ```
91
+
92
+ Make a tool always wait for the person's yes, on every call:
88
93
 
89
- ## Documentation
94
+ ```ts
95
+ const spec: AgentSpec<BaseSessionConfig> = {
96
+ // …
97
+ extensionGate: { confirmTools: { canva: ["create-design", "publish-design"] } },
98
+ };
99
+ ```
100
+
101
+ ## 📸 See it at work
102
+
103
+ [Captain Whiskers](./examples/captain-whiskers), the kit's example agent: a retired pirate cat who tells jokes in your language, with a crew of subagents (one looks for new jokes on the web, one rates them, one tells the time), a logbook and an extension of his own. He runs in the terminal, in a browser and as a Windows app.
104
+
105
+ <div align="center">
106
+
107
+ <img src="https://raw.githubusercontent.com/falkenslab/agent-kit/main/docs/assets/captain-whiskers.png" alt="Captain Whiskers in Windows Terminal: his logo with the mode and the agent-kit version at the top, his crew at work in the middle, a task list under the spinner, and the framed prompt and status bar at the bottom" width="760"/>
108
+
109
+ </div>
110
+
111
+ See [everything he can do](https://falkenslab.github.io/agent-kit/docs/examples/captain-whiskers#what-he-can-do), with screenshots. For a real one, [miyagi](https://github.com/falkenslab/miyagi) is an assistant for Moodle teachers: it grades, answers the forum, builds content and sums up how the class is going, asking before publishing anything students will see. Oversight, memory, safety and the chat come from the kit; miyagi brings what it knows about Moodle.
112
+
113
+ ## 🧰 What's inside
114
+
115
+ | Area | For example |
116
+ | --- | --- |
117
+ | 🎚️ Modes and gates | Autonomous, guided, step by step and plan; the guided gate for extensions' tools, the plan gate, the step gate |
118
+ | ✋ Human in the loop | Approvals, choices, free text, files, manual steps, MCP elicitation forms, a response file for hosts without a keyboard |
119
+ | 🧠 Memory | A knowledge base (an LLM wiki), sources kept as obtained, a memory of the person, runs resumed with `--continue` and `/resume` |
120
+ | 🧩 Extensions | Claude Code plugins with MCP servers (local, npm, remote with OAuth), skills, commands and subagents; scopes, a lock, workspace trust, options, marketplaces and a catalog |
121
+ | 🛡️ Security | A file scope gate, denied paths, subagent gates, a remote-servers gate, isolation from the runner's own Claude Code, redacted transcripts |
122
+ | 🤖 Subagents | Declared types only, in the foreground, without Bash for the main agent |
123
+ | 🖥️ Interfaces | A full-screen Ink chat, a plain readline chat, a progress view, a wizard, themes; a chat controller for desktop apps and web pages |
124
+ | 🌐 Languages | English, Spanish, French and German, for the interface, the replies and the extensions' servers |
125
+
126
+ Every export, with what it takes and what it gives, is in the [API reference](https://falkenslab.github.io/agent-kit/docs/api).
127
+
128
+ ## 📚 Documentation
90
129
 
91
- The full documentation is at **[falkenslab.github.io/agent-kit](https://falkenslab.github.io/agent-kit/)**: a tutorial, guides for every part of the kit (modes, human in the loop, security, tools, subagents, the knowledge base, the terminal UI, runs, languages, themes, hosts without a terminal), recipes, and an API reference generated from the source, for each released version.
130
+ - **[Getting started](https://falkenslab.github.io/agent-kit/docs/getting-started/installation)**: installation and a first agent, step by step.
131
+ - **[Modes](https://falkenslab.github.io/agent-kit/docs/core-concepts/modes)** and **[human in the loop](https://falkenslab.github.io/agent-kit/docs/human-in-the-loop/overview)**: how much the agent does alone, and how it asks.
132
+ - **[Extensions](https://falkenslab.github.io/agent-kit/docs/extensions/overview)**: installing, writing and publishing them.
133
+ - **[Security](https://falkenslab.github.io/agent-kit/docs/security/overview)**: every guardrail, and what the kit doesn't protect.
134
+ - **[The terminal chat](https://falkenslab.github.io/agent-kit/docs/terminal-ui/ink-chat)**: its keys, panels, options and themes.
135
+ - **[Captain Whiskers](https://falkenslab.github.io/agent-kit/docs/examples/captain-whiskers)**: the example agent, piece by piece.
92
136
 
93
- For whoever works on the kit itself, the specification in [`.minispec/`](./.minispec/README.md) explains how each part is built and why, including the SDK behaviors checked by hand that motivate several of its safeguards.
137
+ ## 🛠️ Developing
94
138
 
95
- ## Development
139
+ The design decisions, and the SDK behaviors checked by hand behind several safeguards, are in [`.minispec/`](./.minispec/README.md).
96
140
 
97
141
  | Command | What it does |
98
142
  | --- | --- |
99
143
  | `npm run build` | Builds the kit into `dist/` |
100
144
  | `npm run typecheck` | Checks the types without building |
101
145
  | `npm run lint` | Checks the code style |
102
- | `npm test` | Runs all the tests |
146
+ | `npm test` | Runs all the tests (one file: `npx tsx --test test/tui/promptErrors.test.ts`) |
103
147
  | `npm run docs:install` | Installs the documentation site's dependencies (once) |
104
148
  | `npm run docs:start` | Serves the documentation site locally, reloading on changes |
105
149
  | `npm run docs:build` | Builds the documentation site (fails on broken links) |
106
150
 
107
- To run a single test file:
151
+ To try changes to the kit in an agent without publishing them, point its `package.json` to a local clone (`"file:../agent-kit"`) and run `npm run build` after each change.
108
152
 
153
+ Working on the kit with Claude Code goes through Falken's Lab's own skills (commit, verify, update-docs, release, sync-issues, and the house style for READMEs, sites and Markdown), from the [falkenslab/falkenslab-plugins](https://github.com/falkenslab/falkenslab-plugins) marketplace. Install them once, for all of Falken's Lab's repositories:
154
+
155
+ ```bash
156
+ claude plugin marketplace add falkenslab/falkenslab-plugins
157
+ claude plugin install falkenslab-dev@falkenslab-plugins
158
+ claude plugin install falkenslab-style@falkenslab-plugins
109
159
  ```
110
- npx tsx --test test/tui/promptErrors.test.ts
111
- ```
160
+
161
+ <details>
162
+ <summary>What each plugin brings, and what stays in this repository</summary>
163
+
164
+ - 🔧 **falkenslab-dev**: `commit`, `verify`, `update-docs`, `release`, `sync-issues`, `upgrade-deps` and `add-tool`.
165
+ - 🎨 **falkenslab-style**: `falkenslab-docs`, `falkenslab-site-style`, `falkenslab-terminal-screenshots` and `markdown-writing`.
166
+
167
+ Inside Claude Code, the same with `/plugin marketplace add falkenslab/falkenslab-plugins` and `/plugin install falkenslab-dev@falkenslab-plugins`; `/plugin marketplace update falkenslab-plugins` brings their latest version. This repository keeps only the skills that are agent-kit's own, in `.claude/skills/` (`add-export`, the MiniSpec ones and `upgrade-sdk`), and what its release adds to the general one is in [`CLAUDE.md`](./CLAUDE.md#releasing).
168
+
169
+ </details>
170
+
171
+ <div align="center">
172
+
173
+ Made with 💖 by Falken's Lab Team
174
+
175
+ </div>
@@ -71,6 +71,14 @@ export interface ChatSettings {
71
71
  * The link is shown either way, on a line of its own, for another computer.
72
72
  */
73
73
  openBrowser?: boolean | ((url: string) => Promise<boolean>);
74
+ /**
75
+ * Claude Code's own commands in the `/` menu and the state's `commands` (#87): none by default,
76
+ * since they're for whoever develops with Claude Code (`/batch`, `/agents`, `/mcp`…), not for the
77
+ * agent's person; `"all"`, or the ones named (`["compact", "context"]`). The agent's own, its
78
+ * plugins', its extensions', the project's, the kit's and the view's are always there, and a
79
+ * command typed by hand still reaches the SDK.
80
+ */
81
+ cliCommands?: "all" | readonly string[];
74
82
  }
75
83
  /** One tool call in the transcript. */
76
84
  export interface TranscriptCall {
@@ -183,7 +183,10 @@ export async function createChatController(options, settings = {}) {
183
183
  { name: "extensions", description: t().commandExtensions, argumentHint: "[enable|disable|config|search|install|update|remove|reload]" },
184
184
  ];
185
185
  const details = new Map();
186
- for (const command of [...(commands ?? []).map(({ name, description, argumentHint }) => ({ name, description: description ?? "", argumentHint: argumentHint ?? "" })), ...local])
186
+ // Claude Code's own (builtin) only when the agent asks for them (#87).
187
+ const cli = settings.cliCommands;
188
+ const offered = (commands ?? []).filter((command) => !command.builtin || cli === "all" || (Array.isArray(cli) && cli.includes(command.name)));
189
+ for (const command of [...offered.map(({ name, description, argumentHint }) => ({ name, description: description ?? "", argumentHint: argumentHint ?? "" })), ...local])
187
190
  details.set(command.name, command);
188
191
  for (const name of exits)
189
192
  if (!details.has(name))
@@ -154,8 +154,11 @@ export interface AgentSpec<TConfig extends BaseSessionConfig> {
154
154
  * Whether the kit tells the agent (and each subagent) to reply in the resolved language
155
155
  * (`config.language`, `--language`, the system's), following the human if they write in
156
156
  * another one. On by default; `false` leaves the reply language to the agent's own prompt.
157
+ * When the language isn't English, the kit also says it again after each of the main agent's
158
+ * tool calls and with each message of the person (#85), since the narration between tool calls
159
+ * drifted to English after an English result; `"prompt-only"` keeps only the prompt's line.
157
160
  */
158
- replyInLanguage?: boolean;
161
+ replyInLanguage?: boolean | "prompt-only";
159
162
  /**
160
163
  * Which filesystem settings the session loads (the SDK's `settingSources`): "project" is
161
164
  * the project's `.claude/` (settings, skills, commands) and its CLAUDE.md files, "local"
@@ -1,10 +1,8 @@
1
- import { execFile } from "node:child_process";
2
1
  import { createHash } from "node:crypto";
3
2
  import { cp, mkdtemp, readdir, readFile, rm, stat, writeFile, mkdir } from "node:fs/promises";
4
3
  import os from "node:os";
5
4
  import path from "node:path";
6
5
  import { fileURLToPath } from "node:url";
7
- import { promisify } from "node:util";
8
6
  import { capabilitiesProblem, parseCapabilities, parseDescriptions, providedBy } from "./extensions.js";
9
7
  import { getLanguage } from "./messages/index.js";
10
8
  import { truncate } from "./toolLabels.js";
@@ -14,6 +12,7 @@ import { extensionSecretsPath, fillOptions, missingOptions, optionsNamed, parseO
14
12
  import { isTrusted } from "./workspaceTrust.js";
15
13
  import { checkRequirements, describeMissing, findOnPath, parseRequirements, requirementsProblem } from "./extensionRequirements.js";
16
14
  import { cachedPackageDir, cachedPackageProblem, cachePackage, npxServer, packageCacheDir, packageSpec } from "./npxPackages.js";
15
+ import { cloneRepository } from "./gitClone.js";
17
16
  /** Whether a server is remote (#61). */
18
17
  export function isRemoteServer(server) {
19
18
  return "url" in server;
@@ -203,7 +202,20 @@ async function pluginServers(dir, raw) {
203
202
  }
204
203
  /** `${CLAUDE_PLUGIN_ROOT}`, `${CLAUDE_PLUGIN_DATA}` and `${VAR}` (the agent's variable) in a server's values. */
205
204
  function expand(value, folders) {
206
- return value.replace(/\$\{(\w+)\}/g, (_, name) => name === "CLAUDE_PLUGIN_ROOT" ? folders.root : name === "CLAUDE_PLUGIN_DATA" ? folders.data : (process.env[name] ?? ""));
205
+ return value.replace(/\$\{(\w+)\}/g, (_, name) => (name === "CLAUDE_PLUGIN_ROOT" ? folders.root : name === "CLAUDE_PLUGIN_DATA" ? folders.data : (process.env[name] ?? "")));
206
+ }
207
+ /** The POSIX locale for a session's language: what any program reads to speak the person's language. */
208
+ const LOCALES = { en: "en_US.UTF-8", es: "es_ES.UTF-8", fr: "fr_FR.UTF-8", de: "de_DE.UTF-8" };
209
+ /**
210
+ * A server's locale in the session's language (#86): `LANG` and `LC_MESSAGES`, and `LC_ALL` when the
211
+ * system sets one (it would win). The standard way, so a server asks in the person's language
212
+ * knowing nothing of the kit, and does the same under Claude Code or any other client.
213
+ */
214
+ function localeEnv(language) {
215
+ const locale = language ? LOCALES[language] : undefined;
216
+ if (!locale)
217
+ return {};
218
+ return { LANG: locale, LC_MESSAGES: locale, ...(process.env.LC_ALL ? { LC_ALL: locale } : {}) };
207
219
  }
208
220
  /**
209
221
  * Why one of an extension's servers can't run through the launcher (Node, its script first,
@@ -214,7 +226,8 @@ function serverLaunch(name, server, folders, packages = {}) {
214
226
  const { root } = folders;
215
227
  // Its folders as variables too, as Claude Code gives them to a plugin's servers.
216
228
  const declared = Object.fromEntries(Object.entries(server.env ?? {}).map(([key, value]) => [key, expand(String(value), folders)]));
217
- const env = { CLAUDE_PLUGIN_ROOT: root, CLAUDE_PLUGIN_DATA: folders.data, ...declared };
229
+ // And the session's language as its locale (#86): a server that reads it can ask and answer in it.
230
+ const env = { CLAUDE_PLUGIN_ROOT: root, CLAUDE_PLUGIN_DATA: folders.data, ...localeEnv(folders.language), ...declared };
218
231
  const npx = npxServer(server);
219
232
  if (typeof npx === "string")
220
233
  return `${npx} (its server "${name}")`;
@@ -390,7 +403,7 @@ function externalExtension(installed, manifest, cache, unavailable = []) {
390
403
  const remote = [];
391
404
  if (Object.keys(manifest.servers).length)
392
405
  await mkdir(installed.dataDir, { recursive: true });
393
- const folders = { root: installed.dir, data: installed.dataDir };
406
+ const folders = { root: installed.dir, data: installed.dataDir, language: getLanguage() };
394
407
  // A remote server's options go into its URL and headers here, when the session opens: the
395
408
  // SDK fixes them then, so a changed value takes opening the session again (#61).
396
409
  const values = Object.values(manifest.servers).some(isRemoteServer) ? await readOptionValues(installed.privateDir, installed.name, manifest.userConfig, installed.lock.options) : {};
@@ -525,7 +538,6 @@ async function problem(installed, cache) {
525
538
  return options;
526
539
  return (await serversProblem(manifest, installed.dir, { cache, locked: installed.lock.packages })) ?? { manifest, unavailable };
527
540
  }
528
- const exec = promisify(execFile);
529
541
  /** Whether `source` names a git repository rather than a folder. */
530
542
  export function isGitSource(source) {
531
543
  return /^(https?:\/\/|git@|ssh:\/\/|git:\/\/|file:\/\/|github:)/.test(source) || /\.git(#.*)?$/.test(source);
@@ -549,12 +561,9 @@ export async function addExtension(source, scopeDir, options = {}) {
549
561
  if (isGitSource(source)) {
550
562
  const [url, ref] = source.replace(/^github:/, "https://github.com/").split("#");
551
563
  clone = await mkdtemp(path.join(os.tmpdir(), "agent-kit-extension-"));
552
- // A pinned commit needs the history to check it out; otherwise the tip is enough.
553
- await exec("git", ["clone", ...(options.sha ? [] : ["--depth", "1"]), ...(ref ? ["--branch", ref] : []), url, clone]);
554
- if (options.sha)
555
- await exec("git", ["-C", clone, "checkout", "--quiet", options.sha]);
556
- commit = (await exec("git", ["-C", clone, "rev-parse", "HEAD"])).stdout.trim();
557
- if (options.sha && commit !== options.sha.toLowerCase())
564
+ // Without the git program (#89): a pinned commit takes the history, otherwise the tip is enough.
565
+ commit = await cloneRepository(url, clone, { ...(ref ? { ref } : {}), ...(options.sha ? { sha: options.sha } : {}) });
566
+ if (options.sha && !commit.startsWith(options.sha.toLowerCase()))
558
567
  throw new Error(`${url} has no commit ${options.sha}.`);
559
568
  from = clone;
560
569
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Clones a git repository into `dir` (#89), without the `git` program: an extension or a
3
+ * marketplace from GitHub installs on a computer without Git, as in a desktop app on a clean
4
+ * Windows. Over HTTP(S) with isomorphic-git (pure JavaScript: Node and Electron); a `file://`
5
+ * repository is read where it is and its files written out, never changed; SSH (`git@…`,
6
+ * `ssh://`, `git://`), which isomorphic-git can't speak, goes to the system's `git` when there is
7
+ * one. `ref` is a branch or a tag (the default branch without it); `sha` pins a commit, which must
8
+ * be on that branch's history. Returns the commit checked out. `dir` keeps no `.git`.
9
+ */
10
+ export declare function cloneRepository(url: string, dir: string, options?: {
11
+ ref?: string;
12
+ sha?: string;
13
+ }): Promise<string>;
@@ -0,0 +1,117 @@
1
+ import { execFile } from "node:child_process";
2
+ import fs from "node:fs";
3
+ import { mkdir, writeFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
7
+ import git from "isomorphic-git";
8
+ import http from "isomorphic-git/http/node";
9
+ const exec = promisify(execFile);
10
+ /**
11
+ * Clones a git repository into `dir` (#89), without the `git` program: an extension or a
12
+ * marketplace from GitHub installs on a computer without Git, as in a desktop app on a clean
13
+ * Windows. Over HTTP(S) with isomorphic-git (pure JavaScript: Node and Electron); a `file://`
14
+ * repository is read where it is and its files written out, never changed; SSH (`git@…`,
15
+ * `ssh://`, `git://`), which isomorphic-git can't speak, goes to the system's `git` when there is
16
+ * one. `ref` is a branch or a tag (the default branch without it); `sha` pins a commit, which must
17
+ * be on that branch's history. Returns the commit checked out. `dir` keeps no `.git`.
18
+ */
19
+ export async function cloneRepository(url, dir, options = {}) {
20
+ try {
21
+ if (/^(git@|ssh:\/\/|git:\/\/)/.test(url))
22
+ return await cloneWithGitProgram(url, dir, options);
23
+ if (/^https?:\/\//.test(url))
24
+ return await cloneOverHttp(url, dir, options);
25
+ // A local repository: a file:// URL, or a path (one ending in .git, perhaps bare).
26
+ return await exportLocal(url.startsWith("file://") ? fileURLToPath(url) : path.resolve(url), dir, options);
27
+ }
28
+ catch (error) {
29
+ throw new Error(explain(url, options, error), { cause: error });
30
+ }
31
+ }
32
+ /** Over HTTP(S): the tip of a branch or tag in one shallow fetch, or its history when a commit is pinned. */
33
+ async function cloneOverHttp(url, dir, { ref, sha }) {
34
+ await git.clone({ fs, http, dir, url, ...(ref ? { ref } : {}), singleBranch: true, ...(sha ? {} : { depth: 1 }), noTags: !ref });
35
+ if (sha)
36
+ await git.checkout({ fs, dir, ref: await expandCommit(path.join(dir, ".git"), sha), force: true });
37
+ const commit = await git.resolveRef({ fs, dir, ref: "HEAD" });
38
+ await fs.promises.rm(path.join(dir, ".git"), { recursive: true, force: true });
39
+ return commit;
40
+ }
41
+ /** A local repository: the commit's files written into `dir`, the repository itself untouched (no checkout, no index). */
42
+ async function exportLocal(repo, dir, { ref, sha }) {
43
+ // A working copy's .git, or the repository itself when it's bare.
44
+ const gitdir = fs.existsSync(path.join(repo, ".git")) ? path.join(repo, ".git") : repo;
45
+ const commit = sha ? await expandCommit(gitdir, sha) : await git.resolveRef({ fs, gitdir, ref: ref ?? "HEAD" });
46
+ const { commit: { tree } } = await git.readCommit({ fs, gitdir, oid: commit });
47
+ await git.walk({
48
+ fs,
49
+ gitdir,
50
+ trees: [git.TREE({ ref: tree })],
51
+ map: async (filepath, [entry]) => {
52
+ if (!entry || filepath === ".")
53
+ return true;
54
+ const type = await entry.type();
55
+ const target = path.join(dir, filepath);
56
+ if (type === "tree")
57
+ await mkdir(target, { recursive: true });
58
+ else if (type === "blob") {
59
+ await mkdir(path.dirname(target), { recursive: true });
60
+ await writeFile(target, (await entry.content()) ?? new Uint8Array());
61
+ }
62
+ return true;
63
+ },
64
+ });
65
+ return commit;
66
+ }
67
+ /** A commit given whole or abbreviated, as git takes it: the whole id, or an error when there's no such commit. */
68
+ async function expandCommit(gitdir, sha) {
69
+ const oid = sha.length === 40 ? sha.toLowerCase() : await git.expandOid({ fs, gitdir, oid: sha.toLowerCase() });
70
+ await git.readCommit({ fs, gitdir, oid });
71
+ return oid;
72
+ }
73
+ /** SSH, which isomorphic-git can't speak: the system's `git`, or a clear error without one. */
74
+ async function cloneWithGitProgram(url, dir, { ref, sha }) {
75
+ try {
76
+ await exec("git", ["clone", ...(sha ? [] : ["--depth", "1"]), ...(ref ? ["--branch", ref] : []), url, dir]);
77
+ }
78
+ catch (error) {
79
+ if (error.code === "ENOENT")
80
+ throw Object.assign(new Error("no git"), { code: "NoGitForSsh" });
81
+ throw error;
82
+ }
83
+ if (sha)
84
+ await exec("git", ["-C", dir, "checkout", "--quiet", sha]);
85
+ const commit = (await exec("git", ["-C", dir, "rev-parse", "HEAD"])).stdout.trim();
86
+ await fs.promises.rm(path.join(dir, ".git"), { recursive: true, force: true });
87
+ return commit;
88
+ }
89
+ /** What went wrong, for a person: which repository, and whether it's the network, the repository, the ref or the commit. */
90
+ function explain(url, { ref, sha }, error) {
91
+ const code = error.code ?? "";
92
+ const status = error.data?.statusCode;
93
+ let host = url;
94
+ try {
95
+ host = new URL(url).host || url;
96
+ }
97
+ catch {
98
+ // Not a URL (git@host:…): the address as given.
99
+ }
100
+ if (code === "NoGitForSsh")
101
+ return `${url} is an SSH address, which needs the git program, and this computer has none: use its HTTPS address instead.`;
102
+ if (code === "HttpError" && (status === 401 || status === 403 || status === 404))
103
+ return `${url} isn't a repository this computer can reach: it doesn't exist, or it's private.`;
104
+ if (code === "HttpError")
105
+ return `${host} answered with an error (${status ?? "?"}) for ${url}.`;
106
+ if (["ENOTFOUND", "EAI_AGAIN", "ECONNREFUSED", "ECONNRESET", "ETIMEDOUT"].includes(code))
107
+ return `Couldn't reach ${host} for ${url}: check the network.`;
108
+ if (code === "NotFoundError" || code === "AmbiguousError") {
109
+ if (sha)
110
+ return `${url} has no commit ${sha}${ref ? ` on ${ref}` : ""}.`;
111
+ if (ref)
112
+ return `${url} has no branch or tag ${ref}.`;
113
+ }
114
+ if (code === "NotFoundError" && !/^https?:\/\//.test(url))
115
+ return `${url} isn't a git repository.`;
116
+ return `Couldn't get ${url}${ref ? ` at ${ref}` : ""}: ${error instanceof Error ? error.message : String(error)}`;
117
+ }
@@ -0,0 +1,16 @@
1
+ import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
2
+ import { type Language } from "../language.js";
3
+ /**
4
+ * The reply language, said again next to what the model just read (#85): with only the system
5
+ * prompt's line, far from the turn, its narration between tool calls drifted to English after an
6
+ * English result (a source, a skill, the kit's own messages). One short line, about fifteen tokens.
7
+ */
8
+ export declare function languageReminderLine(language: Language): string;
9
+ /**
10
+ * Its hooks: after each of the main agent's tool calls (a subagent has its own prompt line, and
11
+ * its narration isn't the person's), and with the person's message.
12
+ */
13
+ export declare function createLanguageReminder(language: Language): {
14
+ postToolUse: HookCallback;
15
+ userPromptSubmit: HookCallback;
16
+ };
@@ -0,0 +1,24 @@
1
+ import { LANGUAGE_NAMES } from "../language.js";
2
+ /**
3
+ * The reply language, said again next to what the model just read (#85): with only the system
4
+ * prompt's line, far from the turn, its narration between tool calls drifted to English after an
5
+ * English result (a source, a skill, the kit's own messages). One short line, about fifteen tokens.
6
+ */
7
+ export function languageReminderLine(language) {
8
+ return `Reply language: ${LANGUAGE_NAMES[language]}, your narration between tool calls too.`;
9
+ }
10
+ /**
11
+ * Its hooks: after each of the main agent's tool calls (a subagent has its own prompt line, and
12
+ * its narration isn't the person's), and with the person's message.
13
+ */
14
+ export function createLanguageReminder(language) {
15
+ const line = languageReminderLine(language);
16
+ return {
17
+ postToolUse: async (input) => {
18
+ if (input.agent_id)
19
+ return {};
20
+ return { hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: line } };
21
+ },
22
+ userPromptSubmit: async () => ({ hookSpecificOutput: { hookEventName: "UserPromptSubmit", additionalContext: line } }),
23
+ };
24
+ }
@@ -1,4 +1,5 @@
1
1
  import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
2
+ import { type ToolLabels } from "../toolLabels.js";
2
3
  /**
3
4
  * PreToolUse hook for "interactive" mode: pauses before every action and asks for
4
5
  * confirmation (by keyboard or by file, see humanInput.ts), like reviewing a plan step
@@ -7,5 +8,9 @@ import type { HookCallback } from "@anthropic-ai/claude-agent-sdk";
7
8
  * `isActive` lets one session switch between "guided" and "interactive" while it runs (see
8
9
  * session.ts's `ModeControl`): the hook stays registered and, while inactive, gives no
9
10
  * decision at all, so the tool call goes on exactly as if the hook weren't there.
11
+ *
12
+ * The panel says the call as the chat does (#88), in the person's language: the session's
13
+ * labels (`labels`: the extensions'), else the kit's for its own tools, then the tool's name and
14
+ * its parameters in one line, cut short; the person approving needn't read JSON.
10
15
  */
11
- export declare function createStepGate(runDir: string, isActive?: () => boolean, selfAskingTools?: readonly string[]): HookCallback;
16
+ export declare function createStepGate(runDir: string, isActive?: () => boolean, selfAskingTools?: readonly string[], labels?: () => ToolLabels | undefined): HookCallback;
@@ -1,5 +1,8 @@
1
1
  import { askForDecision } from "./humanInput.js";
2
2
  import { t } from "../messages/index.js";
3
+ import { createFriendlyToolLabel, withToolLabels } from "../toolLabels.js";
4
+ /** How much of a call's parameters the panel shows, as one line (#88). */
5
+ const MAX_PARAMETERS = 300;
3
6
  const SELF_ASKING_SERVERS = ["mcp__approvals__", "mcp__manualLogin__"];
4
7
  const SELF_ASKING_TOOLS = new Set(["TodoWrite"]);
5
8
  /**
@@ -10,8 +13,13 @@ const SELF_ASKING_TOOLS = new Set(["TodoWrite"]);
10
13
  * `isActive` lets one session switch between "guided" and "interactive" while it runs (see
11
14
  * session.ts's `ModeControl`): the hook stays registered and, while inactive, gives no
12
15
  * decision at all, so the tool call goes on exactly as if the hook weren't there.
16
+ *
17
+ * The panel says the call as the chat does (#88), in the person's language: the session's
18
+ * labels (`labels`: the extensions'), else the kit's for its own tools, then the tool's name and
19
+ * its parameters in one line, cut short; the person approving needn't read JSON.
13
20
  */
14
- export function createStepGate(runDir, isActive = () => true, selfAskingTools = []) {
21
+ export function createStepGate(runDir, isActive = () => true, selfAskingTools = [], labels = () => undefined) {
22
+ const describe = withToolLabels(labels, createFriendlyToolLabel());
15
23
  return async (input) => {
16
24
  if (!isActive())
17
25
  return {};
@@ -20,9 +28,10 @@ export function createStepGate(runDir, isActive = () => true, selfAskingTools =
20
28
  // a source) or only keeps the task list isn't asked about first: that would be asking to ask.
21
29
  if (SELF_ASKING_SERVERS.some((prefix) => pre.tool_name.startsWith(prefix)) || SELF_ASKING_TOOLS.has(pre.tool_name) || selfAskingTools.includes(pre.tool_name))
22
30
  return {};
31
+ const compact = JSON.stringify(pre.tool_input ?? {});
23
32
  const answer = await askForDecision(runDir, {
24
33
  title: t().proposedAction,
25
- lines: [t().toolLine(pre.tool_name), t().parametersLine(JSON.stringify(pre.tool_input, null, 2))],
34
+ lines: [describe(pre.tool_name, pre.tool_input), t().toolLine(pre.tool_name), t().parametersLine(compact.length > MAX_PARAMETERS ? `${compact.slice(0, MAX_PARAMETERS)}…` : compact)],
26
35
  });
27
36
  if (answer === "q") {
28
37
  return {
@@ -1,3 +1,12 @@
1
+ /**
2
+ * Marketplaces (ADR-025, #29): a repository of extensions is a Claude Code marketplace as it is,
3
+ * a folder or git repository with `.claude-plugin/marketplace.json` listing plugins and where
4
+ * each comes from. An agent knows the marketplaces added to it, kept in its extensions folder
5
+ * (`marketplaces.json`, and a copy of each in `.marketplaces/<name>/`), and installs from them
6
+ * by `<plugin>@<marketplace>` into either scope, through `addExtension()`: the same copy, hash and
7
+ * lock as any extension. Its official marketplace is known without asking; any other is added
8
+ * only by hand, after a warning (the command asks to type its name).
9
+ */
1
10
  /** Where a marketplace's plugin comes from, as `marketplace.json` says (Claude Code's forms). */
2
11
  export type PluginSource = string | {
3
12
  source: "github";
@@ -1,20 +1,9 @@
1
- import { execFile } from "node:child_process";
2
1
  import { cp, mkdir, mkdtemp, readFile, rm, stat, writeFile } from "node:fs/promises";
3
2
  import os from "node:os";
4
3
  import path from "node:path";
5
- import { promisify } from "node:util";
6
4
  import { addExtension, isGitSource } from "./externalExtensions.js";
7
5
  import { fetchArchivePlugin, fetchNpmPlugin } from "./pluginDownloads.js";
8
- /**
9
- * Marketplaces (ADR-025, #29): a repository of extensions is a Claude Code marketplace as it is,
10
- * a folder or git repository with `.claude-plugin/marketplace.json` listing plugins and where
11
- * each comes from. An agent knows the marketplaces added to it, kept in its extensions folder
12
- * (`marketplaces.json`, and a copy of each in `.marketplaces/<name>/`), and installs from them
13
- * by `<plugin>@<marketplace>` into either scope, through `addExtension()`: the same copy, hash and
14
- * lock as any extension. Its official marketplace is known without asking; any other is added
15
- * only by hand, after a warning (the command asks to type its name).
16
- */
17
- const exec = promisify(execFile);
6
+ import { cloneRepository } from "./gitClone.js";
18
7
  const REGISTRY = "marketplaces.json";
19
8
  const COPIES = ".marketplaces";
20
9
  const NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
@@ -77,9 +66,8 @@ async function fetchMarketplace(source) {
77
66
  try {
78
67
  if (await isGitMarketplace(source)) {
79
68
  const [location, ref] = source.split("#");
80
- await exec("git", ["clone", "--depth", "1", ...(ref ? ["--branch", ref] : []), gitUrl(location), copy]);
81
- const commit = (await exec("git", ["-C", copy, "rev-parse", "HEAD"])).stdout.trim();
82
- await rm(path.join(copy, ".git"), { recursive: true, force: true });
69
+ // Without the git program (#89).
70
+ const commit = await cloneRepository(gitUrl(location), copy, ref ? { ref } : {});
83
71
  return { copy, commit };
84
72
  }
85
73
  await cp(path.resolve(source), copy, { recursive: true, filter: (file) => path.basename(file) !== ".git" });
@@ -31,6 +31,7 @@ import { createElicitationHandler } from "./elicitation.js";
31
31
  import { createReadOnlyHints, createToolHints } from "./readOnlyHints.js";
32
32
  import { projectServerDuplicates } from "./projectServers.js";
33
33
  import { createSignInReminder, SIGN_IN_RULE } from "./hooks/signInReminder.js";
34
+ import { createLanguageReminder } from "./hooks/languageReminder.js";
34
35
  import { agentKitVersion } from "./version.js";
35
36
  /**
36
37
  * Builds the `options` object passed to the Agent SDK's `query()` — everything about
@@ -58,6 +59,8 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
58
59
  // for the interface too, so the agent answers in the language its interface is in.
59
60
  const language = chooseLanguage(config.language);
60
61
  const replyLine = spec.replyInLanguage === false ? null : replyLanguageInstruction(language);
62
+ // Said again next to what the model reads, so its narration doesn't drift to English (#85).
63
+ const languageReminder = spec.replyInLanguage !== false && spec.replyInLanguage !== "prompt-only" && language !== "en" ? createLanguageReminder(language) : undefined;
61
64
  const withReplyLine = (prompt) => (replyLine ? `${prompt}\n\n${replyLine}` : prompt);
62
65
  const modeControl = createModeControl(mode);
63
66
  // The human approval server only exists outside autonomous mode: the point of
@@ -379,7 +382,7 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
379
382
  : []),
380
383
  // Registered wherever the approval tool is, and asking only while the current mode is
381
384
  // "interactive": a guided session can switch to interactive and back (ModeControl).
382
- ...(includeApprovalTool ? [{ hooks: [createStepGate(runDir, () => modeControl.mode === "interactive", fromExtensions((contribution) => contribution.selfAskingTools))] }] : []),
385
+ ...(includeApprovalTool ? [{ hooks: [createStepGate(runDir, () => modeControl.mode === "interactive", fromExtensions((contribution) => contribution.selfAskingTools), () => toolLabels)] }] : []),
383
386
  // Likewise for "plan", which any non-autonomous session can switch into: the agent
384
387
  // only reads and plans while it's on (hooks/planGate.ts).
385
388
  ...(includeApprovalTool
@@ -405,12 +408,14 @@ export async function buildSessionOptions(config, runDir, spec, options = {}) {
405
408
  ]
406
409
  : []),
407
410
  ],
408
- PostToolUse: [{ hooks: [transcriptLogger.postToolUse] }],
411
+ PostToolUse: [{ hooks: [transcriptLogger.postToolUse] }, ...(languageReminder ? [{ hooks: [languageReminder.postToolUse] }] : [])],
409
412
  // The person's message: it ends a batch they approved (#74), and carries the sign-in reminders (#82).
410
- ...(guidedGate || signInExtensions.length
413
+ ...(guidedGate || signInExtensions.length || languageReminder
411
414
  ? {
412
415
  UserPromptSubmit: [
413
416
  ...(guidedGate ? [{ hooks: [guidedGate.userPromptSubmit] }] : []),
417
+ // The reply language, next to the person's message too (#85).
418
+ ...(languageReminder ? [{ hooks: [languageReminder.userPromptSubmit] }] : []),
414
419
  // With the person's message, which extensions now wait for a sign-in (#82).
415
420
  ...(signInExtensions.length ? [{ hooks: [createSignInReminder({ extensions: () => signInExtensions, status: () => late.view?.controls().mcpServerStatus })] }] : []),
416
421
  ],
@@ -17,6 +17,8 @@ export interface ChatTuiOptions {
17
17
  formatAction?: (toolName: string, input: unknown) => string;
18
18
  /** Signing in to an extension: whether its link opens in this computer's browser (the default), or how (see `ChatSettings.openBrowser`, #72). */
19
19
  openBrowser?: ChatSettings["openBrowser"];
20
+ /** Claude Code's own commands in the `/` menu: none by default, `"all"`, or the ones named (see `ChatSettings.cliCommands`, #87). */
21
+ cliCommands?: ChatSettings["cliCommands"];
20
22
  /** Printed once before the first prompt, if given. */
21
23
  welcomeMessage?: string;
22
24
  /** Shown before the cursor at each prompt. */
@@ -77,6 +77,7 @@ export async function runChatTui(options, tuiOptions = {}) {
77
77
  toolLabels: tuiOptions.toolLabels,
78
78
  formatAction: tuiOptions.formatAction,
79
79
  ...(tuiOptions.openBrowser !== undefined ? { openBrowser: tuiOptions.openBrowser } : {}),
80
+ ...(tuiOptions.cliCommands !== undefined ? { cliCommands: tuiOptions.cliCommands } : {}),
80
81
  // Nothing here shows them.
81
82
  promptSuggestions: false,
82
83
  // /resume: a numbered list, and a number typed.
@@ -200,6 +200,7 @@ export async function runChatInk(options, tuiOptions = {}) {
200
200
  toolPhrase: tuiOptions.toolPhrase,
201
201
  formatResult: tuiOptions.formatResult,
202
202
  ...(tuiOptions.openBrowser !== undefined ? { openBrowser: tuiOptions.openBrowser } : {}),
203
+ ...(tuiOptions.cliCommands !== undefined ? { cliCommands: tuiOptions.cliCommands } : {}),
203
204
  promptSuggestions: tuiOptions.promptSuggestions ?? true,
204
205
  // The /resume list in place of the prompt.
205
206
  pick: (title, choices) => input.pick(title, choices),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@falkenslab/agent-kit",
3
- "version": "0.30.1",
3
+ "version": "0.31.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",
@@ -54,6 +54,7 @@
54
54
  "@inquirer/prompts": "^8.5.2",
55
55
  "@types/react": "^19.3.0",
56
56
  "ink": "^6.8.0",
57
+ "isomorphic-git": "^1.44.5",
57
58
  "marked": "^18.0.14",
58
59
  "picocolors": "^1.1.1",
59
60
  "react": "^19.3.0",