@semalt-ai/code 1.8.3 → 1.8.5
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/.claude/settings.local.json +3 -1
- package/CLAUDE.md +4 -1
- package/TECHNICAL_DEBT.md +66 -0
- package/index.js +23 -9
- package/lib/agent.js +407 -129
- package/lib/api.js +105 -39
- package/lib/args.js +22 -0
- package/lib/commands.js +367 -132
- package/lib/config.js +14 -0
- package/lib/constants.js +1 -1
- package/lib/debug.js +106 -0
- package/lib/permissions.js +9 -8
- package/lib/proc.js +96 -0
- package/lib/prompts.js +8 -10
- package/lib/tool_specs.js +14 -7
- package/lib/tools.js +299 -118
- package/lib/ui/chat-history.js +37 -8
- package/lib/ui/create-ui.js +63 -38
- package/lib/ui/diff.js +4 -3
- package/lib/ui/format.js +321 -0
- package/lib/ui/input-field.js +134 -59
- package/lib/ui/layout.js +0 -2
- package/lib/ui/messages.js +44 -0
- package/lib/ui/select.js +114 -0
- package/lib/ui/status-bar.js +135 -28
- package/lib/ui/stream.js +8 -12
- package/lib/ui/terminal.js +12 -4
- package/lib/ui/theme.js +25 -4
- package/lib/ui/utils.js +94 -27
- package/lib/ui/writer.js +391 -45
- package/lib/ui.js +6 -6
- package/package.json +1 -1
- package/lib/ui/legacy.js +0 -130
package/lib/api.js
CHANGED
|
@@ -6,6 +6,65 @@ const { URL } = require('url');
|
|
|
6
6
|
|
|
7
7
|
const { buildToolsSchema, isUIActive } = require('./tools');
|
|
8
8
|
const { TOOL_SPECS } = require('./tool_specs');
|
|
9
|
+
const writer = require('./ui/writer');
|
|
10
|
+
const messages = require('./ui/messages');
|
|
11
|
+
const dbg = require('./debug');
|
|
12
|
+
|
|
13
|
+
// Strict precondition for any payload that includes role:tool messages or
|
|
14
|
+
// assistant.tool_calls: every tool_call_id must reference a non-empty id from
|
|
15
|
+
// a prior assistant tool_calls entry. Catches the upstream "tool result's tool
|
|
16
|
+
// id() not found" 400 before it leaves the client and points at the exact
|
|
17
|
+
// violating message instead of a cryptic provider error.
|
|
18
|
+
function validateToolCallInvariant(msgs) {
|
|
19
|
+
const calledIds = new Set();
|
|
20
|
+
for (let idx = 0; idx < msgs.length; idx++) {
|
|
21
|
+
const m = msgs[idx];
|
|
22
|
+
if (m.role === 'assistant' && Array.isArray(m.tool_calls)) {
|
|
23
|
+
for (let j = 0; j < m.tool_calls.length; j++) {
|
|
24
|
+
const tc = m.tool_calls[j];
|
|
25
|
+
if (!tc || !tc.id) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`Invalid tool_calls invariant: messages[${idx}] role=assistant tool_calls[${j}] has empty id`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
calledIds.add(tc.id);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (let idx = 0; idx < msgs.length; idx++) {
|
|
35
|
+
const m = msgs[idx];
|
|
36
|
+
if (m.role !== 'tool') continue;
|
|
37
|
+
if (!m.tool_call_id) {
|
|
38
|
+
const preview = String(m.content || '').slice(0, 80).replace(/\s+/g, ' ');
|
|
39
|
+
throw new Error(
|
|
40
|
+
`Invalid tool_calls invariant: messages[${idx}] role=tool has empty tool_call_id (content_preview="${preview}")`
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
if (!calledIds.has(m.tool_call_id)) {
|
|
44
|
+
throw new Error(
|
|
45
|
+
`Invalid tool_calls invariant: messages[${idx}] role=tool tool_call_id=${m.tool_call_id} has no matching prior assistant tool_calls`
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function debugDumpMessages(msgs) {
|
|
52
|
+
dbg.logExtended('[messages dump before API request]');
|
|
53
|
+
for (let i = 0; i < msgs.length; i++) {
|
|
54
|
+
const m = msgs[i];
|
|
55
|
+
const callIds = Array.isArray(m.tool_calls)
|
|
56
|
+
? m.tool_calls.map((t) => (t && t.id) || '<EMPTY>').join(',')
|
|
57
|
+
: '';
|
|
58
|
+
const toolCallId = m.tool_call_id !== undefined
|
|
59
|
+
? ` tool_call_id=${m.tool_call_id || '<EMPTY>'}`
|
|
60
|
+
: '';
|
|
61
|
+
const tcs = callIds ? ` tool_calls=[${callIds}]` : '';
|
|
62
|
+
const contentLen = (m.content !== undefined && m.content !== null)
|
|
63
|
+
? ` content_chars=${(m.content + '').length}`
|
|
64
|
+
: '';
|
|
65
|
+
dbg.logExtended(` [${i}] role=${m.role}${toolCallId}${tcs}${contentLen}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
9
68
|
|
|
10
69
|
function createApiClient({ getConfig, saveConfig, ui }) {
|
|
11
70
|
const {
|
|
@@ -17,7 +76,6 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
17
76
|
FG_RED,
|
|
18
77
|
FG_TEAL,
|
|
19
78
|
RST,
|
|
20
|
-
StatusBar,
|
|
21
79
|
StreamRenderer,
|
|
22
80
|
} = ui;
|
|
23
81
|
|
|
@@ -358,6 +416,8 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
358
416
|
const endpoint = apiUrl('/v1/chat/completions');
|
|
359
417
|
|
|
360
418
|
async function doRequest(msgs) {
|
|
419
|
+
if (dbg.isFile()) debugDumpMessages(msgs);
|
|
420
|
+
validateToolCallInvariant(msgs);
|
|
361
421
|
const reqPayload = { ...payload, messages: msgs };
|
|
362
422
|
const reqBody = JSON.stringify(reqPayload);
|
|
363
423
|
const res = await httpRequest(endpoint, {
|
|
@@ -472,10 +532,10 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
472
532
|
const toolCallAcc = [];
|
|
473
533
|
const renderer = new StreamRenderer({ firstLinePrefix: linePrefix, showThink });
|
|
474
534
|
if (!silent) {
|
|
535
|
+
// audit: allowed — non-TUI streaming setup, must interleave with StreamRenderer sync writes.
|
|
475
536
|
process.stdout.write('\n');
|
|
476
537
|
renderer._linesWritten = 1;
|
|
477
538
|
}
|
|
478
|
-
let firstContentToken = true;
|
|
479
539
|
let lineBuffer = '';
|
|
480
540
|
|
|
481
541
|
function escapeXml(s) {
|
|
@@ -515,16 +575,13 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
515
575
|
type: 'function',
|
|
516
576
|
function: { name: t.name, arguments: t.arguments || '{}' },
|
|
517
577
|
}));
|
|
578
|
+
dbg.logExtended(
|
|
579
|
+
`[tool_call finalize] acc_len=${toolCallAcc.length} ` +
|
|
580
|
+
`valid=${validToolCalls.length} nativeTools=${nativeTools} ` +
|
|
581
|
+
`acc=${JSON.stringify(toolCallAcc).slice(0, 400)}`
|
|
582
|
+
);
|
|
518
583
|
if (!nativeTools) appendToolCallsXml();
|
|
519
584
|
if (!silent) renderer.flush();
|
|
520
|
-
const elapsed = (Date.now() - startTime) / 1000;
|
|
521
|
-
const tps = tokenCount / (elapsed || 1);
|
|
522
|
-
if (StatusBar.current) {
|
|
523
|
-
let latency = `${Math.round(tps)} tok/s · ${elapsed.toFixed(1)}s`;
|
|
524
|
-
if (reasoningText) latency += ` · ${estimateTokens(reasoningText)} think`;
|
|
525
|
-
StatusBar.current.liveUpdate({ tokens: `${tokenCount} tok`, latency });
|
|
526
|
-
StatusBar.current.render();
|
|
527
|
-
}
|
|
528
585
|
// Fallback for endpoints that don't honor stream_options.include_usage:
|
|
529
586
|
// estimate prompt/completion tokens locally so the status bar still updates.
|
|
530
587
|
let usage = streamUsage;
|
|
@@ -571,6 +628,10 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
571
628
|
res.setEncoding('utf8');
|
|
572
629
|
|
|
573
630
|
res.on('data', (chunk) => {
|
|
631
|
+
if (dbg.isFile()) {
|
|
632
|
+
const raw = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
633
|
+
dbg.logExtended(`[SSE raw] ${raw.slice(0, 500).replace(/\n/g, '\\n')}`);
|
|
634
|
+
}
|
|
574
635
|
lineBuffer += chunk;
|
|
575
636
|
const lines = lineBuffer.split('\n');
|
|
576
637
|
lineBuffer = lines.pop();
|
|
@@ -579,11 +640,14 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
579
640
|
if (!line.startsWith('data: ')) continue;
|
|
580
641
|
const data = line.slice(6).trim();
|
|
581
642
|
if (data === '[DONE]') {
|
|
643
|
+
dbg.logExtended(`[SSE event] [DONE]`);
|
|
582
644
|
finalize();
|
|
583
645
|
res.destroy();
|
|
584
646
|
return;
|
|
585
647
|
}
|
|
586
648
|
|
|
649
|
+
dbg.logExtended(`[SSE event] ${data.slice(0, 500)}`);
|
|
650
|
+
|
|
587
651
|
try {
|
|
588
652
|
const obj = JSON.parse(data);
|
|
589
653
|
if (obj.usage && (obj.usage.prompt_tokens !== undefined || obj.usage.completion_tokens !== undefined)) {
|
|
@@ -613,6 +677,7 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
613
677
|
if (!inReasoning) {
|
|
614
678
|
inReasoning = true;
|
|
615
679
|
if (showThink && !uiActive) {
|
|
680
|
+
// audit: allowed — non-TUI thinking output, interleaves with StreamRenderer sync writes.
|
|
616
681
|
process.stdout.write(`\n ${FG_DARK}${DIM}⟨thinking⟩${RST}`);
|
|
617
682
|
renderer._linesWritten++;
|
|
618
683
|
}
|
|
@@ -620,27 +685,36 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
620
685
|
reasoningText += reasoning;
|
|
621
686
|
tokenCount++;
|
|
622
687
|
if (showThink && !uiActive) {
|
|
688
|
+
// audit: allowed — non-TUI thinking output, interleaves with StreamRenderer sync writes.
|
|
623
689
|
process.stdout.write(`${FG_DARK}${DIM}${reasoning}${RST}`);
|
|
624
690
|
}
|
|
625
691
|
}
|
|
626
692
|
|
|
693
|
+
// Standard OpenAI tool_call streaming: the announcement chunk
|
|
694
|
+
// carries id + type + function.name with arguments="", and one or
|
|
695
|
+
// more follow-up chunks stream arguments deltas (no id/name).
|
|
696
|
+
// Process every chunk that has delta.tool_calls and patch in
|
|
697
|
+
// whichever fields are present — never gate slot creation or
|
|
698
|
+
// field updates on arguments being non-empty, or the announcement
|
|
699
|
+
// (which carries the only id/name) gets dropped.
|
|
627
700
|
const toolCallsDelta = delta.tool_calls;
|
|
628
701
|
if (Array.isArray(toolCallsDelta)) {
|
|
629
702
|
for (const tc of toolCallsDelta) {
|
|
703
|
+
if (!tc || typeof tc !== 'object') continue;
|
|
630
704
|
const idx = typeof tc.index === 'number' ? tc.index : toolCallAcc.length;
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
if (tc.id) toolCallAcc[idx].id = tc.id;
|
|
634
|
-
if (tc.function?.name) toolCallAcc[idx].name += tc.function.name;
|
|
635
|
-
if (tc.function?.arguments) toolCallAcc[idx].arguments += tc.function.arguments;
|
|
636
|
-
// When the model streams purely via delta.tool_calls (no
|
|
637
|
-
// delta.content), firstContentToken never flips, so the status
|
|
638
|
-
// bar stays on "Thinking…" for the entire tool-call stream.
|
|
639
|
-
// Surface each new tool slot the moment its name is known so
|
|
640
|
-
// the user sees "Using tool: <name>" instead of a frozen UI.
|
|
641
|
-
if (isNew && StatusBar.current && toolCallAcc[idx].name) {
|
|
642
|
-
StatusBar.current.update('tool', `Using tool: ${toolCallAcc[idx].name}`);
|
|
705
|
+
if (!toolCallAcc[idx]) {
|
|
706
|
+
toolCallAcc[idx] = { id: '', name: '', arguments: '' };
|
|
643
707
|
}
|
|
708
|
+
const slot = toolCallAcc[idx];
|
|
709
|
+
if (tc.id) slot.id = tc.id;
|
|
710
|
+
const fnName = tc.function && tc.function.name;
|
|
711
|
+
if (typeof fnName === 'string' && fnName) slot.name = fnName;
|
|
712
|
+
const fnArgs = tc.function && tc.function.arguments;
|
|
713
|
+
if (typeof fnArgs === 'string') slot.arguments += fnArgs;
|
|
714
|
+
dbg.logExtended(
|
|
715
|
+
`[tool_call acc] idx=${idx} id=${slot.id || '<empty>'} ` +
|
|
716
|
+
`name=${slot.name || '<empty>'} args_len=${slot.arguments.length}`
|
|
717
|
+
);
|
|
644
718
|
}
|
|
645
719
|
}
|
|
646
720
|
|
|
@@ -649,30 +723,22 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
649
723
|
if (inReasoning) {
|
|
650
724
|
inReasoning = false;
|
|
651
725
|
if (showThink && !silent) {
|
|
726
|
+
// audit: allowed — non-TUI thinking output, interleaves with StreamRenderer sync writes.
|
|
652
727
|
process.stdout.write(`${FG_DARK}⟨/thinking⟩${RST}\n`);
|
|
653
728
|
renderer._linesWritten++;
|
|
654
729
|
}
|
|
655
730
|
}
|
|
656
731
|
if (onToken) {
|
|
657
|
-
if (firstContentToken) {
|
|
658
|
-
firstContentToken = false;
|
|
659
|
-
if (StatusBar.current) StatusBar.current.update({ status: 'streaming' });
|
|
660
|
-
}
|
|
661
732
|
onToken(content);
|
|
662
733
|
} else {
|
|
663
734
|
renderer.feed(content);
|
|
664
735
|
}
|
|
665
736
|
fullText += content;
|
|
666
737
|
tokenCount++;
|
|
667
|
-
if (tokenCount % 20 === 0 && StatusBar.current) {
|
|
668
|
-
const elapsedSec = (Date.now() - startTime) / 1000 || 0.001;
|
|
669
|
-
StatusBar.current.liveUpdate({
|
|
670
|
-
tokens: `${tokenCount} tok`,
|
|
671
|
-
latency: `${Math.round(tokenCount / elapsedSec)} tok/s`,
|
|
672
|
-
});
|
|
673
|
-
}
|
|
674
738
|
}
|
|
675
|
-
} catch {
|
|
739
|
+
} catch (err) {
|
|
740
|
+
dbg.logExtended(`[SSE parse-error] ${err.message} :: ${data.slice(0, 200)}`);
|
|
741
|
+
}
|
|
676
742
|
}
|
|
677
743
|
});
|
|
678
744
|
|
|
@@ -712,7 +778,7 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
712
778
|
},
|
|
713
779
|
}, body);
|
|
714
780
|
} catch (error) {
|
|
715
|
-
|
|
781
|
+
messages.netError(error.message);
|
|
716
782
|
return '';
|
|
717
783
|
}
|
|
718
784
|
|
|
@@ -724,7 +790,7 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
724
790
|
});
|
|
725
791
|
res.on('end', () => {
|
|
726
792
|
if (res.statusCode !== 200) {
|
|
727
|
-
|
|
793
|
+
messages.netError(`HTTP ${res.statusCode} — ${data}`);
|
|
728
794
|
resolve('');
|
|
729
795
|
return;
|
|
730
796
|
}
|
|
@@ -732,15 +798,15 @@ function createApiClient({ getConfig, saveConfig, ui }) {
|
|
|
732
798
|
try {
|
|
733
799
|
const parsed = JSON.parse(data);
|
|
734
800
|
const content = parsed.choices[0].message.content;
|
|
735
|
-
|
|
801
|
+
writer.scrollback(content);
|
|
736
802
|
resolve(content);
|
|
737
803
|
} catch (error) {
|
|
738
|
-
|
|
804
|
+
messages.netError(`Parse error: ${error.message}`);
|
|
739
805
|
resolve('');
|
|
740
806
|
}
|
|
741
807
|
});
|
|
742
808
|
res.on('error', (error) => {
|
|
743
|
-
|
|
809
|
+
messages.netError(error.message);
|
|
744
810
|
resolve('');
|
|
745
811
|
});
|
|
746
812
|
});
|
package/lib/args.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const debug = require('./debug');
|
|
4
|
+
|
|
3
5
|
function parseArgs(argv) {
|
|
4
6
|
const opts = {};
|
|
5
7
|
const positional = [];
|
|
@@ -62,6 +64,15 @@ function parseArgs(argv) {
|
|
|
62
64
|
case '--debug':
|
|
63
65
|
opts.debug = true;
|
|
64
66
|
break;
|
|
67
|
+
case '--debug-file': {
|
|
68
|
+
const v = argv[++i];
|
|
69
|
+
if (!v || v.startsWith('-')) {
|
|
70
|
+
process.stderr.write(`Error: --debug-file requires a path argument.\n`);
|
|
71
|
+
process.exit(1);
|
|
72
|
+
}
|
|
73
|
+
opts.debugFile = v;
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
65
76
|
case '--system-prompt':
|
|
66
77
|
opts.systemPromptFile = argv[++i];
|
|
67
78
|
break;
|
|
@@ -71,6 +82,17 @@ function parseArgs(argv) {
|
|
|
71
82
|
i++;
|
|
72
83
|
}
|
|
73
84
|
|
|
85
|
+
if (opts.debug && opts.debugFile) {
|
|
86
|
+
process.stderr.write(
|
|
87
|
+
`Error: --debug and --debug-file are mutually exclusive.\n` +
|
|
88
|
+
` Use --debug for inline debug output, or --debug-file <path>\n` +
|
|
89
|
+
` for extended debug traces written to a file.\n`
|
|
90
|
+
);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
debug.init({ debug: opts.debug, debugFile: opts.debugFile });
|
|
95
|
+
|
|
74
96
|
return { opts, positional };
|
|
75
97
|
}
|
|
76
98
|
|