@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/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,6 +272,244 @@ 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
+ ];
236
513
  function groupIntoTurns(messages) {
237
514
  const turns = [];
238
515
  let current = [];
@@ -252,24 +529,28 @@ function groupIntoTurns(messages) {
252
529
  return turns;
253
530
  }
254
531
  function ChatApp(options) {
255
- const runtime = React3.useMemo(() => resolveChatProvider(options), [
256
- options.apiKey,
257
- options.baseUrl,
258
- options.model,
259
- options.provider
260
- ]);
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]
541
+ );
261
542
  const memory = React3.useMemo(
262
543
  () => resolveMemory(options.memoryBackend, options.memoryPath ?? ".agentskit-history.json"),
263
544
  [options.memoryPath, options.memoryBackend]
264
545
  );
265
- const tools = React3.useMemo(() => resolveTools(options.tools), [options.tools]);
546
+ const tools = React3.useMemo(() => resolveTools(toolsFlag), [toolsFlag]);
266
547
  const skills = React3.useMemo(() => {
267
- if (!options.skill) return void 0;
268
- const names = options.skill.split(",").map((s) => s.trim());
548
+ if (!skillFlag) return void 0;
549
+ const names = skillFlag.split(",").map((s) => s.trim());
269
550
  const resolved = names.map((n) => skillRegistry[n]).filter(Boolean);
270
551
  if (resolved.length === 0) return void 0;
271
552
  return resolved;
272
- }, [options.skill]);
553
+ }, [skillFlag]);
273
554
  const chat = ink$1.useChat({
274
555
  adapter: runtime.adapter,
275
556
  memory,
@@ -277,8 +558,109 @@ function ChatApp(options) {
277
558
  tools: tools.length > 0 ? tools : void 0,
278
559
  skills
279
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]);
280
607
  const turns = React3.useMemo(() => groupIntoTurns(chat.messages), [chat.messages]);
281
- const toolNames = options.tools ? options.tools.split(",").map((s) => s.trim()).filter(Boolean) : [];
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
+ );
282
664
  return /* @__PURE__ */ jsxRuntime.jsxs(ink.Box, { flexDirection: "column", gap: 1, children: [
283
665
  /* @__PURE__ */ jsxRuntime.jsx(
284
666
  ink$1.StatusHeader,
@@ -287,7 +669,8 @@ function ChatApp(options) {
287
669
  model: runtime.model,
288
670
  mode: runtime.mode,
289
671
  tools: toolNames,
290
- messageCount: chat.messages.length
672
+ messageCount: chat.messages.length,
673
+ sessionId: options.sessionId
291
674
  }
292
675
  ),
293
676
  /* @__PURE__ */ jsxRuntime.jsx(ink$1.ChatContainer, { children: turns.map((turn, turnIdx) => {
@@ -304,7 +687,18 @@ function ChatApp(options) {
304
687
  assistantSteps
305
688
  ] }) : null,
306
689
  /* @__PURE__ */ jsxRuntime.jsx(ink$1.Message, { message }),
307
- message.toolCalls?.map((toolCall) => /* @__PURE__ */ jsxRuntime.jsx(ink$1.ToolCallView, { toolCall, expanded: true }, toolCall.id))
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))
308
702
  ] }, message.id);
309
703
  }) }, `turn-${turnIdx}`);
310
704
  }) }),
@@ -315,9 +709,38 @@ function ChatApp(options) {
315
709
  label: toolNames.length > 0 ? "agent working" : "thinking"
316
710
  }
317
711
  ),
318
- /* @__PURE__ */ jsxRuntime.jsx(ink$1.InputBar, { chat, placeholder: "Type a message and press Enter\u2026" })
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
+ )
319
729
  ] });
320
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
+ }
321
744
  function renderChatHeader(options) {
322
745
  const runtime = resolveChatProvider(options);
323
746
  const parts = [`provider=${runtime.provider}`];
@@ -426,7 +849,7 @@ function reactStarter(ctx) {
426
849
  return {
427
850
  "package.json": JSON.stringify(
428
851
  {
429
- 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"),
430
853
  private: true,
431
854
  type: "module",
432
855
  scripts: {
@@ -795,8 +1218,8 @@ async function writeStarterProject(options) {
795
1218
  await promises.mkdir(options.targetDir, { recursive: true });
796
1219
  await Promise.all(
797
1220
  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 });
1221
+ const absolutePath = path3__default.default.join(options.targetDir, relativePath);
1222
+ await promises.mkdir(path3__default.default.dirname(absolutePath), { recursive: true });
800
1223
  await promises.writeFile(absolutePath, content, "utf8");
801
1224
  })
802
1225
  );
@@ -812,7 +1235,7 @@ ${kleur__default.default.bold().green("\u25B2")} ${kleur__default.default.bold("
812
1235
  default: defaults.dir ?? "agentskit-app",
813
1236
  validate: (value) => {
814
1237
  if (!value.trim()) return "A directory name is required.";
815
- const abs = path__default.default.resolve(process.cwd(), value);
1238
+ const abs = path3__default.default.resolve(process.cwd(), value);
816
1239
  if (fs.existsSync(abs)) return `${value} already exists. Pick a different name.`;
817
1240
  return true;
818
1241
  }
@@ -890,7 +1313,7 @@ ${kleur__default.default.bold().green("\u25B2")} ${kleur__default.default.bold("
890
1313
  return {
891
1314
  cancelled: false,
892
1315
  options: {
893
- targetDir: path__default.default.resolve(process.cwd(), targetDir),
1316
+ targetDir: path3__default.default.resolve(process.cwd(), targetDir),
894
1317
  template,
895
1318
  provider,
896
1319
  tools,
@@ -907,7 +1330,7 @@ ${kleur__default.default.bold().green("\u25B2")} ${kleur__default.default.bold("
907
1330
  }
908
1331
  }
909
1332
  function printNextSteps(options) {
910
- const dir = path__default.default.relative(process.cwd(), options.targetDir) || ".";
1333
+ const dir = path3__default.default.relative(process.cwd(), options.targetDir) || ".";
911
1334
  const pm = options.packageManager ?? "pnpm";
912
1335
  const installCmd = pm === "npm" ? "npm install" : `${pm} install`;
913
1336
  const runCmd = pm === "npm" ? "npm run dev" : `${pm} dev`;
@@ -1121,17 +1544,17 @@ async function checkNodeVersion() {
1121
1544
  }
1122
1545
  async function checkPnpm() {
1123
1546
  const cwd = process.cwd();
1124
- 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"));
1125
1548
  if (hasPnpm) {
1126
1549
  return { status: "pass", name: "Package manager", detail: "pnpm detected (lockfile)" };
1127
1550
  }
1128
- if (fs.existsSync(path.join(cwd, "package-lock.json"))) {
1551
+ if (fs.existsSync(path3.join(cwd, "package-lock.json"))) {
1129
1552
  return { status: "warn", name: "Package manager", detail: "npm detected \u2014 pnpm recommended for monorepo workflows" };
1130
1553
  }
1131
- if (fs.existsSync(path.join(cwd, "yarn.lock"))) {
1554
+ if (fs.existsSync(path3.join(cwd, "yarn.lock"))) {
1132
1555
  return { status: "pass", name: "Package manager", detail: "yarn detected" };
1133
1556
  }
1134
- 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"))) {
1135
1558
  return { status: "pass", name: "Package manager", detail: "bun detected" };
1136
1559
  }
1137
1560
  return {
@@ -1141,7 +1564,7 @@ async function checkPnpm() {
1141
1564
  };
1142
1565
  }
1143
1566
  async function checkPackageJson() {
1144
- const path4 = path.join(process.cwd(), "package.json");
1567
+ const path4 = path3.join(process.cwd(), "package.json");
1145
1568
  if (!fs.existsSync(path4)) {
1146
1569
  return {
1147
1570
  status: "warn",
@@ -1366,7 +1789,7 @@ var DEFAULT_IGNORE = [
1366
1789
  "**/*.spec.ts"
1367
1790
  ];
1368
1791
  function startDev(options) {
1369
- const entry = path.resolve(process.cwd(), options.entry);
1792
+ const entry = path3.resolve(process.cwd(), options.entry);
1370
1793
  if (!fs.existsSync(entry)) {
1371
1794
  throw new Error(`Entry file not found: ${entry}`);
1372
1795
  }
@@ -1398,7 +1821,7 @@ function startDev(options) {
1398
1821
  };
1399
1822
  const startChild = () => {
1400
1823
  restartCount++;
1401
- 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");
1402
1825
  const c = spawnFn(cmd, baseArgs);
1403
1826
  child = c;
1404
1827
  c.stdout?.on("data", (d) => stdout.write(d));
@@ -1522,34 +1945,87 @@ async function startTunnel(options) {
1522
1945
  // src/commands.ts
1523
1946
  function mergeWithConfig(options, config) {
1524
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;
1525
1950
  return {
1526
1951
  ...options,
1527
1952
  // 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
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
1530
1961
  };
1531
1962
  }
1532
1963
  function createCli() {
1533
1964
  const program = new commander.Command();
1534
1965
  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) => {
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
+ }
1536
1983
  const config = options.config !== false ? await loadConfig() : void 0;
1537
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
+ }
1538
1996
  const chatOptions = {
1539
1997
  apiKey: merged.apiKey ?? options.apiKey,
1540
1998
  baseUrl: merged.baseUrl ?? options.baseUrl,
1541
1999
  provider: merged.provider,
1542
2000
  model: merged.model,
1543
- system: options.system,
1544
- memoryPath: options.memory,
1545
- tools: options.tools,
1546
- skill: options.skill,
1547
- 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,
1548
2007
  agentsKitConfig: config
1549
2008
  };
1550
2009
  process.stdout.write(`${renderChatHeader(chatOptions)}
1551
2010
  `);
1552
- 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
+ }
1553
2029
  });
1554
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) => {
1555
2031
  const task = options.task ?? positionalTask;
@@ -1577,7 +2053,7 @@ function createCli() {
1577
2053
  if (isCi) {
1578
2054
  const template = rawOptions.template ?? "react";
1579
2055
  resolved = {
1580
- targetDir: path__default.default.resolve(process.cwd(), rawOptions.dir),
2056
+ targetDir: path3__default.default.resolve(process.cwd(), rawOptions.dir),
1581
2057
  template,
1582
2058
  provider: rawOptions.provider ?? "demo",
1583
2059
  tools: rawOptions.tools ? rawOptions.tools.split(",").map((t) => t.trim()) : [],
@@ -1597,7 +2073,7 @@ function createCli() {
1597
2073
  await writeStarterProject(resolved);
1598
2074
  if (isCi) {
1599
2075
  process.stdout.write(
1600
- `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) || "."}
1601
2077
  `
1602
2078
  );
1603
2079
  } else {
@@ -1638,6 +2114,43 @@ function createCli() {
1638
2114
  process.exit(1);
1639
2115
  }
1640
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
+ });
1641
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) => {
1642
2155
  const portNum = Number(port);
1643
2156
  if (Number.isNaN(portNum) || portNum < 1 || portNum > 65535) {