@agntk/cli 0.1.2 → 0.2.1

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,975 @@
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
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"
7
+ * npx agntk whats up
8
+ * npx agntk -n "my-agent" "fix the failing tests"
9
+ * npx agntk -n "my-agent" --instructions "you are a deploy bot" "roll back staging"
10
+ * npx agntk -n "my-agent" -i
11
+ * npx agntk list
12
+ * cat error.log | npx agntk -n "debugger" "explain these errors"
10
13
  */
11
14
  // Load .env files before anything else reads process.env
12
15
  import 'dotenv/config';
16
+ import { createInterface } from 'node:readline';
17
+ import { readdirSync, existsSync, writeFileSync, unlinkSync, readFileSync, statSync } from 'node:fs';
18
+ import { resolve, join } from 'node:path';
19
+ import { homedir } from 'node:os';
13
20
  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)
21
+ import { detectApiKey } from './config.js';
18
22
  // ============================================================================
19
- // Help Text
23
+ // Constants
24
+ // ============================================================================
25
+ const AGENTS_DIR = resolve(homedir(), '.agntk', 'agents');
26
+ function createColors(enabled) {
27
+ if (!enabled) {
28
+ const identity = (s) => s;
29
+ return { dim: identity, bold: identity, cyan: identity, yellow: identity, green: identity, red: identity, magenta: identity, blue: identity, white: identity, reset: '' };
30
+ }
31
+ return {
32
+ dim: (s) => `\x1b[2m${s}\x1b[22m`,
33
+ bold: (s) => `\x1b[1m${s}\x1b[22m`,
34
+ cyan: (s) => `\x1b[36m${s}\x1b[39m`,
35
+ yellow: (s) => `\x1b[33m${s}\x1b[39m`,
36
+ green: (s) => `\x1b[32m${s}\x1b[39m`,
37
+ red: (s) => `\x1b[31m${s}\x1b[39m`,
38
+ magenta: (s) => `\x1b[35m${s}\x1b[39m`,
39
+ blue: (s) => `\x1b[34m${s}\x1b[39m`,
40
+ white: (s) => `\x1b[97m${s}\x1b[39m`,
41
+ reset: '\x1b[0m',
42
+ };
43
+ }
44
+ // ============================================================================
45
+ // Spinner — braille-pattern loading indicator
46
+ // ============================================================================
47
+ const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
48
+ const CLEAR_LINE = '\x1b[2K\r';
49
+ function createSpinner(stream, colors, enabled) {
50
+ let interval = null;
51
+ let frameIdx = 0;
52
+ return {
53
+ start(label) {
54
+ if (!enabled)
55
+ return;
56
+ this.stop();
57
+ frameIdx = 0;
58
+ interval = setInterval(() => {
59
+ const frame = SPINNER_FRAMES[frameIdx % SPINNER_FRAMES.length];
60
+ stream.write(`${CLEAR_LINE} ${colors.cyan(frame)} ${colors.dim(label)}`);
61
+ frameIdx++;
62
+ }, 80);
63
+ },
64
+ stop() {
65
+ if (interval) {
66
+ clearInterval(interval);
67
+ interval = null;
68
+ stream.write(CLEAR_LINE);
69
+ }
70
+ },
71
+ };
72
+ }
73
+ function parseCLIArgs(argv) {
74
+ const args = {
75
+ name: null,
76
+ instructions: null,
77
+ prompt: null,
78
+ interactive: false,
79
+ workspace: process.cwd(),
80
+ outputLevel: 'normal',
81
+ maxSteps: 25,
82
+ help: false,
83
+ version: false,
84
+ list: false,
85
+ };
86
+ const positionals = [];
87
+ for (let i = 0; i < argv.length; i++) {
88
+ const arg = argv[i];
89
+ switch (arg) {
90
+ case '--name':
91
+ case '-n':
92
+ args.name = argv[++i] ?? null;
93
+ break;
94
+ case '--instructions':
95
+ args.instructions = argv[++i] ?? null;
96
+ break;
97
+ case '-i':
98
+ case '--interactive':
99
+ args.interactive = true;
100
+ break;
101
+ case '--workspace':
102
+ args.workspace = argv[++i] ?? process.cwd();
103
+ break;
104
+ case '--verbose':
105
+ args.outputLevel = 'verbose';
106
+ break;
107
+ case '-q':
108
+ case '--quiet':
109
+ args.outputLevel = 'quiet';
110
+ break;
111
+ case '--max-steps':
112
+ args.maxSteps = parseInt(argv[++i] ?? '25', 10);
113
+ break;
114
+ case '-h':
115
+ case '--help':
116
+ args.help = true;
117
+ break;
118
+ case '-v':
119
+ case '--version':
120
+ args.version = true;
121
+ break;
122
+ case 'list':
123
+ if (positionals.length === 0) {
124
+ args.list = true;
125
+ }
126
+ else {
127
+ positionals.push(arg);
128
+ }
129
+ break;
130
+ default:
131
+ if (!arg.startsWith('-')) {
132
+ positionals.push(arg);
133
+ }
134
+ break;
135
+ }
136
+ }
137
+ // Interpret positionals:
138
+ // All positionals join into the prompt. Use --name/-n for agent name.
139
+ // agntk "do something" → prompt = "do something"
140
+ // agntk whats up → prompt = "whats up"
141
+ // agntk -n myagent fix the tests → name = "myagent", prompt = "fix the tests"
142
+ if (positionals.length > 0) {
143
+ args.prompt = positionals.join(' ');
144
+ }
145
+ return args;
146
+ }
147
+ // ============================================================================
148
+ // Help
20
149
  // ============================================================================
21
150
  function printHelp() {
22
151
  const version = getVersion();
23
152
  console.log(`
24
- agntk v${version} — portable AI agent
153
+ agntk (${version})zero-config AI agent
25
154
 
26
155
  Usage:
27
- agntk [options] [prompt]
156
+ agntk "prompt"
157
+ agntk -n <name> "prompt"
158
+ agntk -n <name> -i
159
+ agntk list
28
160
 
29
161
  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
162
+ -n, --name <name> Agent name (enables persistent memory)
163
+ --instructions <text> What the agent does (injected as system prompt)
164
+ -i, --interactive Interactive REPL mode
165
+ --workspace <path> Workspace root (default: cwd)
166
+ --max-steps <n> Max tool-loop steps (default: 25)
167
+ --verbose Show full tool args and output
168
+ -q, --quiet Text output only (for piping)
169
+ -v, --version Show version
170
+ -h, --help Show help
171
+
172
+ Commands:
173
+ list List all known agents
43
174
 
44
175
  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"
176
+ agntk "fix the failing tests"
177
+ agntk whats up
178
+ agntk -n coder "fix the failing tests"
179
+ agntk -n ops --instructions "you manage k8s deploys" "roll back staging"
180
+ agntk -n coder -i
181
+ agntk list
182
+ cat error.log | agntk -n debugger "explain"
183
+
184
+ API Key:
185
+ Save permanently: mkdir -p ~/.agntk && echo "OPENROUTER_API_KEY=sk-or-..." > ~/.agntk/.env
186
+ Or per session: export OPENROUTER_API_KEY=sk-or-...
49
187
  `);
50
188
  }
51
189
  // ============================================================================
190
+ // List Agents
191
+ // ============================================================================
192
+ /** Check if a PID is still alive */
193
+ function isPidAlive(pid) {
194
+ try {
195
+ process.kill(pid, 0);
196
+ return true;
197
+ }
198
+ catch {
199
+ return false;
200
+ }
201
+ }
202
+ /** Format a timestamp as relative time (e.g. "2m ago", "3d ago") */
203
+ function relativeTime(date) {
204
+ const now = Date.now();
205
+ const diffMs = now - date.getTime();
206
+ const seconds = Math.floor(diffMs / 1000);
207
+ if (seconds < 60)
208
+ return 'just now';
209
+ const minutes = Math.floor(seconds / 60);
210
+ if (minutes < 60)
211
+ return `${minutes}m ago`;
212
+ const hours = Math.floor(minutes / 60);
213
+ if (hours < 24)
214
+ return `${hours}h ago`;
215
+ const days = Math.floor(hours / 24);
216
+ if (days < 7)
217
+ return `${days}d ago`;
218
+ const weeks = Math.floor(days / 7);
219
+ if (weeks < 4)
220
+ return `${weeks}w ago`;
221
+ const months = Math.floor(days / 30);
222
+ return `${months}mo ago`;
223
+ }
224
+ /** Get lock info for an agent — returns PID if running, null if idle */
225
+ function getAgentLockInfo(agentDir) {
226
+ const lockPath = join(agentDir, '.lock');
227
+ if (!existsSync(lockPath))
228
+ return null;
229
+ try {
230
+ const content = readFileSync(lockPath, 'utf-8').trim();
231
+ const pid = parseInt(content, 10);
232
+ if (isNaN(pid))
233
+ return null;
234
+ const alive = isPidAlive(pid);
235
+ if (!alive) {
236
+ try {
237
+ unlinkSync(lockPath);
238
+ }
239
+ catch { /* ignore */ }
240
+ return null;
241
+ }
242
+ return { pid, alive };
243
+ }
244
+ catch {
245
+ return null;
246
+ }
247
+ }
248
+ /** Acquire a lockfile for an agent */
249
+ function acquireLock(name) {
250
+ const lockPath = join(AGENTS_DIR, name, '.lock');
251
+ try {
252
+ writeFileSync(lockPath, String(process.pid), 'utf-8');
253
+ }
254
+ catch {
255
+ // Agent dir may not exist yet — that's fine, it gets created by the SDK
256
+ }
257
+ }
258
+ /** Release a lockfile for an agent */
259
+ function releaseLock(name) {
260
+ const lockPath = join(AGENTS_DIR, name, '.lock');
261
+ try {
262
+ unlinkSync(lockPath);
263
+ }
264
+ catch {
265
+ // Already cleaned up
266
+ }
267
+ }
268
+ function listAgents() {
269
+ const colors = createColors(process.stdout.isTTY ?? false);
270
+ if (!existsSync(AGENTS_DIR)) {
271
+ console.log(colors.dim('No agents found. Create one with: agntk --name "my-agent" "do something"'));
272
+ return;
273
+ }
274
+ const entries = readdirSync(AGENTS_DIR, { withFileTypes: true });
275
+ const agents = entries.filter((e) => e.isDirectory());
276
+ if (agents.length === 0) {
277
+ console.log(colors.dim('No agents found. Create one with: agntk --name "my-agent" "do something"'));
278
+ return;
279
+ }
280
+ console.log(`\n${colors.bold(`Agents (${agents.length})`)}\n`);
281
+ // Calculate max name length for alignment
282
+ const maxNameLen = Math.max(...agents.map((a) => a.name.length));
283
+ for (const agent of agents) {
284
+ const agentDir = join(AGENTS_DIR, agent.name);
285
+ const memoryPath = join(agentDir, 'memory.md');
286
+ const contextPath = join(agentDir, 'context.md');
287
+ const hasMemory = existsSync(memoryPath);
288
+ const hasContext = existsSync(contextPath);
289
+ // Running detection
290
+ const lockInfo = getAgentLockInfo(agentDir);
291
+ const isRunning = lockInfo !== null;
292
+ // Last active — most recent mtime of any file in the agent dir
293
+ let lastActive = null;
294
+ try {
295
+ const agentFiles = readdirSync(agentDir);
296
+ for (const f of agentFiles) {
297
+ if (f === '.lock')
298
+ continue;
299
+ try {
300
+ const fStat = statSync(join(agentDir, f));
301
+ if (!lastActive || fStat.mtime > lastActive) {
302
+ lastActive = fStat.mtime;
303
+ }
304
+ }
305
+ catch { /* skip */ }
306
+ }
307
+ }
308
+ catch { /* skip */ }
309
+ // Build output line
310
+ const statusIcon = isRunning
311
+ ? colors.green('●')
312
+ : colors.dim('○');
313
+ const nameStr = isRunning
314
+ ? colors.green(colors.bold(agent.name))
315
+ : agent.name;
316
+ const padding = ' '.repeat(maxNameLen - agent.name.length + 2);
317
+ const parts = [];
318
+ if (isRunning) {
319
+ parts.push(colors.green(`running`) + colors.dim(` (pid ${lockInfo.pid})`));
320
+ }
321
+ else {
322
+ parts.push(colors.dim('idle'));
323
+ }
324
+ if (lastActive) {
325
+ parts.push(colors.dim(relativeTime(lastActive)));
326
+ }
327
+ if (hasMemory) {
328
+ parts.push(colors.cyan('🧠 memory'));
329
+ }
330
+ else if (hasContext) {
331
+ parts.push(colors.dim('has context'));
332
+ }
333
+ console.log(` ${statusIcon} ${nameStr}${padding}${parts.join(colors.dim(' · '))}`);
334
+ }
335
+ console.log('');
336
+ }
337
+ // ============================================================================
338
+ // Formatting Helpers
339
+ // ============================================================================
340
+ /** Compact summary of tool args — show key names and short values */
341
+ function summarizeArgs(input, colors) {
342
+ if (!input || typeof input !== 'object')
343
+ return '';
344
+ const obj = input;
345
+ const parts = [];
346
+ for (const [key, val] of Object.entries(obj)) {
347
+ if (val === undefined || val === null)
348
+ continue;
349
+ const str = typeof val === 'string' ? val : JSON.stringify(val);
350
+ const display = str.length > 60 ? str.slice(0, 57) + '...' : str;
351
+ parts.push(`${colors.dim(key + '=')}${colors.yellow(display)}`);
352
+ }
353
+ return parts.join(' ');
354
+ }
355
+ /** Compact summary of tool output */
356
+ function summarizeOutput(output) {
357
+ const raw = typeof output === 'string' ? output : JSON.stringify(output);
358
+ let display = raw;
359
+ try {
360
+ const parsed = JSON.parse(raw);
361
+ if (parsed && typeof parsed.output === 'string') {
362
+ display = parsed.output;
363
+ }
364
+ }
365
+ catch {
366
+ // use raw
367
+ }
368
+ const lines = display.split('\n');
369
+ if (lines.length > 3) {
370
+ return lines.slice(0, 3).join('\n') + `\n ... (${lines.length} lines total)`;
371
+ }
372
+ if (display.length > 200) {
373
+ return display.slice(0, 197) + '...';
374
+ }
375
+ return display;
376
+ }
377
+ /** Format milliseconds into human-readable duration */
378
+ function formatDuration(ms) {
379
+ if (ms < 1000)
380
+ return `${ms}ms`;
381
+ if (ms < 60000)
382
+ return `${(ms / 1000).toFixed(1)}s`;
383
+ const mins = Math.floor(ms / 60000);
384
+ const secs = Math.floor((ms % 60000) / 1000);
385
+ return `${mins}m${secs}s`;
386
+ }
387
+ async function consumeStream(stream, opts) {
388
+ const { output, status, level, colors } = opts;
389
+ const quiet = level === 'quiet';
390
+ const verbose = level === 'verbose';
391
+ const spinner = createSpinner(status, colors, !quiet && (opts.isTTY ?? false));
392
+ const stats = {
393
+ steps: 0,
394
+ toolCalls: 0,
395
+ startTime: Date.now(),
396
+ inputTokens: 0,
397
+ outputTokens: 0,
398
+ };
399
+ let afterToolResult = false;
400
+ let currentStepStart = Date.now();
401
+ let inReasoning = false;
402
+ let hasTextOutput = false;
403
+ let lastToolOutput = null;
404
+ // Reflection tag filtering state machine.
405
+ let inReflection = false;
406
+ let reflectionBuffer = '';
407
+ const REFLECTION_OPEN = '<reflection>';
408
+ const REFLECTION_CLOSE = '</reflection>';
409
+ for await (const chunk of stream) {
410
+ switch (chunk.type) {
411
+ case 'start-step': {
412
+ stats.steps++;
413
+ currentStepStart = Date.now();
414
+ if (!quiet) {
415
+ status.write(`\n${colors.dim('──')} ${colors.blue(colors.bold(`step ${stats.steps}`))} ${colors.dim('──────────────────────────────────────')}\n`);
416
+ }
417
+ break;
418
+ }
419
+ case 'finish-step': {
420
+ if (reflectionBuffer && !inReflection) {
421
+ if (reflectionBuffer.trim()) {
422
+ if (afterToolResult && !quiet) {
423
+ output.write('\n');
424
+ afterToolResult = false;
425
+ }
426
+ hasTextOutput = true;
427
+ output.write(reflectionBuffer);
428
+ }
429
+ reflectionBuffer = '';
430
+ }
431
+ inReflection = false;
432
+ if (!quiet) {
433
+ const elapsed = Date.now() - currentStepStart;
434
+ const reason = chunk.finishReason ?? 'unknown';
435
+ const usage = chunk.usage;
436
+ const tokensIn = usage?.inputTokens ?? 0;
437
+ const tokensOut = usage?.outputTokens ?? 0;
438
+ stats.inputTokens += tokensIn;
439
+ stats.outputTokens += tokensOut;
440
+ const parts = [
441
+ colors.dim(` ${formatDuration(elapsed)}`),
442
+ `${colors.cyan(String(tokensIn))}${colors.dim('→')}${colors.cyan(String(tokensOut))} ${colors.dim('tok')}`,
443
+ ];
444
+ if (reason === 'tool-calls') {
445
+ parts.push(colors.dim('→ tool loop'));
446
+ }
447
+ else if (reason === 'stop') {
448
+ parts.push(colors.green(colors.bold('done')));
449
+ }
450
+ else {
451
+ parts.push(colors.yellow(reason));
452
+ }
453
+ status.write(`${parts.join(colors.dim(' | '))}\n`);
454
+ }
455
+ break;
456
+ }
457
+ case 'reasoning-start': {
458
+ if (!quiet) {
459
+ inReasoning = true;
460
+ status.write(colors.magenta('\n 💭 '));
461
+ }
462
+ break;
463
+ }
464
+ case 'reasoning-delta': {
465
+ if (!quiet && inReasoning) {
466
+ const text = chunk.text ?? '';
467
+ const compacted = text.replace(/\n/g, ' ');
468
+ status.write(colors.magenta(colors.dim(compacted)));
469
+ }
470
+ break;
471
+ }
472
+ case 'reasoning-end': {
473
+ if (!quiet && inReasoning) {
474
+ status.write('\n');
475
+ inReasoning = false;
476
+ }
477
+ break;
478
+ }
479
+ case 'tool-call': {
480
+ stats.toolCalls++;
481
+ if (!quiet) {
482
+ const toolName = chunk.toolName;
483
+ if (verbose) {
484
+ const argsStr = chunk.input ? JSON.stringify(chunk.input, null, 2) : '';
485
+ status.write(`\n ${colors.cyan('▶')} ${colors.cyan(colors.bold(toolName))}\n`);
486
+ if (argsStr) {
487
+ const indented = argsStr.split('\n').map((l) => ` ${l}`).join('\n');
488
+ status.write(`${colors.dim(indented)}\n`);
489
+ }
490
+ }
491
+ else {
492
+ const argsSummary = summarizeArgs(chunk.input, colors);
493
+ const display = argsSummary
494
+ ? ` ${colors.cyan('▶')} ${colors.cyan(colors.bold(toolName))} ${argsSummary}`
495
+ : ` ${colors.cyan('▶')} ${colors.cyan(colors.bold(toolName))}`;
496
+ status.write(`${display}\n`);
497
+ }
498
+ spinner.start(`running ${toolName}...`);
499
+ }
500
+ afterToolResult = false;
501
+ break;
502
+ }
503
+ case 'tool-result': {
504
+ spinner.stop();
505
+ const toolOutputRaw = typeof chunk.output === 'string' ? chunk.output : JSON.stringify(chunk.output);
506
+ try {
507
+ const parsed = JSON.parse(toolOutputRaw);
508
+ lastToolOutput = (parsed && typeof parsed.output === 'string') ? parsed.output : toolOutputRaw;
509
+ }
510
+ catch {
511
+ lastToolOutput = toolOutputRaw;
512
+ }
513
+ if (!quiet) {
514
+ const toolName = chunk.toolName;
515
+ if (verbose) {
516
+ let displayOutput = lastToolOutput ?? '';
517
+ const maxLen = 2000;
518
+ if (displayOutput.length > maxLen) {
519
+ displayOutput = displayOutput.slice(0, maxLen) + `\n... (${displayOutput.length} chars total)`;
520
+ }
521
+ const indented = displayOutput.split('\n').map((l) => ` ${l}`).join('\n');
522
+ status.write(` ${colors.green('✔')} ${colors.green(colors.dim(toolName))} ${colors.dim('returned')}\n`);
523
+ status.write(`${colors.dim(indented)}\n`);
524
+ }
525
+ else {
526
+ const summary = summarizeOutput(chunk.output);
527
+ const firstLine = summary.split('\n')[0];
528
+ const truncated = firstLine.length > 100
529
+ ? firstLine.slice(0, 97) + '...'
530
+ : firstLine;
531
+ status.write(` ${colors.green('✔')} ${colors.green(colors.dim(toolName + ': '))}${colors.dim(truncated)}\n`);
532
+ }
533
+ }
534
+ afterToolResult = true;
535
+ break;
536
+ }
537
+ case 'tool-error': {
538
+ spinner.stop();
539
+ if (!quiet) {
540
+ const toolName = chunk.toolName;
541
+ const error = chunk.error instanceof Error
542
+ ? chunk.error.message
543
+ : String(chunk.error ?? 'unknown error');
544
+ status.write(` ${colors.red('✖')} ${colors.red(colors.bold(toolName))} ${colors.red(error)}\n`);
545
+ }
546
+ afterToolResult = true;
547
+ break;
548
+ }
549
+ case 'text-delta': {
550
+ const rawText = chunk.text ?? '';
551
+ if (!rawText)
552
+ break;
553
+ reflectionBuffer += rawText;
554
+ while (reflectionBuffer.length > 0) {
555
+ if (inReflection) {
556
+ const closeIdx = reflectionBuffer.indexOf(REFLECTION_CLOSE);
557
+ if (closeIdx !== -1) {
558
+ const content = reflectionBuffer.slice(0, closeIdx);
559
+ reflectionBuffer = reflectionBuffer.slice(closeIdx + REFLECTION_CLOSE.length);
560
+ inReflection = false;
561
+ if (verbose) {
562
+ const trimmed = content.trim();
563
+ if (trimmed) {
564
+ status.write(colors.dim(` ... ${trimmed}\n`));
565
+ }
566
+ }
567
+ }
568
+ else {
569
+ break;
570
+ }
571
+ }
572
+ else {
573
+ const openIdx = reflectionBuffer.indexOf(REFLECTION_OPEN);
574
+ if (openIdx !== -1) {
575
+ const before = reflectionBuffer.slice(0, openIdx);
576
+ if (before) {
577
+ if (afterToolResult && !quiet) {
578
+ output.write('\n');
579
+ afterToolResult = false;
580
+ }
581
+ if (before.trim())
582
+ hasTextOutput = true;
583
+ output.write(before);
584
+ }
585
+ reflectionBuffer = reflectionBuffer.slice(openIdx + REFLECTION_OPEN.length);
586
+ inReflection = true;
587
+ }
588
+ else {
589
+ let partialAt = -1;
590
+ for (let i = Math.max(0, reflectionBuffer.length - REFLECTION_OPEN.length); i < reflectionBuffer.length; i++) {
591
+ if (reflectionBuffer[i] === '<') {
592
+ const partial = reflectionBuffer.slice(i);
593
+ if (REFLECTION_OPEN.startsWith(partial)) {
594
+ partialAt = i;
595
+ break;
596
+ }
597
+ }
598
+ }
599
+ if (partialAt !== -1) {
600
+ const safe = reflectionBuffer.slice(0, partialAt);
601
+ if (safe) {
602
+ if (afterToolResult && !quiet) {
603
+ output.write('\n');
604
+ afterToolResult = false;
605
+ }
606
+ if (safe.trim())
607
+ hasTextOutput = true;
608
+ output.write(safe);
609
+ }
610
+ reflectionBuffer = reflectionBuffer.slice(partialAt);
611
+ break;
612
+ }
613
+ else {
614
+ if (afterToolResult && !quiet) {
615
+ output.write('\n');
616
+ afterToolResult = false;
617
+ }
618
+ if (reflectionBuffer.trim())
619
+ hasTextOutput = true;
620
+ output.write(reflectionBuffer);
621
+ reflectionBuffer = '';
622
+ }
623
+ }
624
+ }
625
+ }
626
+ break;
627
+ }
628
+ case 'finish': {
629
+ spinner.stop();
630
+ if (!quiet) {
631
+ const elapsed = Date.now() - stats.startTime;
632
+ const totalUsage = chunk.totalUsage;
633
+ if (totalUsage) {
634
+ stats.inputTokens = totalUsage.inputTokens ?? stats.inputTokens;
635
+ stats.outputTokens = totalUsage.outputTokens ?? stats.outputTokens;
636
+ }
637
+ const stepLabel = `${stats.steps} step${stats.steps !== 1 ? 's' : ''}`;
638
+ const toolLabel = `${stats.toolCalls} tool call${stats.toolCalls !== 1 ? 's' : ''}`;
639
+ const tokLabel = `${colors.cyan(String(stats.inputTokens))}${colors.dim('→')}${colors.cyan(String(stats.outputTokens))} ${colors.dim('tok')}`;
640
+ const timeLabel = colors.dim(formatDuration(elapsed));
641
+ status.write(`\n${colors.dim('──')} ${colors.green(colors.bold('done'))} ${colors.dim('──')} ${colors.dim(stepLabel)} ${colors.dim('|')} ${colors.dim(toolLabel)} ${colors.dim('|')} ${tokLabel} ${colors.dim('|')} ${timeLabel} ${colors.dim('──')}\n`);
642
+ }
643
+ break;
644
+ }
645
+ case 'error': {
646
+ spinner.stop();
647
+ const error = chunk.error instanceof Error
648
+ ? chunk.error.message
649
+ : String(chunk.error ?? 'unknown error');
650
+ status.write(`\n${colors.red(colors.bold('✖ Error:'))} ${colors.red(error)}\n`);
651
+ break;
652
+ }
653
+ default:
654
+ break;
655
+ }
656
+ }
657
+ const hitStepLimit = opts.maxSteps && stats.steps >= opts.maxSteps;
658
+ if (hitStepLimit && !quiet) {
659
+ status.write(`\n${colors.yellow('Warning: step limit reached')} ${colors.dim(`(${opts.maxSteps} steps). Use --max-steps to increase.`)}\n`);
660
+ }
661
+ if (!hasTextOutput && lastToolOutput && stats.toolCalls > 0 && !hitStepLimit) {
662
+ if (!quiet) {
663
+ output.write('\n');
664
+ }
665
+ output.write(lastToolOutput);
666
+ if (!lastToolOutput.endsWith('\n')) {
667
+ output.write('\n');
668
+ }
669
+ }
670
+ return stats;
671
+ }
672
+ // ============================================================================
673
+ // Read piped stdin
674
+ // ============================================================================
675
+ async function readStdin(timeoutMs = 100) {
676
+ if (process.stdin.isTTY)
677
+ return null;
678
+ return new Promise((resolve) => {
679
+ const chunks = [];
680
+ let resolved = false;
681
+ const finish = () => {
682
+ if (resolved)
683
+ return;
684
+ resolved = true;
685
+ resolve(chunks.length === 0 ? null : Buffer.concat(chunks).toString('utf-8'));
686
+ };
687
+ process.stdin.on('data', (chunk) => chunks.push(chunk));
688
+ process.stdin.on('end', finish);
689
+ process.stdin.on('error', () => finish());
690
+ setTimeout(finish, timeoutMs);
691
+ process.stdin.resume();
692
+ });
693
+ }
694
+ // ============================================================================
695
+ // One-Shot Mode
696
+ // ============================================================================
697
+ async function runOneShot(prompt, args) {
698
+ const { createAgent } = await import('@agntk/core');
699
+ const colors = createColors(process.stderr.isTTY ?? false);
700
+ const agent = createAgent({
701
+ name: args.name,
702
+ instructions: args.instructions ?? undefined,
703
+ workspaceRoot: args.workspace,
704
+ maxSteps: args.maxSteps,
705
+ });
706
+ // Acquire lockfile
707
+ acquireLock(args.name);
708
+ const cleanup = () => releaseLock(args.name);
709
+ process.on('exit', cleanup);
710
+ process.on('SIGINT', () => { cleanup(); process.exit(130); });
711
+ process.on('SIGTERM', () => { cleanup(); process.exit(143); });
712
+ if (args.outputLevel !== 'quiet') {
713
+ const toolCount = agent.getToolNames().length;
714
+ process.stderr.write(`${colors.bold('agntk')} ${colors.dim('|')} ${colors.cyan(args.name)} ${colors.dim('|')} ${colors.dim(`${toolCount} tools`)} ${colors.dim('|')} ${colors.dim(`workspace: ${args.workspace}`)}\n`);
715
+ }
716
+ const result = await agent.stream({ prompt });
717
+ await consumeStream(result.fullStream, {
718
+ output: process.stdout,
719
+ status: process.stderr,
720
+ level: args.outputLevel,
721
+ colors,
722
+ maxSteps: args.maxSteps,
723
+ isTTY: process.stderr.isTTY ?? false,
724
+ });
725
+ const finalText = await result.text;
726
+ if (finalText && !finalText.endsWith('\n')) {
727
+ process.stdout.write('\n');
728
+ }
729
+ if (args.outputLevel === 'verbose') {
730
+ const usage = await result.usage;
731
+ if (usage) {
732
+ process.stderr.write(colors.dim(`[usage] ${usage.inputTokens ?? 0} input + ${usage.outputTokens ?? 0} output tokens\n`));
733
+ }
734
+ }
735
+ }
736
+ // ============================================================================
737
+ // REPL Mode
738
+ // ============================================================================
739
+ async function runRepl(args) {
740
+ const { createAgent } = await import('@agntk/core');
741
+ const colors = createColors(process.stdout.isTTY ?? false);
742
+ const agent = createAgent({
743
+ name: args.name,
744
+ instructions: args.instructions ?? undefined,
745
+ workspaceRoot: args.workspace,
746
+ maxSteps: args.maxSteps,
747
+ });
748
+ // Acquire lockfile
749
+ acquireLock(args.name);
750
+ const cleanup = () => releaseLock(args.name);
751
+ process.on('exit', cleanup);
752
+ process.on('SIGINT', () => { cleanup(); process.exit(130); });
753
+ process.on('SIGTERM', () => { cleanup(); process.exit(143); });
754
+ const version = getVersion();
755
+ const output = process.stdout;
756
+ const toolCount = agent.getToolNames().length;
757
+ output.write(`\n${colors.bold('⚡ agntk')} ${colors.dim(`(${version})`)}\n`);
758
+ output.write(`${colors.cyan(colors.bold(args.name))} ${colors.dim('|')} ${colors.dim(`${toolCount} tools`)} ${colors.dim('|')} ${colors.green('memory: on')}\n`);
759
+ output.write(`${colors.dim('Type /help for commands, /exit or Ctrl+C to quit.')}\n\n`);
760
+ const rl = createInterface({
761
+ input: process.stdin,
762
+ output: process.stdout,
763
+ prompt: `${colors.cyan(colors.bold(args.name + ' ❯'))} `,
764
+ terminal: true,
765
+ });
766
+ const history = [];
767
+ const pendingLines = [];
768
+ let busy = false;
769
+ let closed = false;
770
+ async function processLine(trimmed) {
771
+ if (trimmed === '/exit' || trimmed === '/quit') {
772
+ rl.close();
773
+ return;
774
+ }
775
+ if (trimmed === '/help') {
776
+ 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`);
777
+ rl.prompt();
778
+ return;
779
+ }
780
+ if (trimmed === '/tools') {
781
+ const tools = agent.getToolNames();
782
+ output.write(`\n${colors.bold(`Tools (${tools.length})`)}:\n ${tools.join(', ')}\n\n`);
783
+ rl.prompt();
784
+ return;
785
+ }
786
+ if (trimmed === '/verbose') {
787
+ if (args.outputLevel === 'verbose') {
788
+ args.outputLevel = 'normal';
789
+ output.write(`${colors.dim('Verbose output: off')}\n`);
790
+ }
791
+ else {
792
+ args.outputLevel = 'verbose';
793
+ output.write(`${colors.dim('Verbose output: on')}\n`);
794
+ }
795
+ rl.prompt();
796
+ return;
797
+ }
798
+ busy = true;
799
+ rl.pause();
800
+ history.push({ role: 'user', content: trimmed });
801
+ const maxHistoryPairs = 10;
802
+ const recentHistory = history.length > maxHistoryPairs * 2
803
+ ? history.slice(-maxHistoryPairs * 2)
804
+ : history;
805
+ const historyLines = recentHistory.map((h) => h.role === 'user' ? `[User]: ${h.content}` : `[Assistant]: ${h.content}`);
806
+ const contextPrompt = [
807
+ '<conversation_history>',
808
+ ...historyLines.slice(0, -1),
809
+ '</conversation_history>',
810
+ '',
811
+ recentHistory[recentHistory.length - 1].content,
812
+ ].join('\n');
813
+ try {
814
+ output.write('\n');
815
+ const result = await agent.stream({ prompt: contextPrompt });
816
+ await consumeStream(result.fullStream, {
817
+ output,
818
+ status: process.stderr,
819
+ level: args.outputLevel,
820
+ colors,
821
+ maxSteps: args.maxSteps,
822
+ isTTY: process.stderr.isTTY ?? false,
823
+ });
824
+ const responseText = (await result.text) ?? '';
825
+ if (responseText && !responseText.endsWith('\n')) {
826
+ output.write('\n');
827
+ }
828
+ history.push({ role: 'assistant', content: responseText });
829
+ }
830
+ catch (error) {
831
+ const msg = error instanceof Error ? error.message : String(error);
832
+ output.write(`\n${colors.red('Error:')} ${msg}\n`);
833
+ }
834
+ output.write('\n');
835
+ busy = false;
836
+ rl.resume();
837
+ while (pendingLines.length > 0) {
838
+ const next = pendingLines.shift();
839
+ if (next) {
840
+ await processLine(next);
841
+ }
842
+ }
843
+ if (!closed) {
844
+ rl.prompt();
845
+ }
846
+ }
847
+ return new Promise((resolvePromise) => {
848
+ rl.prompt();
849
+ rl.on('line', (line) => {
850
+ const trimmed = line.trim();
851
+ if (!trimmed) {
852
+ rl.prompt();
853
+ return;
854
+ }
855
+ if (busy) {
856
+ pendingLines.push(trimmed);
857
+ return;
858
+ }
859
+ processLine(trimmed).catch((err) => {
860
+ const msg = err instanceof Error ? err.message : String(err);
861
+ output.write(`\n${colors.red('Error:')} ${msg}\n`);
862
+ rl.prompt();
863
+ });
864
+ });
865
+ rl.on('close', () => {
866
+ closed = true;
867
+ const finish = () => {
868
+ output.write(`\n${colors.dim('👋 Goodbye!')}\n`);
869
+ resolvePromise();
870
+ };
871
+ if (busy) {
872
+ const interval = setInterval(() => {
873
+ if (!busy) {
874
+ clearInterval(interval);
875
+ finish();
876
+ }
877
+ }, 100);
878
+ }
879
+ else {
880
+ finish();
881
+ }
882
+ });
883
+ rl.on('SIGINT', () => {
884
+ output.write('\n');
885
+ rl.close();
886
+ });
887
+ });
888
+ }
889
+ // ============================================================================
52
890
  // Main
53
891
  // ============================================================================
54
892
  async function main() {
55
- const args = parseArgs(process.argv.slice(2));
56
- // Handle --version
893
+ const args = parseCLIArgs(process.argv.slice(2));
894
+ // Fast paths — no heavy imports
57
895
  if (args.version) {
58
- console.log(`agntk v${getVersion()}`);
896
+ console.log(`agntk (${getVersion()})`);
59
897
  process.exit(0);
60
898
  }
61
- // Handle --help
62
899
  if (args.help) {
63
900
  printHelp();
64
901
  process.exit(0);
65
902
  }
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);
903
+ if (args.list) {
904
+ listAgents();
72
905
  process.exit(0);
73
906
  }
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)');
907
+ // Validate: need a name
908
+ if (!args.name) {
909
+ if (!args.prompt && process.stdin.isTTY) {
910
+ console.error('Error: No prompt provided.\n' +
911
+ 'Usage: agntk "your prompt"\n' +
912
+ ' agntk -n <name> "your prompt"\n' +
913
+ ' agntk -n <name> -i\n' +
914
+ ' agntk list\n' +
915
+ ' agntk -h');
916
+ process.exit(1);
917
+ }
918
+ // Default name if no --name flag was given
919
+ args.name = 'default';
920
+ }
921
+ // Check API key
922
+ const apiKeyResult = detectApiKey();
923
+ if (!apiKeyResult) {
924
+ console.error('Error: No API key found.\n\n' +
925
+ ' 1. Get a key at https://openrouter.ai/keys\n' +
926
+ ' 2. Save it permanently:\n\n' +
927
+ ' mkdir -p ~/.agntk && echo "OPENROUTER_API_KEY=sk-or-..." > ~/.agntk/.env\n\n' +
928
+ ' Or export for this session only:\n\n' +
929
+ ' export OPENROUTER_API_KEY=sk-or-...\n');
89
930
  process.exit(1);
90
931
  }
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;
932
+ // Warn if workspace is the home directory (likely unintentional)
933
+ if (args.workspace === homedir()) {
934
+ process.stderr.write('Warning: Workspace is your home directory.\n' +
935
+ ' Run from a project directory, or use --workspace <path>\n\n');
936
+ }
937
+ // Interactive mode
938
+ if (args.interactive) {
939
+ await runRepl(args);
940
+ process.exit(0);
941
+ }
942
+ // Build final prompt (handle piped stdin)
943
+ let prompt = args.prompt;
96
944
  const pipedInput = await readStdin();
97
945
  if (pipedInput) {
98
- if (prompt) {
99
- prompt = `${pipedInput}\n\n${prompt}`;
100
- }
101
- else {
102
- prompt = pipedInput;
103
- }
946
+ prompt = prompt ? `${pipedInput}\n\n${prompt}` : pipedInput;
104
947
  }
105
948
  if (!prompt) {
106
949
  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)');
950
+ 'Usage: agntk "your prompt"\n' +
951
+ ' agntk -n <name> "your prompt"\n' +
952
+ ' agntk -n <name> -i\n' +
953
+ ' agntk -h');
110
954
  process.exit(1);
111
955
  }
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);
956
+ // One-shot mode
957
+ await runOneShot(prompt, args);
958
+ // Flush observability traces before exit
959
+ try {
960
+ const { shutdownObservability } = await import('@agntk/core');
961
+ await shutdownObservability();
962
+ }
963
+ catch {
964
+ // Observability not available — that's fine
119
965
  }
120
966
  process.exit(0);
121
967
  }
122
968
  main().catch((err) => {
123
969
  console.error('Fatal error:', err instanceof Error ? err.message : String(err));
970
+ if (process.env.DEBUG) {
971
+ console.error(err instanceof Error ? err.stack : '');
972
+ }
124
973
  process.exit(1);
125
974
  });
126
975
  //# sourceMappingURL=cli.js.map