@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/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