@moda-ai/cli 1.15.1 → 1.16.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/dist/ask-stream.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
// failure: it is returned as an outcome (`errorMessage`) and rendered the same
|
|
14
14
|
// way buffered agent errors render.
|
|
15
15
|
import { resolveApiKey, resolveModaBaseUrl } from './config.js';
|
|
16
|
+
import { stripAnsi } from './markdown-terminal.js';
|
|
16
17
|
/** Signals "fall back to the buffered ask endpoint" — never surfaced raw. */
|
|
17
18
|
export class AskStreamError extends Error {
|
|
18
19
|
constructor(message) {
|
|
@@ -116,8 +117,10 @@ export class AskStreamCollector {
|
|
|
116
117
|
case 'tool_start': {
|
|
117
118
|
const tool = event.tool;
|
|
118
119
|
const id = typeof tool?.id === 'string' ? tool.id : '';
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
// Backend-controlled strings — strip ANSI so a rogue/compromised backend
|
|
121
|
+
// (or prompt-injected LLM output) can't inject terminal control sequences.
|
|
122
|
+
const name = stripAnsi(typeof tool?.tool === 'string' ? tool.tool : 'tool');
|
|
123
|
+
const title = stripAnsi(typeof tool?.title === 'string' ? tool.title : name);
|
|
121
124
|
if (id)
|
|
122
125
|
this.toolNames.set(id, name);
|
|
123
126
|
this.onProgress(`• ${name} — ${title}…`);
|
|
@@ -191,7 +194,9 @@ export class AskStreamCollector {
|
|
|
191
194
|
*/
|
|
192
195
|
const TOOL_CARD_DETAIL_MAX = 100;
|
|
193
196
|
function oneLine(text) {
|
|
194
|
-
|
|
197
|
+
// Strip ANSI first: the source is backend-controlled tool summaries/errors,
|
|
198
|
+
// and ESC bytes survive the whitespace collapse below.
|
|
199
|
+
const collapsed = stripAnsi(text).replace(/\s+/g, ' ').trim();
|
|
195
200
|
return collapsed.length > TOOL_CARD_DETAIL_MAX
|
|
196
201
|
? `${collapsed.slice(0, TOOL_CARD_DETAIL_MAX - 1)}…`
|
|
197
202
|
: collapsed;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export interface MarkdownTerminalOptions {
|
|
2
|
+
/** When false, all structural fixes still apply but zero ANSI codes are emitted. */
|
|
3
|
+
color: boolean;
|
|
4
|
+
}
|
|
5
|
+
/** Minimal ANSI styling helpers shared by the renderer and ask chrome. */
|
|
6
|
+
export interface TerminalStyles {
|
|
7
|
+
bold(text: string): string;
|
|
8
|
+
italic(text: string): string;
|
|
9
|
+
dim(text: string): string;
|
|
10
|
+
code(text: string): string;
|
|
11
|
+
/** Section heading: bold + underline. */
|
|
12
|
+
heading(text: string): string;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The CLI's color convention (mirrors `src/tui.ts` / `src/analyst-tui.ts`):
|
|
16
|
+
* color only for a human at a TTY, honoring `NO_COLOR` (present with ANY value,
|
|
17
|
+
* including the empty string, disables — per the no-color.org spec) and
|
|
18
|
+
* `TERM=dumb`.
|
|
19
|
+
*/
|
|
20
|
+
export declare function shouldColorTerminalOutput(opts?: {
|
|
21
|
+
isTTY?: boolean;
|
|
22
|
+
env?: NodeJS.ProcessEnv;
|
|
23
|
+
}): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* ANSI style constructors. Closers restore the specific attribute (22/23/39)
|
|
26
|
+
* rather than a full reset so nested styles (code inside bold, bold inside a
|
|
27
|
+
* heading) survive. When `color` is false every helper is the identity.
|
|
28
|
+
*/
|
|
29
|
+
export declare function terminalStyles(color: boolean): TerminalStyles;
|
|
30
|
+
/** Remove ANSI/OSC control sequences from untrusted content. */
|
|
31
|
+
export declare function stripAnsi(text: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Render an agent markdown answer for direct terminal display.
|
|
34
|
+
*
|
|
35
|
+
* - Headers lose their `#` sigils; h1/h2 render bold+underlined, h3+ bold,
|
|
36
|
+
* with a blank line before.
|
|
37
|
+
* - `**bold**`, `*italic*`/`_italic_`, and backtick `code` spans lose their
|
|
38
|
+
* sigils and gain ANSI styling (bold / italic / cyan) when `color` is on.
|
|
39
|
+
* - List items are re-indented to consistent 2-space-per-level steps based on
|
|
40
|
+
* their original RELATIVE nesting (odd indents round to the nearest level; a
|
|
41
|
+
* top-level-looking item after deeper items stays top-level). Bullets render
|
|
42
|
+
* `•` at level 0 and `-` deeper; ordered items keep their numbers.
|
|
43
|
+
* - `|` table rows and fenced code blocks pass through untouched (only ANSI
|
|
44
|
+
* control sequences are stripped, since that content is backend-controlled).
|
|
45
|
+
* - Runs of 3+ blank lines collapse to a single blank line.
|
|
46
|
+
* - Prose is never reflowed or wrapped.
|
|
47
|
+
*/
|
|
48
|
+
export declare function renderMarkdownForTerminal(markdown: string, opts: MarkdownTerminalOptions): string;
|
|
49
|
+
//# sourceMappingURL=markdown-terminal.d.ts.map
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// src/markdown-terminal.ts
|
|
2
|
+
//
|
|
3
|
+
// Pure markdown -> terminal renderer for human-mode output (`moda ask`).
|
|
4
|
+
// The cloud agent answers in markdown; printing it raw leaves literal `##`
|
|
5
|
+
// header sigils, `**bold**` asterisks, and the model's erratic list
|
|
6
|
+
// indentation on screen. This module strips the sigils, applies ANSI styling
|
|
7
|
+
// when the terminal supports it, and normalizes list nesting to consistent
|
|
8
|
+
// 2-space steps — WITHOUT reflowing prose (terminals wrap themselves) and
|
|
9
|
+
// WITHOUT touching `|` tables or fenced code blocks.
|
|
10
|
+
//
|
|
11
|
+
// No dependencies: raw ANSI escape constants only (the CLI has no color lib).
|
|
12
|
+
/**
|
|
13
|
+
* The CLI's color convention (mirrors `src/tui.ts` / `src/analyst-tui.ts`):
|
|
14
|
+
* color only for a human at a TTY, honoring `NO_COLOR` (present with ANY value,
|
|
15
|
+
* including the empty string, disables — per the no-color.org spec) and
|
|
16
|
+
* `TERM=dumb`.
|
|
17
|
+
*/
|
|
18
|
+
export function shouldColorTerminalOutput(opts = {}) {
|
|
19
|
+
const env = opts.env ?? process.env;
|
|
20
|
+
const isTTY = opts.isTTY ?? process.stdout.isTTY === true;
|
|
21
|
+
if (!isTTY)
|
|
22
|
+
return false;
|
|
23
|
+
if (env.NO_COLOR !== undefined)
|
|
24
|
+
return false;
|
|
25
|
+
if (env.TERM === 'dumb')
|
|
26
|
+
return false;
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* ANSI style constructors. Closers restore the specific attribute (22/23/39)
|
|
31
|
+
* rather than a full reset so nested styles (code inside bold, bold inside a
|
|
32
|
+
* heading) survive. When `color` is false every helper is the identity.
|
|
33
|
+
*/
|
|
34
|
+
export function terminalStyles(color) {
|
|
35
|
+
const paint = (open, close) => (text) => color ? `\u001b[${open}m${text}\u001b[${close}m` : text;
|
|
36
|
+
return {
|
|
37
|
+
bold: paint('1', '22'),
|
|
38
|
+
italic: paint('3', '23'),
|
|
39
|
+
dim: paint('2', '22'),
|
|
40
|
+
code: paint('36', '39'),
|
|
41
|
+
heading: paint('1;4', '22;24'),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const FENCE_RE = /^\s*(`{3,}|~{3,})/;
|
|
45
|
+
const FENCE_CLOSE_RE = /^\s*(`{3,}|~{3,})\s*$/;
|
|
46
|
+
const TABLE_ROW_RE = /^\s*\|.*\S/;
|
|
47
|
+
// Matches ANSI/OSC control sequences: CSI (ESC [ … final byte), OSC (ESC ] …
|
|
48
|
+
// BEL or ST), and other two-byte escapes. Backend-controlled content (table
|
|
49
|
+
// cells, code-fence bodies, streamed tool names/titles) is passed through
|
|
50
|
+
// verbatim, so any escape sequences an LLM or a compromised backend emits must
|
|
51
|
+
// be stripped before they reach the terminal — otherwise they could clear the
|
|
52
|
+
// screen, spoof UI, or trigger OSC side-effects in some emulators.
|
|
53
|
+
const ANSI_RE = /\u001b(?:\[[0-9;?]*[ -/]*[@-~]|\][^\u0007\u001b]*(?:\u0007|\u001b\\)|[@-Z\\-_])/g;
|
|
54
|
+
/** Remove ANSI/OSC control sequences from untrusted content. */
|
|
55
|
+
export function stripAnsi(text) {
|
|
56
|
+
return text.replace(ANSI_RE, '');
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* A fenced code block's opening marker: the fence character (`` ` `` or `~`)
|
|
60
|
+
* and its length. CommonMark closes a fence only with the same character and at
|
|
61
|
+
* least as many markers, so tracking these prevents a `~~~` line from closing a
|
|
62
|
+
* ` ``` ` block (and a 3-backtick line from closing a 4-backtick opener).
|
|
63
|
+
*/
|
|
64
|
+
function parseFenceOpen(line) {
|
|
65
|
+
const match = FENCE_RE.exec(line);
|
|
66
|
+
if (!match)
|
|
67
|
+
return null;
|
|
68
|
+
return { char: match[1][0], len: match[1].length };
|
|
69
|
+
}
|
|
70
|
+
/** Whether `line` closes an open fence per the CommonMark same-char/length rule. */
|
|
71
|
+
function isFenceClose(line, fence) {
|
|
72
|
+
const match = FENCE_CLOSE_RE.exec(line);
|
|
73
|
+
return match !== null && match[1][0] === fence.char && match[1].length >= fence.len;
|
|
74
|
+
}
|
|
75
|
+
const HEADER_RE = /^(#{1,6})\s+(.*?)\s*#*\s*$/;
|
|
76
|
+
const BULLET_RE = /^(\s*)([-*+])\s+(.*)$/;
|
|
77
|
+
const ORDERED_RE = /^(\s*)(\d+)[.)]\s+(.*)$/;
|
|
78
|
+
/**
|
|
79
|
+
* Render an agent markdown answer for direct terminal display.
|
|
80
|
+
*
|
|
81
|
+
* - Headers lose their `#` sigils; h1/h2 render bold+underlined, h3+ bold,
|
|
82
|
+
* with a blank line before.
|
|
83
|
+
* - `**bold**`, `*italic*`/`_italic_`, and backtick `code` spans lose their
|
|
84
|
+
* sigils and gain ANSI styling (bold / italic / cyan) when `color` is on.
|
|
85
|
+
* - List items are re-indented to consistent 2-space-per-level steps based on
|
|
86
|
+
* their original RELATIVE nesting (odd indents round to the nearest level; a
|
|
87
|
+
* top-level-looking item after deeper items stays top-level). Bullets render
|
|
88
|
+
* `•` at level 0 and `-` deeper; ordered items keep their numbers.
|
|
89
|
+
* - `|` table rows and fenced code blocks pass through untouched (only ANSI
|
|
90
|
+
* control sequences are stripped, since that content is backend-controlled).
|
|
91
|
+
* - Runs of 3+ blank lines collapse to a single blank line.
|
|
92
|
+
* - Prose is never reflowed or wrapped.
|
|
93
|
+
*/
|
|
94
|
+
export function renderMarkdownForTerminal(markdown, opts) {
|
|
95
|
+
const style = terminalStyles(opts.color);
|
|
96
|
+
const out = [];
|
|
97
|
+
// Original indent width backing each active list level; index == level.
|
|
98
|
+
const listStack = [];
|
|
99
|
+
// The open fence's char + length, or null outside a fenced code block.
|
|
100
|
+
let fence = null;
|
|
101
|
+
for (const raw of markdown.replace(/\r\n/g, '\n').split('\n')) {
|
|
102
|
+
if (fence) {
|
|
103
|
+
out.push(stripAnsi(raw));
|
|
104
|
+
if (isFenceClose(raw, fence))
|
|
105
|
+
fence = null;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const opened = parseFenceOpen(raw);
|
|
109
|
+
if (opened) {
|
|
110
|
+
fence = opened;
|
|
111
|
+
listStack.length = 0;
|
|
112
|
+
out.push(stripAnsi(raw));
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (TABLE_ROW_RE.test(raw)) {
|
|
116
|
+
// Tables depend on exact column spacing — pass through monospaced.
|
|
117
|
+
out.push(stripAnsi(raw));
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
const line = raw.replace(/\s+$/, '');
|
|
121
|
+
if (line === '') {
|
|
122
|
+
out.push('');
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
const header = HEADER_RE.exec(line);
|
|
126
|
+
if (header) {
|
|
127
|
+
listStack.length = 0;
|
|
128
|
+
// Strip inline sigils inside the header text (plain), then style the
|
|
129
|
+
// whole line so nested `**bold**` can't fight the heading attributes.
|
|
130
|
+
const text = renderInline(header[2], terminalStyles(false));
|
|
131
|
+
if (out.length > 0 && out[out.length - 1] !== '')
|
|
132
|
+
out.push('');
|
|
133
|
+
out.push(header[1].length <= 2 ? style.heading(text) : style.bold(text));
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const bullet = BULLET_RE.exec(line);
|
|
137
|
+
const ordered = bullet ? null : ORDERED_RE.exec(line);
|
|
138
|
+
if (bullet || ordered) {
|
|
139
|
+
const match = (bullet ?? ordered);
|
|
140
|
+
const level = resolveListLevel(match[1].length, listStack);
|
|
141
|
+
const indent = ' '.repeat(level);
|
|
142
|
+
const text = renderInline(match[3], style);
|
|
143
|
+
if (bullet) {
|
|
144
|
+
out.push(`${indent}${level === 0 ? '•' : '-'} ${text}`);
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
out.push(`${indent}${match[2]}. ${text}`);
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
// Unindented prose ends any open list; indented non-list lines are treated
|
|
152
|
+
// as continuations and keep the current nesting context.
|
|
153
|
+
if (!/^\s{2,}/.test(line))
|
|
154
|
+
listStack.length = 0;
|
|
155
|
+
out.push(renderInline(line, style));
|
|
156
|
+
}
|
|
157
|
+
return collapseBlankRuns(out).join('\n');
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Map an item's original indent width onto a consistent nesting level.
|
|
161
|
+
* `stack[level]` remembers the original indent that opened that level:
|
|
162
|
+
* meaningfully shallower indents pop back out, meaningfully deeper indents
|
|
163
|
+
* open a new level, and anything within +/-1 space sticks to the current
|
|
164
|
+
* level (rounding the model's odd 3/5/7-space indents to the nearest level).
|
|
165
|
+
*/
|
|
166
|
+
function resolveListLevel(indent, stack) {
|
|
167
|
+
while (stack.length > 0 && indent < stack[stack.length - 1] - 1)
|
|
168
|
+
stack.pop();
|
|
169
|
+
if (stack.length === 0 || indent > stack[stack.length - 1] + 1)
|
|
170
|
+
stack.push(indent);
|
|
171
|
+
return stack.length - 1;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Strip/style inline markdown sigils: `code` spans first (protected via
|
|
175
|
+
* placeholders so asterisks/underscores inside them are never re-parsed),
|
|
176
|
+
* then `**bold**`/`__bold__`, then word-boundary `*italic*`/`_italic_`
|
|
177
|
+
* (mid-word underscores as in `silent_empty_result` never match).
|
|
178
|
+
*/
|
|
179
|
+
function renderInline(text, style) {
|
|
180
|
+
const codeSpans = [];
|
|
181
|
+
// Strip any ANSI escapes already in the (backend-controlled) text BEFORE we
|
|
182
|
+
// add our own styling, so this only removes injected sequences — never the
|
|
183
|
+
// codes the style helpers emit below.
|
|
184
|
+
let result = stripAnsi(text).replace(/`([^`]+)`/g, (_, code) => {
|
|
185
|
+
codeSpans.push(code);
|
|
186
|
+
return `\u0000${codeSpans.length - 1}\u0000`;
|
|
187
|
+
});
|
|
188
|
+
result = result.replace(/\*\*([^\s*](?:[^*]*[^\s*])?)\*\*/g, (_, inner) => style.bold(inner));
|
|
189
|
+
result = result.replace(/__([^\s_](?:[^_]*[^\s_])?)__/g, (_, inner) => style.bold(inner));
|
|
190
|
+
result = result.replace(/(?<![\w*])\*([^\s*](?:[^*\n]*[^\s*])?)\*(?![\w*])/g, (_, inner) => style.italic(inner));
|
|
191
|
+
result = result.replace(/(?<![\w_])_([^\s_](?:[^_\n]*[^\s_])?)_(?![\w_])/g, (_, inner) => style.italic(inner));
|
|
192
|
+
return result.replace(/\u0000(\d+)\u0000/g, (_, index) => style.code(codeSpans[Number(index)]));
|
|
193
|
+
}
|
|
194
|
+
/** Collapse runs of 3+ blank lines down to a single blank line. */
|
|
195
|
+
function collapseBlankRuns(lines) {
|
|
196
|
+
const collapsed = [];
|
|
197
|
+
let blanks = 0;
|
|
198
|
+
for (const line of lines) {
|
|
199
|
+
if (line === '') {
|
|
200
|
+
blanks += 1;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (blanks > 0)
|
|
204
|
+
collapsed.push('', ...(blanks === 2 ? [''] : []));
|
|
205
|
+
blanks = 0;
|
|
206
|
+
collapsed.push(line);
|
|
207
|
+
}
|
|
208
|
+
if (blanks > 0)
|
|
209
|
+
collapsed.push('', ...(blanks === 2 ? [''] : []));
|
|
210
|
+
return collapsed;
|
|
211
|
+
}
|
|
212
|
+
//# sourceMappingURL=markdown-terminal.js.map
|
|
@@ -166,6 +166,8 @@ export declare function shouldStreamAsk(context: Pick<CommandContext, 'outputMod
|
|
|
166
166
|
* confidence — the buffered endpoint also returns HTTP 200 in that case).
|
|
167
167
|
*/
|
|
168
168
|
export declare function askResultFromStreamOutcome(outcome: AskStreamOutcome, question: string): ProductionAskResult;
|
|
169
|
-
export declare function renderAskHuman(result: ProductionAskResult
|
|
169
|
+
export declare function renderAskHuman(result: ProductionAskResult, options?: {
|
|
170
|
+
color?: boolean;
|
|
171
|
+
}): string;
|
|
170
172
|
export {};
|
|
171
173
|
//# sourceMappingURL=production-intelligence.d.ts.map
|
|
@@ -5,6 +5,7 @@ import { streamAsk } from './ask-stream.js';
|
|
|
5
5
|
import { resolveModaBaseUrl } from './config.js';
|
|
6
6
|
import { CliAuthError, CliInputError } from './errors.js';
|
|
7
7
|
import { EXIT_DEGRADED, EXIT_OK, safeWriteStderr } from './output.js';
|
|
8
|
+
import { renderMarkdownForTerminal, shouldColorTerminalOutput, stripAnsi, terminalStyles, } from './markdown-terminal.js';
|
|
8
9
|
import { asNumber, asRecord, asString } from './transforms.js';
|
|
9
10
|
import { INTELLIGENCE_SCHEMA_VERSION, } from './intelligence-schema.js';
|
|
10
11
|
const INVESTIGATION_SCHEMA_VERSION = 'production_investigation.v0.1';
|
|
@@ -262,6 +263,9 @@ export function shouldStreamAsk(context, isTTY = process.stdout.isTTY === true)
|
|
|
262
263
|
*/
|
|
263
264
|
async function askModaForContext(context, question, daysBack) {
|
|
264
265
|
if (shouldStreamAsk(context)) {
|
|
266
|
+
// Progress lines go to stderr, so key color off the stderr TTY. Tool cards
|
|
267
|
+
// (`• …`) are dimmed subtly; phase lines (`▸ …`) stay at full strength.
|
|
268
|
+
const progressStyle = terminalStyles(shouldColorTerminalOutput({ isTTY: process.stderr.isTTY === true }));
|
|
265
269
|
try {
|
|
266
270
|
const outcome = await streamAsk({
|
|
267
271
|
question,
|
|
@@ -269,7 +273,7 @@ async function askModaForContext(context, question, daysBack) {
|
|
|
269
273
|
profile: context.profile,
|
|
270
274
|
harness: localHarnessSummary(context.cwd),
|
|
271
275
|
timeoutMs: askTimeoutMs(),
|
|
272
|
-
onProgress: (line) => safeWriteStderr(`${line}\n`),
|
|
276
|
+
onProgress: (line) => safeWriteStderr(`${line.startsWith('•') ? progressStyle.dim(line) : line}\n`),
|
|
273
277
|
});
|
|
274
278
|
return askResultFromStreamOutcome(outcome, question);
|
|
275
279
|
}
|
|
@@ -1023,22 +1027,26 @@ function renderInvestigationHuman(investigation) {
|
|
|
1023
1027
|
}
|
|
1024
1028
|
return lines.join('\n');
|
|
1025
1029
|
}
|
|
1026
|
-
export function renderAskHuman(result) {
|
|
1030
|
+
export function renderAskHuman(result, options = {}) {
|
|
1031
|
+
// Same convention as the rest of the CLI: color only for a human at a TTY,
|
|
1032
|
+
// honoring NO_COLOR and TERM=dumb. All information is identical either way.
|
|
1033
|
+
const color = options.color ?? shouldColorTerminalOutput();
|
|
1034
|
+
const style = terminalStyles(color);
|
|
1027
1035
|
const lines = [];
|
|
1028
|
-
lines.push('Moda ask');
|
|
1029
|
-
lines.push(
|
|
1036
|
+
lines.push(style.heading('Moda ask'));
|
|
1037
|
+
lines.push(`${style.dim('Question:')} ${result.question}`);
|
|
1030
1038
|
if (result.degraded) {
|
|
1031
1039
|
lines.push('');
|
|
1032
1040
|
lines.push('[degraded] Moda Cloud was unavailable — this answer is synthesized from local Data API evidence (lower trust).');
|
|
1033
1041
|
}
|
|
1034
1042
|
lines.push('');
|
|
1035
|
-
lines.push(result.answer);
|
|
1043
|
+
lines.push(renderMarkdownForTerminal(result.answer, { color }));
|
|
1036
1044
|
lines.push('');
|
|
1037
|
-
lines.push(`Confidence: ${result.confidence}`);
|
|
1045
|
+
lines.push(`Confidence: ${style.bold(result.confidence)}`);
|
|
1038
1046
|
lines.push(`Source: ${result.source}${result.degraded ? ' (degraded)' : ''}`);
|
|
1039
1047
|
if (result.evidenceRefs.length > 0) {
|
|
1040
1048
|
lines.push('');
|
|
1041
|
-
lines.push('Evidence');
|
|
1049
|
+
lines.push(style.bold('Evidence'));
|
|
1042
1050
|
// Subtle grounding marker: flag model-inferred evidence so a human reader
|
|
1043
1051
|
// can weight it. Ids/anchors stay hidden in the human projection.
|
|
1044
1052
|
const grounding = new Map(result.intelligence.evidence_refs.map((ref) => [ref.id, ref.grounding]));
|
|
@@ -1047,8 +1055,11 @@ export function renderAskHuman(result) {
|
|
|
1047
1055
|
const marker = grounding.get(ref.id) === 'inferred' ? ' (inferred)' : '';
|
|
1048
1056
|
// Backend hrefs are root-relative dashboard paths; print the absolute
|
|
1049
1057
|
// URL so terminals auto-linkify it and the reader can click through.
|
|
1050
|
-
|
|
1051
|
-
|
|
1058
|
+
// Strip ANSI from all backend-controlled fields before printing.
|
|
1059
|
+
const label = stripAnsi(ref.label);
|
|
1060
|
+
const path = ref.path ? ` (${stripAnsi(ref.path)})` : '';
|
|
1061
|
+
const link = ref.href ? `\n ${style.dim(`${dashboardBase}${stripAnsi(ref.href)}`)}` : '';
|
|
1062
|
+
lines.push(` - ${label}${path}${marker}${link}`);
|
|
1052
1063
|
}
|
|
1053
1064
|
}
|
|
1054
1065
|
const unverifiedClaims = result.intelligence.answer_claims.filter((claim) => claim.status !== 'grounded').length;
|
|
@@ -1058,15 +1069,15 @@ export function renderAskHuman(result) {
|
|
|
1058
1069
|
}
|
|
1059
1070
|
if (result.nextCommands.length > 0) {
|
|
1060
1071
|
lines.push('');
|
|
1061
|
-
lines.push('Next commands');
|
|
1072
|
+
lines.push(style.bold('Next commands'));
|
|
1062
1073
|
for (const command of result.nextCommands.slice(0, 5))
|
|
1063
|
-
lines.push(` ${command}`);
|
|
1074
|
+
lines.push(` ${stripAnsi(command)}`);
|
|
1064
1075
|
}
|
|
1065
1076
|
if (result.warnings.length > 0) {
|
|
1066
1077
|
lines.push('');
|
|
1067
|
-
lines.push('Warnings');
|
|
1078
|
+
lines.push(style.bold('Warnings'));
|
|
1068
1079
|
for (const warning of result.warnings.slice(0, 3))
|
|
1069
|
-
lines.push(` - ${warning}`);
|
|
1080
|
+
lines.push(` - ${stripAnsi(warning)}`);
|
|
1070
1081
|
}
|
|
1071
1082
|
return lines.join('\n');
|
|
1072
1083
|
}
|
package/package.json
CHANGED