@steerable/agent-cli 0.6.57

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.
@@ -0,0 +1,308 @@
1
+ const CONFIG_KEYS = [
2
+ 'provider',
3
+ 'vendorId',
4
+ 'model',
5
+ 'baseUrl',
6
+ 'apiKey',
7
+ 'temperature',
8
+ 'systemPrompt',
9
+ 'maxTotalTokens',
10
+ 'execTimeoutSeconds',
11
+ ];
12
+ const NUMBER_KEYS = new Set(['temperature', 'maxTotalTokens', 'execTimeoutSeconds']);
13
+ export async function chatCommand(io, stdout, stderr, open, help) {
14
+ const sub = io.positionals[1];
15
+ if (io.help && !sub) {
16
+ stdout.write(`${help}\n`);
17
+ return 0;
18
+ }
19
+ if (sub === 'list')
20
+ return open((client) => listChats(client, io, stdout, stderr));
21
+ if (sub === 'show' || sub === 'rm' || sub === 'export') {
22
+ const id = io.positionals[2];
23
+ if (!id) {
24
+ stderr.write(`missing chat id\n${help}\n`);
25
+ return 2;
26
+ }
27
+ if (sub === 'show')
28
+ return open((client) => showChat(client, id, io, stdout, stderr));
29
+ if (sub === 'rm')
30
+ return open((client) => removeChat(client, id, stdout, stderr));
31
+ const format = io.format ?? 'md';
32
+ if (format !== 'md' && format !== 'json') {
33
+ stderr.write(`unknown --format value\n${help}\n`);
34
+ return 2;
35
+ }
36
+ return open((client) => exportChat(client, id, io, stdout, stderr, help));
37
+ }
38
+ stderr.write(`${sub ? `unknown chat command: ${sub}\n` : ''}${help}\n`);
39
+ return 2;
40
+ }
41
+ export async function skillsCommand(io, stdout, stderr, open, help) {
42
+ if (io.help || io.positionals[1] === undefined) {
43
+ stdout.write(`${help}\n`);
44
+ return io.help ? 0 : 2;
45
+ }
46
+ if (io.positionals[1] !== 'list') {
47
+ stderr.write(`unknown skills command: ${io.positionals[1]}\n${help}\n`);
48
+ return 2;
49
+ }
50
+ return open(async (client) => {
51
+ const listed = await client.request('GET', '/api/v2/chat-agents/skills', undefined);
52
+ if (listed.status !== 200)
53
+ return fail(stderr, 'skills list', listed.status);
54
+ const skills = listed.data.skills ?? [];
55
+ if (io.json) {
56
+ stdout.write(`${JSON.stringify(skills)}\n`);
57
+ return 0;
58
+ }
59
+ if (skills.length === 0) {
60
+ stdout.write('no skills\n');
61
+ return 0;
62
+ }
63
+ for (const skill of skills) {
64
+ stdout.write(`${skill.name}\t${skill.origin ?? ''}\t${skill.description ?? ''}\n`);
65
+ }
66
+ return 0;
67
+ });
68
+ }
69
+ export async function mcpCommand(io, stdout, stderr, open, help) {
70
+ const sub = io.positionals[1];
71
+ if (io.help && !sub) {
72
+ stdout.write(`${help}\n`);
73
+ return 0;
74
+ }
75
+ if (sub === 'list') {
76
+ return open(async (client) => {
77
+ const listed = await client.request('GET', '/api/v2/mcp/servers', undefined);
78
+ if (listed.status !== 200)
79
+ return fail(stderr, 'mcp list', listed.status);
80
+ const servers = listed.data.servers ?? [];
81
+ if (io.json) {
82
+ stdout.write(`${JSON.stringify(servers)}\n`);
83
+ return 0;
84
+ }
85
+ if (servers.length === 0) {
86
+ stdout.write('no mcp servers\n');
87
+ return 0;
88
+ }
89
+ for (const server of servers) {
90
+ const command = [server.command, ...(server.args ?? [])].filter(Boolean).join(' ');
91
+ stdout.write(`${server.name}\t${command}\n`);
92
+ }
93
+ return 0;
94
+ });
95
+ }
96
+ if (sub === 'add') {
97
+ const name = io.positionals[2];
98
+ const command = io.positionals[3];
99
+ const args = io.positionals.slice(4);
100
+ if (!name || !command) {
101
+ stderr.write(`missing mcp command\n${help}\n`);
102
+ return 2;
103
+ }
104
+ return open(async (client) => {
105
+ const created = await client.request('POST', '/api/v2/mcp/servers', {
106
+ name,
107
+ transport: 'stdio',
108
+ command,
109
+ args,
110
+ });
111
+ if (created.status !== 200)
112
+ return fail(stderr, 'mcp add', created.status);
113
+ stdout.write(`added ${name}\n`);
114
+ return 0;
115
+ });
116
+ }
117
+ if (sub === 'rm') {
118
+ const name = io.positionals[2];
119
+ if (!name) {
120
+ stderr.write(`missing mcp name\n${help}\n`);
121
+ return 2;
122
+ }
123
+ return open(async (client) => {
124
+ const listed = await client.request('GET', '/api/v2/mcp/servers', undefined);
125
+ if (listed.status !== 200)
126
+ return fail(stderr, 'mcp list', listed.status);
127
+ const servers = listed.data.servers ?? [];
128
+ const match = servers.find((server) => server.name === name);
129
+ if (!match?.id) {
130
+ stderr.write(`mcp server not found: ${name}\n`);
131
+ return 1;
132
+ }
133
+ const removed = await client.request('DELETE', `/api/v2/mcp/servers/${encodeURIComponent(match.id)}`, undefined);
134
+ if (removed.status !== 200)
135
+ return fail(stderr, 'mcp rm', removed.status);
136
+ stdout.write(`removed ${name}\n`);
137
+ return 0;
138
+ });
139
+ }
140
+ stderr.write(`${sub ? `unknown mcp command: ${sub}\n` : ''}${help}\n`);
141
+ return 2;
142
+ }
143
+ export async function configCommand(io, stdout, stderr, open, help) {
144
+ const sub = io.positionals[1];
145
+ if (io.help && !sub) {
146
+ stdout.write(`${help}\n`);
147
+ return 0;
148
+ }
149
+ const key = io.positionals[2];
150
+ if ((sub !== 'get' && sub !== 'set') || !key || !isConfigKey(key)) {
151
+ stderr.write(`${help}\n`);
152
+ return 2;
153
+ }
154
+ if (sub === 'get') {
155
+ return open(async (client) => {
156
+ const current = await readConfig(client, stderr);
157
+ if (!current)
158
+ return 1;
159
+ const value = current[key];
160
+ stdout.write(`${key}=${value === undefined ? '' : String(value)}\n`);
161
+ return 0;
162
+ });
163
+ }
164
+ const raw = io.positionals[3];
165
+ if (raw === undefined) {
166
+ stderr.write(`missing config value\n${help}\n`);
167
+ return 2;
168
+ }
169
+ const value = coerceConfigValue(key, raw);
170
+ if (value === undefined) {
171
+ stderr.write(`invalid config value\n${help}\n`);
172
+ return 2;
173
+ }
174
+ return open(async (client) => {
175
+ const current = await readConfig(client, stderr);
176
+ if (!current)
177
+ return 1;
178
+ const saved = await client.request('POST', '/api/v2/local-settings/llm', { ...current, [key]: value });
179
+ if (saved.status !== 200)
180
+ return fail(stderr, 'config set', saved.status);
181
+ stdout.write(`${key}=${String(value)}\n`);
182
+ return 0;
183
+ });
184
+ }
185
+ async function listChats(client, io, stdout, stderr) {
186
+ const listed = await client.request('GET', '/api/v2/chats', undefined);
187
+ if (listed.status !== 200)
188
+ return fail(stderr, 'chat list', listed.status);
189
+ const chats = listed.data.chats ?? [];
190
+ if (io.json) {
191
+ stdout.write(`${JSON.stringify(chats)}\n`);
192
+ return 0;
193
+ }
194
+ if (chats.length === 0) {
195
+ stdout.write('no chats\n');
196
+ return 0;
197
+ }
198
+ for (const chat of chats)
199
+ stdout.write(`${chat.id}\t${chat.title ?? ''}\n`);
200
+ return 0;
201
+ }
202
+ async function showChat(client, id, io, stdout, stderr) {
203
+ const loaded = await loadChat(client, id, stderr);
204
+ if (!loaded)
205
+ return 1;
206
+ if (io.json) {
207
+ stdout.write(`${JSON.stringify(loaded)}\n`);
208
+ return 0;
209
+ }
210
+ stdout.write(`${loaded.chat.id}\t${loaded.chat.title ?? ''}\n`);
211
+ for (const message of loaded.messages) {
212
+ stdout.write(`${message.role}\t${oneLine(message.content)}\n`);
213
+ }
214
+ return 0;
215
+ }
216
+ async function removeChat(client, id, stdout, stderr) {
217
+ const removed = await client.request('DELETE', `/api/v2/chats/${encodeURIComponent(id)}`, undefined);
218
+ if (removed.status === 409) {
219
+ stderr.write(`${messageOf(removed.data, '该会话正在另一个进程中运行')}\n`);
220
+ return 6;
221
+ }
222
+ if (removed.status === 404) {
223
+ stderr.write('chat not found\n');
224
+ return 1;
225
+ }
226
+ if (removed.status !== 200)
227
+ return fail(stderr, 'chat rm', removed.status);
228
+ stdout.write(`deleted ${id}\n`);
229
+ return 0;
230
+ }
231
+ async function exportChat(client, id, io, stdout, stderr, help) {
232
+ const format = io.format ?? 'md';
233
+ if (format !== 'md' && format !== 'json') {
234
+ stderr.write(`unknown --format value\n${help}\n`);
235
+ return 2;
236
+ }
237
+ const loaded = await loadChat(client, id, stderr);
238
+ if (!loaded)
239
+ return 1;
240
+ if (format === 'json') {
241
+ stdout.write(`${JSON.stringify(loaded, null, 2)}\n`);
242
+ return 0;
243
+ }
244
+ const title = loaded.chat.title || loaded.chat.id;
245
+ const parts = [`# ${title}`, ''];
246
+ for (const message of loaded.messages) {
247
+ parts.push(`## ${message.role}`, '', message.content, '');
248
+ }
249
+ stdout.write(`${parts.join('\n').replace(/\n$/, '')}\n`);
250
+ return 0;
251
+ }
252
+ async function loadChat(client, id, stderr) {
253
+ const encoded = encodeURIComponent(id);
254
+ const chat = await client.request('GET', `/api/v2/chats/${encoded}`, undefined);
255
+ if (chat.status === 404) {
256
+ stderr.write('chat not found\n');
257
+ return null;
258
+ }
259
+ if (chat.status !== 200) {
260
+ fail(stderr, 'chat', chat.status);
261
+ return null;
262
+ }
263
+ const messages = await client.request('GET', `/api/v2/chats/${encoded}/messages`, undefined);
264
+ if (messages.status !== 200) {
265
+ fail(stderr, 'chat messages', messages.status);
266
+ return null;
267
+ }
268
+ return {
269
+ chat: chat.data,
270
+ messages: messages.data.messages ?? [],
271
+ };
272
+ }
273
+ async function readConfig(client, stderr) {
274
+ const current = await client.request('GET', '/api/v2/local-settings/llm', undefined);
275
+ if (current.status !== 200 || !current.data || typeof current.data !== 'object') {
276
+ fail(stderr, 'config get', current.status);
277
+ return null;
278
+ }
279
+ return current.data;
280
+ }
281
+ function isConfigKey(key) {
282
+ return CONFIG_KEYS.includes(key);
283
+ }
284
+ function coerceConfigValue(key, raw) {
285
+ if (!NUMBER_KEYS.has(key))
286
+ return raw;
287
+ const value = Number(raw);
288
+ if (!Number.isFinite(value))
289
+ return undefined;
290
+ if (key === 'execTimeoutSeconds' && value <= 0)
291
+ return undefined;
292
+ return value;
293
+ }
294
+ function fail(stderr, label, status) {
295
+ stderr.write(`${label} failed (${status})\n`);
296
+ return 1;
297
+ }
298
+ function messageOf(data, fallback) {
299
+ if (data && typeof data === 'object' && 'message' in data) {
300
+ const message = data.message;
301
+ if (typeof message === 'string' && message)
302
+ return message;
303
+ }
304
+ return fallback;
305
+ }
306
+ function oneLine(content) {
307
+ return content.replace(/[\r\n\t]+/g, ' ');
308
+ }
package/dist/help.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ export declare function rootHelp(commands?: readonly {
2
+ name: string;
3
+ summary: string;
4
+ }[]): string;
5
+ export declare function runHelp(): string;
6
+ export declare function chatHelp(): string;
7
+ export declare function skillsHelp(): string;
8
+ export declare function mcpHelp(): string;
9
+ export declare function configHelp(): string;
10
+ export declare function tuiHelp(): string;
11
+ export declare function doctorHelp(): string;
package/dist/help.js ADDED
@@ -0,0 +1,118 @@
1
+ const EXAMPLES = `
2
+ Examples:
3
+ <product> run "列出当前目录" --approve allow-read --stream-json
4
+ <product> run --chat <id> "继续"
5
+ <product> chat list
6
+ <product> chat show <id>
7
+ <product> chat export <id> --format md
8
+ <product> skills list
9
+ <product> mcp list
10
+ <product> config get model
11
+ <product> doctor
12
+ <product> tui
13
+ `.trim();
14
+ export function rootHelp(commands = []) {
15
+ const extra = commands
16
+ .map((command) => ` <product> ${command.name} ${command.summary}`)
17
+ .join('\n');
18
+ const pack = extra ? `\n${extra}` : '';
19
+ return `
20
+ Usage:
21
+ <product> run "<task>" [--chat <id>] [--approve deny|allow-read|allow-all]
22
+ [--json | --stream-json] [--cwd <dir>] [--timeout <dur>]
23
+ [--data-dir <dir>] [--agent <id>] [--file <path>...]
24
+ <product> chat list | show <id> | rm <id> | export <id> [--format md|json]
25
+ <product> skills list
26
+ <product> mcp list | add <name> -- <cmd...> | rm <name>
27
+ <product> config get <key> | set <key> <value>
28
+ <product> doctor
29
+ <product> tui${pack}
30
+
31
+ ${EXAMPLES}
32
+ `.trim();
33
+ }
34
+ export function runHelp() {
35
+ return `
36
+ Usage:
37
+ <product> run "<task>" [--chat <id>] [--approve deny|allow-read|allow-all]
38
+ [--json | --stream-json] [--data-dir <dir>]
39
+
40
+ --approve deny is the default. A denied approval exits 3. A busy chat exits 6.
41
+
42
+ ${EXAMPLES}
43
+ `.trim();
44
+ }
45
+ export function chatHelp() {
46
+ return `
47
+ Usage:
48
+ <product> chat list [--json] [--data-dir <dir>]
49
+ <product> chat show <id> [--json]
50
+ <product> chat rm <id>
51
+ <product> chat export <id> [--format md|json]
52
+
53
+ rm of a chat held by another process exits 6.
54
+
55
+ Examples:
56
+ <product> chat list
57
+ <product> chat show <id>
58
+ <product> chat export <id> --format md
59
+ `.trim();
60
+ }
61
+ export function skillsHelp() {
62
+ return `
63
+ Usage:
64
+ <product> skills list [--json] [--data-dir <dir>]
65
+
66
+ Examples:
67
+ <product> skills list
68
+ `.trim();
69
+ }
70
+ export function mcpHelp() {
71
+ return `
72
+ Usage:
73
+ <product> mcp list [--json]
74
+ <product> mcp add <name> -- <cmd...>
75
+ <product> mcp rm <name>
76
+
77
+ Examples:
78
+ <product> mcp add files -- npx -y @modelcontextprotocol/server-filesystem .
79
+ <product> mcp rm files
80
+ `.trim();
81
+ }
82
+ export function configHelp() {
83
+ return `
84
+ Usage:
85
+ <product> config get <key>
86
+ <product> config set <key> <value>
87
+
88
+ Keys: provider, vendorId, model, baseUrl, apiKey, temperature, systemPrompt,
89
+ maxTotalTokens, execTimeoutSeconds.
90
+
91
+ Examples:
92
+ <product> config get model
93
+ <product> config set model demo-model
94
+ `.trim();
95
+ }
96
+ export function tuiHelp() {
97
+ return `
98
+ Usage:
99
+ <product> tui [--data-dir <dir>]
100
+
101
+ Starts the terminal interface. When stdin is not a terminal, use run instead.
102
+
103
+ /new /model /clear /help
104
+ Ctrl+C interrupts the current turn. Ctrl+L lists chats.
105
+
106
+ Examples:
107
+ <product> tui
108
+ `.trim();
109
+ }
110
+ export function doctorHelp() {
111
+ return `
112
+ Usage:
113
+ <product> doctor [--data-dir <dir>]
114
+
115
+ Examples:
116
+ <product> doctor
117
+ `.trim();
118
+ }
@@ -0,0 +1,2 @@
1
+ export { createCli } from './cli.js';
2
+ export type { CliOptions } from './cli.js';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { createCli } from './cli.js';
package/dist/run.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ import type { AgentClient } from '@steerable/agent-client';
2
+ import { type ApprovePolicy } from './approve.js';
3
+ export interface RunRequest {
4
+ task: string;
5
+ chatId?: string;
6
+ agentId?: string;
7
+ cwd?: string;
8
+ files: string[];
9
+ policy: ApprovePolicy;
10
+ json: boolean;
11
+ streamJson: boolean;
12
+ timeoutMs?: number;
13
+ }
14
+ export declare function runTurn(client: AgentClient, request: RunRequest, stdout: NodeJS.WritableStream, stderr: NodeJS.WritableStream, signal: AbortSignal): Promise<number>;
package/dist/run.js ADDED
@@ -0,0 +1,121 @@
1
+ import { decisionFor, denied } from './approve.js';
2
+ export async function runTurn(client, request, stdout, stderr, signal) {
3
+ if (request.policy === 'allow-all') {
4
+ stderr.write('warning: --approve allow-all allows every tool call in this run\n');
5
+ }
6
+ let chatId = request.chatId;
7
+ if (!chatId) {
8
+ const created = await client.request('POST', '/api/v2/chats/new', {
9
+ ...(request.agentId ? { agentId: request.agentId } : {}),
10
+ });
11
+ if (created.status !== 200) {
12
+ stderr.write(`failed to create chat (${created.status})\n`);
13
+ return 1;
14
+ }
15
+ chatId = created.data.chatId;
16
+ if (!chatId) {
17
+ stderr.write('failed to create chat\n');
18
+ return 1;
19
+ }
20
+ }
21
+ let exitCode = 0;
22
+ let deniedApproval = false;
23
+ const text = [];
24
+ const approvals = watchApprovals(client, request.policy, () => {
25
+ deniedApproval = true;
26
+ });
27
+ try {
28
+ for await (const event of client.stream(`/api/v2/chats/${encodeURIComponent(chatId)}/send`, { message: messageText(request), ...(request.agentId ? { agentId: request.agentId } : {}) }, signal)) {
29
+ const next = observeEvent(event, request, stdout, stderr, text);
30
+ if (next !== 0)
31
+ exitCode = next;
32
+ }
33
+ }
34
+ finally {
35
+ approvals.stop();
36
+ }
37
+ if (signal.aborted && exitCode === 0)
38
+ exitCode = 1;
39
+ if (deniedApproval)
40
+ exitCode = 3;
41
+ if (client.lastStatus === 409)
42
+ exitCode = 6;
43
+ if (request.json) {
44
+ stdout.write(`${JSON.stringify({
45
+ ok: exitCode === 0,
46
+ chatId,
47
+ text: text.join(''),
48
+ exitCode,
49
+ })}\n`);
50
+ }
51
+ return exitCode;
52
+ }
53
+ function messageText(request) {
54
+ const lines = [request.task];
55
+ if (request.cwd)
56
+ lines.push(`Working directory: ${request.cwd}`);
57
+ if (request.files.length > 0) {
58
+ lines.push('Files:');
59
+ for (const file of request.files)
60
+ lines.push(`- ${file}`);
61
+ }
62
+ return lines.join('\n');
63
+ }
64
+ function watchApprovals(client, policy, onDenied) {
65
+ let stopped = false;
66
+ const loop = (async () => {
67
+ for await (const event of client.events()) {
68
+ if (stopped)
69
+ return;
70
+ if (event.channel === 'approval:request') {
71
+ const prompt = event.payload;
72
+ if (!prompt.requestId)
73
+ continue;
74
+ const kind = decisionFor(policy, prompt);
75
+ if (denied(kind))
76
+ onDenied();
77
+ await client.decideApproval(prompt.requestId, kind);
78
+ }
79
+ else if (event.channel === 'ask-user:request') {
80
+ const prompt = event.payload;
81
+ if (prompt.requestId)
82
+ await client.answerAsk(prompt.requestId, {});
83
+ }
84
+ }
85
+ })();
86
+ void loop;
87
+ return {
88
+ stop() {
89
+ stopped = true;
90
+ },
91
+ };
92
+ }
93
+ function observeEvent(event, request, stdout, stderr, text) {
94
+ if (request.streamJson)
95
+ stdout.write(`${JSON.stringify(event)}\n`);
96
+ if (event.type === 'content' && typeof event.content === 'string') {
97
+ text.push(event.content);
98
+ if (!request.json && !request.streamJson)
99
+ stdout.write(event.content);
100
+ }
101
+ if (event.type === 'tool_call' && !request.json && !request.streamJson) {
102
+ const name = typeof event.payload?.name === 'string' ? event.payload.name : 'tool';
103
+ stderr.write(`tool ${name}\n`);
104
+ }
105
+ if (event.type === 'error') {
106
+ const message = typeof event.message === 'string' ? event.message : 'error';
107
+ if (!request.streamJson)
108
+ stderr.write(`${message}\n`);
109
+ if (event.code === 'chat_busy')
110
+ return 6;
111
+ if (message.includes('storage upgrade blocked') || message.includes('newer than this program'))
112
+ return 75;
113
+ return 1;
114
+ }
115
+ if (event.type === 'budget_exhausted') {
116
+ if (!request.streamJson)
117
+ stderr.write('budget exhausted\n');
118
+ return 4;
119
+ }
120
+ return 0;
121
+ }
@@ -0,0 +1,55 @@
1
+ import { type Keybinding, type KeybindingsConfig } from '@earendil-works/pi-tui';
2
+ declare module '@earendil-works/pi-tui' {
3
+ interface Keybindings {
4
+ 'agent.interrupt': true;
5
+ 'agent.chats': true;
6
+ 'agent.approval.allowOnce': true;
7
+ 'agent.approval.allowSession': true;
8
+ 'agent.approval.allowAlways': true;
9
+ 'agent.approval.denyOnce': true;
10
+ 'agent.approval.denySession': true;
11
+ 'agent.approval.denyAlways': true;
12
+ 'agent.approval.abort': true;
13
+ }
14
+ }
15
+ export declare const AGENT_KEYBINDINGS: {
16
+ readonly 'agent.interrupt': {
17
+ readonly defaultKeys: "ctrl+c";
18
+ readonly description: "Interrupt the current turn or leave";
19
+ };
20
+ readonly 'agent.chats': {
21
+ readonly defaultKeys: "ctrl+l";
22
+ readonly description: "Show chats";
23
+ };
24
+ readonly 'agent.approval.allowOnce': {
25
+ readonly defaultKeys: "y";
26
+ readonly description: "Allow this tool once";
27
+ };
28
+ readonly 'agent.approval.allowSession': {
29
+ readonly defaultKeys: "s";
30
+ readonly description: "Allow this tool for the chat";
31
+ };
32
+ readonly 'agent.approval.allowAlways': {
33
+ readonly defaultKeys: "a";
34
+ readonly description: "Always allow this tool";
35
+ };
36
+ readonly 'agent.approval.denyOnce': {
37
+ readonly defaultKeys: "n";
38
+ readonly description: "Deny this tool once";
39
+ };
40
+ readonly 'agent.approval.denySession': {
41
+ readonly defaultKeys: "shift+n";
42
+ readonly description: "Deny this tool for the chat";
43
+ };
44
+ readonly 'agent.approval.denyAlways': {
45
+ readonly defaultKeys: "shift+a";
46
+ readonly description: "Always deny this tool";
47
+ };
48
+ readonly 'agent.approval.abort': {
49
+ readonly defaultKeys: "escape";
50
+ readonly description: "Abort the turn";
51
+ };
52
+ };
53
+ export declare function installAgentKeybindings(user?: KeybindingsConfig): void;
54
+ export declare function ensureAgentKeybindings(): void;
55
+ export declare function keyLabel(id: Keybinding): string;
@@ -0,0 +1,40 @@
1
+ import { getKeybindings, KeybindingsManager, setKeybindings, TUI_KEYBINDINGS, } from '@earendil-works/pi-tui';
2
+ export const AGENT_KEYBINDINGS = {
3
+ 'agent.interrupt': { defaultKeys: 'ctrl+c', description: 'Interrupt the current turn or leave' },
4
+ 'agent.chats': { defaultKeys: 'ctrl+l', description: 'Show chats' },
5
+ 'agent.approval.allowOnce': { defaultKeys: 'y', description: 'Allow this tool once' },
6
+ 'agent.approval.allowSession': { defaultKeys: 's', description: 'Allow this tool for the chat' },
7
+ 'agent.approval.allowAlways': { defaultKeys: 'a', description: 'Always allow this tool' },
8
+ 'agent.approval.denyOnce': { defaultKeys: 'n', description: 'Deny this tool once' },
9
+ 'agent.approval.denySession': { defaultKeys: 'shift+n', description: 'Deny this tool for the chat' },
10
+ 'agent.approval.denyAlways': { defaultKeys: 'shift+a', description: 'Always deny this tool' },
11
+ 'agent.approval.abort': { defaultKeys: 'escape', description: 'Abort the turn' },
12
+ };
13
+ const DEFINITIONS = { ...TUI_KEYBINDINGS, ...AGENT_KEYBINDINGS };
14
+ export function installAgentKeybindings(user = {}) {
15
+ setKeybindings(new KeybindingsManager(DEFINITIONS, user));
16
+ }
17
+ export function ensureAgentKeybindings() {
18
+ if (getKeybindings().getKeys('agent.interrupt').length === 0)
19
+ installAgentKeybindings();
20
+ }
21
+ export function keyLabel(id) {
22
+ ensureAgentKeybindings();
23
+ const key = getKeybindings().getKeys(id)[0] ?? '';
24
+ return formatKey(key);
25
+ }
26
+ function formatKey(key) {
27
+ if (key === 'escape')
28
+ return 'Esc';
29
+ if (/^shift\+[a-z]$/.test(key))
30
+ return key.slice('shift+'.length).toUpperCase();
31
+ if (!key.includes('+'))
32
+ return key;
33
+ return key.split('+').map((part) => {
34
+ if (part === 'ctrl')
35
+ return 'Ctrl';
36
+ if (part.length === 1)
37
+ return part.toUpperCase();
38
+ return part[0].toUpperCase() + part.slice(1);
39
+ }).join('+');
40
+ }
@@ -0,0 +1,8 @@
1
+ import { type Terminal } from '@earendil-works/pi-tui';
2
+ import type { AgentClient } from '@steerable/agent-client';
3
+ export declare function runTui(options: {
4
+ client: AgentClient;
5
+ product: string;
6
+ dataDir?: string;
7
+ terminal?: Terminal;
8
+ }): Promise<number>;