@agntk/cli 0.2.0 → 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
@@ -3,17 +3,18 @@
3
3
  * @fileoverview CLI entry point for agntk — zero-config AI agent.
4
4
  *
5
5
  * Usage:
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
6
+ * npx agntk "do something"
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
9
11
  * npx agntk list
10
- * npx agntk "my-agent" "what were you working on?"
11
- * cat error.log | npx agntk --name "debugger" "explain these errors"
12
+ * cat error.log | npx agntk -n "debugger" "explain these errors"
12
13
  */
13
14
  // Load .env files before anything else reads process.env
14
15
  import 'dotenv/config';
15
16
  import { createInterface } from 'node:readline';
16
- import { readdirSync, existsSync } from 'node:fs';
17
+ import { readdirSync, existsSync, writeFileSync, unlinkSync, readFileSync, statSync } from 'node:fs';
17
18
  import { resolve, join } from 'node:path';
18
19
  import { homedir } from 'node:os';
19
20
  import { getVersion } from './version.js';
@@ -25,7 +26,7 @@ const AGENTS_DIR = resolve(homedir(), '.agntk', 'agents');
25
26
  function createColors(enabled) {
26
27
  if (!enabled) {
27
28
  const identity = (s) => s;
28
- return { dim: identity, bold: identity, cyan: identity, yellow: identity, green: identity, red: identity, magenta: identity, reset: '' };
29
+ return { dim: identity, bold: identity, cyan: identity, yellow: identity, green: identity, red: identity, magenta: identity, blue: identity, white: identity, reset: '' };
29
30
  }
30
31
  return {
31
32
  dim: (s) => `\x1b[2m${s}\x1b[22m`,
@@ -35,9 +36,40 @@ function createColors(enabled) {
35
36
  green: (s) => `\x1b[32m${s}\x1b[39m`,
36
37
  red: (s) => `\x1b[31m${s}\x1b[39m`,
37
38
  magenta: (s) => `\x1b[35m${s}\x1b[39m`,
39
+ blue: (s) => `\x1b[34m${s}\x1b[39m`,
40
+ white: (s) => `\x1b[97m${s}\x1b[39m`,
38
41
  reset: '\x1b[0m',
39
42
  };
40
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
+ }
41
73
  function parseCLIArgs(argv) {
42
74
  const args = {
43
75
  name: null,
@@ -103,28 +135,12 @@ function parseCLIArgs(argv) {
103
135
  }
104
136
  }
105
137
  // 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
- }
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(' ');
128
144
  }
129
145
  return args;
130
146
  }
@@ -137,13 +153,13 @@ function printHelp() {
137
153
  agntk (${version}) — zero-config AI agent
138
154
 
139
155
  Usage:
140
- agntk --name <name> "prompt"
141
- agntk --name <name> -i
142
- agntk <name> "prompt"
156
+ agntk "prompt"
157
+ agntk -n <name> "prompt"
158
+ agntk -n <name> -i
143
159
  agntk list
144
160
 
145
161
  Options:
146
- -n, --name <name> Agent name (required for new agents)
162
+ -n, --name <name> Agent name (enables persistent memory)
147
163
  --instructions <text> What the agent does (injected as system prompt)
148
164
  -i, --interactive Interactive REPL mode
149
165
  --workspace <path> Workspace root (default: cwd)
@@ -157,36 +173,164 @@ function printHelp() {
157
173
  list List all known agents
158
174
 
159
175
  Examples:
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?"
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
164
181
  agntk list
165
- cat error.log | agntk --name "debugger" "explain"
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-...
166
187
  `);
167
188
  }
168
189
  // ============================================================================
169
190
  // List Agents
170
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
+ }
171
268
  function listAgents() {
269
+ const colors = createColors(process.stdout.isTTY ?? false);
172
270
  if (!existsSync(AGENTS_DIR)) {
173
- console.log('No agents found. Create one with: agntk --name "my-agent" "do something"');
271
+ console.log(colors.dim('No agents found. Create one with: agntk --name "my-agent" "do something"'));
174
272
  return;
175
273
  }
176
274
  const entries = readdirSync(AGENTS_DIR, { withFileTypes: true });
177
275
  const agents = entries.filter((e) => e.isDirectory());
178
276
  if (agents.length === 0) {
179
- console.log('No agents found. Create one with: agntk --name "my-agent" "do something"');
277
+ console.log(colors.dim('No agents found. Create one with: agntk --name "my-agent" "do something"'));
180
278
  return;
181
279
  }
182
- console.log(`\nKnown agents (${agents.length}):\n`);
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));
183
283
  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');
284
+ const agentDir = join(AGENTS_DIR, agent.name);
285
+ const memoryPath = join(agentDir, 'memory.md');
286
+ const contextPath = join(agentDir, 'context.md');
186
287
  const hasMemory = existsSync(memoryPath);
187
288
  const hasContext = existsSync(contextPath);
188
- const status = hasMemory || hasContext ? '●' : '○';
189
- console.log(` ${status} ${agent.name}${hasMemory ? ' (has memory)' : ''}`);
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(' · '))}`);
190
334
  }
191
335
  console.log('');
192
336
  }
@@ -194,7 +338,7 @@ function listAgents() {
194
338
  // Formatting Helpers
195
339
  // ============================================================================
196
340
  /** Compact summary of tool args — show key names and short values */
197
- function summarizeArgs(input) {
341
+ function summarizeArgs(input, colors) {
198
342
  if (!input || typeof input !== 'object')
199
343
  return '';
200
344
  const obj = input;
@@ -204,7 +348,7 @@ function summarizeArgs(input) {
204
348
  continue;
205
349
  const str = typeof val === 'string' ? val : JSON.stringify(val);
206
350
  const display = str.length > 60 ? str.slice(0, 57) + '...' : str;
207
- parts.push(`${key}=${display}`);
351
+ parts.push(`${colors.dim(key + '=')}${colors.yellow(display)}`);
208
352
  }
209
353
  return parts.join(' ');
210
354
  }
@@ -244,6 +388,7 @@ async function consumeStream(stream, opts) {
244
388
  const { output, status, level, colors } = opts;
245
389
  const quiet = level === 'quiet';
246
390
  const verbose = level === 'verbose';
391
+ const spinner = createSpinner(status, colors, !quiet && (opts.isTTY ?? false));
247
392
  const stats = {
248
393
  steps: 0,
249
394
  toolCalls: 0,
@@ -267,7 +412,7 @@ async function consumeStream(stream, opts) {
267
412
  stats.steps++;
268
413
  currentStepStart = Date.now();
269
414
  if (!quiet) {
270
- status.write(`\n${colors.dim(`── step ${stats.steps} ──────────────────────────────────────`)}\n`);
415
+ status.write(`\n${colors.dim('──')} ${colors.blue(colors.bold(`step ${stats.steps}`))} ${colors.dim('──────────────────────────────────────')}\n`);
271
416
  }
272
417
  break;
273
418
  }
@@ -294,13 +439,13 @@ async function consumeStream(stream, opts) {
294
439
  stats.outputTokens += tokensOut;
295
440
  const parts = [
296
441
  colors.dim(` ${formatDuration(elapsed)}`),
297
- colors.dim(`${tokensIn}→${tokensOut} tok`),
442
+ `${colors.cyan(String(tokensIn))}${colors.dim('→')}${colors.cyan(String(tokensOut))} ${colors.dim('tok')}`,
298
443
  ];
299
444
  if (reason === 'tool-calls') {
300
445
  parts.push(colors.dim('→ tool loop'));
301
446
  }
302
447
  else if (reason === 'stop') {
303
- parts.push(colors.green('done'));
448
+ parts.push(colors.green(colors.bold('done')));
304
449
  }
305
450
  else {
306
451
  parts.push(colors.yellow(reason));
@@ -312,7 +457,7 @@ async function consumeStream(stream, opts) {
312
457
  case 'reasoning-start': {
313
458
  if (!quiet) {
314
459
  inReasoning = true;
315
- status.write(colors.dim('\n ... '));
460
+ status.write(colors.magenta('\n 💭 '));
316
461
  }
317
462
  break;
318
463
  }
@@ -320,7 +465,7 @@ async function consumeStream(stream, opts) {
320
465
  if (!quiet && inReasoning) {
321
466
  const text = chunk.text ?? '';
322
467
  const compacted = text.replace(/\n/g, ' ');
323
- status.write(colors.dim(compacted));
468
+ status.write(colors.magenta(colors.dim(compacted)));
324
469
  }
325
470
  break;
326
471
  }
@@ -337,24 +482,26 @@ async function consumeStream(stream, opts) {
337
482
  const toolName = chunk.toolName;
338
483
  if (verbose) {
339
484
  const argsStr = chunk.input ? JSON.stringify(chunk.input, null, 2) : '';
340
- status.write(`\n ${colors.cyan('>')} ${colors.bold(toolName)}\n`);
485
+ status.write(`\n ${colors.cyan('')} ${colors.cyan(colors.bold(toolName))}\n`);
341
486
  if (argsStr) {
342
487
  const indented = argsStr.split('\n').map((l) => ` ${l}`).join('\n');
343
488
  status.write(`${colors.dim(indented)}\n`);
344
489
  }
345
490
  }
346
491
  else {
347
- const argsSummary = summarizeArgs(chunk.input);
492
+ const argsSummary = summarizeArgs(chunk.input, colors);
348
493
  const display = argsSummary
349
- ? ` ${colors.cyan('>')} ${colors.bold(toolName)} ${colors.dim(argsSummary)}`
350
- : ` ${colors.cyan('>')} ${colors.bold(toolName)}`;
494
+ ? ` ${colors.cyan('')} ${colors.cyan(colors.bold(toolName))} ${argsSummary}`
495
+ : ` ${colors.cyan('')} ${colors.cyan(colors.bold(toolName))}`;
351
496
  status.write(`${display}\n`);
352
497
  }
498
+ spinner.start(`running ${toolName}...`);
353
499
  }
354
500
  afterToolResult = false;
355
501
  break;
356
502
  }
357
503
  case 'tool-result': {
504
+ spinner.stop();
358
505
  const toolOutputRaw = typeof chunk.output === 'string' ? chunk.output : JSON.stringify(chunk.output);
359
506
  try {
360
507
  const parsed = JSON.parse(toolOutputRaw);
@@ -372,7 +519,7 @@ async function consumeStream(stream, opts) {
372
519
  displayOutput = displayOutput.slice(0, maxLen) + `\n... (${displayOutput.length} chars total)`;
373
520
  }
374
521
  const indented = displayOutput.split('\n').map((l) => ` ${l}`).join('\n');
375
- status.write(` ${colors.green('<')} ${colors.dim(toolName)} ${colors.dim('returned')}\n`);
522
+ status.write(` ${colors.green('')} ${colors.green(colors.dim(toolName))} ${colors.dim('returned')}\n`);
376
523
  status.write(`${colors.dim(indented)}\n`);
377
524
  }
378
525
  else {
@@ -381,19 +528,20 @@ async function consumeStream(stream, opts) {
381
528
  const truncated = firstLine.length > 100
382
529
  ? firstLine.slice(0, 97) + '...'
383
530
  : firstLine;
384
- status.write(` ${colors.green('<')} ${colors.dim(toolName + ': ' + truncated)}\n`);
531
+ status.write(` ${colors.green('')} ${colors.green(colors.dim(toolName + ': '))}${colors.dim(truncated)}\n`);
385
532
  }
386
533
  }
387
534
  afterToolResult = true;
388
535
  break;
389
536
  }
390
537
  case 'tool-error': {
538
+ spinner.stop();
391
539
  if (!quiet) {
392
540
  const toolName = chunk.toolName;
393
541
  const error = chunk.error instanceof Error
394
542
  ? chunk.error.message
395
543
  : String(chunk.error ?? 'unknown error');
396
- status.write(` ${colors.red('x')} ${colors.bold(toolName)} ${colors.red(error)}\n`);
544
+ status.write(` ${colors.red('')} ${colors.red(colors.bold(toolName))} ${colors.red(error)}\n`);
397
545
  }
398
546
  afterToolResult = true;
399
547
  break;
@@ -478,6 +626,7 @@ async function consumeStream(stream, opts) {
478
626
  break;
479
627
  }
480
628
  case 'finish': {
629
+ spinner.stop();
481
630
  if (!quiet) {
482
631
  const elapsed = Date.now() - stats.startTime;
483
632
  const totalUsage = chunk.totalUsage;
@@ -485,15 +634,20 @@ async function consumeStream(stream, opts) {
485
634
  stats.inputTokens = totalUsage.inputTokens ?? stats.inputTokens;
486
635
  stats.outputTokens = totalUsage.outputTokens ?? stats.outputTokens;
487
636
  }
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`);
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`);
489
642
  }
490
643
  break;
491
644
  }
492
645
  case 'error': {
646
+ spinner.stop();
493
647
  const error = chunk.error instanceof Error
494
648
  ? chunk.error.message
495
649
  : String(chunk.error ?? 'unknown error');
496
- status.write(`\n${colors.red('Error:')} ${error}\n`);
650
+ status.write(`\n${colors.red(colors.bold('Error:'))} ${colors.red(error)}\n`);
497
651
  break;
498
652
  }
499
653
  default:
@@ -549,9 +703,15 @@ async function runOneShot(prompt, args) {
549
703
  workspaceRoot: args.workspace,
550
704
  maxSteps: args.maxSteps,
551
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); });
552
712
  if (args.outputLevel !== 'quiet') {
553
713
  const toolCount = agent.getToolNames().length;
554
- process.stderr.write(`${colors.dim(`agntk | ${args.name} | ${toolCount} tools | workspace: ${args.workspace}`)}\n`);
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`);
555
715
  }
556
716
  const result = await agent.stream({ prompt });
557
717
  await consumeStream(result.fullStream, {
@@ -560,6 +720,7 @@ async function runOneShot(prompt, args) {
560
720
  level: args.outputLevel,
561
721
  colors,
562
722
  maxSteps: args.maxSteps,
723
+ isTTY: process.stderr.isTTY ?? false,
563
724
  });
564
725
  const finalText = await result.text;
565
726
  if (finalText && !finalText.endsWith('\n')) {
@@ -584,16 +745,22 @@ async function runRepl(args) {
584
745
  workspaceRoot: args.workspace,
585
746
  maxSteps: args.maxSteps,
586
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); });
587
754
  const version = getVersion();
588
755
  const output = process.stdout;
589
756
  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`);
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`);
592
759
  output.write(`${colors.dim('Type /help for commands, /exit or Ctrl+C to quit.')}\n\n`);
593
760
  const rl = createInterface({
594
761
  input: process.stdin,
595
762
  output: process.stdout,
596
- prompt: `${colors.cyan(args.name + '>')} `,
763
+ prompt: `${colors.cyan(colors.bold(args.name + ''))} `,
597
764
  terminal: true,
598
765
  });
599
766
  const history = [];
@@ -652,6 +819,7 @@ async function runRepl(args) {
652
819
  level: args.outputLevel,
653
820
  colors,
654
821
  maxSteps: args.maxSteps,
822
+ isTTY: process.stderr.isTTY ?? false,
655
823
  });
656
824
  const responseText = (await result.text) ?? '';
657
825
  if (responseText && !responseText.endsWith('\n')) {
@@ -697,7 +865,7 @@ async function runRepl(args) {
697
865
  rl.on('close', () => {
698
866
  closed = true;
699
867
  const finish = () => {
700
- output.write(`\n${colors.dim('Goodbye!')}\n`);
868
+ output.write(`\n${colors.dim('👋 Goodbye!')}\n`);
701
869
  resolvePromise();
702
870
  };
703
871
  if (busy) {
@@ -739,14 +907,15 @@ async function main() {
739
907
  // Validate: need a name
740
908
  if (!args.name) {
741
909
  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' +
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' +
745
914
  ' agntk list\n' +
746
915
  ' agntk -h');
747
916
  process.exit(1);
748
917
  }
749
- // Default name if only prompt was given
918
+ // Default name if no --name flag was given
750
919
  args.name = 'default';
751
920
  }
752
921
  // Check API key
@@ -754,11 +923,17 @@ async function main() {
754
923
  if (!apiKeyResult) {
755
924
  console.error('Error: No API key found.\n\n' +
756
925
  ' 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.');
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');
760
930
  process.exit(1);
761
931
  }
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
+ }
762
937
  // Interactive mode
763
938
  if (args.interactive) {
764
939
  await runRepl(args);
@@ -772,8 +947,9 @@ async function main() {
772
947
  }
773
948
  if (!prompt) {
774
949
  console.error('Error: No prompt provided.\n' +
775
- 'Usage: agntk --name "my-agent" "your prompt"\n' +
776
- ' agntk --name "my-agent" -i\n' +
950
+ 'Usage: agntk "your prompt"\n' +
951
+ ' agntk -n <name> "your prompt"\n' +
952
+ ' agntk -n <name> -i\n' +
777
953
  ' agntk -h');
778
954
  process.exit(1);
779
955
  }