@agentskit/cli 0.5.2 → 0.6.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/bin.cjs CHANGED
@@ -4,17 +4,19 @@
4
4
  var React3 = require('react');
5
5
  var ink = require('ink');
6
6
  var commander = require('commander');
7
- var path = require('path');
7
+ var fs = require('fs');
8
+ var os = require('os');
9
+ var path3 = require('path');
8
10
  var promises = require('fs/promises');
9
11
  var ink$1 = require('@agentskit/ink');
10
12
  var adapters = require('@agentskit/adapters');
11
13
  var tools = require('@agentskit/tools');
12
14
  var skills = require('@agentskit/skills');
13
15
  var memory = require('@agentskit/memory');
16
+ var crypto = require('crypto');
14
17
  var jsxRuntime = require('react/jsx-runtime');
15
18
  var prompts = require('@inquirer/prompts');
16
19
  var kleur = require('kleur');
17
- var fs = require('fs');
18
20
  var runtime = require('@agentskit/runtime');
19
21
  var child_process = require('child_process');
20
22
  var chokidar = require('chokidar');
@@ -22,7 +24,7 @@ var chokidar = require('chokidar');
22
24
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
23
25
 
24
26
  var React3__default = /*#__PURE__*/_interopDefault(React3);
25
- var path__default = /*#__PURE__*/_interopDefault(path);
27
+ var path3__default = /*#__PURE__*/_interopDefault(path3);
26
28
  var kleur__default = /*#__PURE__*/_interopDefault(kleur);
27
29
  var chokidar__default = /*#__PURE__*/_interopDefault(chokidar);
28
30
 
@@ -44,7 +46,7 @@ async function loadTsConfig(path4) {
44
46
  }
45
47
  async function loadPackageJsonConfig(dir) {
46
48
  try {
47
- const raw = await promises.readFile(path.join(dir, "package.json"), "utf8");
49
+ const raw = await promises.readFile(path3.join(dir, "package.json"), "utf8");
48
50
  const pkg = JSON.parse(raw);
49
51
  if (pkg.agentskit && typeof pkg.agentskit === "object") {
50
52
  return pkg.agentskit;
@@ -54,16 +56,38 @@ async function loadPackageJsonConfig(dir) {
54
56
  return void 0;
55
57
  }
56
58
  }
57
- async function loadConfig(options) {
58
- const cwd = path.resolve(process.cwd());
59
- const tsPath = path.join(cwd, ".agentskit.config.ts");
60
- const tsConfig = await loadTsConfig(tsPath);
59
+ function mergeConfigs(base, override) {
60
+ if (!base && !override) return void 0;
61
+ if (!base) return override;
62
+ if (!override) return base;
63
+ return {
64
+ ...base,
65
+ ...override,
66
+ tools: { ...base.tools, ...override.tools },
67
+ defaults: { ...base.defaults, ...override.defaults },
68
+ runtime: { ...base.runtime, ...override.runtime },
69
+ observability: { ...base.observability, ...override.observability }
70
+ };
71
+ }
72
+ async function loadLocalConfig(cwd) {
73
+ const tsConfig = await loadTsConfig(path3.join(cwd, ".agentskit.config.ts"));
61
74
  if (tsConfig) return tsConfig;
62
- const jsonPath = path.join(cwd, ".agentskit.config.json");
63
- const jsonConfig = await loadJsonConfig(jsonPath);
75
+ const jsonConfig = await loadJsonConfig(path3.join(cwd, ".agentskit.config.json"));
64
76
  if (jsonConfig) return jsonConfig;
65
77
  return await loadPackageJsonConfig(cwd);
66
78
  }
79
+ async function loadGlobalConfig(home) {
80
+ const globalDir = path3.join(os.homedir(), ".agentskit");
81
+ const tsConfig = await loadTsConfig(path3.join(globalDir, "config.ts"));
82
+ if (tsConfig) return tsConfig;
83
+ return await loadJsonConfig(path3.join(globalDir, "config.json"));
84
+ }
85
+ async function loadConfig(options) {
86
+ const cwd = path3.resolve(process.cwd());
87
+ const global = await loadGlobalConfig();
88
+ const local = await loadLocalConfig(cwd);
89
+ return mergeConfigs(global, local);
90
+ }
67
91
  var providers = {
68
92
  openai: {
69
93
  label: "OpenAI",
@@ -181,26 +205,41 @@ var skillRegistry = {
181
205
  critic: skills.critic,
182
206
  summarizer: skills.summarizer
183
207
  };
208
+ function instantiate(kind) {
209
+ switch (kind) {
210
+ case "web_search":
211
+ return [tools.webSearch()];
212
+ case "fetch_url":
213
+ return [tools.fetchUrl()];
214
+ case "filesystem":
215
+ return tools.filesystem({ basePath: process.cwd() });
216
+ case "shell":
217
+ return [tools.shell({ timeout: 3e4 })];
218
+ }
219
+ }
220
+ function gateTool(tool) {
221
+ if (tool.requiresConfirmation === false) return tool;
222
+ return { ...tool, requiresConfirmation: true };
223
+ }
184
224
  function resolveTools(toolNames) {
185
- if (!toolNames) return [];
186
- const tools$1 = [];
187
- for (const name of toolNames.split(",").map((s) => s.trim())) {
225
+ if (!toolNames) {
226
+ return [...instantiate("web_search"), ...instantiate("fetch_url")].map(gateTool);
227
+ }
228
+ const tools = [];
229
+ for (const name of toolNames.split(",").map((s) => s.trim()).filter(Boolean)) {
188
230
  switch (name) {
189
231
  case "web_search":
190
- tools$1.push(tools.webSearch());
191
- break;
232
+ case "fetch_url":
192
233
  case "filesystem":
193
- tools$1.push(...tools.filesystem({ basePath: process.cwd() }));
194
- break;
195
234
  case "shell":
196
- tools$1.push(tools.shell({ timeout: 3e4 }));
235
+ tools.push(...instantiate(name));
197
236
  break;
198
237
  default:
199
238
  process.stderr.write(`Unknown tool: ${name}
200
239
  `);
201
240
  }
202
241
  }
203
- return tools$1;
242
+ return tools;
204
243
  }
205
244
  function resolveSkill(skillName) {
206
245
  if (!skillName) return void 0;
@@ -233,41 +272,475 @@ function resolveMemory(backend, memoryPath) {
233
272
  return memory.fileChatMemory(memoryPath);
234
273
  }
235
274
  }
275
+ var ROOT = path3.join(os.homedir(), ".agentskit", "sessions");
276
+ var META_SUFFIX = ".meta.json";
277
+ function cwdHash(cwd = process.cwd()) {
278
+ return crypto.createHash("sha256").update(cwd).digest("hex").slice(0, 12);
279
+ }
280
+ function dirFor(cwd = process.cwd()) {
281
+ return path3.join(ROOT, cwdHash(cwd));
282
+ }
283
+ function ensureDir(dir) {
284
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
285
+ }
286
+ function generateSessionId() {
287
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
288
+ const suffix = crypto.randomBytes(3).toString("hex");
289
+ return `${ts}-${suffix}`;
290
+ }
291
+ function sessionFilePath(id, cwd = process.cwd()) {
292
+ ensureDir(dirFor(cwd));
293
+ return path3.join(dirFor(cwd), `${id}.json`);
294
+ }
295
+ function metaPath(id, cwd = process.cwd()) {
296
+ return path3.join(dirFor(cwd), `${id}${META_SUFFIX}`);
297
+ }
298
+ function readMeta(id, cwd = process.cwd()) {
299
+ const path4 = metaPath(id, cwd);
300
+ if (!fs.existsSync(path4)) return null;
301
+ try {
302
+ return JSON.parse(fs.readFileSync(path4, "utf8"));
303
+ } catch {
304
+ return null;
305
+ }
306
+ }
307
+ function writeSessionMeta(meta, cwd = process.cwd()) {
308
+ ensureDir(dirFor(cwd));
309
+ fs.writeFileSync(metaPath(meta.id, cwd), JSON.stringify(meta, null, 2));
310
+ }
311
+ function derivePreview(messages) {
312
+ const firstUser = messages.find((m) => m.role === "user" && m.content.trim());
313
+ if (!firstUser) return "(empty)";
314
+ const single = firstUser.content.replace(/\s+/g, " ").trim();
315
+ return single.length > 80 ? `${single.slice(0, 80)}\u2026` : single;
316
+ }
317
+ function listSessions(cwd = process.cwd()) {
318
+ const dir = dirFor(cwd);
319
+ if (!fs.existsSync(dir)) return [];
320
+ const entries = fs.readdirSync(dir);
321
+ const records = [];
322
+ for (const entry of entries) {
323
+ if (!entry.endsWith(".json") || entry.endsWith(META_SUFFIX)) continue;
324
+ const id = entry.replace(/\.json$/, "");
325
+ const meta = readMeta(id, cwd);
326
+ const file = path3.join(dir, entry);
327
+ if (meta) {
328
+ records.push({ metadata: meta, file });
329
+ } else {
330
+ const stats = fs.statSync(file);
331
+ records.push({
332
+ metadata: {
333
+ id,
334
+ cwd,
335
+ createdAt: stats.birthtime.toISOString(),
336
+ updatedAt: stats.mtime.toISOString(),
337
+ messageCount: 0,
338
+ preview: "(legacy session)"
339
+ },
340
+ file
341
+ });
342
+ }
343
+ }
344
+ records.sort((a, b) => b.metadata.updatedAt.localeCompare(a.metadata.updatedAt));
345
+ return records;
346
+ }
347
+ function findLatestSession(cwd = process.cwd()) {
348
+ const all = listSessions(cwd);
349
+ return all[0] ?? null;
350
+ }
351
+ function findSession(id, cwd = process.cwd()) {
352
+ const exact = listSessions(cwd).find((s) => s.metadata.id === id);
353
+ if (exact) return exact;
354
+ const prefix = listSessions(cwd).find((s) => s.metadata.id.startsWith(id));
355
+ return prefix ?? null;
356
+ }
357
+ function resolveSession(input2) {
358
+ const cwd = input2.cwd ?? process.cwd();
359
+ if (input2.explicitPath) {
360
+ return { id: "custom", file: input2.explicitPath, isNew: !fs.existsSync(input2.explicitPath) };
361
+ }
362
+ if (input2.forceNew) {
363
+ const id2 = generateSessionId();
364
+ return { id: id2, file: sessionFilePath(id2, cwd), isNew: true };
365
+ }
366
+ if (input2.resumeId) {
367
+ const target = input2.resumeId === true ? findLatestSession(cwd) : findSession(input2.resumeId, cwd);
368
+ if (target) {
369
+ return { id: target.metadata.id, file: target.file, isNew: false };
370
+ }
371
+ process.stderr.write(
372
+ `No session matching "${String(input2.resumeId)}" \u2014 starting a new one.
373
+ `
374
+ );
375
+ }
376
+ const latest = findLatestSession(cwd);
377
+ if (latest) return { id: latest.metadata.id, file: latest.file, isNew: false };
378
+ const id = generateSessionId();
379
+ return { id, file: sessionFilePath(id, cwd), isNew: true };
380
+ }
381
+
382
+ // src/slash-commands.ts
383
+ function parseSlashCommand(input2) {
384
+ if (!input2.startsWith("/")) return null;
385
+ const match = input2.slice(1).match(/^(\S+)\s*([\s\S]*)$/);
386
+ if (!match) return null;
387
+ return { name: match[1], args: match[2] ?? "" };
388
+ }
389
+ function createSlashRegistry(commands) {
390
+ const map = /* @__PURE__ */ new Map();
391
+ for (const cmd of commands) {
392
+ map.set(cmd.name, cmd);
393
+ for (const alias of cmd.aliases ?? []) map.set(alias, cmd);
394
+ }
395
+ return map;
396
+ }
397
+ var builtinSlashCommands = [
398
+ {
399
+ name: "help",
400
+ aliases: ["?"],
401
+ description: "List available slash commands.",
402
+ run(ctx) {
403
+ const seen = /* @__PURE__ */ new Set();
404
+ const lines = [];
405
+ for (const cmd of ctx.commands) {
406
+ if (seen.has(cmd.name)) continue;
407
+ seen.add(cmd.name);
408
+ const suffix = cmd.usage ? ` (${cmd.usage})` : "";
409
+ lines.push(` /${cmd.name.padEnd(10)} ${cmd.description}${suffix}`);
410
+ }
411
+ ctx.feedback(`Slash commands:
412
+ ${lines.join("\n")}`, "info");
413
+ }
414
+ },
415
+ {
416
+ name: "model",
417
+ description: "Switch the active model.",
418
+ usage: "/model <name>",
419
+ run(ctx, args) {
420
+ const value = args.trim();
421
+ if (!value) {
422
+ ctx.feedback(
423
+ `Current model: ${ctx.runtime.model ?? "unset"}. Usage: /model <name>`,
424
+ "warn"
425
+ );
426
+ return;
427
+ }
428
+ ctx.setModel(value);
429
+ ctx.feedback(`Model \u2192 ${value}`, "success");
430
+ }
431
+ },
432
+ {
433
+ name: "provider",
434
+ description: "Switch the adapter provider.",
435
+ usage: "/provider openai|anthropic|gemini|ollama|deepseek|grok|kimi|demo",
436
+ run(ctx, args) {
437
+ const value = args.trim();
438
+ if (!value) {
439
+ ctx.feedback(
440
+ `Current provider: ${ctx.runtime.provider}. Usage: /provider <name>`,
441
+ "warn"
442
+ );
443
+ return;
444
+ }
445
+ ctx.setProvider(value);
446
+ ctx.feedback(`Provider \u2192 ${value}`, "success");
447
+ }
448
+ },
449
+ {
450
+ name: "base-url",
451
+ aliases: ["baseurl"],
452
+ description: "Override provider base URL.",
453
+ usage: "/base-url <url|clear>",
454
+ run(ctx, args) {
455
+ const value = args.trim();
456
+ if (!value || value === "clear") {
457
+ ctx.setBaseUrl(void 0);
458
+ ctx.feedback("Base URL cleared.", "success");
459
+ return;
460
+ }
461
+ ctx.setBaseUrl(value);
462
+ ctx.feedback(`Base URL \u2192 ${value}`, "success");
463
+ }
464
+ },
465
+ {
466
+ name: "tools",
467
+ description: "Set active tools (comma-separated) or clear them.",
468
+ usage: "/tools web_search,fetch_url | /tools clear",
469
+ run(ctx, args) {
470
+ const value = args.trim();
471
+ if (!value || value === "clear") {
472
+ ctx.setTools(void 0);
473
+ ctx.feedback("Tools reset to defaults.", "success");
474
+ return;
475
+ }
476
+ ctx.setTools(value);
477
+ ctx.feedback(`Tools \u2192 ${value}`, "success");
478
+ }
479
+ },
480
+ {
481
+ name: "skill",
482
+ description: "Set active skill(s) (comma-separated) or clear them.",
483
+ usage: "/skill researcher,coder | /skill clear",
484
+ run(ctx, args) {
485
+ const value = args.trim();
486
+ if (!value || value === "clear") {
487
+ ctx.setSkill(void 0);
488
+ ctx.feedback("Skills cleared.", "success");
489
+ return;
490
+ }
491
+ ctx.setSkill(value);
492
+ ctx.feedback(`Skills \u2192 ${value}`, "success");
493
+ }
494
+ },
495
+ {
496
+ name: "clear",
497
+ aliases: ["reset"],
498
+ description: "Clear the conversation history in this session.",
499
+ async run(ctx) {
500
+ await ctx.chat.clear();
501
+ ctx.feedback("History cleared.", "success");
502
+ }
503
+ },
504
+ {
505
+ name: "exit",
506
+ aliases: ["quit", "q"],
507
+ description: "Exit the chat.",
508
+ run() {
509
+ process.exit(0);
510
+ }
511
+ }
512
+ ];
513
+ function groupIntoTurns(messages) {
514
+ const turns = [];
515
+ let current = [];
516
+ for (const message of messages) {
517
+ if (message.role === "user") {
518
+ if (current.length > 0) turns.push(current);
519
+ current = [message];
520
+ } else if (message.role === "system") {
521
+ if (current.length > 0) turns.push(current);
522
+ turns.push([message]);
523
+ current = [];
524
+ } else {
525
+ current.push(message);
526
+ }
527
+ }
528
+ if (current.length > 0) turns.push(current);
529
+ return turns;
530
+ }
236
531
  function ChatApp(options) {
237
- const adapter = React3.useMemo(
238
- () => resolveChatProvider(options).adapter,
239
- [options.apiKey, options.baseUrl, options.model, options.provider]
532
+ const [provider, setProvider] = React3.useState(options.provider);
533
+ const [model, setModel] = React3.useState(options.model);
534
+ const [apiKey, setApiKey] = React3.useState(options.apiKey);
535
+ const [baseUrl, setBaseUrl] = React3.useState(options.baseUrl);
536
+ const [toolsFlag, setToolsFlag] = React3.useState(options.tools);
537
+ const [skillFlag, setSkillFlag] = React3.useState(options.skill);
538
+ const runtime = React3.useMemo(
539
+ () => resolveChatProvider({ provider, model, apiKey, baseUrl }),
540
+ [provider, model, apiKey, baseUrl]
240
541
  );
241
542
  const memory = React3.useMemo(
242
543
  () => resolveMemory(options.memoryBackend, options.memoryPath ?? ".agentskit-history.json"),
243
544
  [options.memoryPath, options.memoryBackend]
244
545
  );
245
- const tools = React3.useMemo(() => resolveTools(options.tools), [options.tools]);
546
+ const tools = React3.useMemo(() => resolveTools(toolsFlag), [toolsFlag]);
246
547
  const skills = React3.useMemo(() => {
247
- if (!options.skill) return void 0;
248
- const names = options.skill.split(",").map((s) => s.trim());
548
+ if (!skillFlag) return void 0;
549
+ const names = skillFlag.split(",").map((s) => s.trim());
249
550
  const resolved = names.map((n) => skillRegistry[n]).filter(Boolean);
250
551
  if (resolved.length === 0) return void 0;
251
552
  return resolved;
252
- }, [options.skill]);
553
+ }, [skillFlag]);
253
554
  const chat = ink$1.useChat({
254
- adapter,
555
+ adapter: runtime.adapter,
255
556
  memory,
256
557
  systemPrompt: options.system,
257
558
  tools: tools.length > 0 ? tools : void 0,
258
559
  skills
259
560
  });
561
+ const [sessionAllowed, setSessionAllowed] = React3.useState(/* @__PURE__ */ new Set());
562
+ const autoApprovedRef = React3.useRef(/* @__PURE__ */ new Set());
563
+ React3.useEffect(() => {
564
+ if (sessionAllowed.size === 0) return;
565
+ for (const message of chat.messages) {
566
+ for (const call of message.toolCalls ?? []) {
567
+ if (call.status === "requires_confirmation" && sessionAllowed.has(call.name) && !autoApprovedRef.current.has(call.id)) {
568
+ autoApprovedRef.current.add(call.id);
569
+ void chat.approve(call.id);
570
+ }
571
+ }
572
+ }
573
+ }, [chat.messages, sessionAllowed, chat.approve]);
574
+ const handleApproveAlways = (toolCallId, toolName) => {
575
+ setSessionAllowed((prev) => {
576
+ if (prev.has(toolName)) return prev;
577
+ const next = new Set(prev);
578
+ next.add(toolName);
579
+ return next;
580
+ });
581
+ autoApprovedRef.current.add(toolCallId);
582
+ void chat.approve(toolCallId);
583
+ };
584
+ const sessionCreatedAtRef = React3.useRef(void 0);
585
+ const messageCount = chat.messages.length;
586
+ const firstUserContent = chat.messages.find((m) => m.role === "user")?.content ?? "";
587
+ React3.useEffect(() => {
588
+ const sessionId = options.sessionId;
589
+ if (!sessionId || sessionId === "custom") return;
590
+ if (!sessionCreatedAtRef.current) {
591
+ sessionCreatedAtRef.current = (/* @__PURE__ */ new Date()).toISOString();
592
+ }
593
+ try {
594
+ writeSessionMeta({
595
+ id: sessionId,
596
+ cwd: process.cwd(),
597
+ createdAt: sessionCreatedAtRef.current,
598
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
599
+ messageCount,
600
+ preview: derivePreview(chat.messages),
601
+ provider: runtime.provider,
602
+ model: runtime.model
603
+ });
604
+ } catch {
605
+ }
606
+ }, [options.sessionId, messageCount, firstUserContent, runtime.provider, runtime.model]);
607
+ const turns = React3.useMemo(() => groupIntoTurns(chat.messages), [chat.messages]);
608
+ const toolNames = toolsFlag ? toolsFlag.split(",").map((s) => s.trim()).filter(Boolean) : [];
609
+ const [feedback, setFeedback] = React3.useState(null);
610
+ const slashCommands = React3.useMemo(
611
+ () => [...builtinSlashCommands, ...options.slashCommands ?? []],
612
+ [options.slashCommands]
613
+ );
614
+ const slashRegistry = React3.useMemo(() => createSlashRegistry(slashCommands), [slashCommands]);
615
+ const handleSubmitInput = async (raw) => {
616
+ const parsed = parseSlashCommand(raw);
617
+ if (!parsed) {
618
+ setFeedback(null);
619
+ return false;
620
+ }
621
+ const cmd = slashRegistry.get(parsed.name);
622
+ if (!cmd) {
623
+ setFeedback({
624
+ message: `Unknown command: /${parsed.name}. Type /help for the list.`,
625
+ kind: "error"
626
+ });
627
+ return true;
628
+ }
629
+ const ctx = {
630
+ chat,
631
+ runtime: {
632
+ provider: runtime.provider,
633
+ model: runtime.model,
634
+ mode: runtime.mode,
635
+ baseUrl,
636
+ tools: toolsFlag,
637
+ skill: skillFlag
638
+ },
639
+ setProvider,
640
+ setModel,
641
+ setApiKey,
642
+ setBaseUrl,
643
+ setTools: setToolsFlag,
644
+ setSkill: setSkillFlag,
645
+ feedback: (message, kind = "info") => setFeedback({ message, kind }),
646
+ commands: slashCommands
647
+ };
648
+ try {
649
+ await cmd.run(ctx, parsed.args);
650
+ } catch (err) {
651
+ const message = err instanceof Error ? err.message : String(err);
652
+ setFeedback({ message: `/${parsed.name} failed: ${message}`, kind: "error" });
653
+ }
654
+ return true;
655
+ };
656
+ const awaitingConfirmation = React3.useMemo(
657
+ () => chat.messages.some(
658
+ (message) => message.toolCalls?.some(
659
+ (call) => call.status === "requires_confirmation" && !sessionAllowed.has(call.name)
660
+ )
661
+ ),
662
+ [chat.messages, sessionAllowed]
663
+ );
260
664
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", gap: 1, children: [
261
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { bold: true, color: "cyan", children: "AgentsKit CLI" }),
262
- /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { dimColor: true, children: "Press Enter to send. Live providers use env vars or --api-key, and demo mode stays available for zero-config usage." }),
263
- /* @__PURE__ */ jsxRuntime.jsx(ink$1.ChatContainer, { children: chat.messages.map((message) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", marginBottom: 1, children: [
264
- /* @__PURE__ */ jsxRuntime.jsx(ink$1.Message, { message }),
265
- message.toolCalls?.map((toolCall) => /* @__PURE__ */ jsxRuntime.jsx(ink$1.ToolCallView, { toolCall }, toolCall.id))
266
- ] }, message.id)) }),
267
- /* @__PURE__ */ jsxRuntime.jsx(ink$1.ThinkingIndicator, { visible: chat.status === "streaming" }),
268
- /* @__PURE__ */ jsxRuntime.jsx(ink$1.InputBar, { chat, placeholder: "Type a message and press Enter..." })
665
+ /* @__PURE__ */ jsxRuntime.jsx(
666
+ ink$1.StatusHeader,
667
+ {
668
+ provider: runtime.provider,
669
+ model: runtime.model,
670
+ mode: runtime.mode,
671
+ tools: toolNames,
672
+ messageCount: chat.messages.length,
673
+ sessionId: options.sessionId
674
+ }
675
+ ),
676
+ /* @__PURE__ */ jsxRuntime.jsx(ink$1.ChatContainer, { children: turns.map((turn, turnIdx) => {
677
+ const assistantSteps = turn.filter((m) => m.role === "assistant").length;
678
+ let stepIndex = 0;
679
+ return /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { flexDirection: "column", gap: 1, children: turn.map((message) => {
680
+ const showStep = message.role === "assistant" && assistantSteps > 1;
681
+ if (showStep) stepIndex++;
682
+ return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
683
+ showStep ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { dimColor: true, children: [
684
+ "\u21BB step ",
685
+ stepIndex,
686
+ "/",
687
+ assistantSteps
688
+ ] }) : null,
689
+ /* @__PURE__ */ jsxRuntime.jsx(ink$1.Message, { message }),
690
+ message.toolCalls?.map((toolCall) => /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", children: [
691
+ /* @__PURE__ */ jsxRuntime.jsx(ink$1.ToolCallView, { toolCall, expanded: true }),
692
+ toolCall.status === "requires_confirmation" && !sessionAllowed.has(toolCall.name) ? /* @__PURE__ */ jsxRuntime.jsx(
693
+ ink$1.ToolConfirmation,
694
+ {
695
+ toolCall,
696
+ onApprove: chat.approve,
697
+ onDeny: chat.deny,
698
+ onApproveAlways: handleApproveAlways
699
+ }
700
+ ) : null
701
+ ] }, toolCall.id))
702
+ ] }, message.id);
703
+ }) }, `turn-${turnIdx}`);
704
+ }) }),
705
+ /* @__PURE__ */ jsxRuntime.jsx(
706
+ ink$1.ThinkingIndicator,
707
+ {
708
+ visible: chat.status === "streaming",
709
+ label: toolNames.length > 0 ? "agent working" : "thinking"
710
+ }
711
+ ),
712
+ chat.error ? /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", children: [
713
+ /* @__PURE__ */ jsxRuntime.jsxs(ink.Text, { color: "red", bold: true, children: [
714
+ "\u2717 ",
715
+ chat.error.name || "Error"
716
+ ] }),
717
+ /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: "red", children: chat.error.message })
718
+ ] }) : null,
719
+ feedback ? /* @__PURE__ */ jsxRuntime.jsx(ink.Box, { borderStyle: "round", borderColor: feedbackBorder(feedback.kind), paddingX: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ink.Text, { color: feedbackBorder(feedback.kind), children: feedback.message }) }) : null,
720
+ /* @__PURE__ */ jsxRuntime.jsx(
721
+ ink$1.InputBar,
722
+ {
723
+ chat,
724
+ placeholder: "Type a message or /help for commands",
725
+ disabled: awaitingConfirmation,
726
+ onSubmitInput: handleSubmitInput
727
+ }
728
+ )
269
729
  ] });
270
730
  }
731
+ function feedbackBorder(kind) {
732
+ switch (kind) {
733
+ case "error":
734
+ return "red";
735
+ case "warn":
736
+ return "yellow";
737
+ case "success":
738
+ return "green";
739
+ case "info":
740
+ default:
741
+ return "cyan";
742
+ }
743
+ }
271
744
  function renderChatHeader(options) {
272
745
  const runtime = resolveChatProvider(options);
273
746
  const parts = [`provider=${runtime.provider}`];
@@ -376,7 +849,7 @@ function reactStarter(ctx) {
376
849
  return {
377
850
  "package.json": JSON.stringify(
378
851
  {
379
- name: path__default.default.basename(ctx.template === "react" ? "agentskit-react-app" : "agentskit-app"),
852
+ name: path3__default.default.basename(ctx.template === "react" ? "agentskit-react-app" : "agentskit-app"),
380
853
  private: true,
381
854
  type: "module",
382
855
  scripts: {
@@ -745,8 +1218,8 @@ async function writeStarterProject(options) {
745
1218
  await promises.mkdir(options.targetDir, { recursive: true });
746
1219
  await Promise.all(
747
1220
  Object.entries(files).map(async ([relativePath, content]) => {
748
- const absolutePath = path__default.default.join(options.targetDir, relativePath);
749
- await promises.mkdir(path__default.default.dirname(absolutePath), { recursive: true });
1221
+ const absolutePath = path3__default.default.join(options.targetDir, relativePath);
1222
+ await promises.mkdir(path3__default.default.dirname(absolutePath), { recursive: true });
750
1223
  await promises.writeFile(absolutePath, content, "utf8");
751
1224
  })
752
1225
  );
@@ -762,7 +1235,7 @@ ${kleur__default.default.bold().green("\u25B2")} ${kleur__default.default.bold("
762
1235
  default: defaults.dir ?? "agentskit-app",
763
1236
  validate: (value) => {
764
1237
  if (!value.trim()) return "A directory name is required.";
765
- const abs = path__default.default.resolve(process.cwd(), value);
1238
+ const abs = path3__default.default.resolve(process.cwd(), value);
766
1239
  if (fs.existsSync(abs)) return `${value} already exists. Pick a different name.`;
767
1240
  return true;
768
1241
  }
@@ -840,7 +1313,7 @@ ${kleur__default.default.bold().green("\u25B2")} ${kleur__default.default.bold("
840
1313
  return {
841
1314
  cancelled: false,
842
1315
  options: {
843
- targetDir: path__default.default.resolve(process.cwd(), targetDir),
1316
+ targetDir: path3__default.default.resolve(process.cwd(), targetDir),
844
1317
  template,
845
1318
  provider,
846
1319
  tools,
@@ -857,7 +1330,7 @@ ${kleur__default.default.bold().green("\u25B2")} ${kleur__default.default.bold("
857
1330
  }
858
1331
  }
859
1332
  function printNextSteps(options) {
860
- const dir = path__default.default.relative(process.cwd(), options.targetDir) || ".";
1333
+ const dir = path3__default.default.relative(process.cwd(), options.targetDir) || ".";
861
1334
  const pm = options.packageManager ?? "pnpm";
862
1335
  const installCmd = pm === "npm" ? "npm install" : `${pm} install`;
863
1336
  const runCmd = pm === "npm" ? "npm run dev" : `${pm} dev`;
@@ -1071,17 +1544,17 @@ async function checkNodeVersion() {
1071
1544
  }
1072
1545
  async function checkPnpm() {
1073
1546
  const cwd = process.cwd();
1074
- const hasPnpm = fs.existsSync(path.join(cwd, "pnpm-lock.yaml")) || fs.existsSync(path.join(cwd, "pnpm-workspace.yaml"));
1547
+ const hasPnpm = fs.existsSync(path3.join(cwd, "pnpm-lock.yaml")) || fs.existsSync(path3.join(cwd, "pnpm-workspace.yaml"));
1075
1548
  if (hasPnpm) {
1076
1549
  return { status: "pass", name: "Package manager", detail: "pnpm detected (lockfile)" };
1077
1550
  }
1078
- if (fs.existsSync(path.join(cwd, "package-lock.json"))) {
1551
+ if (fs.existsSync(path3.join(cwd, "package-lock.json"))) {
1079
1552
  return { status: "warn", name: "Package manager", detail: "npm detected \u2014 pnpm recommended for monorepo workflows" };
1080
1553
  }
1081
- if (fs.existsSync(path.join(cwd, "yarn.lock"))) {
1554
+ if (fs.existsSync(path3.join(cwd, "yarn.lock"))) {
1082
1555
  return { status: "pass", name: "Package manager", detail: "yarn detected" };
1083
1556
  }
1084
- if (fs.existsSync(path.join(cwd, "bun.lock")) || fs.existsSync(path.join(cwd, "bun.lockb"))) {
1557
+ if (fs.existsSync(path3.join(cwd, "bun.lock")) || fs.existsSync(path3.join(cwd, "bun.lockb"))) {
1085
1558
  return { status: "pass", name: "Package manager", detail: "bun detected" };
1086
1559
  }
1087
1560
  return {
@@ -1091,7 +1564,7 @@ async function checkPnpm() {
1091
1564
  };
1092
1565
  }
1093
1566
  async function checkPackageJson() {
1094
- const path4 = path.join(process.cwd(), "package.json");
1567
+ const path4 = path3.join(process.cwd(), "package.json");
1095
1568
  if (!fs.existsSync(path4)) {
1096
1569
  return {
1097
1570
  status: "warn",
@@ -1316,7 +1789,7 @@ var DEFAULT_IGNORE = [
1316
1789
  "**/*.spec.ts"
1317
1790
  ];
1318
1791
  function startDev(options) {
1319
- const entry = path.resolve(process.cwd(), options.entry);
1792
+ const entry = path3.resolve(process.cwd(), options.entry);
1320
1793
  if (!fs.existsSync(entry)) {
1321
1794
  throw new Error(`Entry file not found: ${entry}`);
1322
1795
  }
@@ -1348,7 +1821,7 @@ function startDev(options) {
1348
1821
  };
1349
1822
  const startChild = () => {
1350
1823
  restartCount++;
1351
- banner(`\u25B8 starting ${kleur__default.default.bold(path.basename(entry))} (restart #${restartCount - 1})`, "cyan");
1824
+ banner(`\u25B8 starting ${kleur__default.default.bold(path3.basename(entry))} (restart #${restartCount - 1})`, "cyan");
1352
1825
  const c = spawnFn(cmd, baseArgs);
1353
1826
  child = c;
1354
1827
  c.stdout?.on("data", (d) => stdout.write(d));
@@ -1472,34 +1945,87 @@ async function startTunnel(options) {
1472
1945
  // src/commands.ts
1473
1946
  function mergeWithConfig(options, config) {
1474
1947
  if (!config) return options;
1948
+ const d = config.defaults ?? {};
1949
+ const resolvedApiKey = options.apiKey ?? (d.apiKeyEnv ? process.env[d.apiKeyEnv] : void 0) ?? d.apiKey;
1475
1950
  return {
1476
1951
  ...options,
1477
1952
  // Config defaults — only apply if CLI flag wasn't set
1478
- provider: options.provider !== "demo" ? options.provider : config.defaults?.provider ?? options.provider,
1479
- model: options.model ?? config.defaults?.model
1953
+ provider: options.provider !== "demo" ? options.provider : d.provider ?? options.provider,
1954
+ model: options.model ?? d.model,
1955
+ apiKey: resolvedApiKey,
1956
+ baseUrl: options.baseUrl ?? d.baseUrl,
1957
+ tools: options.tools ?? d.tools,
1958
+ skill: options.skill ?? d.skill,
1959
+ system: options.system ?? d.system,
1960
+ memoryBackend: options.memoryBackend ?? d.memoryBackend
1480
1961
  };
1481
1962
  }
1482
1963
  function createCli() {
1483
1964
  const program = new commander.Command();
1484
1965
  program.name("agentskit").description("AgentsKit CLI for chat demos and project bootstrapping.");
1485
- program.command("chat").description("Start a terminal chat session.").option("--provider <provider>", "Provider to use", "demo").option("--model <model>", "Model name").option("--api-key <key>", "API key for the selected provider").option("--base-url <url>", "Override provider base URL").option("--system <prompt>", "System prompt").option("--memory <path>", "Path for file-based memory", ".agentskit-history.json").option("--tools <tools>", "Comma-separated tools: web_search,filesystem,shell").option("--skill <skills>", "Comma-separated skills: researcher,coder,planner,critic,summarizer").option("--memory-backend <backend>", "Memory backend: file (default), sqlite").option("--no-config", "Skip loading .agentskit.config.json").action(async (options) => {
1966
+ program.command("chat").description("Start a terminal chat session.").option("--provider <provider>", "Provider to use", "demo").option("--model <model>", "Model name").option("--api-key <key>", "API key for the selected provider").option("--base-url <url>", "Override provider base URL").option("--system <prompt>", "System prompt").option("--memory <path>", "Explicit memory file path (overrides session management)").option("--tools <tools>", "Comma-separated tools: web_search,fetch_url,filesystem,shell").option("--skill <skills>", "Comma-separated skills: researcher,coder,planner,critic,summarizer").option("--memory-backend <backend>", "Memory backend: file (default), sqlite").option("--new", "Start a fresh chat session (ignore previous conversations in this directory)").option("--resume [id]", "Resume a prior session by id; omit id to resume the latest").option("--list-sessions", "List saved sessions for this directory and exit").option("--no-config", "Skip loading .agentskit.config.json").action(async (options) => {
1967
+ if (options.listSessions) {
1968
+ const sessions = listSessions();
1969
+ if (sessions.length === 0) {
1970
+ process.stdout.write("No saved sessions for this directory.\n");
1971
+ return;
1972
+ }
1973
+ for (const s of sessions) {
1974
+ const { id, updatedAt, messageCount, preview, model } = s.metadata;
1975
+ process.stdout.write(
1976
+ `${id} ${updatedAt} msgs=${messageCount}${model ? ` model=${model}` : ""}
1977
+ ${preview}
1978
+ `
1979
+ );
1980
+ }
1981
+ return;
1982
+ }
1486
1983
  const config = options.config !== false ? await loadConfig() : void 0;
1487
1984
  const merged = mergeWithConfig(options, config);
1985
+ const session = resolveSession({
1986
+ explicitPath: options.memory,
1987
+ forceNew: Boolean(options.new),
1988
+ resumeId: options.resume
1989
+ });
1990
+ if (!session.isNew && !options.memory) {
1991
+ process.stdout.write(
1992
+ `Resuming session ${session.id}. Start fresh with --new or list with --list-sessions.
1993
+ `
1994
+ );
1995
+ }
1488
1996
  const chatOptions = {
1489
1997
  apiKey: merged.apiKey ?? options.apiKey,
1490
1998
  baseUrl: merged.baseUrl ?? options.baseUrl,
1491
1999
  provider: merged.provider,
1492
2000
  model: merged.model,
1493
- system: options.system,
1494
- memoryPath: options.memory,
1495
- tools: options.tools,
1496
- skill: options.skill,
1497
- memoryBackend: options.memoryBackend,
2001
+ system: merged.system ?? options.system,
2002
+ memoryPath: session.file,
2003
+ sessionId: session.id,
2004
+ tools: merged.tools ?? options.tools,
2005
+ skill: merged.skill ?? options.skill,
2006
+ memoryBackend: merged.memoryBackend ?? options.memoryBackend,
1498
2007
  agentsKitConfig: config
1499
2008
  };
1500
2009
  process.stdout.write(`${renderChatHeader(chatOptions)}
1501
2010
  `);
1502
- ink.render(React3__default.default.createElement(ChatApp, chatOptions));
2011
+ const instance = ink.render(React3__default.default.createElement(ChatApp, chatOptions));
2012
+ await instance.waitUntilExit();
2013
+ if (options.memory) {
2014
+ process.stdout.write(
2015
+ `
2016
+ Session saved to ${session.file}. Resume with --memory ${session.file}
2017
+ `
2018
+ );
2019
+ } else {
2020
+ process.stdout.write(
2021
+ `
2022
+ Session saved. Resume with:
2023
+ agentskit chat --resume ${session.id}
2024
+ Or start fresh with:
2025
+ agentskit chat --new
2026
+ `
2027
+ );
2028
+ }
1503
2029
  });
1504
2030
  program.command("run [task]").description("Execute an agent task and output the result.").option("--task <task>", "Task string (alternative to positional argument)").option("--provider <provider>", "Provider to use", "demo").option("--model <model>", "Model name").option("--api-key <key>", "API key for the selected provider").option("--base-url <url>", "Override provider base URL").option("--skill <skill>", "Single skill to use").option("--skills <skills>", "Comma-separated skills (composed together)").option("--tools <tools>", "Comma-separated tools: web_search,filesystem,shell").option("--memory <path>", "Path for memory persistence").option("--memory-backend <backend>", "Memory backend: file (default), sqlite").option("--system-prompt <prompt>", "System prompt").option("--max-steps <steps>", "Maximum agent steps", "10").option("--verbose", "Stream agent steps to stderr").option("--pretty", "Use rich Ink-based output").option("--no-config", "Skip loading .agentskit.config.json").action(async (positionalTask, options) => {
1505
2031
  const task = options.task ?? positionalTask;
@@ -1527,7 +2053,7 @@ function createCli() {
1527
2053
  if (isCi) {
1528
2054
  const template = rawOptions.template ?? "react";
1529
2055
  resolved = {
1530
- targetDir: path__default.default.resolve(process.cwd(), rawOptions.dir),
2056
+ targetDir: path3__default.default.resolve(process.cwd(), rawOptions.dir),
1531
2057
  template,
1532
2058
  provider: rawOptions.provider ?? "demo",
1533
2059
  tools: rawOptions.tools ? rawOptions.tools.split(",").map((t) => t.trim()) : [],
@@ -1547,7 +2073,7 @@ function createCli() {
1547
2073
  await writeStarterProject(resolved);
1548
2074
  if (isCi) {
1549
2075
  process.stdout.write(
1550
- `Created ${resolved.template} starter in ${path__default.default.relative(process.cwd(), resolved.targetDir) || "."}
2076
+ `Created ${resolved.template} starter in ${path3__default.default.relative(process.cwd(), resolved.targetDir) || "."}
1551
2077
  `
1552
2078
  );
1553
2079
  } else {
@@ -1588,6 +2114,43 @@ function createCli() {
1588
2114
  process.exit(1);
1589
2115
  }
1590
2116
  });
2117
+ program.command("config").description("Show or scaffold the AgentsKit config.").argument("[action]", 'Action: "init" to create a template, "show" to print the merged config.', "show").option("--global", "Write/read the global config at ~/.agentskit/config.json (default)").option("--local", "Write/read a project-level .agentskit.config.json in the current directory").option("--force", "Overwrite an existing config file").action(async (action, options) => {
2118
+ const isLocal = Boolean(options.local);
2119
+ const targetPath = isLocal ? path3__default.default.join(process.cwd(), ".agentskit.config.json") : path3__default.default.join(os.homedir(), ".agentskit", "config.json");
2120
+ if (action === "show") {
2121
+ const config = await loadConfig();
2122
+ process.stdout.write(JSON.stringify(config ?? {}, null, 2) + "\n");
2123
+ return;
2124
+ }
2125
+ if (action !== "init") {
2126
+ process.stderr.write(`Unknown action: ${action}. Use "init" or "show".
2127
+ `);
2128
+ process.exit(2);
2129
+ }
2130
+ if (fs.existsSync(targetPath) && !options.force) {
2131
+ process.stderr.write(`Config already exists at ${targetPath}. Re-run with --force to overwrite.
2132
+ `);
2133
+ process.exit(1);
2134
+ }
2135
+ const template = {
2136
+ defaults: {
2137
+ provider: "openai",
2138
+ baseUrl: "https://openrouter.ai/api",
2139
+ apiKeyEnv: "OPENROUTER_API_KEY",
2140
+ model: "openai/gpt-oss-120b:free",
2141
+ tools: "web_search,fetch_url"
2142
+ }
2143
+ };
2144
+ fs.mkdirSync(path3__default.default.dirname(targetPath), { recursive: true });
2145
+ fs.writeFileSync(targetPath, JSON.stringify(template, null, 2) + "\n");
2146
+ process.stdout.write(
2147
+ `Wrote ${targetPath}
2148
+ Edit it to taste, then run:
2149
+ agentskit chat
2150
+ (flags on the CLI still win over config values.)
2151
+ `
2152
+ );
2153
+ });
1591
2154
  program.command("tunnel <port>").description("Open a public URL pointing to a local port (great for webhooks).").option("--subdomain <name>", "Hint for a stable subdomain (provider may decline)").option("--host <host>", "Local hostname", "localhost").action(async (port, options) => {
1592
2155
  const portNum = Number(port);
1593
2156
  if (Number.isNaN(portNum) || portNum < 1 || portNum > 65535) {