@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 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 `aroli` or `steerable-cli`.
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 `aroli` or `steerable-cli`.
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) {
@@ -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 `aroli`. @returns `0` when the version is new enough, otherwise `1`. */
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;
@@ -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 `aroli`. @returns `0` when the version is new enough, otherwise `1`. */
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 record = { kind: 'ask_user', prompt: prompt.prompt ?? '' };
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;
@@ -126,6 +126,7 @@ export declare class AgentTui implements Component {
126
126
  private refreshGoal;
127
127
  private refreshLoops;
128
128
  private reloadMessages;
129
+ private transcriptFrom;
129
130
  private showList;
130
131
  private showTasks;
131
132
  private exportChat;
@@ -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.todos = [];
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: payload.toolName || 'tool',
1109
- summary: toolArgs(payload.arguments),
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 = intro || asked[0] || '';
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 (!line.args)
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 (!next.args && prior.args)
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
- this.lines = historyRows(records).map((row) => {
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' && 'command' in value) {
1626
- const command = value.command;
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 '';
@@ -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'))
@@ -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;
@@ -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 mark = action.success === false || action.error != null ? '✗' : '✓';
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.2",
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.2",
46
- "@steerable/agent-shell": "0.7.2",
47
- "@steerable/agent-protocol": "0.7.2"
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",