@agntk/cli 0.1.1 → 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.d.ts +7 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +748 -75
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +4 -40
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +3 -40
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +1 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -4
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/args.d.ts +0 -40
- package/dist/args.d.ts.map +0 -1
- package/dist/args.js +0 -90
- package/dist/args.js.map +0 -1
- package/dist/repl.d.ts +0 -31
- package/dist/repl.d.ts.map +0 -1
- package/dist/repl.js +0 -213
- package/dist/repl.js.map +0 -1
- package/dist/runner.d.ts +0 -38
- package/dist/runner.d.ts.map +0 -1
- package/dist/runner.js +0 -214
- package/dist/runner.js.map +0 -1
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 --
|
|
8
|
-
* npx agntk -i
|
|
9
|
-
*
|
|
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 {
|
|
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
|
-
//
|
|
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
|
|
137
|
+
agntk (${version}) — zero-config AI agent
|
|
25
138
|
|
|
26
139
|
Usage:
|
|
27
|
-
agntk
|
|
140
|
+
agntk --name <name> "prompt"
|
|
141
|
+
agntk --name <name> -i
|
|
142
|
+
agntk <name> "prompt"
|
|
143
|
+
agntk list
|
|
28
144
|
|
|
29
145
|
Options:
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
-
|
|
33
|
-
--
|
|
34
|
-
--
|
|
35
|
-
--
|
|
36
|
-
--
|
|
37
|
-
|
|
38
|
-
--
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 "
|
|
46
|
-
agntk
|
|
47
|
-
agntk --
|
|
48
|
-
|
|
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 =
|
|
56
|
-
//
|
|
725
|
+
const args = parseCLIArgs(process.argv.slice(2));
|
|
726
|
+
// Fast paths — no heavy imports
|
|
57
727
|
if (args.version) {
|
|
58
|
-
console.log(`agntk
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
//
|
|
75
|
-
if (
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
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
|
|
108
|
-
' agntk -i
|
|
109
|
-
' agntk -h
|
|
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
|
-
//
|
|
113
|
-
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|