@agentskit/cli 0.5.3 → 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/index.cjs CHANGED
@@ -4,17 +4,19 @@
4
4
  var React3 = require('react');
5
5
  var ink$1 = 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 = 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,39 @@ async function loadPackageJsonConfig(dir) {
54
56
  return void 0;
55
57
  }
56
58
  }
57
- async function loadConfig(options) {
58
- const cwd = path.resolve(options?.cwd ?? 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
+ if (home === null) return void 0;
81
+ const globalDir = path3.join(home ?? os.homedir(), ".agentskit");
82
+ const tsConfig = await loadTsConfig(path3.join(globalDir, "config.ts"));
83
+ if (tsConfig) return tsConfig;
84
+ return await loadJsonConfig(path3.join(globalDir, "config.json"));
85
+ }
86
+ async function loadConfig(options) {
87
+ const cwd = path3.resolve(options?.cwd ?? process.cwd());
88
+ const global = await loadGlobalConfig(options?.home);
89
+ const local = await loadLocalConfig(cwd);
90
+ return mergeConfigs(global, local);
91
+ }
67
92
  var providers = {
68
93
  openai: {
69
94
  label: "OpenAI",
@@ -181,26 +206,41 @@ var skillRegistry = {
181
206
  critic: skills.critic,
182
207
  summarizer: skills.summarizer
183
208
  };
209
+ function instantiate(kind) {
210
+ switch (kind) {
211
+ case "web_search":
212
+ return [tools.webSearch()];
213
+ case "fetch_url":
214
+ return [tools.fetchUrl()];
215
+ case "filesystem":
216
+ return tools.filesystem({ basePath: process.cwd() });
217
+ case "shell":
218
+ return [tools.shell({ timeout: 3e4 })];
219
+ }
220
+ }
221
+ function gateTool(tool) {
222
+ if (tool.requiresConfirmation === false) return tool;
223
+ return { ...tool, requiresConfirmation: true };
224
+ }
184
225
  function resolveTools(toolNames) {
185
- if (!toolNames) return [];
186
- const tools$1 = [];
187
- for (const name of toolNames.split(",").map((s) => s.trim())) {
226
+ if (!toolNames) {
227
+ return [...instantiate("web_search"), ...instantiate("fetch_url")].map(gateTool);
228
+ }
229
+ const tools = [];
230
+ for (const name of toolNames.split(",").map((s) => s.trim()).filter(Boolean)) {
188
231
  switch (name) {
189
232
  case "web_search":
190
- tools$1.push(tools.webSearch());
191
- break;
233
+ case "fetch_url":
192
234
  case "filesystem":
193
- tools$1.push(...tools.filesystem({ basePath: process.cwd() }));
194
- break;
195
235
  case "shell":
196
- tools$1.push(tools.shell({ timeout: 3e4 }));
236
+ tools.push(...instantiate(name));
197
237
  break;
198
238
  default:
199
239
  process.stderr.write(`Unknown tool: ${name}
200
240
  `);
201
241
  }
202
242
  }
203
- return tools$1;
243
+ return tools;
204
244
  }
205
245
  function resolveSkill(skillName) {
206
246
  if (!skillName) return void 0;
@@ -233,6 +273,244 @@ function resolveMemory(backend, memoryPath) {
233
273
  return memory.fileChatMemory(memoryPath);
234
274
  }
235
275
  }
276
+ var ROOT = path3.join(os.homedir(), ".agentskit", "sessions");
277
+ var META_SUFFIX = ".meta.json";
278
+ function cwdHash(cwd = process.cwd()) {
279
+ return crypto.createHash("sha256").update(cwd).digest("hex").slice(0, 12);
280
+ }
281
+ function dirFor(cwd = process.cwd()) {
282
+ return path3.join(ROOT, cwdHash(cwd));
283
+ }
284
+ function ensureDir(dir) {
285
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
286
+ }
287
+ function generateSessionId() {
288
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
289
+ const suffix = crypto.randomBytes(3).toString("hex");
290
+ return `${ts}-${suffix}`;
291
+ }
292
+ function sessionFilePath(id, cwd = process.cwd()) {
293
+ ensureDir(dirFor(cwd));
294
+ return path3.join(dirFor(cwd), `${id}.json`);
295
+ }
296
+ function metaPath(id, cwd = process.cwd()) {
297
+ return path3.join(dirFor(cwd), `${id}${META_SUFFIX}`);
298
+ }
299
+ function readMeta(id, cwd = process.cwd()) {
300
+ const path4 = metaPath(id, cwd);
301
+ if (!fs.existsSync(path4)) return null;
302
+ try {
303
+ return JSON.parse(fs.readFileSync(path4, "utf8"));
304
+ } catch {
305
+ return null;
306
+ }
307
+ }
308
+ function writeSessionMeta(meta, cwd = process.cwd()) {
309
+ ensureDir(dirFor(cwd));
310
+ fs.writeFileSync(metaPath(meta.id, cwd), JSON.stringify(meta, null, 2));
311
+ }
312
+ function derivePreview(messages) {
313
+ const firstUser = messages.find((m) => m.role === "user" && m.content.trim());
314
+ if (!firstUser) return "(empty)";
315
+ const single = firstUser.content.replace(/\s+/g, " ").trim();
316
+ return single.length > 80 ? `${single.slice(0, 80)}\u2026` : single;
317
+ }
318
+ function listSessions(cwd = process.cwd()) {
319
+ const dir = dirFor(cwd);
320
+ if (!fs.existsSync(dir)) return [];
321
+ const entries = fs.readdirSync(dir);
322
+ const records = [];
323
+ for (const entry of entries) {
324
+ if (!entry.endsWith(".json") || entry.endsWith(META_SUFFIX)) continue;
325
+ const id = entry.replace(/\.json$/, "");
326
+ const meta = readMeta(id, cwd);
327
+ const file = path3.join(dir, entry);
328
+ if (meta) {
329
+ records.push({ metadata: meta, file });
330
+ } else {
331
+ const stats = fs.statSync(file);
332
+ records.push({
333
+ metadata: {
334
+ id,
335
+ cwd,
336
+ createdAt: stats.birthtime.toISOString(),
337
+ updatedAt: stats.mtime.toISOString(),
338
+ messageCount: 0,
339
+ preview: "(legacy session)"
340
+ },
341
+ file
342
+ });
343
+ }
344
+ }
345
+ records.sort((a, b) => b.metadata.updatedAt.localeCompare(a.metadata.updatedAt));
346
+ return records;
347
+ }
348
+ function findLatestSession(cwd = process.cwd()) {
349
+ const all = listSessions(cwd);
350
+ return all[0] ?? null;
351
+ }
352
+ function findSession(id, cwd = process.cwd()) {
353
+ const exact = listSessions(cwd).find((s) => s.metadata.id === id);
354
+ if (exact) return exact;
355
+ const prefix = listSessions(cwd).find((s) => s.metadata.id.startsWith(id));
356
+ return prefix ?? null;
357
+ }
358
+ function resolveSession(input2) {
359
+ const cwd = input2.cwd ?? process.cwd();
360
+ if (input2.explicitPath) {
361
+ return { id: "custom", file: input2.explicitPath, isNew: !fs.existsSync(input2.explicitPath) };
362
+ }
363
+ if (input2.forceNew) {
364
+ const id2 = generateSessionId();
365
+ return { id: id2, file: sessionFilePath(id2, cwd), isNew: true };
366
+ }
367
+ if (input2.resumeId) {
368
+ const target = input2.resumeId === true ? findLatestSession(cwd) : findSession(input2.resumeId, cwd);
369
+ if (target) {
370
+ return { id: target.metadata.id, file: target.file, isNew: false };
371
+ }
372
+ process.stderr.write(
373
+ `No session matching "${String(input2.resumeId)}" \u2014 starting a new one.
374
+ `
375
+ );
376
+ }
377
+ const latest = findLatestSession(cwd);
378
+ if (latest) return { id: latest.metadata.id, file: latest.file, isNew: false };
379
+ const id = generateSessionId();
380
+ return { id, file: sessionFilePath(id, cwd), isNew: true };
381
+ }
382
+
383
+ // src/slash-commands.ts
384
+ function parseSlashCommand(input2) {
385
+ if (!input2.startsWith("/")) return null;
386
+ const match = input2.slice(1).match(/^(\S+)\s*([\s\S]*)$/);
387
+ if (!match) return null;
388
+ return { name: match[1], args: match[2] ?? "" };
389
+ }
390
+ function createSlashRegistry(commands) {
391
+ const map = /* @__PURE__ */ new Map();
392
+ for (const cmd of commands) {
393
+ map.set(cmd.name, cmd);
394
+ for (const alias of cmd.aliases ?? []) map.set(alias, cmd);
395
+ }
396
+ return map;
397
+ }
398
+ var builtinSlashCommands = [
399
+ {
400
+ name: "help",
401
+ aliases: ["?"],
402
+ description: "List available slash commands.",
403
+ run(ctx) {
404
+ const seen = /* @__PURE__ */ new Set();
405
+ const lines = [];
406
+ for (const cmd of ctx.commands) {
407
+ if (seen.has(cmd.name)) continue;
408
+ seen.add(cmd.name);
409
+ const suffix = cmd.usage ? ` (${cmd.usage})` : "";
410
+ lines.push(` /${cmd.name.padEnd(10)} ${cmd.description}${suffix}`);
411
+ }
412
+ ctx.feedback(`Slash commands:
413
+ ${lines.join("\n")}`, "info");
414
+ }
415
+ },
416
+ {
417
+ name: "model",
418
+ description: "Switch the active model.",
419
+ usage: "/model <name>",
420
+ run(ctx, args) {
421
+ const value = args.trim();
422
+ if (!value) {
423
+ ctx.feedback(
424
+ `Current model: ${ctx.runtime.model ?? "unset"}. Usage: /model <name>`,
425
+ "warn"
426
+ );
427
+ return;
428
+ }
429
+ ctx.setModel(value);
430
+ ctx.feedback(`Model \u2192 ${value}`, "success");
431
+ }
432
+ },
433
+ {
434
+ name: "provider",
435
+ description: "Switch the adapter provider.",
436
+ usage: "/provider openai|anthropic|gemini|ollama|deepseek|grok|kimi|demo",
437
+ run(ctx, args) {
438
+ const value = args.trim();
439
+ if (!value) {
440
+ ctx.feedback(
441
+ `Current provider: ${ctx.runtime.provider}. Usage: /provider <name>`,
442
+ "warn"
443
+ );
444
+ return;
445
+ }
446
+ ctx.setProvider(value);
447
+ ctx.feedback(`Provider \u2192 ${value}`, "success");
448
+ }
449
+ },
450
+ {
451
+ name: "base-url",
452
+ aliases: ["baseurl"],
453
+ description: "Override provider base URL.",
454
+ usage: "/base-url <url|clear>",
455
+ run(ctx, args) {
456
+ const value = args.trim();
457
+ if (!value || value === "clear") {
458
+ ctx.setBaseUrl(void 0);
459
+ ctx.feedback("Base URL cleared.", "success");
460
+ return;
461
+ }
462
+ ctx.setBaseUrl(value);
463
+ ctx.feedback(`Base URL \u2192 ${value}`, "success");
464
+ }
465
+ },
466
+ {
467
+ name: "tools",
468
+ description: "Set active tools (comma-separated) or clear them.",
469
+ usage: "/tools web_search,fetch_url | /tools clear",
470
+ run(ctx, args) {
471
+ const value = args.trim();
472
+ if (!value || value === "clear") {
473
+ ctx.setTools(void 0);
474
+ ctx.feedback("Tools reset to defaults.", "success");
475
+ return;
476
+ }
477
+ ctx.setTools(value);
478
+ ctx.feedback(`Tools \u2192 ${value}`, "success");
479
+ }
480
+ },
481
+ {
482
+ name: "skill",
483
+ description: "Set active skill(s) (comma-separated) or clear them.",
484
+ usage: "/skill researcher,coder | /skill clear",
485
+ run(ctx, args) {
486
+ const value = args.trim();
487
+ if (!value || value === "clear") {
488
+ ctx.setSkill(void 0);
489
+ ctx.feedback("Skills cleared.", "success");
490
+ return;
491
+ }
492
+ ctx.setSkill(value);
493
+ ctx.feedback(`Skills \u2192 ${value}`, "success");
494
+ }
495
+ },
496
+ {
497
+ name: "clear",
498
+ aliases: ["reset"],
499
+ description: "Clear the conversation history in this session.",
500
+ async run(ctx) {
501
+ await ctx.chat.clear();
502
+ ctx.feedback("History cleared.", "success");
503
+ }
504
+ },
505
+ {
506
+ name: "exit",
507
+ aliases: ["quit", "q"],
508
+ description: "Exit the chat.",
509
+ run() {
510
+ process.exit(0);
511
+ }
512
+ }
513
+ ];
236
514
  function groupIntoTurns(messages) {
237
515
  const turns = [];
238
516
  let current = [];
@@ -252,24 +530,28 @@ function groupIntoTurns(messages) {
252
530
  return turns;
253
531
  }
254
532
  function ChatApp(options) {
255
- const runtime = React3.useMemo(() => resolveChatProvider(options), [
256
- options.apiKey,
257
- options.baseUrl,
258
- options.model,
259
- options.provider
260
- ]);
533
+ const [provider, setProvider] = React3.useState(options.provider);
534
+ const [model, setModel] = React3.useState(options.model);
535
+ const [apiKey, setApiKey] = React3.useState(options.apiKey);
536
+ const [baseUrl, setBaseUrl] = React3.useState(options.baseUrl);
537
+ const [toolsFlag, setToolsFlag] = React3.useState(options.tools);
538
+ const [skillFlag, setSkillFlag] = React3.useState(options.skill);
539
+ const runtime = React3.useMemo(
540
+ () => resolveChatProvider({ provider, model, apiKey, baseUrl }),
541
+ [provider, model, apiKey, baseUrl]
542
+ );
261
543
  const memory = React3.useMemo(
262
544
  () => resolveMemory(options.memoryBackend, options.memoryPath ?? ".agentskit-history.json"),
263
545
  [options.memoryPath, options.memoryBackend]
264
546
  );
265
- const tools = React3.useMemo(() => resolveTools(options.tools), [options.tools]);
547
+ const tools = React3.useMemo(() => resolveTools(toolsFlag), [toolsFlag]);
266
548
  const skills = React3.useMemo(() => {
267
- if (!options.skill) return void 0;
268
- const names = options.skill.split(",").map((s) => s.trim());
549
+ if (!skillFlag) return void 0;
550
+ const names = skillFlag.split(",").map((s) => s.trim());
269
551
  const resolved = names.map((n) => skillRegistry[n]).filter(Boolean);
270
552
  if (resolved.length === 0) return void 0;
271
553
  return resolved;
272
- }, [options.skill]);
554
+ }, [skillFlag]);
273
555
  const chat = ink.useChat({
274
556
  adapter: runtime.adapter,
275
557
  memory,
@@ -277,8 +559,109 @@ function ChatApp(options) {
277
559
  tools: tools.length > 0 ? tools : void 0,
278
560
  skills
279
561
  });
562
+ const [sessionAllowed, setSessionAllowed] = React3.useState(/* @__PURE__ */ new Set());
563
+ const autoApprovedRef = React3.useRef(/* @__PURE__ */ new Set());
564
+ React3.useEffect(() => {
565
+ if (sessionAllowed.size === 0) return;
566
+ for (const message of chat.messages) {
567
+ for (const call of message.toolCalls ?? []) {
568
+ if (call.status === "requires_confirmation" && sessionAllowed.has(call.name) && !autoApprovedRef.current.has(call.id)) {
569
+ autoApprovedRef.current.add(call.id);
570
+ void chat.approve(call.id);
571
+ }
572
+ }
573
+ }
574
+ }, [chat.messages, sessionAllowed, chat.approve]);
575
+ const handleApproveAlways = (toolCallId, toolName) => {
576
+ setSessionAllowed((prev) => {
577
+ if (prev.has(toolName)) return prev;
578
+ const next = new Set(prev);
579
+ next.add(toolName);
580
+ return next;
581
+ });
582
+ autoApprovedRef.current.add(toolCallId);
583
+ void chat.approve(toolCallId);
584
+ };
585
+ const sessionCreatedAtRef = React3.useRef(void 0);
586
+ const messageCount = chat.messages.length;
587
+ const firstUserContent = chat.messages.find((m) => m.role === "user")?.content ?? "";
588
+ React3.useEffect(() => {
589
+ const sessionId = options.sessionId;
590
+ if (!sessionId || sessionId === "custom") return;
591
+ if (!sessionCreatedAtRef.current) {
592
+ sessionCreatedAtRef.current = (/* @__PURE__ */ new Date()).toISOString();
593
+ }
594
+ try {
595
+ writeSessionMeta({
596
+ id: sessionId,
597
+ cwd: process.cwd(),
598
+ createdAt: sessionCreatedAtRef.current,
599
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
600
+ messageCount,
601
+ preview: derivePreview(chat.messages),
602
+ provider: runtime.provider,
603
+ model: runtime.model
604
+ });
605
+ } catch {
606
+ }
607
+ }, [options.sessionId, messageCount, firstUserContent, runtime.provider, runtime.model]);
280
608
  const turns = React3.useMemo(() => groupIntoTurns(chat.messages), [chat.messages]);
281
- const toolNames = options.tools ? options.tools.split(",").map((s) => s.trim()).filter(Boolean) : [];
609
+ const toolNames = toolsFlag ? toolsFlag.split(",").map((s) => s.trim()).filter(Boolean) : [];
610
+ const [feedback, setFeedback] = React3.useState(null);
611
+ const slashCommands = React3.useMemo(
612
+ () => [...builtinSlashCommands, ...options.slashCommands ?? []],
613
+ [options.slashCommands]
614
+ );
615
+ const slashRegistry = React3.useMemo(() => createSlashRegistry(slashCommands), [slashCommands]);
616
+ const handleSubmitInput = async (raw) => {
617
+ const parsed = parseSlashCommand(raw);
618
+ if (!parsed) {
619
+ setFeedback(null);
620
+ return false;
621
+ }
622
+ const cmd = slashRegistry.get(parsed.name);
623
+ if (!cmd) {
624
+ setFeedback({
625
+ message: `Unknown command: /${parsed.name}. Type /help for the list.`,
626
+ kind: "error"
627
+ });
628
+ return true;
629
+ }
630
+ const ctx = {
631
+ chat,
632
+ runtime: {
633
+ provider: runtime.provider,
634
+ model: runtime.model,
635
+ mode: runtime.mode,
636
+ baseUrl,
637
+ tools: toolsFlag,
638
+ skill: skillFlag
639
+ },
640
+ setProvider,
641
+ setModel,
642
+ setApiKey,
643
+ setBaseUrl,
644
+ setTools: setToolsFlag,
645
+ setSkill: setSkillFlag,
646
+ feedback: (message, kind = "info") => setFeedback({ message, kind }),
647
+ commands: slashCommands
648
+ };
649
+ try {
650
+ await cmd.run(ctx, parsed.args);
651
+ } catch (err) {
652
+ const message = err instanceof Error ? err.message : String(err);
653
+ setFeedback({ message: `/${parsed.name} failed: ${message}`, kind: "error" });
654
+ }
655
+ return true;
656
+ };
657
+ const awaitingConfirmation = React3.useMemo(
658
+ () => chat.messages.some(
659
+ (message) => message.toolCalls?.some(
660
+ (call) => call.status === "requires_confirmation" && !sessionAllowed.has(call.name)
661
+ )
662
+ ),
663
+ [chat.messages, sessionAllowed]
664
+ );
282
665
  return /* @__PURE__ */ jsxRuntime.jsxs(ink$1.Box, { flexDirection: "column", gap: 1, children: [
283
666
  /* @__PURE__ */ jsxRuntime.jsx(
284
667
  ink.StatusHeader,
@@ -287,7 +670,8 @@ function ChatApp(options) {
287
670
  model: runtime.model,
288
671
  mode: runtime.mode,
289
672
  tools: toolNames,
290
- messageCount: chat.messages.length
673
+ messageCount: chat.messages.length,
674
+ sessionId: options.sessionId
291
675
  }
292
676
  ),
293
677
  /* @__PURE__ */ jsxRuntime.jsx(ink.ChatContainer, { children: turns.map((turn, turnIdx) => {
@@ -304,7 +688,18 @@ function ChatApp(options) {
304
688
  assistantSteps
305
689
  ] }) : null,
306
690
  /* @__PURE__ */ jsxRuntime.jsx(ink.Message, { message }),
307
- message.toolCalls?.map((toolCall) => /* @__PURE__ */ jsxRuntime.jsx(ink.ToolCallView, { toolCall, expanded: true }, toolCall.id))
691
+ message.toolCalls?.map((toolCall) => /* @__PURE__ */ jsxRuntime.jsxs(ink$1.Box, { flexDirection: "column", children: [
692
+ /* @__PURE__ */ jsxRuntime.jsx(ink.ToolCallView, { toolCall, expanded: true }),
693
+ toolCall.status === "requires_confirmation" && !sessionAllowed.has(toolCall.name) ? /* @__PURE__ */ jsxRuntime.jsx(
694
+ ink.ToolConfirmation,
695
+ {
696
+ toolCall,
697
+ onApprove: chat.approve,
698
+ onDeny: chat.deny,
699
+ onApproveAlways: handleApproveAlways
700
+ }
701
+ ) : null
702
+ ] }, toolCall.id))
308
703
  ] }, message.id);
309
704
  }) }, `turn-${turnIdx}`);
310
705
  }) }),
@@ -315,9 +710,38 @@ function ChatApp(options) {
315
710
  label: toolNames.length > 0 ? "agent working" : "thinking"
316
711
  }
317
712
  ),
318
- /* @__PURE__ */ jsxRuntime.jsx(ink.InputBar, { chat, placeholder: "Type a message and press Enter\u2026" })
713
+ chat.error ? /* @__PURE__ */ jsxRuntime.jsxs(ink$1.Box, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", children: [
714
+ /* @__PURE__ */ jsxRuntime.jsxs(ink$1.Text, { color: "red", bold: true, children: [
715
+ "\u2717 ",
716
+ chat.error.name || "Error"
717
+ ] }),
718
+ /* @__PURE__ */ jsxRuntime.jsx(ink$1.Text, { color: "red", children: chat.error.message })
719
+ ] }) : null,
720
+ feedback ? /* @__PURE__ */ jsxRuntime.jsx(ink$1.Box, { borderStyle: "round", borderColor: feedbackBorder(feedback.kind), paddingX: 1, children: /* @__PURE__ */ jsxRuntime.jsx(ink$1.Text, { color: feedbackBorder(feedback.kind), children: feedback.message }) }) : null,
721
+ /* @__PURE__ */ jsxRuntime.jsx(
722
+ ink.InputBar,
723
+ {
724
+ chat,
725
+ placeholder: "Type a message or /help for commands",
726
+ disabled: awaitingConfirmation,
727
+ onSubmitInput: handleSubmitInput
728
+ }
729
+ )
319
730
  ] });
320
731
  }
732
+ function feedbackBorder(kind) {
733
+ switch (kind) {
734
+ case "error":
735
+ return "red";
736
+ case "warn":
737
+ return "yellow";
738
+ case "success":
739
+ return "green";
740
+ case "info":
741
+ default:
742
+ return "cyan";
743
+ }
744
+ }
321
745
  function renderChatHeader(options) {
322
746
  const runtime = resolveChatProvider(options);
323
747
  const parts = [`provider=${runtime.provider}`];
@@ -426,7 +850,7 @@ function reactStarter(ctx) {
426
850
  return {
427
851
  "package.json": JSON.stringify(
428
852
  {
429
- name: path__default.default.basename(ctx.template === "react" ? "agentskit-react-app" : "agentskit-app"),
853
+ name: path3__default.default.basename(ctx.template === "react" ? "agentskit-react-app" : "agentskit-app"),
430
854
  private: true,
431
855
  type: "module",
432
856
  scripts: {
@@ -795,8 +1219,8 @@ async function writeStarterProject(options) {
795
1219
  await promises.mkdir(options.targetDir, { recursive: true });
796
1220
  await Promise.all(
797
1221
  Object.entries(files).map(async ([relativePath, content]) => {
798
- const absolutePath = path__default.default.join(options.targetDir, relativePath);
799
- await promises.mkdir(path__default.default.dirname(absolutePath), { recursive: true });
1222
+ const absolutePath = path3__default.default.join(options.targetDir, relativePath);
1223
+ await promises.mkdir(path3__default.default.dirname(absolutePath), { recursive: true });
800
1224
  await promises.writeFile(absolutePath, content, "utf8");
801
1225
  })
802
1226
  );
@@ -812,7 +1236,7 @@ ${kleur__default.default.bold().green("\u25B2")} ${kleur__default.default.bold("
812
1236
  default: defaults.dir ?? "agentskit-app",
813
1237
  validate: (value) => {
814
1238
  if (!value.trim()) return "A directory name is required.";
815
- const abs = path__default.default.resolve(process.cwd(), value);
1239
+ const abs = path3__default.default.resolve(process.cwd(), value);
816
1240
  if (fs.existsSync(abs)) return `${value} already exists. Pick a different name.`;
817
1241
  return true;
818
1242
  }
@@ -890,7 +1314,7 @@ ${kleur__default.default.bold().green("\u25B2")} ${kleur__default.default.bold("
890
1314
  return {
891
1315
  cancelled: false,
892
1316
  options: {
893
- targetDir: path__default.default.resolve(process.cwd(), targetDir),
1317
+ targetDir: path3__default.default.resolve(process.cwd(), targetDir),
894
1318
  template,
895
1319
  provider,
896
1320
  tools,
@@ -907,7 +1331,7 @@ ${kleur__default.default.bold().green("\u25B2")} ${kleur__default.default.bold("
907
1331
  }
908
1332
  }
909
1333
  function printNextSteps(options) {
910
- const dir = path__default.default.relative(process.cwd(), options.targetDir) || ".";
1334
+ const dir = path3__default.default.relative(process.cwd(), options.targetDir) || ".";
911
1335
  const pm = options.packageManager ?? "pnpm";
912
1336
  const installCmd = pm === "npm" ? "npm install" : `${pm} install`;
913
1337
  const runCmd = pm === "npm" ? "npm run dev" : `${pm} dev`;
@@ -1121,17 +1545,17 @@ async function checkNodeVersion() {
1121
1545
  }
1122
1546
  async function checkPnpm() {
1123
1547
  const cwd = process.cwd();
1124
- const hasPnpm = fs.existsSync(path.join(cwd, "pnpm-lock.yaml")) || fs.existsSync(path.join(cwd, "pnpm-workspace.yaml"));
1548
+ const hasPnpm = fs.existsSync(path3.join(cwd, "pnpm-lock.yaml")) || fs.existsSync(path3.join(cwd, "pnpm-workspace.yaml"));
1125
1549
  if (hasPnpm) {
1126
1550
  return { status: "pass", name: "Package manager", detail: "pnpm detected (lockfile)" };
1127
1551
  }
1128
- if (fs.existsSync(path.join(cwd, "package-lock.json"))) {
1552
+ if (fs.existsSync(path3.join(cwd, "package-lock.json"))) {
1129
1553
  return { status: "warn", name: "Package manager", detail: "npm detected \u2014 pnpm recommended for monorepo workflows" };
1130
1554
  }
1131
- if (fs.existsSync(path.join(cwd, "yarn.lock"))) {
1555
+ if (fs.existsSync(path3.join(cwd, "yarn.lock"))) {
1132
1556
  return { status: "pass", name: "Package manager", detail: "yarn detected" };
1133
1557
  }
1134
- if (fs.existsSync(path.join(cwd, "bun.lock")) || fs.existsSync(path.join(cwd, "bun.lockb"))) {
1558
+ if (fs.existsSync(path3.join(cwd, "bun.lock")) || fs.existsSync(path3.join(cwd, "bun.lockb"))) {
1135
1559
  return { status: "pass", name: "Package manager", detail: "bun detected" };
1136
1560
  }
1137
1561
  return {
@@ -1141,7 +1565,7 @@ async function checkPnpm() {
1141
1565
  };
1142
1566
  }
1143
1567
  async function checkPackageJson() {
1144
- const path4 = path.join(process.cwd(), "package.json");
1568
+ const path4 = path3.join(process.cwd(), "package.json");
1145
1569
  if (!fs.existsSync(path4)) {
1146
1570
  return {
1147
1571
  status: "warn",
@@ -1366,7 +1790,7 @@ var DEFAULT_IGNORE = [
1366
1790
  "**/*.spec.ts"
1367
1791
  ];
1368
1792
  function startDev(options) {
1369
- const entry = path.resolve(process.cwd(), options.entry);
1793
+ const entry = path3.resolve(process.cwd(), options.entry);
1370
1794
  if (!fs.existsSync(entry)) {
1371
1795
  throw new Error(`Entry file not found: ${entry}`);
1372
1796
  }
@@ -1398,7 +1822,7 @@ function startDev(options) {
1398
1822
  };
1399
1823
  const startChild = () => {
1400
1824
  restartCount++;
1401
- banner(`\u25B8 starting ${kleur__default.default.bold(path.basename(entry))} (restart #${restartCount - 1})`, "cyan");
1825
+ banner(`\u25B8 starting ${kleur__default.default.bold(path3.basename(entry))} (restart #${restartCount - 1})`, "cyan");
1402
1826
  const c = spawnFn(cmd, baseArgs);
1403
1827
  child = c;
1404
1828
  c.stdout?.on("data", (d) => stdout.write(d));
@@ -1522,34 +1946,87 @@ async function startTunnel(options) {
1522
1946
  // src/commands.ts
1523
1947
  function mergeWithConfig(options, config) {
1524
1948
  if (!config) return options;
1949
+ const d = config.defaults ?? {};
1950
+ const resolvedApiKey = options.apiKey ?? (d.apiKeyEnv ? process.env[d.apiKeyEnv] : void 0) ?? d.apiKey;
1525
1951
  return {
1526
1952
  ...options,
1527
1953
  // Config defaults — only apply if CLI flag wasn't set
1528
- provider: options.provider !== "demo" ? options.provider : config.defaults?.provider ?? options.provider,
1529
- model: options.model ?? config.defaults?.model
1954
+ provider: options.provider !== "demo" ? options.provider : d.provider ?? options.provider,
1955
+ model: options.model ?? d.model,
1956
+ apiKey: resolvedApiKey,
1957
+ baseUrl: options.baseUrl ?? d.baseUrl,
1958
+ tools: options.tools ?? d.tools,
1959
+ skill: options.skill ?? d.skill,
1960
+ system: options.system ?? d.system,
1961
+ memoryBackend: options.memoryBackend ?? d.memoryBackend
1530
1962
  };
1531
1963
  }
1532
1964
  function createCli() {
1533
1965
  const program = new commander.Command();
1534
1966
  program.name("agentskit").description("AgentsKit CLI for chat demos and project bootstrapping.");
1535
- 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) => {
1967
+ 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) => {
1968
+ if (options.listSessions) {
1969
+ const sessions = listSessions();
1970
+ if (sessions.length === 0) {
1971
+ process.stdout.write("No saved sessions for this directory.\n");
1972
+ return;
1973
+ }
1974
+ for (const s of sessions) {
1975
+ const { id, updatedAt, messageCount, preview, model } = s.metadata;
1976
+ process.stdout.write(
1977
+ `${id} ${updatedAt} msgs=${messageCount}${model ? ` model=${model}` : ""}
1978
+ ${preview}
1979
+ `
1980
+ );
1981
+ }
1982
+ return;
1983
+ }
1536
1984
  const config = options.config !== false ? await loadConfig() : void 0;
1537
1985
  const merged = mergeWithConfig(options, config);
1986
+ const session = resolveSession({
1987
+ explicitPath: options.memory,
1988
+ forceNew: Boolean(options.new),
1989
+ resumeId: options.resume
1990
+ });
1991
+ if (!session.isNew && !options.memory) {
1992
+ process.stdout.write(
1993
+ `Resuming session ${session.id}. Start fresh with --new or list with --list-sessions.
1994
+ `
1995
+ );
1996
+ }
1538
1997
  const chatOptions = {
1539
1998
  apiKey: merged.apiKey ?? options.apiKey,
1540
1999
  baseUrl: merged.baseUrl ?? options.baseUrl,
1541
2000
  provider: merged.provider,
1542
2001
  model: merged.model,
1543
- system: options.system,
1544
- memoryPath: options.memory,
1545
- tools: options.tools,
1546
- skill: options.skill,
1547
- memoryBackend: options.memoryBackend,
2002
+ system: merged.system ?? options.system,
2003
+ memoryPath: session.file,
2004
+ sessionId: session.id,
2005
+ tools: merged.tools ?? options.tools,
2006
+ skill: merged.skill ?? options.skill,
2007
+ memoryBackend: merged.memoryBackend ?? options.memoryBackend,
1548
2008
  agentsKitConfig: config
1549
2009
  };
1550
2010
  process.stdout.write(`${renderChatHeader(chatOptions)}
1551
2011
  `);
1552
- ink$1.render(React3__default.default.createElement(ChatApp, chatOptions));
2012
+ const instance = ink$1.render(React3__default.default.createElement(ChatApp, chatOptions));
2013
+ await instance.waitUntilExit();
2014
+ if (options.memory) {
2015
+ process.stdout.write(
2016
+ `
2017
+ Session saved to ${session.file}. Resume with --memory ${session.file}
2018
+ `
2019
+ );
2020
+ } else {
2021
+ process.stdout.write(
2022
+ `
2023
+ Session saved. Resume with:
2024
+ agentskit chat --resume ${session.id}
2025
+ Or start fresh with:
2026
+ agentskit chat --new
2027
+ `
2028
+ );
2029
+ }
1553
2030
  });
1554
2031
  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) => {
1555
2032
  const task = options.task ?? positionalTask;
@@ -1577,7 +2054,7 @@ function createCli() {
1577
2054
  if (isCi) {
1578
2055
  const template = rawOptions.template ?? "react";
1579
2056
  resolved = {
1580
- targetDir: path__default.default.resolve(process.cwd(), rawOptions.dir),
2057
+ targetDir: path3__default.default.resolve(process.cwd(), rawOptions.dir),
1581
2058
  template,
1582
2059
  provider: rawOptions.provider ?? "demo",
1583
2060
  tools: rawOptions.tools ? rawOptions.tools.split(",").map((t) => t.trim()) : [],
@@ -1597,7 +2074,7 @@ function createCli() {
1597
2074
  await writeStarterProject(resolved);
1598
2075
  if (isCi) {
1599
2076
  process.stdout.write(
1600
- `Created ${resolved.template} starter in ${path__default.default.relative(process.cwd(), resolved.targetDir) || "."}
2077
+ `Created ${resolved.template} starter in ${path3__default.default.relative(process.cwd(), resolved.targetDir) || "."}
1601
2078
  `
1602
2079
  );
1603
2080
  } else {
@@ -1638,6 +2115,43 @@ function createCli() {
1638
2115
  process.exit(1);
1639
2116
  }
1640
2117
  });
2118
+ 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) => {
2119
+ const isLocal = Boolean(options.local);
2120
+ const targetPath = isLocal ? path3__default.default.join(process.cwd(), ".agentskit.config.json") : path3__default.default.join(os.homedir(), ".agentskit", "config.json");
2121
+ if (action === "show") {
2122
+ const config = await loadConfig();
2123
+ process.stdout.write(JSON.stringify(config ?? {}, null, 2) + "\n");
2124
+ return;
2125
+ }
2126
+ if (action !== "init") {
2127
+ process.stderr.write(`Unknown action: ${action}. Use "init" or "show".
2128
+ `);
2129
+ process.exit(2);
2130
+ }
2131
+ if (fs.existsSync(targetPath) && !options.force) {
2132
+ process.stderr.write(`Config already exists at ${targetPath}. Re-run with --force to overwrite.
2133
+ `);
2134
+ process.exit(1);
2135
+ }
2136
+ const template = {
2137
+ defaults: {
2138
+ provider: "openai",
2139
+ baseUrl: "https://openrouter.ai/api",
2140
+ apiKeyEnv: "OPENROUTER_API_KEY",
2141
+ model: "openai/gpt-oss-120b:free",
2142
+ tools: "web_search,fetch_url"
2143
+ }
2144
+ };
2145
+ fs.mkdirSync(path3__default.default.dirname(targetPath), { recursive: true });
2146
+ fs.writeFileSync(targetPath, JSON.stringify(template, null, 2) + "\n");
2147
+ process.stdout.write(
2148
+ `Wrote ${targetPath}
2149
+ Edit it to taste, then run:
2150
+ agentskit chat
2151
+ (flags on the CLI still win over config values.)
2152
+ `
2153
+ );
2154
+ });
1641
2155
  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) => {
1642
2156
  const portNum = Number(port);
1643
2157
  if (Number.isNaN(portNum) || portNum < 1 || portNum > 65535) {