@steerable/agent-cli 0.7.2 → 0.7.4
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/dist/approve.d.ts +2 -0
- package/dist/approve.js +2 -0
- package/dist/help.d.ts +1 -1
- package/dist/help.js +1 -1
- package/dist/node-version.d.ts +1 -1
- package/dist/node-version.js +1 -1
- package/dist/run.js +9 -4
- package/dist/tui/session.d.ts +1 -0
- package/dist/tui/session.js +140 -27
- package/dist/tui/smoke-entry.js +9 -0
- package/dist/tui/transcript.d.ts +3 -0
- package/dist/tui/transcript.js +114 -1
- package/package.json +4 -4
package/dist/approve.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export declare function parseApprovePolicy(raw: string | undefined): ApprovePoli
|
|
|
14
14
|
export declare function decisionFor(spec: ApprovalSpec, prompt: {
|
|
15
15
|
mode?: string;
|
|
16
16
|
}, surface: ApprovalSurface): ApprovalDecisionKind | 'prompt';
|
|
17
|
+
/** The sidecar copies this reason onto the stored tool row as `approval.auto`. */
|
|
18
|
+
export declare const UNATTENDED_APPROVAL_REASON = "unattended";
|
|
17
19
|
export declare function unattendedAskText(): string;
|
|
18
20
|
export declare function autoAnswer(questions: {
|
|
19
21
|
id: string;
|
package/dist/approve.js
CHANGED
|
@@ -30,6 +30,8 @@ export function decisionFor(spec, prompt, surface) {
|
|
|
30
30
|
return 'prompt';
|
|
31
31
|
return 'deny_once';
|
|
32
32
|
}
|
|
33
|
+
/** The sidecar copies this reason onto the stored tool row as `approval.auto`. */
|
|
34
|
+
export const UNATTENDED_APPROVAL_REASON = 'unattended';
|
|
33
35
|
export function unattendedAskText() {
|
|
34
36
|
return cliText('ask.unattended');
|
|
35
37
|
}
|
package/dist/help.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare const COMMAND_NAME_TOKEN = "<product>";
|
|
|
16
16
|
* A blank name, the token itself, or a name that is not a command token is left unchanged.
|
|
17
17
|
*
|
|
18
18
|
* @param text Help or protected-directory text.
|
|
19
|
-
* @param commandName Bin name such as `
|
|
19
|
+
* @param commandName Bin name such as `agent` or `steerable-cli`.
|
|
20
20
|
* @returns Text with the placeholder replaced when the name is usable.
|
|
21
21
|
*/
|
|
22
22
|
export declare function renderCommandName(text: string, commandName: string | undefined): string;
|
package/dist/help.js
CHANGED
|
@@ -135,7 +135,7 @@ export const COMMAND_NAME_TOKEN = '<product>';
|
|
|
135
135
|
* A blank name, the token itself, or a name that is not a command token is left unchanged.
|
|
136
136
|
*
|
|
137
137
|
* @param text Help or protected-directory text.
|
|
138
|
-
* @param commandName Bin name such as `
|
|
138
|
+
* @param commandName Bin name such as `agent` or `steerable-cli`.
|
|
139
139
|
* @returns Text with the placeholder replaced when the name is usable.
|
|
140
140
|
*/
|
|
141
141
|
export function renderCommandName(text, commandName) {
|
package/dist/node-version.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export interface NodeVersionWriter {
|
|
|
11
11
|
}
|
|
12
12
|
/** @param current Installed version, with or without a leading `v`. @param required `major.minor.patch`. @returns Whether `current` is at least `required`. */
|
|
13
13
|
export declare function satisfies(current: string, required: string): boolean;
|
|
14
|
-
/** @param current Installed version. @param stderr Where the failure text is written. @param product Command name, such as `
|
|
14
|
+
/** @param current Installed version. @param stderr Where the failure text is written. @param product Command name, such as `agent`. @returns `0` when the version is new enough, otherwise `1`. */
|
|
15
15
|
export declare function ensureSupportedNode(current: string, stderr: NodeVersionWriter, product: string): number;
|
|
16
16
|
/** @param current Installed version. @returns One doctor row, including whether it meets {@link REQUIRED_NODE}. */
|
|
17
17
|
export declare function nodeDoctorLine(current: string): string;
|
package/dist/node-version.js
CHANGED
|
@@ -18,7 +18,7 @@ export function satisfies(current, required) {
|
|
|
18
18
|
}
|
|
19
19
|
return true;
|
|
20
20
|
}
|
|
21
|
-
/** @param current Installed version. @param stderr Where the failure text is written. @param product Command name, such as `
|
|
21
|
+
/** @param current Installed version. @param stderr Where the failure text is written. @param product Command name, such as `agent`. @returns `0` when the version is new enough, otherwise `1`. */
|
|
22
22
|
export function ensureSupportedNode(current, stderr, product) {
|
|
23
23
|
if (satisfies(current, REQUIRED_NODE))
|
|
24
24
|
return 0;
|
package/dist/run.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { autoAnswer, decisionFor, denied } from './approve.js';
|
|
1
|
+
import { autoAnswer, decisionFor, denied, UNATTENDED_APPROVAL_REASON } from './approve.js';
|
|
2
2
|
import { cliText } from './copy.js';
|
|
3
3
|
import { ruleStatusLine } from './rules.js';
|
|
4
4
|
export async function runTurn(client, request, stdout, stderr, signal) {
|
|
@@ -253,7 +253,7 @@ function watchApprovals(client, spec, request, stdout, stderr, autoAnswered, str
|
|
|
253
253
|
if (!request.json && !request.streamJson && record.toolName === 'network_egress' && kind === 'allow_once') {
|
|
254
254
|
stderr.write(`${egressProgressLine(prompt.arguments)}\n`);
|
|
255
255
|
}
|
|
256
|
-
await client.decideApproval(prompt.requestId, kind);
|
|
256
|
+
await client.decideApproval(prompt.requestId, kind, kind === 'allow_once' ? UNATTENDED_APPROVAL_REASON : undefined);
|
|
257
257
|
}
|
|
258
258
|
else if (event.channel === 'ask-user:request') {
|
|
259
259
|
const prompt = event.payload;
|
|
@@ -261,10 +261,11 @@ function watchApprovals(client, spec, request, stdout, stderr, autoAnswered, str
|
|
|
261
261
|
continue;
|
|
262
262
|
if (spec.answerAsk) {
|
|
263
263
|
const questions = prompt.questions?.length ? prompt.questions : [{ id: 'answer' }];
|
|
264
|
-
const
|
|
264
|
+
const asked = questions.map((question) => question.text?.trim() ?? '').find((text) => text.length > 0) ?? '';
|
|
265
|
+
const record = { kind: 'ask_user', prompt: (asked || prompt.prompt || prompt.intro || '').trim() };
|
|
265
266
|
autoAnswered.push(record);
|
|
266
267
|
writeAnswerBeforeTool(streamOrder, record, stdout, request.streamJson);
|
|
267
|
-
await client.answerAsk(prompt.requestId, { answers: autoAnswer(questions) });
|
|
268
|
+
await client.answerAsk(prompt.requestId, { answers: autoAnswer(questions), unattended: true });
|
|
268
269
|
}
|
|
269
270
|
else {
|
|
270
271
|
await client.answerAsk(prompt.requestId, {});
|
|
@@ -337,6 +338,10 @@ function writeToolProgress(event, stderr, printed) {
|
|
|
337
338
|
if (!settled)
|
|
338
339
|
continue;
|
|
339
340
|
const name = typeof row.tool === 'string' ? row.tool : 'tool';
|
|
341
|
+
// The grant line is written when the approval is allowed. A later snapshot
|
|
342
|
+
// of the same grant must not print a second row.
|
|
343
|
+
if (name === 'network_egress')
|
|
344
|
+
continue;
|
|
340
345
|
const key = typeof row.id === 'string' && row.id ? row.id : `${name}:${toolProgressLine(row)}`;
|
|
341
346
|
if (printed.has(key))
|
|
342
347
|
continue;
|
package/dist/tui/session.d.ts
CHANGED
package/dist/tui/session.js
CHANGED
|
@@ -6,7 +6,7 @@ import path from 'node:path';
|
|
|
6
6
|
import { getKeybindings } from '@earendil-works/pi-tui';
|
|
7
7
|
import { listBusyChatIds } from '@steerable/agent-client';
|
|
8
8
|
import { saveAttachmentFiles } from '@steerable/agent-shell/attachments';
|
|
9
|
-
import { autoAnswer, decisionFor } from '../approve.js';
|
|
9
|
+
import { autoAnswer, decisionFor, UNATTENDED_APPROVAL_REASON } from '../approve.js';
|
|
10
10
|
import { cliText } from '../copy.js';
|
|
11
11
|
import { applyChildEvent, childLines } from './children.js';
|
|
12
12
|
import { readSystemClipboard, saveClipboardImage, writeSystemClipboard } from './clipboard.js';
|
|
@@ -21,7 +21,7 @@ import { searchHits, searchLabel } from './search.js';
|
|
|
21
21
|
import { ruleStatusLine } from '../rules.js';
|
|
22
22
|
import { transcriptEdge } from './scroll.js';
|
|
23
23
|
import { todosFromAction } from './todos.js';
|
|
24
|
-
import { formatDuration, historyRows, toolOutput, toolStatus } from './transcript.js';
|
|
24
|
+
import { askRowSummary, formatDuration, historyRows, toolOutput, toolStatus } from './transcript.js';
|
|
25
25
|
import { formatUsage } from './usage.js';
|
|
26
26
|
export class AgentTui {
|
|
27
27
|
client;
|
|
@@ -66,6 +66,7 @@ export class AgentTui {
|
|
|
66
66
|
ensureEgressLine(args) {
|
|
67
67
|
const target = egressTarget(args);
|
|
68
68
|
const existing = [...this.lines].reverse().find((line) => line.kind === 'tool' && line.name === 'network_egress');
|
|
69
|
+
// Egress is an approval, not a tool result. The row is this grant.
|
|
69
70
|
if (existing && !existing.auto) {
|
|
70
71
|
if (!existing.args && target)
|
|
71
72
|
existing.args = target;
|
|
@@ -600,15 +601,7 @@ export class AgentTui {
|
|
|
600
601
|
const records = messages.status === 200
|
|
601
602
|
? (messages.data.messages ?? [])
|
|
602
603
|
: [];
|
|
603
|
-
this.
|
|
604
|
-
this.lines = historyRows(records).map((row) => {
|
|
605
|
-
if (row.kind !== 'tool')
|
|
606
|
-
return { kind: row.kind, text: row.text };
|
|
607
|
-
const next = todosFromAction(row.action.tool, row.action.arguments, row.action.result);
|
|
608
|
-
if (next)
|
|
609
|
-
this.todos = next;
|
|
610
|
-
return toolLine(row.action);
|
|
611
|
-
});
|
|
604
|
+
this.lines = this.transcriptFrom(records);
|
|
612
605
|
this.help = false;
|
|
613
606
|
this.search = null;
|
|
614
607
|
this.panel = null;
|
|
@@ -1052,7 +1045,9 @@ export class AgentTui {
|
|
|
1052
1045
|
const duration = typeof payload.durationMs === 'number' ? formatDuration(payload.durationMs) : '';
|
|
1053
1046
|
const mark = failed ? '✗' : '✓';
|
|
1054
1047
|
const base = duration ? `${mark} ${duration}` : mark;
|
|
1055
|
-
tool.status = tool.auto ? appendAuto(base) : base;
|
|
1048
|
+
tool.status = !failed && tool.auto ? appendAuto(base) : base;
|
|
1049
|
+
if (failed)
|
|
1050
|
+
tool.auto = false;
|
|
1056
1051
|
return;
|
|
1057
1052
|
}
|
|
1058
1053
|
if (String(event.type) === 'reasoning' && typeof event.content === 'string') {
|
|
@@ -1099,14 +1094,17 @@ export class AgentTui {
|
|
|
1099
1094
|
}
|
|
1100
1095
|
if (decision === 'allow_once' && payload.toolName)
|
|
1101
1096
|
this.markAutoTool(payload.toolName);
|
|
1102
|
-
void this.client.decideApproval(payload.requestId, decision);
|
|
1097
|
+
void this.client.decideApproval(payload.requestId, decision, decision === 'allow_once' ? UNATTENDED_APPROVAL_REASON : undefined);
|
|
1103
1098
|
this.touch();
|
|
1104
1099
|
continue;
|
|
1105
1100
|
}
|
|
1101
|
+
const toolName = payload.toolName || 'tool';
|
|
1106
1102
|
this.approval = {
|
|
1107
1103
|
requestId: payload.requestId,
|
|
1108
|
-
toolName
|
|
1109
|
-
summary:
|
|
1104
|
+
toolName,
|
|
1105
|
+
summary: toolName === 'network_egress'
|
|
1106
|
+
? (egressApprovalTarget(payload.arguments) || toolArgs(payload.arguments))
|
|
1107
|
+
: toolArgs(payload.arguments),
|
|
1110
1108
|
};
|
|
1111
1109
|
}
|
|
1112
1110
|
else if (event.channel === 'ask-user:request') {
|
|
@@ -1118,16 +1116,17 @@ export class AgentTui {
|
|
|
1118
1116
|
const questions = payload.questions?.length ? payload.questions : [{ id: 'answer' }];
|
|
1119
1117
|
void this.client.answerAsk(payload.requestId, {
|
|
1120
1118
|
answers: autoAnswer(questions.flatMap((question) => question.id ? [{ id: question.id }] : [])),
|
|
1119
|
+
unattended: true,
|
|
1121
1120
|
});
|
|
1122
1121
|
const asked = questions
|
|
1123
1122
|
.map((question) => question.text?.trim() ?? '')
|
|
1124
1123
|
.filter((text) => text.length > 0);
|
|
1125
1124
|
const answer = cliText('ask.unattended');
|
|
1126
|
-
const args =
|
|
1125
|
+
const args = asked[0] || intro || '';
|
|
1127
1126
|
const output = [...asked.map((text) => `问 ${text}`), `答 ${answer}`].join('\n');
|
|
1128
1127
|
const line = this.latestAskLine();
|
|
1129
1128
|
if (line && !autoAnswerStatus(line.status)) {
|
|
1130
|
-
if (
|
|
1129
|
+
if (args)
|
|
1131
1130
|
line.args = args;
|
|
1132
1131
|
line.status = autoAnswerMark();
|
|
1133
1132
|
line.output = output;
|
|
@@ -1188,20 +1187,29 @@ export class AgentTui {
|
|
|
1188
1187
|
: -1;
|
|
1189
1188
|
if (index < 0 && next.name === 'ask_user')
|
|
1190
1189
|
index = this.unboundAskLine();
|
|
1190
|
+
if (index < 0 && next.name === 'network_egress') {
|
|
1191
|
+
index = this.lines.findIndex((line) => line.kind === 'tool'
|
|
1192
|
+
&& line.name === 'network_egress'
|
|
1193
|
+
&& !line.id
|
|
1194
|
+
&& (line.args ?? '') === (next.args ?? ''));
|
|
1195
|
+
}
|
|
1191
1196
|
const prior = index >= 0 ? this.lines[index] : undefined;
|
|
1192
1197
|
const keepAnswer = autoAnswerStatus(prior?.status);
|
|
1198
|
+
const failed = next.status?.startsWith('✗') === true;
|
|
1193
1199
|
const auto = prior?.auto === true || (!keepAnswer && this.takePendingAuto(next.name ?? ''));
|
|
1194
|
-
if (auto) {
|
|
1200
|
+
if (auto && !failed) {
|
|
1195
1201
|
next.auto = true;
|
|
1196
1202
|
next.status = appendAuto(next.status);
|
|
1197
1203
|
}
|
|
1198
1204
|
if (index >= 0) {
|
|
1199
1205
|
next.open = prior?.open;
|
|
1200
|
-
if (keepAnswer && prior) {
|
|
1206
|
+
if (keepAnswer && prior && !failed) {
|
|
1201
1207
|
next.status = autoAnswerMark();
|
|
1202
1208
|
if (prior.output)
|
|
1203
1209
|
next.output = prior.output;
|
|
1204
|
-
if (
|
|
1210
|
+
if (prior.args && !askRowSummary(action))
|
|
1211
|
+
next.args = prior.args;
|
|
1212
|
+
else if (!next.args && prior.args)
|
|
1205
1213
|
next.args = prior.args;
|
|
1206
1214
|
}
|
|
1207
1215
|
else if (prior?.output && !next.output) {
|
|
@@ -1293,8 +1301,13 @@ export class AgentTui {
|
|
|
1293
1301
|
if (response.status !== 200)
|
|
1294
1302
|
return;
|
|
1295
1303
|
const records = response.data.messages ?? [];
|
|
1304
|
+
const prior = this.lines;
|
|
1305
|
+
this.lines = retainMissingEgress(prior, keepAutoMarks(prior, this.transcriptFrom(records)));
|
|
1306
|
+
this.touch();
|
|
1307
|
+
}
|
|
1308
|
+
transcriptFrom(records) {
|
|
1296
1309
|
this.todos = [];
|
|
1297
|
-
|
|
1310
|
+
return historyRows(records).map((row) => {
|
|
1298
1311
|
if (row.kind !== 'tool')
|
|
1299
1312
|
return { kind: row.kind, text: row.text };
|
|
1300
1313
|
const next = todosFromAction(row.action.tool, row.action.arguments, row.action.result);
|
|
@@ -1302,7 +1315,6 @@ export class AgentTui {
|
|
|
1302
1315
|
this.todos = next;
|
|
1303
1316
|
return toolLine(row.action);
|
|
1304
1317
|
});
|
|
1305
|
-
this.touch();
|
|
1306
1318
|
}
|
|
1307
1319
|
async showList(method, requestPath, key, format, empty, unavailable) {
|
|
1308
1320
|
const listed = await this.client.request(method, requestPath, undefined);
|
|
@@ -1516,6 +1528,16 @@ function egressTarget(value) {
|
|
|
1516
1528
|
return host;
|
|
1517
1529
|
return `${host}:${port}`;
|
|
1518
1530
|
}
|
|
1531
|
+
/** Approval prompt keeps the port. The settled tool row drops 80 and 443. */
|
|
1532
|
+
function egressApprovalTarget(value) {
|
|
1533
|
+
if (!value || typeof value !== 'object')
|
|
1534
|
+
return '';
|
|
1535
|
+
const record = value;
|
|
1536
|
+
const host = typeof record.host === 'string' ? record.host : '';
|
|
1537
|
+
if (!host)
|
|
1538
|
+
return '';
|
|
1539
|
+
return typeof record.port === 'number' ? `${host}:${record.port}` : host;
|
|
1540
|
+
}
|
|
1519
1541
|
function autoAnswerMark() {
|
|
1520
1542
|
return `✓ ${cliText('tui.toolAutoAnswer')}`;
|
|
1521
1543
|
}
|
|
@@ -1523,6 +1545,93 @@ function autoAnswerStatus(status) {
|
|
|
1523
1545
|
const phrase = cliText('tui.toolAutoAnswer');
|
|
1524
1546
|
return status === phrase || status === autoAnswerMark() || (status?.endsWith(` ${phrase}`) ?? false);
|
|
1525
1547
|
}
|
|
1548
|
+
/**
|
|
1549
|
+
* An egress grant is not a tool result, so reloaded history does not contain
|
|
1550
|
+
* that row. Keep the live row when the saved transcript has no line for it.
|
|
1551
|
+
*/
|
|
1552
|
+
function retainMissingEgress(prior, next) {
|
|
1553
|
+
const present = new Set(next
|
|
1554
|
+
.filter((line) => line.kind === 'tool' && line.name === 'network_egress')
|
|
1555
|
+
.map((line) => line.args ?? ''));
|
|
1556
|
+
const missing = prior.filter((line) => {
|
|
1557
|
+
if (line.kind !== 'tool' || line.name !== 'network_egress')
|
|
1558
|
+
return false;
|
|
1559
|
+
const host = line.args ?? '';
|
|
1560
|
+
if (!host)
|
|
1561
|
+
return present.size === 0;
|
|
1562
|
+
return !present.has(host);
|
|
1563
|
+
});
|
|
1564
|
+
if (missing.length === 0)
|
|
1565
|
+
return next;
|
|
1566
|
+
const out = [...next];
|
|
1567
|
+
for (const line of missing) {
|
|
1568
|
+
const at = prior.indexOf(line);
|
|
1569
|
+
let insertAt = out.length;
|
|
1570
|
+
for (let index = at + 1; index < prior.length; index += 1) {
|
|
1571
|
+
const found = findReloadedAnchor(out, prior[index]);
|
|
1572
|
+
if (found >= 0) {
|
|
1573
|
+
insertAt = found;
|
|
1574
|
+
break;
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
out.splice(insertAt, 0, line);
|
|
1578
|
+
present.add(line.args ?? '');
|
|
1579
|
+
}
|
|
1580
|
+
return out;
|
|
1581
|
+
}
|
|
1582
|
+
function findReloadedAnchor(lines, anchor) {
|
|
1583
|
+
if (!anchor)
|
|
1584
|
+
return -1;
|
|
1585
|
+
if (anchor.kind === 'tool') {
|
|
1586
|
+
if (anchor.id) {
|
|
1587
|
+
const byId = lines.findIndex((line) => line.kind === 'tool' && line.id === anchor.id);
|
|
1588
|
+
if (byId >= 0)
|
|
1589
|
+
return byId;
|
|
1590
|
+
}
|
|
1591
|
+
const matches = lines.flatMap((line, index) => (line.kind === 'tool' && line.name === anchor.name ? [index] : []));
|
|
1592
|
+
return matches.length === 1 ? matches[0] : -1;
|
|
1593
|
+
}
|
|
1594
|
+
if (!anchor.text)
|
|
1595
|
+
return -1;
|
|
1596
|
+
return lines.findIndex((line) => line.kind === anchor.kind && line.text === anchor.text);
|
|
1597
|
+
}
|
|
1598
|
+
function keepAutoMarks(prior, next) {
|
|
1599
|
+
const byId = new Map();
|
|
1600
|
+
const spare = new Map();
|
|
1601
|
+
for (const line of prior) {
|
|
1602
|
+
if (line.kind !== 'tool' || (!line.auto && !autoAnswerStatus(line.status)))
|
|
1603
|
+
continue;
|
|
1604
|
+
if (line.id)
|
|
1605
|
+
byId.set(line.id, line);
|
|
1606
|
+
else {
|
|
1607
|
+
const name = line.name ?? '';
|
|
1608
|
+
const queued = spare.get(name) ?? [];
|
|
1609
|
+
queued.push(line);
|
|
1610
|
+
spare.set(name, queued);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
return next.map((line) => {
|
|
1614
|
+
if (line.kind !== 'tool')
|
|
1615
|
+
return line;
|
|
1616
|
+
const queued = spare.get(line.name ?? '');
|
|
1617
|
+
const match = (line.id ? byId.get(line.id) : undefined) ?? queued?.shift();
|
|
1618
|
+
if (!match)
|
|
1619
|
+
return line;
|
|
1620
|
+
if (match.auto && !line.status?.startsWith('✗')) {
|
|
1621
|
+
line.auto = true;
|
|
1622
|
+
if (!autoAnswerStatus(line.status))
|
|
1623
|
+
line.status = appendAuto(line.status);
|
|
1624
|
+
}
|
|
1625
|
+
if (autoAnswerStatus(match.status)) {
|
|
1626
|
+
line.status = autoAnswerMark();
|
|
1627
|
+
if (!line.output && match.output)
|
|
1628
|
+
line.output = match.output;
|
|
1629
|
+
if (!line.args && match.args)
|
|
1630
|
+
line.args = match.args;
|
|
1631
|
+
}
|
|
1632
|
+
return line;
|
|
1633
|
+
});
|
|
1634
|
+
}
|
|
1526
1635
|
function appendAuto(status) {
|
|
1527
1636
|
const auto = cliText('tui.toolAuto');
|
|
1528
1637
|
const session = cliText('tui.toolSessionAllow');
|
|
@@ -1544,11 +1653,13 @@ function openToolStatus(status) {
|
|
|
1544
1653
|
function toolLine(action) {
|
|
1545
1654
|
const name = typeof action.tool === 'string' && action.tool ? action.tool : 'tool';
|
|
1546
1655
|
const title = typeof action.view?.title === 'string' ? action.view.title : '';
|
|
1656
|
+
const egress = name === 'network_egress' ? egressTarget(action.arguments) : '';
|
|
1657
|
+
const asked = askRowSummary(action);
|
|
1547
1658
|
return {
|
|
1548
1659
|
kind: 'tool',
|
|
1549
1660
|
id: typeof action.id === 'string' ? action.id : undefined,
|
|
1550
1661
|
name,
|
|
1551
|
-
args: title && title !== name ? title : toolArgs(action.arguments),
|
|
1662
|
+
args: egress || asked || (title && title !== name ? title : toolArgs(action.arguments)),
|
|
1552
1663
|
status: toolStatus(action),
|
|
1553
1664
|
output: toolOutput(action),
|
|
1554
1665
|
...(diffOf(action) ? { diff: diffOf(action) } : {}),
|
|
@@ -1622,10 +1733,12 @@ async function editWithEditor(text) {
|
|
|
1622
1733
|
function toolArgs(value) {
|
|
1623
1734
|
if (typeof value === 'string')
|
|
1624
1735
|
return value;
|
|
1625
|
-
if (value && typeof value === 'object'
|
|
1626
|
-
const
|
|
1627
|
-
if (typeof command === 'string')
|
|
1628
|
-
return command;
|
|
1736
|
+
if (value && typeof value === 'object') {
|
|
1737
|
+
const record = value;
|
|
1738
|
+
if (typeof record.command === 'string')
|
|
1739
|
+
return record.command;
|
|
1740
|
+
if (typeof record.url === 'string')
|
|
1741
|
+
return record.url;
|
|
1629
1742
|
}
|
|
1630
1743
|
if (value === undefined)
|
|
1631
1744
|
return '';
|
package/dist/tui/smoke-entry.js
CHANGED
|
@@ -24,6 +24,15 @@ const client = {
|
|
|
24
24
|
return { status: 200, data: { model: 'demo-model' } };
|
|
25
25
|
if (method === 'POST' && requestPath === '/api/v2/chats/new')
|
|
26
26
|
return { status: 200, data: { chatId: 'chat-1' } };
|
|
27
|
+
if (method === 'POST' && requestPath === '/api/v2/projects/resolve') {
|
|
28
|
+
return {
|
|
29
|
+
status: 200,
|
|
30
|
+
data: {
|
|
31
|
+
project: { id: 'proj-1', name: 'cli', folderPath: process.cwd() },
|
|
32
|
+
created: false,
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
}
|
|
27
36
|
if (method === 'GET' && requestPath.endsWith('/goal'))
|
|
28
37
|
return { status: 200, data: { goal } };
|
|
29
38
|
if (method === 'GET' && requestPath.endsWith('/loops'))
|
package/dist/tui/transcript.d.ts
CHANGED
|
@@ -12,11 +12,14 @@ export interface ToolAction {
|
|
|
12
12
|
approval?: {
|
|
13
13
|
kind?: string;
|
|
14
14
|
cached?: boolean;
|
|
15
|
+
auto?: boolean;
|
|
15
16
|
};
|
|
16
17
|
}
|
|
17
18
|
export declare function formatDuration(ms: number): string;
|
|
18
19
|
export declare function toolStatus(action: ToolAction): string;
|
|
19
20
|
export declare function toolOutput(action: ToolAction): string;
|
|
21
|
+
/** First question, for the folded tool row. The intro stays out of that column. */
|
|
22
|
+
export declare function askRowSummary(action: ToolAction): string;
|
|
20
23
|
export declare function plainMarkdown(source: string): string;
|
|
21
24
|
export interface StoredMessage {
|
|
22
25
|
role?: string;
|
package/dist/tui/transcript.js
CHANGED
|
@@ -11,20 +11,133 @@ export function toolStatus(action) {
|
|
|
11
11
|
const settled = typeof action.durationMs === 'number' || typeof action.success === 'boolean' || action.error != null;
|
|
12
12
|
if (!settled)
|
|
13
13
|
return '…';
|
|
14
|
-
const
|
|
14
|
+
const failed = action.success === false || action.error != null;
|
|
15
|
+
if (!failed && askUserReplay(action)?.auto)
|
|
16
|
+
return `✓ ${cliText('tui.toolAutoAnswer')}`;
|
|
17
|
+
const mark = failed ? '✗' : '✓';
|
|
15
18
|
const duration = typeof action.durationMs === 'number' ? formatDuration(action.durationMs) : '';
|
|
16
19
|
const base = duration ? `${mark} ${duration}` : mark;
|
|
17
20
|
if (action.approval?.kind === 'allow_for_session' && action.approval.cached === true) {
|
|
18
21
|
return `${base} ${cliText('tui.toolSessionAllow')}`;
|
|
19
22
|
}
|
|
23
|
+
if (!failed && action.approval?.auto === true)
|
|
24
|
+
return `${base} ${cliText('tui.toolAuto')}`;
|
|
20
25
|
return base;
|
|
21
26
|
}
|
|
22
27
|
export function toolOutput(action) {
|
|
23
28
|
const error = textOf(action.error);
|
|
24
29
|
if (error)
|
|
25
30
|
return clip(error);
|
|
31
|
+
// A reopened chat only has the stored result object, which has no string field the generic extractor reads.
|
|
32
|
+
const replay = askUserReplay(action);
|
|
33
|
+
if (replay)
|
|
34
|
+
return clip(replay.lines);
|
|
26
35
|
return clip(textOf(action.result));
|
|
27
36
|
}
|
|
37
|
+
/** First question, for the folded tool row. The intro stays out of that column. */
|
|
38
|
+
export function askRowSummary(action) {
|
|
39
|
+
if (action.tool !== 'ask_user')
|
|
40
|
+
return '';
|
|
41
|
+
return firstQuestionText(action.result) || firstQuestionText(action.arguments);
|
|
42
|
+
}
|
|
43
|
+
function askUserReplay(action) {
|
|
44
|
+
if (action.tool !== 'ask_user')
|
|
45
|
+
return null;
|
|
46
|
+
const record = unwrapAsk(action.result);
|
|
47
|
+
if (!record)
|
|
48
|
+
return null;
|
|
49
|
+
const answers = record.answers;
|
|
50
|
+
const asked = [];
|
|
51
|
+
const rendered = [];
|
|
52
|
+
for (const question of record.questions) {
|
|
53
|
+
if (!question || typeof question !== 'object')
|
|
54
|
+
continue;
|
|
55
|
+
const row = question;
|
|
56
|
+
const text = typeof row.text === 'string' ? row.text.trim() : '';
|
|
57
|
+
if (!text)
|
|
58
|
+
continue;
|
|
59
|
+
asked.push(text);
|
|
60
|
+
const id = typeof row.id === 'string' ? row.id : '';
|
|
61
|
+
rendered.push(id ? answerText(answers[id]) : '');
|
|
62
|
+
}
|
|
63
|
+
if (asked.length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
const phrase = cliText('ask.unattended');
|
|
66
|
+
const filled = rendered.filter((value) => value.length > 0);
|
|
67
|
+
const first = filled[0];
|
|
68
|
+
const shared = first !== undefined && filled.length === asked.length && filled.every((value) => value === first);
|
|
69
|
+
const auto = record.unattended === true || (shared && first === phrase);
|
|
70
|
+
const lines = shared
|
|
71
|
+
? [...asked.map((text) => `问 ${text}`), `答 ${first}`].join('\n')
|
|
72
|
+
: asked.map((text, index) => {
|
|
73
|
+
const answer = rendered[index] ?? '';
|
|
74
|
+
return answer ? `问 ${text}\n答 ${answer}` : `问 ${text}`;
|
|
75
|
+
}).join('\n');
|
|
76
|
+
return { lines, auto };
|
|
77
|
+
}
|
|
78
|
+
function unwrapAsk(result) {
|
|
79
|
+
if (typeof result === 'string') {
|
|
80
|
+
const trimmed = result.trim();
|
|
81
|
+
if (!trimmed.startsWith('{'))
|
|
82
|
+
return null;
|
|
83
|
+
try {
|
|
84
|
+
const parsed = JSON.parse(trimmed);
|
|
85
|
+
return unwrapAsk(parsed);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (!result || typeof result !== 'object' || Array.isArray(result))
|
|
92
|
+
return null;
|
|
93
|
+
const record = result;
|
|
94
|
+
if (Array.isArray(record.questions) && record.answers && typeof record.answers === 'object' && !Array.isArray(record.answers)) {
|
|
95
|
+
return {
|
|
96
|
+
questions: record.questions,
|
|
97
|
+
answers: record.answers,
|
|
98
|
+
...(record.unattended === true ? { unattended: true } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (record.value && typeof record.value === 'object') {
|
|
102
|
+
const inner = unwrapAsk(record.value);
|
|
103
|
+
if (inner)
|
|
104
|
+
return record.unattended === true ? { ...inner, unattended: true } : inner;
|
|
105
|
+
}
|
|
106
|
+
if (record.data && typeof record.data === 'object') {
|
|
107
|
+
const inner = unwrapAsk(record.data);
|
|
108
|
+
if (inner)
|
|
109
|
+
return record.unattended === true ? { ...inner, unattended: true } : inner;
|
|
110
|
+
}
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
function firstQuestionText(value) {
|
|
114
|
+
const unwrapped = unwrapAsk(value);
|
|
115
|
+
const questions = unwrapped?.questions ?? questionList(value);
|
|
116
|
+
for (const question of questions) {
|
|
117
|
+
if (!question || typeof question !== 'object')
|
|
118
|
+
continue;
|
|
119
|
+
const text = question.text;
|
|
120
|
+
if (typeof text === 'string' && text.trim())
|
|
121
|
+
return text.trim();
|
|
122
|
+
}
|
|
123
|
+
return '';
|
|
124
|
+
}
|
|
125
|
+
function questionList(value) {
|
|
126
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
127
|
+
return [];
|
|
128
|
+
const questions = value.questions;
|
|
129
|
+
return Array.isArray(questions) ? questions : [];
|
|
130
|
+
}
|
|
131
|
+
function answerText(value) {
|
|
132
|
+
if (typeof value === 'string')
|
|
133
|
+
return value.trim();
|
|
134
|
+
if (!Array.isArray(value))
|
|
135
|
+
return '';
|
|
136
|
+
return value
|
|
137
|
+
.filter((item) => typeof item === 'string' && item.trim().length > 0)
|
|
138
|
+
.map((item) => item.trim())
|
|
139
|
+
.join('、');
|
|
140
|
+
}
|
|
28
141
|
export function plainMarkdown(source) {
|
|
29
142
|
const out = [];
|
|
30
143
|
let fence = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@steerable/agent-cli",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.4",
|
|
4
4
|
"description": "Product-neutral command line for a Steerable host. Products supply the bin name and brand.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://steerableframework.com/",
|
|
@@ -42,9 +42,9 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@earendil-works/pi-tui": "0.87.1",
|
|
44
44
|
"@opentui/core": "0.5.12",
|
|
45
|
-
"@steerable/agent-client": "0.7.
|
|
46
|
-
"@steerable/agent-shell": "0.7.
|
|
47
|
-
"@steerable/agent-protocol": "0.7.
|
|
45
|
+
"@steerable/agent-client": "0.7.4",
|
|
46
|
+
"@steerable/agent-shell": "0.7.4",
|
|
47
|
+
"@steerable/agent-protocol": "0.7.4"
|
|
48
48
|
},
|
|
49
49
|
"devDependencies": {
|
|
50
50
|
"@types/node": "^22.10.5",
|