@agntk/cli 0.2.1 → 0.3.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/LICENSE +22 -0
- package/dist/cli.js +20 -14
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/package.json +13 -13
- package/dist/args.d.ts +0 -40
- package/dist/args.d.ts.map +0 -1
- package/dist/args.js +0 -90
- package/dist/args.js.map +0 -1
- package/dist/cli-v2.d.ts +0 -14
- package/dist/cli-v2.d.ts.map +0 -1
- package/dist/cli-v2.js +0 -848
- package/dist/cli-v2.js.map +0 -1
- package/dist/repl.d.ts +0 -31
- package/dist/repl.d.ts.map +0 -1
- package/dist/repl.js +0 -217
- package/dist/repl.js.map +0 -1
- package/dist/runner.d.ts +0 -38
- package/dist/runner.d.ts.map +0 -1
- package/dist/runner.js +0 -218
- package/dist/runner.js.map +0 -1
package/dist/cli-v2.js
DELETED
|
@@ -1,848 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* @fileoverview V2 CLI entry point — zero-config agent.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* npx agntk-v2 --name "my-agent" "do something"
|
|
7
|
-
* npx agntk-v2 --name "my-agent" --instructions "you are a deploy bot" "roll back staging"
|
|
8
|
-
* npx agntk-v2 --name "my-agent" -i
|
|
9
|
-
* npx agntk-v2 list
|
|
10
|
-
* npx agntk-v2 "my-agent" "what were you working on?"
|
|
11
|
-
* cat error.log | npx agntk-v2 --name "debugger" "explain these errors"
|
|
12
|
-
*/
|
|
13
|
-
// Load .env files before anything else reads process.env
|
|
14
|
-
import 'dotenv/config';
|
|
15
|
-
import { createInterface } from 'node:readline';
|
|
16
|
-
import { readdirSync, existsSync } from 'node:fs';
|
|
17
|
-
import { resolve, join } from 'node:path';
|
|
18
|
-
import { homedir } from 'node:os';
|
|
19
|
-
import { getVersion } from './version.js';
|
|
20
|
-
import { detectApiKey } from './config.js';
|
|
21
|
-
// ============================================================================
|
|
22
|
-
// Constants
|
|
23
|
-
// ============================================================================
|
|
24
|
-
const AGENTS_DIR = resolve(homedir(), '.agntk', 'agents');
|
|
25
|
-
function createColors(enabled) {
|
|
26
|
-
if (!enabled) {
|
|
27
|
-
const identity = (s) => s;
|
|
28
|
-
return { dim: identity, bold: identity, cyan: identity, yellow: identity, green: identity, red: identity, magenta: identity, reset: '' };
|
|
29
|
-
}
|
|
30
|
-
return {
|
|
31
|
-
dim: (s) => `\x1b[2m${s}\x1b[22m`,
|
|
32
|
-
bold: (s) => `\x1b[1m${s}\x1b[22m`,
|
|
33
|
-
cyan: (s) => `\x1b[36m${s}\x1b[39m`,
|
|
34
|
-
yellow: (s) => `\x1b[33m${s}\x1b[39m`,
|
|
35
|
-
green: (s) => `\x1b[32m${s}\x1b[39m`,
|
|
36
|
-
red: (s) => `\x1b[31m${s}\x1b[39m`,
|
|
37
|
-
magenta: (s) => `\x1b[35m${s}\x1b[39m`,
|
|
38
|
-
reset: '\x1b[0m',
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
function parseV2Args(argv) {
|
|
42
|
-
const args = {
|
|
43
|
-
name: null,
|
|
44
|
-
instructions: null,
|
|
45
|
-
prompt: null,
|
|
46
|
-
interactive: false,
|
|
47
|
-
workspace: process.cwd(),
|
|
48
|
-
outputLevel: 'normal',
|
|
49
|
-
maxSteps: 25,
|
|
50
|
-
help: false,
|
|
51
|
-
version: false,
|
|
52
|
-
list: false,
|
|
53
|
-
};
|
|
54
|
-
const positionals = [];
|
|
55
|
-
for (let i = 0; i < argv.length; i++) {
|
|
56
|
-
const arg = argv[i];
|
|
57
|
-
switch (arg) {
|
|
58
|
-
case '--name':
|
|
59
|
-
case '-n':
|
|
60
|
-
args.name = argv[++i] ?? null;
|
|
61
|
-
break;
|
|
62
|
-
case '--instructions':
|
|
63
|
-
args.instructions = argv[++i] ?? null;
|
|
64
|
-
break;
|
|
65
|
-
case '-i':
|
|
66
|
-
case '--interactive':
|
|
67
|
-
args.interactive = true;
|
|
68
|
-
break;
|
|
69
|
-
case '--workspace':
|
|
70
|
-
args.workspace = argv[++i] ?? process.cwd();
|
|
71
|
-
break;
|
|
72
|
-
case '--verbose':
|
|
73
|
-
args.outputLevel = 'verbose';
|
|
74
|
-
break;
|
|
75
|
-
case '-q':
|
|
76
|
-
case '--quiet':
|
|
77
|
-
args.outputLevel = 'quiet';
|
|
78
|
-
break;
|
|
79
|
-
case '--max-steps':
|
|
80
|
-
args.maxSteps = parseInt(argv[++i] ?? '25', 10);
|
|
81
|
-
break;
|
|
82
|
-
case '-h':
|
|
83
|
-
case '--help':
|
|
84
|
-
args.help = true;
|
|
85
|
-
break;
|
|
86
|
-
case '-v':
|
|
87
|
-
case '--version':
|
|
88
|
-
args.version = true;
|
|
89
|
-
break;
|
|
90
|
-
case 'list':
|
|
91
|
-
if (positionals.length === 0) {
|
|
92
|
-
args.list = true;
|
|
93
|
-
}
|
|
94
|
-
else {
|
|
95
|
-
positionals.push(arg);
|
|
96
|
-
}
|
|
97
|
-
break;
|
|
98
|
-
default:
|
|
99
|
-
if (!arg.startsWith('-')) {
|
|
100
|
-
positionals.push(arg);
|
|
101
|
-
}
|
|
102
|
-
break;
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
// Interpret positionals:
|
|
106
|
-
// agntk-v2 "prompt" → name from --name flag, prompt from positional
|
|
107
|
-
// agntk-v2 "agent-name" "prompt" → first is agent name, second is prompt
|
|
108
|
-
if (positionals.length === 1 && !args.name) {
|
|
109
|
-
// Single positional with no --name: it's the prompt
|
|
110
|
-
args.prompt = positionals[0];
|
|
111
|
-
}
|
|
112
|
-
else if (positionals.length === 1 && args.name) {
|
|
113
|
-
// --name provided, single positional is the prompt
|
|
114
|
-
args.prompt = positionals[0];
|
|
115
|
-
}
|
|
116
|
-
else if (positionals.length === 2) {
|
|
117
|
-
// Two positionals: first is name, second is prompt
|
|
118
|
-
if (!args.name) {
|
|
119
|
-
args.name = positionals[0];
|
|
120
|
-
}
|
|
121
|
-
args.prompt = positionals[1];
|
|
122
|
-
}
|
|
123
|
-
else if (positionals.length > 2) {
|
|
124
|
-
// First is name, rest joined as prompt
|
|
125
|
-
if (!args.name) {
|
|
126
|
-
args.name = positionals[0];
|
|
127
|
-
args.prompt = positionals.slice(1).join(' ');
|
|
128
|
-
}
|
|
129
|
-
else {
|
|
130
|
-
args.prompt = positionals.join(' ');
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
return args;
|
|
134
|
-
}
|
|
135
|
-
// ============================================================================
|
|
136
|
-
// Help
|
|
137
|
-
// ============================================================================
|
|
138
|
-
function printHelp() {
|
|
139
|
-
const version = getVersion();
|
|
140
|
-
console.log(`
|
|
141
|
-
agntk v2 (${version}) — zero-config AI agent
|
|
142
|
-
|
|
143
|
-
Usage:
|
|
144
|
-
agntk-v2 --name <name> "prompt"
|
|
145
|
-
agntk-v2 --name <name> -i
|
|
146
|
-
agntk-v2 <name> "prompt"
|
|
147
|
-
agntk-v2 list
|
|
148
|
-
|
|
149
|
-
Options:
|
|
150
|
-
-n, --name <name> Agent name (required for new agents)
|
|
151
|
-
--instructions <text> What the agent does (injected as system prompt)
|
|
152
|
-
-i, --interactive Interactive REPL mode
|
|
153
|
-
--workspace <path> Workspace root (default: cwd)
|
|
154
|
-
--max-steps <n> Max tool-loop steps (default: 25)
|
|
155
|
-
--verbose Show full tool args and output
|
|
156
|
-
-q, --quiet Text output only (for piping)
|
|
157
|
-
-v, --version Show version
|
|
158
|
-
-h, --help Show help
|
|
159
|
-
|
|
160
|
-
Commands:
|
|
161
|
-
list List all known agents
|
|
162
|
-
|
|
163
|
-
Examples:
|
|
164
|
-
agntk-v2 --name "coder" "fix the failing tests"
|
|
165
|
-
agntk-v2 --name "ops" --instructions "you manage k8s deploys" "roll back staging"
|
|
166
|
-
agntk-v2 --name "coder" -i
|
|
167
|
-
agntk-v2 "coder" "what were you working on?"
|
|
168
|
-
agntk-v2 list
|
|
169
|
-
cat error.log | agntk-v2 --name "debugger" "explain"
|
|
170
|
-
`);
|
|
171
|
-
}
|
|
172
|
-
// ============================================================================
|
|
173
|
-
// List Agents
|
|
174
|
-
// ============================================================================
|
|
175
|
-
function listAgents() {
|
|
176
|
-
if (!existsSync(AGENTS_DIR)) {
|
|
177
|
-
console.log('No agents found. Create one with: agntk-v2 --name "my-agent" "do something"');
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
const entries = readdirSync(AGENTS_DIR, { withFileTypes: true });
|
|
181
|
-
const agents = entries.filter((e) => e.isDirectory());
|
|
182
|
-
if (agents.length === 0) {
|
|
183
|
-
console.log('No agents found. Create one with: agntk-v2 --name "my-agent" "do something"');
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
console.log(`\nKnown agents (${agents.length}):\n`);
|
|
187
|
-
for (const agent of agents) {
|
|
188
|
-
const memoryPath = join(AGENTS_DIR, agent.name, 'memory.md');
|
|
189
|
-
const contextPath = join(AGENTS_DIR, agent.name, 'context.md');
|
|
190
|
-
const hasMemory = existsSync(memoryPath);
|
|
191
|
-
const hasContext = existsSync(contextPath);
|
|
192
|
-
const status = hasMemory || hasContext ? '●' : '○';
|
|
193
|
-
console.log(` ${status} ${agent.name}${hasMemory ? ' (has memory)' : ''}`);
|
|
194
|
-
}
|
|
195
|
-
console.log('');
|
|
196
|
-
}
|
|
197
|
-
// ============================================================================
|
|
198
|
-
// Formatting Helpers
|
|
199
|
-
// ============================================================================
|
|
200
|
-
/** Compact summary of tool args — show key names and short values */
|
|
201
|
-
function summarizeArgs(input) {
|
|
202
|
-
if (!input || typeof input !== 'object')
|
|
203
|
-
return '';
|
|
204
|
-
const obj = input;
|
|
205
|
-
const parts = [];
|
|
206
|
-
for (const [key, val] of Object.entries(obj)) {
|
|
207
|
-
if (val === undefined || val === null)
|
|
208
|
-
continue;
|
|
209
|
-
const str = typeof val === 'string' ? val : JSON.stringify(val);
|
|
210
|
-
// Truncate long values
|
|
211
|
-
const display = str.length > 60 ? str.slice(0, 57) + '...' : str;
|
|
212
|
-
parts.push(`${key}=${display}`);
|
|
213
|
-
}
|
|
214
|
-
return parts.join(' ');
|
|
215
|
-
}
|
|
216
|
-
/** Compact summary of tool output */
|
|
217
|
-
function summarizeOutput(output) {
|
|
218
|
-
const raw = typeof output === 'string' ? output : JSON.stringify(output);
|
|
219
|
-
// Try to extract the meaningful part from { success, output } wrappers
|
|
220
|
-
let display = raw;
|
|
221
|
-
try {
|
|
222
|
-
const parsed = JSON.parse(raw);
|
|
223
|
-
if (parsed && typeof parsed.output === 'string') {
|
|
224
|
-
display = parsed.output;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
catch {
|
|
228
|
-
// use raw
|
|
229
|
-
}
|
|
230
|
-
// Count lines
|
|
231
|
-
const lines = display.split('\n');
|
|
232
|
-
if (lines.length > 3) {
|
|
233
|
-
return lines.slice(0, 3).join('\n') + `\n ... (${lines.length} lines total)`;
|
|
234
|
-
}
|
|
235
|
-
if (display.length > 200) {
|
|
236
|
-
return display.slice(0, 197) + '...';
|
|
237
|
-
}
|
|
238
|
-
return display;
|
|
239
|
-
}
|
|
240
|
-
/** Format milliseconds into human-readable duration */
|
|
241
|
-
function formatDuration(ms) {
|
|
242
|
-
if (ms < 1000)
|
|
243
|
-
return `${ms}ms`;
|
|
244
|
-
if (ms < 60000)
|
|
245
|
-
return `${(ms / 1000).toFixed(1)}s`;
|
|
246
|
-
const mins = Math.floor(ms / 60000);
|
|
247
|
-
const secs = Math.floor((ms % 60000) / 1000);
|
|
248
|
-
return `${mins}m${secs}s`;
|
|
249
|
-
}
|
|
250
|
-
async function consumeStream(stream, opts) {
|
|
251
|
-
const { output, status, level, colors } = opts;
|
|
252
|
-
const quiet = level === 'quiet';
|
|
253
|
-
const verbose = level === 'verbose';
|
|
254
|
-
const stats = {
|
|
255
|
-
steps: 0,
|
|
256
|
-
toolCalls: 0,
|
|
257
|
-
startTime: Date.now(),
|
|
258
|
-
inputTokens: 0,
|
|
259
|
-
outputTokens: 0,
|
|
260
|
-
};
|
|
261
|
-
let afterToolResult = false;
|
|
262
|
-
let currentStepStart = Date.now();
|
|
263
|
-
let inReasoning = false;
|
|
264
|
-
let hasTextOutput = false;
|
|
265
|
-
let lastToolOutput = null;
|
|
266
|
-
// Reflection tag filtering state machine.
|
|
267
|
-
// Strips <reflection>...</reflection> blocks from text output (shown dimmed in verbose mode).
|
|
268
|
-
let inReflection = false;
|
|
269
|
-
let reflectionBuffer = ''; // Accumulates text that might be the start of a tag
|
|
270
|
-
const REFLECTION_OPEN = '<reflection>';
|
|
271
|
-
const REFLECTION_CLOSE = '</reflection>';
|
|
272
|
-
for await (const chunk of stream) {
|
|
273
|
-
switch (chunk.type) {
|
|
274
|
-
// ── Step lifecycle ──────────────────────────────────────────────
|
|
275
|
-
case 'start-step': {
|
|
276
|
-
stats.steps++;
|
|
277
|
-
currentStepStart = Date.now();
|
|
278
|
-
if (!quiet) {
|
|
279
|
-
status.write(`\n${colors.dim(`── step ${stats.steps} ──────────────────────────────────────`)}\n`);
|
|
280
|
-
}
|
|
281
|
-
break;
|
|
282
|
-
}
|
|
283
|
-
case 'finish-step': {
|
|
284
|
-
// Flush any leftover reflection buffer (e.g. a partial '<' that never became a tag)
|
|
285
|
-
if (reflectionBuffer && !inReflection) {
|
|
286
|
-
if (reflectionBuffer.trim()) {
|
|
287
|
-
if (afterToolResult && !quiet) {
|
|
288
|
-
output.write('\n');
|
|
289
|
-
afterToolResult = false;
|
|
290
|
-
}
|
|
291
|
-
hasTextOutput = true;
|
|
292
|
-
output.write(reflectionBuffer);
|
|
293
|
-
}
|
|
294
|
-
reflectionBuffer = '';
|
|
295
|
-
}
|
|
296
|
-
// Reset reflection state for the next step
|
|
297
|
-
inReflection = false;
|
|
298
|
-
if (!quiet) {
|
|
299
|
-
const elapsed = Date.now() - currentStepStart;
|
|
300
|
-
const reason = chunk.finishReason ?? 'unknown';
|
|
301
|
-
const usage = chunk.usage;
|
|
302
|
-
const tokensIn = usage?.inputTokens ?? 0;
|
|
303
|
-
const tokensOut = usage?.outputTokens ?? 0;
|
|
304
|
-
stats.inputTokens += tokensIn;
|
|
305
|
-
stats.outputTokens += tokensOut;
|
|
306
|
-
const parts = [
|
|
307
|
-
colors.dim(` ${formatDuration(elapsed)}`),
|
|
308
|
-
colors.dim(`${tokensIn}→${tokensOut} tok`),
|
|
309
|
-
];
|
|
310
|
-
if (reason === 'tool-calls') {
|
|
311
|
-
parts.push(colors.dim('→ tool loop'));
|
|
312
|
-
}
|
|
313
|
-
else if (reason === 'stop') {
|
|
314
|
-
parts.push(colors.green('✓ done'));
|
|
315
|
-
}
|
|
316
|
-
else {
|
|
317
|
-
parts.push(colors.yellow(reason));
|
|
318
|
-
}
|
|
319
|
-
status.write(`${parts.join(colors.dim(' | '))}\n`);
|
|
320
|
-
}
|
|
321
|
-
break;
|
|
322
|
-
}
|
|
323
|
-
// ── Reasoning (thinking) ────────────────────────────────────────
|
|
324
|
-
case 'reasoning-start': {
|
|
325
|
-
if (!quiet) {
|
|
326
|
-
inReasoning = true;
|
|
327
|
-
status.write(colors.dim('\n 💭 '));
|
|
328
|
-
}
|
|
329
|
-
break;
|
|
330
|
-
}
|
|
331
|
-
case 'reasoning-delta': {
|
|
332
|
-
if (!quiet && inReasoning) {
|
|
333
|
-
const text = chunk.text ?? '';
|
|
334
|
-
// Show reasoning as dim italic text, compacted
|
|
335
|
-
const compacted = text.replace(/\n/g, ' ');
|
|
336
|
-
status.write(colors.dim(compacted));
|
|
337
|
-
}
|
|
338
|
-
break;
|
|
339
|
-
}
|
|
340
|
-
case 'reasoning-end': {
|
|
341
|
-
if (!quiet && inReasoning) {
|
|
342
|
-
status.write('\n');
|
|
343
|
-
inReasoning = false;
|
|
344
|
-
}
|
|
345
|
-
break;
|
|
346
|
-
}
|
|
347
|
-
// ── Tool calls ──────────────────────────────────────────────────
|
|
348
|
-
case 'tool-call': {
|
|
349
|
-
stats.toolCalls++;
|
|
350
|
-
if (!quiet) {
|
|
351
|
-
const toolName = chunk.toolName;
|
|
352
|
-
if (verbose) {
|
|
353
|
-
// Full args dump
|
|
354
|
-
const argsStr = chunk.input ? JSON.stringify(chunk.input, null, 2) : '';
|
|
355
|
-
status.write(`\n ${colors.cyan('⚡')} ${colors.bold(toolName)}\n`);
|
|
356
|
-
if (argsStr) {
|
|
357
|
-
// Indent args
|
|
358
|
-
const indented = argsStr.split('\n').map((l) => ` ${l}`).join('\n');
|
|
359
|
-
status.write(`${colors.dim(indented)}\n`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
else {
|
|
363
|
-
// Compact: tool name + key args summary
|
|
364
|
-
const argsSummary = summarizeArgs(chunk.input);
|
|
365
|
-
const display = argsSummary
|
|
366
|
-
? ` ${colors.cyan('⚡')} ${colors.bold(toolName)} ${colors.dim(argsSummary)}`
|
|
367
|
-
: ` ${colors.cyan('⚡')} ${colors.bold(toolName)}`;
|
|
368
|
-
status.write(`${display}\n`);
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
afterToolResult = false;
|
|
372
|
-
break;
|
|
373
|
-
}
|
|
374
|
-
case 'tool-result': {
|
|
375
|
-
// Always track the last tool output — if the model produces no text,
|
|
376
|
-
// we'll show this as the response so the user isn't left with nothing.
|
|
377
|
-
const toolOutputRaw = typeof chunk.output === 'string' ? chunk.output : JSON.stringify(chunk.output);
|
|
378
|
-
try {
|
|
379
|
-
const parsed = JSON.parse(toolOutputRaw);
|
|
380
|
-
lastToolOutput = (parsed && typeof parsed.output === 'string') ? parsed.output : toolOutputRaw;
|
|
381
|
-
}
|
|
382
|
-
catch {
|
|
383
|
-
lastToolOutput = toolOutputRaw;
|
|
384
|
-
}
|
|
385
|
-
if (!quiet) {
|
|
386
|
-
const toolName = chunk.toolName;
|
|
387
|
-
if (verbose) {
|
|
388
|
-
// Full output dump
|
|
389
|
-
let displayOutput = lastToolOutput ?? '';
|
|
390
|
-
const maxLen = 2000;
|
|
391
|
-
if (displayOutput.length > maxLen) {
|
|
392
|
-
displayOutput = displayOutput.slice(0, maxLen) + `\n... (${displayOutput.length} chars total)`;
|
|
393
|
-
}
|
|
394
|
-
const indented = displayOutput.split('\n').map((l) => ` ${l}`).join('\n');
|
|
395
|
-
status.write(` ${colors.green('→')} ${colors.dim(toolName)} ${colors.dim('returned')}\n`);
|
|
396
|
-
status.write(`${colors.dim(indented)}\n`);
|
|
397
|
-
}
|
|
398
|
-
else {
|
|
399
|
-
// Compact: tool name + short summary of output
|
|
400
|
-
const summary = summarizeOutput(chunk.output);
|
|
401
|
-
const firstLine = summary.split('\n')[0];
|
|
402
|
-
const truncated = firstLine.length > 100
|
|
403
|
-
? firstLine.slice(0, 97) + '...'
|
|
404
|
-
: firstLine;
|
|
405
|
-
status.write(` ${colors.green('→')} ${colors.dim(toolName + ': ' + truncated)}\n`);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
afterToolResult = true;
|
|
409
|
-
break;
|
|
410
|
-
}
|
|
411
|
-
case 'tool-error': {
|
|
412
|
-
if (!quiet) {
|
|
413
|
-
const toolName = chunk.toolName;
|
|
414
|
-
const error = chunk.error instanceof Error
|
|
415
|
-
? chunk.error.message
|
|
416
|
-
: String(chunk.error ?? 'unknown error');
|
|
417
|
-
status.write(` ${colors.red('✗')} ${colors.bold(toolName)} ${colors.red(error)}\n`);
|
|
418
|
-
}
|
|
419
|
-
afterToolResult = true;
|
|
420
|
-
break;
|
|
421
|
-
}
|
|
422
|
-
// ── Text output (the actual response) ───────────────────────────
|
|
423
|
-
case 'text-delta': {
|
|
424
|
-
const rawText = chunk.text ?? '';
|
|
425
|
-
if (!rawText)
|
|
426
|
-
break;
|
|
427
|
-
// Reflection filter: strips <reflection>...</reflection> blocks from output.
|
|
428
|
-
// In verbose mode, shows reflection content dimmed on stderr.
|
|
429
|
-
// Uses a buffer to handle tags that arrive across multiple chunks.
|
|
430
|
-
reflectionBuffer += rawText;
|
|
431
|
-
// Process the buffer
|
|
432
|
-
while (reflectionBuffer.length > 0) {
|
|
433
|
-
if (inReflection) {
|
|
434
|
-
// Inside reflection — look for closing tag
|
|
435
|
-
const closeIdx = reflectionBuffer.indexOf(REFLECTION_CLOSE);
|
|
436
|
-
if (closeIdx !== -1) {
|
|
437
|
-
const content = reflectionBuffer.slice(0, closeIdx);
|
|
438
|
-
reflectionBuffer = reflectionBuffer.slice(closeIdx + REFLECTION_CLOSE.length);
|
|
439
|
-
inReflection = false;
|
|
440
|
-
// Show reflection dimmed in verbose mode
|
|
441
|
-
if (verbose) {
|
|
442
|
-
const trimmed = content.trim();
|
|
443
|
-
if (trimmed) {
|
|
444
|
-
status.write(colors.dim(` 💭 ${trimmed}\n`));
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
else {
|
|
449
|
-
// No close tag yet — check if buffer ends with partial </reflection>
|
|
450
|
-
// Keep waiting for more data
|
|
451
|
-
break;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
else {
|
|
455
|
-
// Not in reflection — look for opening tag
|
|
456
|
-
const openIdx = reflectionBuffer.indexOf(REFLECTION_OPEN);
|
|
457
|
-
if (openIdx !== -1) {
|
|
458
|
-
// Emit text before the tag
|
|
459
|
-
const before = reflectionBuffer.slice(0, openIdx);
|
|
460
|
-
if (before) {
|
|
461
|
-
if (afterToolResult && !quiet) {
|
|
462
|
-
output.write('\n');
|
|
463
|
-
afterToolResult = false;
|
|
464
|
-
}
|
|
465
|
-
// Don't count whitespace-only as "text output"
|
|
466
|
-
if (before.trim())
|
|
467
|
-
hasTextOutput = true;
|
|
468
|
-
output.write(before);
|
|
469
|
-
}
|
|
470
|
-
reflectionBuffer = reflectionBuffer.slice(openIdx + REFLECTION_OPEN.length);
|
|
471
|
-
inReflection = true;
|
|
472
|
-
}
|
|
473
|
-
else {
|
|
474
|
-
// Check if buffer might end with a partial '<reflection>' tag
|
|
475
|
-
// Find the last '<' that could be the start of the opening tag
|
|
476
|
-
let partialAt = -1;
|
|
477
|
-
for (let i = Math.max(0, reflectionBuffer.length - REFLECTION_OPEN.length); i < reflectionBuffer.length; i++) {
|
|
478
|
-
if (reflectionBuffer[i] === '<') {
|
|
479
|
-
const partial = reflectionBuffer.slice(i);
|
|
480
|
-
if (REFLECTION_OPEN.startsWith(partial)) {
|
|
481
|
-
partialAt = i;
|
|
482
|
-
break;
|
|
483
|
-
}
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
if (partialAt !== -1) {
|
|
487
|
-
// Emit text before the potential partial tag
|
|
488
|
-
const safe = reflectionBuffer.slice(0, partialAt);
|
|
489
|
-
if (safe) {
|
|
490
|
-
if (afterToolResult && !quiet) {
|
|
491
|
-
output.write('\n');
|
|
492
|
-
afterToolResult = false;
|
|
493
|
-
}
|
|
494
|
-
if (safe.trim())
|
|
495
|
-
hasTextOutput = true;
|
|
496
|
-
output.write(safe);
|
|
497
|
-
}
|
|
498
|
-
reflectionBuffer = reflectionBuffer.slice(partialAt);
|
|
499
|
-
// Wait for more data to complete the tag
|
|
500
|
-
break;
|
|
501
|
-
}
|
|
502
|
-
else {
|
|
503
|
-
// No tag or partial tag — emit everything
|
|
504
|
-
if (afterToolResult && !quiet) {
|
|
505
|
-
output.write('\n');
|
|
506
|
-
afterToolResult = false;
|
|
507
|
-
}
|
|
508
|
-
if (reflectionBuffer.trim())
|
|
509
|
-
hasTextOutput = true;
|
|
510
|
-
output.write(reflectionBuffer);
|
|
511
|
-
reflectionBuffer = '';
|
|
512
|
-
}
|
|
513
|
-
}
|
|
514
|
-
}
|
|
515
|
-
}
|
|
516
|
-
break;
|
|
517
|
-
}
|
|
518
|
-
// ── Stream lifecycle ────────────────────────────────────────────
|
|
519
|
-
case 'finish': {
|
|
520
|
-
if (!quiet) {
|
|
521
|
-
const elapsed = Date.now() - stats.startTime;
|
|
522
|
-
const totalUsage = chunk.totalUsage;
|
|
523
|
-
if (totalUsage) {
|
|
524
|
-
stats.inputTokens = totalUsage.inputTokens ?? stats.inputTokens;
|
|
525
|
-
stats.outputTokens = totalUsage.outputTokens ?? stats.outputTokens;
|
|
526
|
-
}
|
|
527
|
-
status.write(`\n${colors.dim(`── done ── ${stats.steps} step${stats.steps !== 1 ? 's' : ''}, ${stats.toolCalls} tool call${stats.toolCalls !== 1 ? 's' : ''}, ${stats.inputTokens}→${stats.outputTokens} tok, ${formatDuration(elapsed)} ──`)}\n`);
|
|
528
|
-
}
|
|
529
|
-
break;
|
|
530
|
-
}
|
|
531
|
-
case 'error': {
|
|
532
|
-
const error = chunk.error instanceof Error
|
|
533
|
-
? chunk.error.message
|
|
534
|
-
: String(chunk.error ?? 'unknown error');
|
|
535
|
-
status.write(`\n${colors.red('Error:')} ${error}\n`);
|
|
536
|
-
break;
|
|
537
|
-
}
|
|
538
|
-
// Silently skip: start, raw, source, file, text-start, text-end,
|
|
539
|
-
// tool-input-start, tool-input-delta, tool-input-end, tool-approval-request, etc.
|
|
540
|
-
default:
|
|
541
|
-
break;
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
// Detect if the agent was cut off by the step limit
|
|
545
|
-
const hitStepLimit = opts.maxSteps && stats.steps >= opts.maxSteps;
|
|
546
|
-
if (hitStepLimit && !quiet) {
|
|
547
|
-
status.write(`\n${colors.yellow('⚠ Agent stopped: step limit reached')} ${colors.dim(`(${opts.maxSteps} steps). Use --max-steps to increase.`)}\n`);
|
|
548
|
-
}
|
|
549
|
-
// If the model produced no text but tools returned output,
|
|
550
|
-
// show the last tool result so the user isn't left with nothing.
|
|
551
|
-
if (!hasTextOutput && lastToolOutput && stats.toolCalls > 0 && !hitStepLimit) {
|
|
552
|
-
if (!quiet) {
|
|
553
|
-
output.write('\n');
|
|
554
|
-
}
|
|
555
|
-
output.write(lastToolOutput);
|
|
556
|
-
if (!lastToolOutput.endsWith('\n')) {
|
|
557
|
-
output.write('\n');
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
return stats;
|
|
561
|
-
}
|
|
562
|
-
// ============================================================================
|
|
563
|
-
// Read piped stdin
|
|
564
|
-
// ============================================================================
|
|
565
|
-
async function readStdin(timeoutMs = 100) {
|
|
566
|
-
if (process.stdin.isTTY)
|
|
567
|
-
return null;
|
|
568
|
-
return new Promise((resolve) => {
|
|
569
|
-
const chunks = [];
|
|
570
|
-
let resolved = false;
|
|
571
|
-
const finish = () => {
|
|
572
|
-
if (resolved)
|
|
573
|
-
return;
|
|
574
|
-
resolved = true;
|
|
575
|
-
resolve(chunks.length === 0 ? null : Buffer.concat(chunks).toString('utf-8'));
|
|
576
|
-
};
|
|
577
|
-
process.stdin.on('data', (chunk) => chunks.push(chunk));
|
|
578
|
-
process.stdin.on('end', finish);
|
|
579
|
-
process.stdin.on('error', () => finish());
|
|
580
|
-
setTimeout(finish, timeoutMs);
|
|
581
|
-
process.stdin.resume();
|
|
582
|
-
});
|
|
583
|
-
}
|
|
584
|
-
// ============================================================================
|
|
585
|
-
// One-Shot Mode
|
|
586
|
-
// ============================================================================
|
|
587
|
-
async function runOneShot(prompt, args) {
|
|
588
|
-
const { createAgent } = await import('@agntk/core');
|
|
589
|
-
const colors = createColors(process.stderr.isTTY ?? false);
|
|
590
|
-
const agent = createAgent({
|
|
591
|
-
name: args.name,
|
|
592
|
-
instructions: args.instructions ?? undefined,
|
|
593
|
-
workspaceRoot: args.workspace,
|
|
594
|
-
maxSteps: args.maxSteps,
|
|
595
|
-
});
|
|
596
|
-
if (args.outputLevel !== 'quiet') {
|
|
597
|
-
const toolCount = agent.getToolNames().length;
|
|
598
|
-
process.stderr.write(`${colors.dim(`agntk-v2 | ${args.name} | ${toolCount} tools | workspace: ${args.workspace}`)}\n`);
|
|
599
|
-
}
|
|
600
|
-
const result = await agent.stream({ prompt });
|
|
601
|
-
const stats = await consumeStream(result.fullStream, {
|
|
602
|
-
output: process.stdout,
|
|
603
|
-
status: process.stderr,
|
|
604
|
-
level: args.outputLevel,
|
|
605
|
-
colors,
|
|
606
|
-
maxSteps: args.maxSteps,
|
|
607
|
-
});
|
|
608
|
-
const finalText = await result.text;
|
|
609
|
-
if (finalText && !finalText.endsWith('\n')) {
|
|
610
|
-
process.stdout.write('\n');
|
|
611
|
-
}
|
|
612
|
-
// Final usage from result (may be more accurate than stream stats)
|
|
613
|
-
if (args.outputLevel === 'verbose') {
|
|
614
|
-
const usage = await result.usage;
|
|
615
|
-
if (usage) {
|
|
616
|
-
process.stderr.write(colors.dim(`[usage] ${usage.inputTokens ?? 0} input + ${usage.outputTokens ?? 0} output tokens\n`));
|
|
617
|
-
}
|
|
618
|
-
}
|
|
619
|
-
}
|
|
620
|
-
// ============================================================================
|
|
621
|
-
// REPL Mode
|
|
622
|
-
// ============================================================================
|
|
623
|
-
async function runRepl(args) {
|
|
624
|
-
const { createAgent } = await import('@agntk/core');
|
|
625
|
-
const colors = createColors(process.stdout.isTTY ?? false);
|
|
626
|
-
const agent = createAgent({
|
|
627
|
-
name: args.name,
|
|
628
|
-
instructions: args.instructions ?? undefined,
|
|
629
|
-
workspaceRoot: args.workspace,
|
|
630
|
-
maxSteps: args.maxSteps,
|
|
631
|
-
});
|
|
632
|
-
const version = getVersion();
|
|
633
|
-
const output = process.stdout;
|
|
634
|
-
const toolCount = agent.getToolNames().length;
|
|
635
|
-
output.write(`\n${colors.bold(`agntk v2`)} ${colors.dim(`(${version})`)}\n`);
|
|
636
|
-
output.write(`${colors.cyan(args.name)} ${colors.dim(`| ${toolCount} tools | memory: on`)}\n`);
|
|
637
|
-
output.write(`${colors.dim('Type /help for commands, /exit or Ctrl+C to quit.')}\n\n`);
|
|
638
|
-
const rl = createInterface({
|
|
639
|
-
input: process.stdin,
|
|
640
|
-
output: process.stdout,
|
|
641
|
-
prompt: `${colors.cyan(args.name + '>')} `,
|
|
642
|
-
terminal: true,
|
|
643
|
-
});
|
|
644
|
-
const history = [];
|
|
645
|
-
// Queue for lines received while agent is busy.
|
|
646
|
-
// Readline fires 'line' events even while the previous async handler is in-flight.
|
|
647
|
-
// We pause readline during agent calls to prevent concurrent execution.
|
|
648
|
-
const pendingLines = [];
|
|
649
|
-
let busy = false;
|
|
650
|
-
let closed = false;
|
|
651
|
-
async function processLine(trimmed) {
|
|
652
|
-
// REPL commands — synchronous, no agent call
|
|
653
|
-
if (trimmed === '/exit' || trimmed === '/quit') {
|
|
654
|
-
rl.close();
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
if (trimmed === '/help') {
|
|
658
|
-
output.write(`\n ${colors.bold('/help')} Show commands\n ${colors.bold('/tools')} List available tools\n ${colors.bold('/verbose')} Toggle verbose output\n ${colors.bold('/exit')} Quit\n\n`);
|
|
659
|
-
rl.prompt();
|
|
660
|
-
return;
|
|
661
|
-
}
|
|
662
|
-
if (trimmed === '/tools') {
|
|
663
|
-
const tools = agent.getToolNames();
|
|
664
|
-
output.write(`\n${colors.bold(`Tools (${tools.length})`)}:\n ${tools.join(', ')}\n\n`);
|
|
665
|
-
rl.prompt();
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
if (trimmed === '/verbose') {
|
|
669
|
-
if (args.outputLevel === 'verbose') {
|
|
670
|
-
args.outputLevel = 'normal';
|
|
671
|
-
output.write(`${colors.dim('Verbose output: off')}\n`);
|
|
672
|
-
}
|
|
673
|
-
else {
|
|
674
|
-
args.outputLevel = 'verbose';
|
|
675
|
-
output.write(`${colors.dim('Verbose output: on')}\n`);
|
|
676
|
-
}
|
|
677
|
-
rl.prompt();
|
|
678
|
-
return;
|
|
679
|
-
}
|
|
680
|
-
// Agent call — mark busy and pause readline
|
|
681
|
-
busy = true;
|
|
682
|
-
rl.pause();
|
|
683
|
-
// Build context-aware prompt with conversation history
|
|
684
|
-
history.push({ role: 'user', content: trimmed });
|
|
685
|
-
const maxHistoryPairs = 10;
|
|
686
|
-
const recentHistory = history.length > maxHistoryPairs * 2
|
|
687
|
-
? history.slice(-maxHistoryPairs * 2)
|
|
688
|
-
: history;
|
|
689
|
-
const historyLines = recentHistory.map((h) => h.role === 'user' ? `[User]: ${h.content}` : `[Assistant]: ${h.content}`);
|
|
690
|
-
const contextPrompt = [
|
|
691
|
-
'<conversation_history>',
|
|
692
|
-
...historyLines.slice(0, -1),
|
|
693
|
-
'</conversation_history>',
|
|
694
|
-
'',
|
|
695
|
-
recentHistory[recentHistory.length - 1].content,
|
|
696
|
-
].join('\n');
|
|
697
|
-
try {
|
|
698
|
-
output.write('\n');
|
|
699
|
-
const result = await agent.stream({ prompt: contextPrompt });
|
|
700
|
-
await consumeStream(result.fullStream, {
|
|
701
|
-
output,
|
|
702
|
-
status: process.stderr,
|
|
703
|
-
level: args.outputLevel,
|
|
704
|
-
colors,
|
|
705
|
-
maxSteps: args.maxSteps,
|
|
706
|
-
});
|
|
707
|
-
const responseText = (await result.text) ?? '';
|
|
708
|
-
if (responseText && !responseText.endsWith('\n')) {
|
|
709
|
-
output.write('\n');
|
|
710
|
-
}
|
|
711
|
-
history.push({ role: 'assistant', content: responseText });
|
|
712
|
-
}
|
|
713
|
-
catch (error) {
|
|
714
|
-
const msg = error instanceof Error ? error.message : String(error);
|
|
715
|
-
output.write(`\n${colors.red('Error:')} ${msg}\n`);
|
|
716
|
-
}
|
|
717
|
-
output.write('\n');
|
|
718
|
-
busy = false;
|
|
719
|
-
rl.resume();
|
|
720
|
-
// Drain any lines that queued while we were busy
|
|
721
|
-
while (pendingLines.length > 0) {
|
|
722
|
-
const next = pendingLines.shift();
|
|
723
|
-
if (next) {
|
|
724
|
-
await processLine(next);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
if (!closed) {
|
|
728
|
-
rl.prompt();
|
|
729
|
-
}
|
|
730
|
-
}
|
|
731
|
-
return new Promise((resolvePromise) => {
|
|
732
|
-
rl.prompt();
|
|
733
|
-
rl.on('line', (line) => {
|
|
734
|
-
const trimmed = line.trim();
|
|
735
|
-
if (!trimmed) {
|
|
736
|
-
rl.prompt();
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
if (busy) {
|
|
740
|
-
// Agent is working — queue this line for later
|
|
741
|
-
pendingLines.push(trimmed);
|
|
742
|
-
return;
|
|
743
|
-
}
|
|
744
|
-
// Process immediately
|
|
745
|
-
processLine(trimmed).catch((err) => {
|
|
746
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
747
|
-
output.write(`\n${colors.red('Error:')} ${msg}\n`);
|
|
748
|
-
rl.prompt();
|
|
749
|
-
});
|
|
750
|
-
});
|
|
751
|
-
rl.on('close', () => {
|
|
752
|
-
closed = true;
|
|
753
|
-
// If the agent is still working (e.g. stdin pipe ended), wait for it to finish.
|
|
754
|
-
const finish = () => {
|
|
755
|
-
output.write(`\n${colors.dim('Goodbye!')}\n`);
|
|
756
|
-
resolvePromise();
|
|
757
|
-
};
|
|
758
|
-
if (busy) {
|
|
759
|
-
// Poll until the in-flight agent call and pending line drain completes
|
|
760
|
-
const interval = setInterval(() => {
|
|
761
|
-
if (!busy) {
|
|
762
|
-
clearInterval(interval);
|
|
763
|
-
finish();
|
|
764
|
-
}
|
|
765
|
-
}, 100);
|
|
766
|
-
}
|
|
767
|
-
else {
|
|
768
|
-
finish();
|
|
769
|
-
}
|
|
770
|
-
});
|
|
771
|
-
rl.on('SIGINT', () => {
|
|
772
|
-
output.write('\n');
|
|
773
|
-
rl.close();
|
|
774
|
-
});
|
|
775
|
-
});
|
|
776
|
-
}
|
|
777
|
-
// ============================================================================
|
|
778
|
-
// Main
|
|
779
|
-
// ============================================================================
|
|
780
|
-
async function main() {
|
|
781
|
-
const args = parseV2Args(process.argv.slice(2));
|
|
782
|
-
// Fast paths — no heavy imports
|
|
783
|
-
if (args.version) {
|
|
784
|
-
console.log(`agntk v2 (${getVersion()})`);
|
|
785
|
-
process.exit(0);
|
|
786
|
-
}
|
|
787
|
-
if (args.help) {
|
|
788
|
-
printHelp();
|
|
789
|
-
process.exit(0);
|
|
790
|
-
}
|
|
791
|
-
if (args.list) {
|
|
792
|
-
listAgents();
|
|
793
|
-
process.exit(0);
|
|
794
|
-
}
|
|
795
|
-
// Validate: need a name
|
|
796
|
-
if (!args.name) {
|
|
797
|
-
// If no name and no prompt, show help
|
|
798
|
-
if (!args.prompt && process.stdin.isTTY) {
|
|
799
|
-
console.error('Error: No agent name provided.\n' +
|
|
800
|
-
'Usage: agntk-v2 --name "my-agent" "your prompt"\n' +
|
|
801
|
-
' agntk-v2 --name "my-agent" -i\n' +
|
|
802
|
-
' agntk-v2 list\n' +
|
|
803
|
-
' agntk-v2 -h');
|
|
804
|
-
process.exit(1);
|
|
805
|
-
}
|
|
806
|
-
// Default name if only prompt was given
|
|
807
|
-
args.name = 'default';
|
|
808
|
-
}
|
|
809
|
-
// Check API key
|
|
810
|
-
const apiKeyResult = detectApiKey();
|
|
811
|
-
if (!apiKeyResult) {
|
|
812
|
-
console.error('Error: No API key found.\n\n' +
|
|
813
|
-
' 1. Get a key at https://openrouter.ai/keys\n' +
|
|
814
|
-
' 2. Add to your shell profile:\n\n' +
|
|
815
|
-
' export OPENROUTER_API_KEY=sk-or-...\n\n' +
|
|
816
|
-
' Then restart your terminal.');
|
|
817
|
-
process.exit(1);
|
|
818
|
-
}
|
|
819
|
-
// Interactive mode
|
|
820
|
-
if (args.interactive) {
|
|
821
|
-
await runRepl(args);
|
|
822
|
-
process.exit(0);
|
|
823
|
-
}
|
|
824
|
-
// Build final prompt (handle piped stdin)
|
|
825
|
-
let prompt = args.prompt;
|
|
826
|
-
const pipedInput = await readStdin();
|
|
827
|
-
if (pipedInput) {
|
|
828
|
-
prompt = prompt ? `${pipedInput}\n\n${prompt}` : pipedInput;
|
|
829
|
-
}
|
|
830
|
-
if (!prompt) {
|
|
831
|
-
console.error('Error: No prompt provided.\n' +
|
|
832
|
-
'Usage: agntk-v2 --name "my-agent" "your prompt"\n' +
|
|
833
|
-
' agntk-v2 --name "my-agent" -i\n' +
|
|
834
|
-
' agntk-v2 -h');
|
|
835
|
-
process.exit(1);
|
|
836
|
-
}
|
|
837
|
-
// One-shot mode
|
|
838
|
-
await runOneShot(prompt, args);
|
|
839
|
-
process.exit(0);
|
|
840
|
-
}
|
|
841
|
-
main().catch((err) => {
|
|
842
|
-
console.error('Fatal error:', err instanceof Error ? err.message : String(err));
|
|
843
|
-
if (process.env.DEBUG) {
|
|
844
|
-
console.error(err instanceof Error ? err.stack : '');
|
|
845
|
-
}
|
|
846
|
-
process.exit(1);
|
|
847
|
-
});
|
|
848
|
-
//# sourceMappingURL=cli-v2.js.map
|