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