@agentskit/cli 0.5.3 → 0.7.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.
@@ -1,33 +1,37 @@
1
1
  #!/usr/bin/env node
2
- import { mkdir, writeFile, readFile } from 'fs/promises';
3
- import path, { resolve, join, basename } from 'path';
2
+ import { readdir, stat, mkdir, writeFile, glob, readFile } from 'fs/promises';
3
+ import { homedir } from 'os';
4
+ import path, { join, resolve, isAbsolute, basename } from 'path';
4
5
  import { kimi, grok, deepseek, ollama, gemini, anthropic, openai } from '@agentskit/adapters';
5
- import React3, { useMemo, useState, useEffect } from 'react';
6
+ import { randomBytes, createHash } from 'crypto';
7
+ import { writeFileSync, existsSync, readdirSync, statSync, readFileSync, mkdirSync } from 'fs';
8
+ import { spawn } from 'child_process';
9
+ import React2, { useMemo, useState, useEffect, useRef } from 'react';
6
10
  import { Box, Text, render } from 'ink';
7
- import { useChat, StatusHeader, ChatContainer, Message, ToolCallView, ThinkingIndicator, InputBar } from '@agentskit/ink';
8
- import { shell, filesystem, webSearch } from '@agentskit/tools';
11
+ import { useChat, StatusHeader, ChatContainer, Message, ToolCallView, ToolConfirmation, ThinkingIndicator, InputBar } from '@agentskit/ink';
12
+ import { shell, filesystem, fetchUrl, webSearch } from '@agentskit/tools';
9
13
  import { summarizer, critic, planner, coder, researcher, composeSkills } from '@agentskit/skills';
10
- import { fileChatMemory, sqliteChatMemory } from '@agentskit/memory';
14
+ import { fileVectorMemory, fileChatMemory, sqliteChatMemory } from '@agentskit/memory';
11
15
  import { jsxs, jsx } from 'react/jsx-runtime';
16
+ import { pathToFileURL } from 'url';
12
17
  import { createRuntime } from '@agentskit/runtime';
13
- import { existsSync } from 'fs';
14
- import { spawn } from 'child_process';
15
18
  import chokidar from 'chokidar';
16
19
  import kleur3 from 'kleur';
20
+ import { createRAG } from '@agentskit/rag';
17
21
  import { Command } from 'commander';
18
22
  import { input, select, checkbox, confirm } from '@inquirer/prompts';
19
23
 
20
- async function loadJsonConfig(path4) {
24
+ async function loadJsonConfig(path5) {
21
25
  try {
22
- const raw = await readFile(path4, "utf8");
26
+ const raw = await readFile(path5, "utf8");
23
27
  return JSON.parse(raw);
24
28
  } catch {
25
29
  return void 0;
26
30
  }
27
31
  }
28
- async function loadTsConfig(path4) {
32
+ async function loadTsConfig(path5) {
29
33
  try {
30
- const mod = await import(path4);
34
+ const mod = await import(path5);
31
35
  return mod.default ?? mod;
32
36
  } catch {
33
37
  return void 0;
@@ -45,16 +49,39 @@ async function loadPackageJsonConfig(dir) {
45
49
  return void 0;
46
50
  }
47
51
  }
48
- async function loadConfig(options) {
49
- const cwd = resolve(options?.cwd ?? process.cwd());
50
- const tsPath = join(cwd, ".agentskit.config.ts");
51
- const tsConfig = await loadTsConfig(tsPath);
52
+ function mergeConfigs(base, override) {
53
+ if (!base && !override) return void 0;
54
+ if (!base) return override;
55
+ if (!override) return base;
56
+ return {
57
+ ...base,
58
+ ...override,
59
+ tools: { ...base.tools, ...override.tools },
60
+ defaults: { ...base.defaults, ...override.defaults },
61
+ runtime: { ...base.runtime, ...override.runtime },
62
+ observability: { ...base.observability, ...override.observability }
63
+ };
64
+ }
65
+ async function loadLocalConfig(cwd) {
66
+ const tsConfig = await loadTsConfig(join(cwd, ".agentskit.config.ts"));
52
67
  if (tsConfig) return tsConfig;
53
- const jsonPath = join(cwd, ".agentskit.config.json");
54
- const jsonConfig = await loadJsonConfig(jsonPath);
68
+ const jsonConfig = await loadJsonConfig(join(cwd, ".agentskit.config.json"));
55
69
  if (jsonConfig) return jsonConfig;
56
70
  return await loadPackageJsonConfig(cwd);
57
71
  }
72
+ async function loadGlobalConfig(home) {
73
+ if (home === null) return void 0;
74
+ const globalDir = join(home ?? homedir(), ".agentskit");
75
+ const tsConfig = await loadTsConfig(join(globalDir, "config.ts"));
76
+ if (tsConfig) return tsConfig;
77
+ return await loadJsonConfig(join(globalDir, "config.json"));
78
+ }
79
+ async function loadConfig(options) {
80
+ const cwd = resolve(options?.cwd ?? process.cwd());
81
+ const global = await loadGlobalConfig(options?.home);
82
+ const local = await loadLocalConfig(cwd);
83
+ return mergeConfigs(global, local);
84
+ }
58
85
  var providers = {
59
86
  openai: {
60
87
  label: "OpenAI",
@@ -114,7 +141,7 @@ function createDemoAdapter(provider, model) {
114
141
  ].join(" ");
115
142
  for (const chunk of reply.match(/.{1,18}/g) ?? []) {
116
143
  if (cancelled) return;
117
- await new Promise((resolve2) => setTimeout(resolve2, 35));
144
+ await new Promise((resolve4) => setTimeout(resolve4, 35));
118
145
  yield { type: "text", content: chunk };
119
146
  }
120
147
  yield { type: "done" };
@@ -165,6 +192,533 @@ function resolveChatProvider(options) {
165
192
  summary: `${entry.label} live adapter`
166
193
  };
167
194
  }
195
+ var ROOT = join(homedir(), ".agentskit", "sessions");
196
+ var META_SUFFIX = ".meta.json";
197
+ function cwdHash(cwd = process.cwd()) {
198
+ return createHash("sha256").update(cwd).digest("hex").slice(0, 12);
199
+ }
200
+ function dirFor(cwd = process.cwd()) {
201
+ return join(ROOT, cwdHash(cwd));
202
+ }
203
+ function ensureDir(dir) {
204
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
205
+ }
206
+ function generateSessionId() {
207
+ const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
208
+ const suffix = randomBytes(3).toString("hex");
209
+ return `${ts}-${suffix}`;
210
+ }
211
+ function sessionFilePath(id, cwd = process.cwd()) {
212
+ ensureDir(dirFor(cwd));
213
+ return join(dirFor(cwd), `${id}.json`);
214
+ }
215
+ function metaPath(id, cwd = process.cwd()) {
216
+ return join(dirFor(cwd), `${id}${META_SUFFIX}`);
217
+ }
218
+ function readMeta(id, cwd = process.cwd()) {
219
+ const path5 = metaPath(id, cwd);
220
+ if (!existsSync(path5)) return null;
221
+ try {
222
+ return JSON.parse(readFileSync(path5, "utf8"));
223
+ } catch {
224
+ return null;
225
+ }
226
+ }
227
+ function writeSessionMeta(meta, cwd = process.cwd()) {
228
+ ensureDir(dirFor(cwd));
229
+ writeFileSync(metaPath(meta.id, cwd), JSON.stringify(meta, null, 2));
230
+ }
231
+ function derivePreview(messages) {
232
+ const firstUser = messages.find((m) => m.role === "user" && m.content.trim());
233
+ if (!firstUser) return "(empty)";
234
+ const single = firstUser.content.replace(/\s+/g, " ").trim();
235
+ return single.length > 80 ? `${single.slice(0, 80)}\u2026` : single;
236
+ }
237
+ function listSessions(cwd = process.cwd()) {
238
+ const dir = dirFor(cwd);
239
+ if (!existsSync(dir)) return [];
240
+ const entries = readdirSync(dir);
241
+ const records = [];
242
+ for (const entry of entries) {
243
+ if (!entry.endsWith(".json") || entry.endsWith(META_SUFFIX)) continue;
244
+ const id = entry.replace(/\.json$/, "");
245
+ const meta = readMeta(id, cwd);
246
+ const file = join(dir, entry);
247
+ if (meta) {
248
+ records.push({ metadata: meta, file });
249
+ } else {
250
+ const stats = statSync(file);
251
+ records.push({
252
+ metadata: {
253
+ id,
254
+ cwd,
255
+ createdAt: stats.birthtime.toISOString(),
256
+ updatedAt: stats.mtime.toISOString(),
257
+ messageCount: 0,
258
+ preview: "(legacy session)"
259
+ },
260
+ file
261
+ });
262
+ }
263
+ }
264
+ records.sort((a, b) => b.metadata.updatedAt.localeCompare(a.metadata.updatedAt));
265
+ return records;
266
+ }
267
+ function findLatestSession(cwd = process.cwd()) {
268
+ const all = listSessions(cwd);
269
+ return all[0] ?? null;
270
+ }
271
+ function findSession(id, cwd = process.cwd()) {
272
+ const all = listSessions(cwd);
273
+ const exact = all.find((s) => s.metadata.id === id || s.metadata.label === id);
274
+ if (exact) return exact;
275
+ const prefix = all.find((s) => s.metadata.id.startsWith(id));
276
+ return prefix ?? null;
277
+ }
278
+ function renameSession(id, label, cwd = process.cwd()) {
279
+ const record = findSession(id, cwd);
280
+ if (!record) throw new Error(`No session matching "${id}".`);
281
+ const next = { ...record.metadata, label, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
282
+ writeSessionMeta(next, cwd);
283
+ return next;
284
+ }
285
+ function forkSession(id, cwd = process.cwd()) {
286
+ const record = findSession(id, cwd);
287
+ if (!record) throw new Error(`No session matching "${id}".`);
288
+ const newId = generateSessionId();
289
+ const newFile = sessionFilePath(newId, cwd);
290
+ if (existsSync(record.file)) {
291
+ writeFileSync(newFile, readFileSync(record.file, "utf8"));
292
+ }
293
+ const now = (/* @__PURE__ */ new Date()).toISOString();
294
+ writeSessionMeta(
295
+ {
296
+ ...record.metadata,
297
+ id: newId,
298
+ createdAt: now,
299
+ updatedAt: now,
300
+ forkedFrom: record.metadata.id,
301
+ label: void 0
302
+ },
303
+ cwd
304
+ );
305
+ return { id: newId, file: newFile, isNew: true };
306
+ }
307
+ function resolveSession(input2) {
308
+ const cwd = input2.cwd ?? process.cwd();
309
+ if (input2.explicitPath) {
310
+ return { id: "custom", file: input2.explicitPath, isNew: !existsSync(input2.explicitPath) };
311
+ }
312
+ if (input2.forceNew) {
313
+ const id2 = generateSessionId();
314
+ return { id: id2, file: sessionFilePath(id2, cwd), isNew: true };
315
+ }
316
+ if (input2.resumeId) {
317
+ const target = input2.resumeId === true ? findLatestSession(cwd) : findSession(input2.resumeId, cwd);
318
+ if (target) {
319
+ return { id: target.metadata.id, file: target.file, isNew: false };
320
+ }
321
+ process.stderr.write(
322
+ `No session matching "${String(input2.resumeId)}" \u2014 starting a new one.
323
+ `
324
+ );
325
+ }
326
+ const latest = findLatestSession(cwd);
327
+ if (latest) return { id: latest.metadata.id, file: latest.file, isNew: false };
328
+ const id = generateSessionId();
329
+ return { id, file: sessionFilePath(id, cwd), isNew: true };
330
+ }
331
+
332
+ // src/extensibility/telemetry/pricing.ts
333
+ var builtinPricing = {
334
+ "gpt-4o": { inputPerM: 2.5, outputPerM: 10 },
335
+ "gpt-4o-mini": { inputPerM: 0.15, outputPerM: 0.6 },
336
+ "gpt-4.1": { inputPerM: 2, outputPerM: 8 },
337
+ "gpt-4.1-mini": { inputPerM: 0.4, outputPerM: 1.6 },
338
+ "claude-opus-4": { inputPerM: 15, outputPerM: 75 },
339
+ "claude-sonnet-4": { inputPerM: 3, outputPerM: 15 },
340
+ "claude-haiku-4": { inputPerM: 0.8, outputPerM: 4 },
341
+ "gemini-2.5-pro": { inputPerM: 1.25, outputPerM: 10 },
342
+ "gemini-2.5-flash": { inputPerM: 0.3, outputPerM: 2.5 }
343
+ };
344
+ var customPricing = {};
345
+ function registerPricing(model, pricing) {
346
+ customPricing[model] = pricing;
347
+ }
348
+ function getPricing(model) {
349
+ if (!model) return void 0;
350
+ if (customPricing[model]) return customPricing[model];
351
+ if (builtinPricing[model]) return builtinPricing[model];
352
+ const short = model.includes("/") ? model.split("/").pop() : model;
353
+ return customPricing[short] ?? builtinPricing[short];
354
+ }
355
+ function computeCost(model, usage) {
356
+ if (!model) return void 0;
357
+ const pricing = getPricing(model);
358
+ if (!pricing) return void 0;
359
+ const inputUsd = usage.promptTokens / 1e6 * pricing.inputPerM;
360
+ const outputUsd = usage.completionTokens / 1e6 * pricing.outputPerM;
361
+ return {
362
+ model,
363
+ inputUsd,
364
+ outputUsd,
365
+ totalUsd: inputUsd + outputUsd
366
+ };
367
+ }
368
+
369
+ // src/extensibility/permissions/policy.ts
370
+ var defaultPolicy = {
371
+ mode: "default",
372
+ rules: []
373
+ };
374
+ function evaluatePolicy(policy, toolName) {
375
+ if (policy.mode === "bypassPermissions") return "allow";
376
+ if (policy.mode === "plan") return "ask";
377
+ for (const rule of policy.rules) {
378
+ if (matchesRule(rule, toolName)) return rule.action;
379
+ }
380
+ if (policy.mode === "acceptEdits" && /^(fs_write|edit|write_file)/.test(toolName)) {
381
+ return "allow";
382
+ }
383
+ return "ask";
384
+ }
385
+ function matchesRule(rule, toolName) {
386
+ if (rule.tool instanceof RegExp) return rule.tool.test(toolName);
387
+ const str = rule.tool;
388
+ if (str.startsWith("re:")) return new RegExp(str.slice(3)).test(toolName);
389
+ return str === toolName;
390
+ }
391
+ function applyPolicyToTool(policy, tool) {
392
+ const action = evaluatePolicy(policy, tool.name);
393
+ if (action === "deny") return null;
394
+ if (action === "allow") return { ...tool, requiresConfirmation: false };
395
+ return { ...tool, requiresConfirmation: true };
396
+ }
397
+ function applyPolicyToTools(policy, tools) {
398
+ const out = [];
399
+ for (const tool of tools) {
400
+ const gated = applyPolicyToTool(policy, tool);
401
+ if (gated) out.push(gated);
402
+ }
403
+ return out;
404
+ }
405
+
406
+ // src/extensibility/hooks/runner.ts
407
+ var HookDispatcher = class {
408
+ constructor(handlers = [], onError = (_h, err) => process.stderr.write(
409
+ `[agentskit] hook error: ${err instanceof Error ? err.message : String(err)}
410
+ `
411
+ )) {
412
+ this.onError = onError;
413
+ this.handlers = /* @__PURE__ */ new Map();
414
+ for (const handler of handlers) this.register(handler);
415
+ }
416
+ register(handler) {
417
+ const list = this.handlers.get(handler.event) ?? [];
418
+ list.push(handler);
419
+ this.handlers.set(handler.event, list);
420
+ }
421
+ async dispatch(event, payload) {
422
+ const list = this.handlers.get(event) ?? [];
423
+ let current = { ...payload, event };
424
+ for (const handler of list) {
425
+ if (!this.matches(handler, current)) continue;
426
+ let result;
427
+ try {
428
+ result = await handler.run(current);
429
+ } catch (err) {
430
+ this.onError(handler, err);
431
+ continue;
432
+ }
433
+ if (result.decision === "block") {
434
+ return { payload: current, blocked: true, reason: result.reason };
435
+ }
436
+ if (result.decision === "modify") {
437
+ current = { ...result.payload, event };
438
+ }
439
+ }
440
+ return { payload: current, blocked: false };
441
+ }
442
+ matches(handler, payload) {
443
+ if (!handler.matcher) return true;
444
+ if (typeof handler.matcher === "function") return handler.matcher(payload);
445
+ return handler.matcher.test(String(payload.tool ?? payload.prompt ?? ""));
446
+ }
447
+ };
448
+ function configHooksToHandlers(config) {
449
+ if (!config) return [];
450
+ const handlers = [];
451
+ for (const [event, entries] of Object.entries(config)) {
452
+ for (const entry of entries) {
453
+ handlers.push({
454
+ event,
455
+ matcher: entry.matcher ? new RegExp(entry.matcher) : void 0,
456
+ run: (payload) => runShellHook(entry, payload)
457
+ });
458
+ }
459
+ }
460
+ return handlers;
461
+ }
462
+ function runShellHook(entry, payload) {
463
+ return new Promise((resolvePromise) => {
464
+ const timeoutMs = entry.timeout ?? 5e3;
465
+ const child = spawn("sh", ["-c", entry.run], {
466
+ stdio: ["pipe", "pipe", "inherit"]
467
+ });
468
+ let stdout = "";
469
+ child.stdout.on("data", (chunk) => {
470
+ stdout += chunk.toString();
471
+ });
472
+ const timer = setTimeout(() => {
473
+ child.kill("SIGTERM");
474
+ }, timeoutMs);
475
+ child.on("close", (code) => {
476
+ clearTimeout(timer);
477
+ if (code !== 0) {
478
+ resolvePromise({
479
+ decision: "block",
480
+ reason: `shell hook exited with code ${code}`
481
+ });
482
+ return;
483
+ }
484
+ const trimmed = stdout.trim();
485
+ if (!trimmed) {
486
+ resolvePromise({ decision: "continue" });
487
+ return;
488
+ }
489
+ try {
490
+ const parsed = JSON.parse(trimmed);
491
+ resolvePromise(parsed);
492
+ } catch {
493
+ resolvePromise({ decision: "continue" });
494
+ }
495
+ });
496
+ child.on("error", (err) => {
497
+ clearTimeout(timer);
498
+ resolvePromise({ decision: "block", reason: err.message });
499
+ });
500
+ try {
501
+ child.stdin.write(JSON.stringify(payload));
502
+ child.stdin.end();
503
+ } catch {
504
+ }
505
+ });
506
+ }
507
+
508
+ // src/slash-commands.ts
509
+ function parseSlashCommand(input2) {
510
+ if (!input2.startsWith("/")) return null;
511
+ const match = input2.slice(1).match(/^(\S+)\s*([\s\S]*)$/);
512
+ if (!match) return null;
513
+ return { name: match[1], args: match[2] ?? "" };
514
+ }
515
+ function createSlashRegistry(commands) {
516
+ const map = /* @__PURE__ */ new Map();
517
+ for (const cmd of commands) {
518
+ map.set(cmd.name, cmd);
519
+ for (const alias of cmd.aliases ?? []) map.set(alias, cmd);
520
+ }
521
+ return map;
522
+ }
523
+ var builtinSlashCommands = [
524
+ {
525
+ name: "help",
526
+ aliases: ["?"],
527
+ description: "List available slash commands.",
528
+ run(ctx) {
529
+ const seen = /* @__PURE__ */ new Set();
530
+ const lines = [];
531
+ for (const cmd of ctx.commands) {
532
+ if (seen.has(cmd.name)) continue;
533
+ seen.add(cmd.name);
534
+ const suffix = cmd.usage ? ` (${cmd.usage})` : "";
535
+ lines.push(` /${cmd.name.padEnd(10)} ${cmd.description}${suffix}`);
536
+ }
537
+ ctx.feedback(`Slash commands:
538
+ ${lines.join("\n")}`, "info");
539
+ }
540
+ },
541
+ {
542
+ name: "model",
543
+ description: "Switch the active model.",
544
+ usage: "/model <name>",
545
+ run(ctx, args) {
546
+ const value = args.trim();
547
+ if (!value) {
548
+ ctx.feedback(
549
+ `Current model: ${ctx.runtime.model ?? "unset"}. Usage: /model <name>`,
550
+ "warn"
551
+ );
552
+ return;
553
+ }
554
+ ctx.setModel(value);
555
+ ctx.feedback(`Model \u2192 ${value}`, "success");
556
+ }
557
+ },
558
+ {
559
+ name: "provider",
560
+ description: "Switch the adapter provider.",
561
+ usage: "/provider openai|anthropic|gemini|ollama|deepseek|grok|kimi|demo",
562
+ run(ctx, args) {
563
+ const value = args.trim();
564
+ if (!value) {
565
+ ctx.feedback(
566
+ `Current provider: ${ctx.runtime.provider}. Usage: /provider <name>`,
567
+ "warn"
568
+ );
569
+ return;
570
+ }
571
+ ctx.setProvider(value);
572
+ ctx.feedback(`Provider \u2192 ${value}`, "success");
573
+ }
574
+ },
575
+ {
576
+ name: "base-url",
577
+ aliases: ["baseurl"],
578
+ description: "Override provider base URL.",
579
+ usage: "/base-url <url|clear>",
580
+ run(ctx, args) {
581
+ const value = args.trim();
582
+ if (!value || value === "clear") {
583
+ ctx.setBaseUrl(void 0);
584
+ ctx.feedback("Base URL cleared.", "success");
585
+ return;
586
+ }
587
+ ctx.setBaseUrl(value);
588
+ ctx.feedback(`Base URL \u2192 ${value}`, "success");
589
+ }
590
+ },
591
+ {
592
+ name: "tools",
593
+ description: "Set active tools (comma-separated) or clear them.",
594
+ usage: "/tools web_search,fetch_url | /tools clear",
595
+ run(ctx, args) {
596
+ const value = args.trim();
597
+ if (!value || value === "clear") {
598
+ ctx.setTools(void 0);
599
+ ctx.feedback("Tools reset to defaults.", "success");
600
+ return;
601
+ }
602
+ ctx.setTools(value);
603
+ ctx.feedback(`Tools \u2192 ${value}`, "success");
604
+ }
605
+ },
606
+ {
607
+ name: "skill",
608
+ description: "Set active skill(s) (comma-separated) or clear them.",
609
+ usage: "/skill researcher,coder | /skill clear",
610
+ run(ctx, args) {
611
+ const value = args.trim();
612
+ if (!value || value === "clear") {
613
+ ctx.setSkill(void 0);
614
+ ctx.feedback("Skills cleared.", "success");
615
+ return;
616
+ }
617
+ ctx.setSkill(value);
618
+ ctx.feedback(`Skills \u2192 ${value}`, "success");
619
+ }
620
+ },
621
+ {
622
+ name: "clear",
623
+ aliases: ["reset"],
624
+ description: "Clear the conversation history in this session.",
625
+ async run(ctx) {
626
+ await ctx.chat.clear();
627
+ ctx.feedback("History cleared.", "success");
628
+ }
629
+ },
630
+ {
631
+ name: "usage",
632
+ description: "Show the cumulative token usage for this session.",
633
+ run(ctx) {
634
+ const usage = ctx.chat.usage;
635
+ if (!usage || usage.totalTokens === 0) {
636
+ ctx.feedback("No usage reported yet for this session.", "info");
637
+ return;
638
+ }
639
+ ctx.feedback(
640
+ `Tokens \u2014 prompt=${usage.promptTokens} completion=${usage.completionTokens} total=${usage.totalTokens}`,
641
+ "info"
642
+ );
643
+ }
644
+ },
645
+ {
646
+ name: "cost",
647
+ description: "Estimate the cost so far for the current model.",
648
+ run(ctx) {
649
+ const usage = ctx.chat.usage;
650
+ const model = ctx.runtime.model;
651
+ if (!usage || usage.totalTokens === 0) {
652
+ ctx.feedback("No usage reported yet for this session.", "info");
653
+ return;
654
+ }
655
+ const cost = computeCost(model, usage);
656
+ if (!cost) {
657
+ ctx.feedback(
658
+ `No pricing registered for model "${model ?? "unset"}". Register with registerPricing() or provide a known model name.`,
659
+ "warn"
660
+ );
661
+ return;
662
+ }
663
+ ctx.feedback(
664
+ `$${cost.totalUsd.toFixed(4)} total (in=$${cost.inputUsd.toFixed(4)} out=$${cost.outputUsd.toFixed(4)} model=${cost.model})`,
665
+ "info"
666
+ );
667
+ }
668
+ },
669
+ {
670
+ name: "rename",
671
+ description: "Attach a human-readable label to the current session.",
672
+ usage: "/rename <label>",
673
+ run(ctx, args) {
674
+ const label = args.trim();
675
+ const sessionId = ctx.runtime.sessionId;
676
+ if (!sessionId || sessionId === "custom") {
677
+ ctx.feedback("Rename is only available for managed sessions.", "warn");
678
+ return;
679
+ }
680
+ if (!label) {
681
+ ctx.feedback("Usage: /rename <label>", "warn");
682
+ return;
683
+ }
684
+ try {
685
+ renameSession(sessionId, label);
686
+ ctx.feedback(`Session labeled "${label}".`, "success");
687
+ } catch (err) {
688
+ ctx.feedback(`/rename failed: ${err instanceof Error ? err.message : String(err)}`, "error");
689
+ }
690
+ }
691
+ },
692
+ {
693
+ name: "fork",
694
+ description: "Branch a copy of the current session. Does not switch to it.",
695
+ run(ctx) {
696
+ const sessionId = ctx.runtime.sessionId;
697
+ if (!sessionId || sessionId === "custom") {
698
+ ctx.feedback("Fork is only available for managed sessions.", "warn");
699
+ return;
700
+ }
701
+ try {
702
+ const result = forkSession(sessionId);
703
+ ctx.feedback(
704
+ `Forked into ${result.id}. Resume with:
705
+ agentskit chat --resume ${result.id}`,
706
+ "success"
707
+ );
708
+ } catch (err) {
709
+ ctx.feedback(`/fork failed: ${err instanceof Error ? err.message : String(err)}`, "error");
710
+ }
711
+ }
712
+ },
713
+ {
714
+ name: "exit",
715
+ aliases: ["quit", "q"],
716
+ description: "Exit the chat.",
717
+ run() {
718
+ process.exit(0);
719
+ }
720
+ }
721
+ ];
168
722
  var skillRegistry = {
169
723
  researcher,
170
724
  coder,
@@ -172,19 +726,34 @@ var skillRegistry = {
172
726
  critic,
173
727
  summarizer
174
728
  };
729
+ function instantiate(kind) {
730
+ switch (kind) {
731
+ case "web_search":
732
+ return [webSearch()];
733
+ case "fetch_url":
734
+ return [fetchUrl()];
735
+ case "filesystem":
736
+ return filesystem({ basePath: process.cwd() });
737
+ case "shell":
738
+ return [shell({ timeout: 3e4 })];
739
+ }
740
+ }
741
+ function gateTool(tool) {
742
+ if (tool.requiresConfirmation === false) return tool;
743
+ return { ...tool, requiresConfirmation: true };
744
+ }
175
745
  function resolveTools(toolNames) {
176
- if (!toolNames) return [];
746
+ if (!toolNames) {
747
+ return [...instantiate("web_search"), ...instantiate("fetch_url")].map(gateTool);
748
+ }
177
749
  const tools = [];
178
- for (const name of toolNames.split(",").map((s) => s.trim())) {
750
+ for (const name of toolNames.split(",").map((s) => s.trim()).filter(Boolean)) {
179
751
  switch (name) {
180
752
  case "web_search":
181
- tools.push(webSearch());
182
- break;
753
+ case "fetch_url":
183
754
  case "filesystem":
184
- tools.push(...filesystem({ basePath: process.cwd() }));
185
- break;
186
755
  case "shell":
187
- tools.push(shell({ timeout: 3e4 }));
756
+ tools.push(...instantiate(name));
188
757
  break;
189
758
  default:
190
759
  process.stderr.write(`Unknown tool: ${name}
@@ -224,6 +793,108 @@ function resolveMemory(backend, memoryPath) {
224
793
  return fileChatMemory(memoryPath);
225
794
  }
226
795
  }
796
+
797
+ // src/runtime/use-runtime.ts
798
+ function useRuntime(options) {
799
+ const [provider, setProvider] = useState(options.provider);
800
+ const [model, setModel] = useState(options.model);
801
+ const [apiKey, setApiKey] = useState(options.apiKey);
802
+ const [baseUrl, setBaseUrl] = useState(options.baseUrl);
803
+ const [toolsFlag, setToolsFlag] = useState(options.tools);
804
+ const [skillFlag, setSkillFlag] = useState(options.skill);
805
+ const runtime = useMemo(
806
+ () => resolveChatProvider({ provider, model, apiKey, baseUrl }),
807
+ [provider, model, apiKey, baseUrl]
808
+ );
809
+ const memory = useMemo(
810
+ () => resolveMemory(options.memoryBackend, options.memoryPath ?? ".agentskit-history.json"),
811
+ [options.memoryPath, options.memoryBackend]
812
+ );
813
+ const tools = useMemo(() => {
814
+ const resolved = resolveTools(toolsFlag);
815
+ if (!options.permissionPolicy) return resolved;
816
+ return applyPolicyToTools(options.permissionPolicy, resolved);
817
+ }, [toolsFlag, options.permissionPolicy]);
818
+ const skills = useMemo(() => {
819
+ if (!skillFlag) return void 0;
820
+ const names = skillFlag.split(",").map((s) => s.trim());
821
+ const resolved = names.map((n) => skillRegistry[n]).filter(Boolean);
822
+ if (resolved.length === 0) return void 0;
823
+ return resolved;
824
+ }, [skillFlag]);
825
+ return {
826
+ runtime,
827
+ memory,
828
+ tools,
829
+ skills,
830
+ state: { provider, model, apiKey, baseUrl, toolsFlag, skillFlag },
831
+ setProvider,
832
+ setModel,
833
+ setApiKey,
834
+ setBaseUrl,
835
+ setToolsFlag,
836
+ setSkillFlag
837
+ };
838
+ }
839
+ function useToolPermissions(chat) {
840
+ const [sessionAllowed, setSessionAllowed] = useState(/* @__PURE__ */ new Set());
841
+ const autoApprovedRef = useRef(/* @__PURE__ */ new Set());
842
+ useEffect(() => {
843
+ if (sessionAllowed.size === 0) return;
844
+ for (const message of chat.messages) {
845
+ for (const call of message.toolCalls ?? []) {
846
+ if (call.status === "requires_confirmation" && sessionAllowed.has(call.name) && !autoApprovedRef.current.has(call.id)) {
847
+ autoApprovedRef.current.add(call.id);
848
+ void chat.approve(call.id);
849
+ }
850
+ }
851
+ }
852
+ }, [chat.messages, sessionAllowed, chat.approve]);
853
+ const handleApproveAlways = (toolCallId, toolName) => {
854
+ setSessionAllowed((prev) => {
855
+ if (prev.has(toolName)) return prev;
856
+ const next = new Set(prev);
857
+ next.add(toolName);
858
+ return next;
859
+ });
860
+ autoApprovedRef.current.add(toolCallId);
861
+ void chat.approve(toolCallId);
862
+ };
863
+ const awaitingConfirmation = useMemo(
864
+ () => chat.messages.some(
865
+ (message) => message.toolCalls?.some(
866
+ (call) => call.status === "requires_confirmation" && !sessionAllowed.has(call.name)
867
+ )
868
+ ),
869
+ [chat.messages, sessionAllowed]
870
+ );
871
+ return { sessionAllowed, handleApproveAlways, awaitingConfirmation };
872
+ }
873
+ function useSessionMeta(options) {
874
+ const sessionCreatedAtRef = useRef(void 0);
875
+ const messageCount = options.messages.length;
876
+ const firstUserContent = options.messages.find((m) => m.role === "user")?.content ?? "";
877
+ useEffect(() => {
878
+ const sessionId = options.sessionId;
879
+ if (!sessionId || sessionId === "custom") return;
880
+ if (!sessionCreatedAtRef.current) {
881
+ sessionCreatedAtRef.current = (/* @__PURE__ */ new Date()).toISOString();
882
+ }
883
+ try {
884
+ writeSessionMeta({
885
+ id: sessionId,
886
+ cwd: process.cwd(),
887
+ createdAt: sessionCreatedAtRef.current,
888
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
889
+ messageCount,
890
+ preview: derivePreview(options.messages),
891
+ provider: options.provider,
892
+ model: options.model
893
+ });
894
+ } catch {
895
+ }
896
+ }, [options.sessionId, messageCount, firstUserContent, options.provider, options.model]);
897
+ }
227
898
  function groupIntoTurns(messages) {
228
899
  const turns = [];
229
900
  let current = [];
@@ -243,33 +914,121 @@ function groupIntoTurns(messages) {
243
914
  return turns;
244
915
  }
245
916
  function ChatApp(options) {
246
- const runtime = useMemo(() => resolveChatProvider(options), [
247
- options.apiKey,
248
- options.baseUrl,
249
- options.model,
250
- options.provider
251
- ]);
252
- const memory = useMemo(
253
- () => resolveMemory(options.memoryBackend, options.memoryPath ?? ".agentskit-history.json"),
254
- [options.memoryPath, options.memoryBackend]
255
- );
256
- const tools = useMemo(() => resolveTools(options.tools), [options.tools]);
257
- const skills = useMemo(() => {
258
- if (!options.skill) return void 0;
259
- const names = options.skill.split(",").map((s) => s.trim());
260
- const resolved = names.map((n) => skillRegistry[n]).filter(Boolean);
261
- if (resolved.length === 0) return void 0;
262
- return resolved;
263
- }, [options.skill]);
917
+ const {
918
+ runtime,
919
+ memory,
920
+ tools,
921
+ skills,
922
+ state: { baseUrl, toolsFlag, skillFlag },
923
+ setProvider,
924
+ setModel,
925
+ setApiKey,
926
+ setBaseUrl,
927
+ setToolsFlag,
928
+ setSkillFlag
929
+ } = useRuntime(options);
930
+ const mergedTools = useMemo(() => {
931
+ const extra = options.extraTools ?? [];
932
+ return [...tools, ...extra];
933
+ }, [tools, options.extraTools]);
934
+ const mergedSkills = useMemo(() => {
935
+ const extra = options.extraSkills ?? [];
936
+ if (!skills && extra.length === 0) return void 0;
937
+ return [...skills ?? [], ...extra];
938
+ }, [skills, options.extraSkills]);
264
939
  const chat = useChat({
265
940
  adapter: runtime.adapter,
266
941
  memory,
267
942
  systemPrompt: options.system,
268
- tools: tools.length > 0 ? tools : void 0,
269
- skills
943
+ tools: mergedTools.length > 0 ? mergedTools : void 0,
944
+ skills: mergedSkills
945
+ });
946
+ const { sessionAllowed, handleApproveAlways, awaitingConfirmation } = useToolPermissions(chat);
947
+ useSessionMeta({
948
+ sessionId: options.sessionId,
949
+ messages: chat.messages,
950
+ provider: runtime.provider,
951
+ model: runtime.model
270
952
  });
271
953
  const turns = useMemo(() => groupIntoTurns(chat.messages), [chat.messages]);
272
- const toolNames = options.tools ? options.tools.split(",").map((s) => s.trim()).filter(Boolean) : [];
954
+ const toolNames = toolsFlag ? toolsFlag.split(",").map((s) => s.trim()).filter(Boolean) : [];
955
+ const [feedback, setFeedback] = useState(null);
956
+ const hookDispatcher = useMemo(
957
+ () => new HookDispatcher(options.hookHandlers ?? []),
958
+ [options.hookHandlers]
959
+ );
960
+ useEffect(() => {
961
+ void hookDispatcher.dispatch("SessionStart", {
962
+ event: "SessionStart",
963
+ sessionId: options.sessionId,
964
+ provider: runtime.provider,
965
+ model: runtime.model
966
+ });
967
+ return () => {
968
+ void hookDispatcher.dispatch("SessionEnd", {
969
+ event: "SessionEnd",
970
+ sessionId: options.sessionId
971
+ });
972
+ };
973
+ }, [hookDispatcher]);
974
+ const slashCommands = useMemo(
975
+ () => [...builtinSlashCommands, ...options.slashCommands ?? []],
976
+ [options.slashCommands]
977
+ );
978
+ const slashRegistry = useMemo(() => createSlashRegistry(slashCommands), [slashCommands]);
979
+ const handleSubmitInput = async (raw) => {
980
+ const parsed = parseSlashCommand(raw);
981
+ if (!parsed) {
982
+ const hookResult = await hookDispatcher.dispatch("UserPromptSubmit", {
983
+ event: "UserPromptSubmit",
984
+ prompt: raw
985
+ });
986
+ if (hookResult.blocked) {
987
+ setFeedback({
988
+ message: `Prompt blocked: ${hookResult.reason ?? "hook refused"}`,
989
+ kind: "warn"
990
+ });
991
+ return true;
992
+ }
993
+ setFeedback(null);
994
+ return false;
995
+ }
996
+ const cmd = slashRegistry.get(parsed.name);
997
+ if (!cmd) {
998
+ setFeedback({
999
+ message: `Unknown command: /${parsed.name}. Type /help for the list.`,
1000
+ kind: "error"
1001
+ });
1002
+ return true;
1003
+ }
1004
+ const ctx = {
1005
+ chat,
1006
+ runtime: {
1007
+ provider: runtime.provider,
1008
+ model: runtime.model,
1009
+ mode: runtime.mode,
1010
+ baseUrl,
1011
+ tools: toolsFlag,
1012
+ skill: skillFlag,
1013
+ sessionId: options.sessionId
1014
+ },
1015
+ setProvider,
1016
+ setModel,
1017
+ setApiKey,
1018
+ setBaseUrl,
1019
+ setTools: setToolsFlag,
1020
+ setSkill: setSkillFlag,
1021
+ feedback: (message, kind = "info") => setFeedback({ message, kind }),
1022
+ commands: slashCommands
1023
+ };
1024
+ try {
1025
+ await cmd.run(ctx, parsed.args);
1026
+ } catch (err) {
1027
+ const message = err instanceof Error ? err.message : String(err);
1028
+ setFeedback({ message: `/${parsed.name} failed: ${message}`, kind: "error" });
1029
+ }
1030
+ return true;
1031
+ };
273
1032
  return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", gap: 1, children: [
274
1033
  /* @__PURE__ */ jsx(
275
1034
  StatusHeader,
@@ -278,7 +1037,8 @@ function ChatApp(options) {
278
1037
  model: runtime.model,
279
1038
  mode: runtime.mode,
280
1039
  tools: toolNames,
281
- messageCount: chat.messages.length
1040
+ messageCount: chat.messages.length,
1041
+ sessionId: options.sessionId
282
1042
  }
283
1043
  ),
284
1044
  /* @__PURE__ */ jsx(ChatContainer, { children: turns.map((turn, turnIdx) => {
@@ -295,7 +1055,18 @@ function ChatApp(options) {
295
1055
  assistantSteps
296
1056
  ] }) : null,
297
1057
  /* @__PURE__ */ jsx(Message, { message }),
298
- message.toolCalls?.map((toolCall) => /* @__PURE__ */ jsx(ToolCallView, { toolCall, expanded: true }, toolCall.id))
1058
+ message.toolCalls?.map((toolCall) => /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1059
+ /* @__PURE__ */ jsx(ToolCallView, { toolCall, expanded: true }),
1060
+ toolCall.status === "requires_confirmation" && !sessionAllowed.has(toolCall.name) ? /* @__PURE__ */ jsx(
1061
+ ToolConfirmation,
1062
+ {
1063
+ toolCall,
1064
+ onApprove: chat.approve,
1065
+ onDeny: chat.deny,
1066
+ onApproveAlways: handleApproveAlways
1067
+ }
1068
+ ) : null
1069
+ ] }, toolCall.id))
299
1070
  ] }, message.id);
300
1071
  }) }, `turn-${turnIdx}`);
301
1072
  }) }),
@@ -306,9 +1077,38 @@ function ChatApp(options) {
306
1077
  label: toolNames.length > 0 ? "agent working" : "thinking"
307
1078
  }
308
1079
  ),
309
- /* @__PURE__ */ jsx(InputBar, { chat, placeholder: "Type a message and press Enter\u2026" })
1080
+ chat.error ? /* @__PURE__ */ jsxs(Box, { borderStyle: "round", borderColor: "red", paddingX: 1, flexDirection: "column", children: [
1081
+ /* @__PURE__ */ jsxs(Text, { color: "red", bold: true, children: [
1082
+ "\u2717 ",
1083
+ chat.error.name || "Error"
1084
+ ] }),
1085
+ /* @__PURE__ */ jsx(Text, { color: "red", children: chat.error.message })
1086
+ ] }) : null,
1087
+ feedback ? /* @__PURE__ */ jsx(Box, { borderStyle: "round", borderColor: feedbackBorder(feedback.kind), paddingX: 1, children: /* @__PURE__ */ jsx(Text, { color: feedbackBorder(feedback.kind), children: feedback.message }) }) : null,
1088
+ /* @__PURE__ */ jsx(
1089
+ InputBar,
1090
+ {
1091
+ chat,
1092
+ placeholder: "Type a message or /help for commands",
1093
+ disabled: awaitingConfirmation,
1094
+ onSubmitInput: handleSubmitInput
1095
+ }
1096
+ )
310
1097
  ] });
311
1098
  }
1099
+ function feedbackBorder(kind) {
1100
+ switch (kind) {
1101
+ case "error":
1102
+ return "red";
1103
+ case "warn":
1104
+ return "yellow";
1105
+ case "success":
1106
+ return "green";
1107
+ case "info":
1108
+ default:
1109
+ return "cyan";
1110
+ }
1111
+ }
312
1112
  function renderChatHeader(options) {
313
1113
  const runtime = resolveChatProvider(options);
314
1114
  const parts = [`provider=${runtime.provider}`];
@@ -319,32 +1119,317 @@ function renderChatHeader(options) {
319
1119
  if (options.memoryBackend) parts.push(`memory=${options.memoryBackend}`);
320
1120
  return parts.join(" ");
321
1121
  }
322
- var PROVIDER_IMPORT = {
323
- openai: "openai",
324
- anthropic: "anthropic",
325
- gemini: "gemini",
326
- ollama: "ollama"
327
- };
328
- var PROVIDER_DEFAULT_MODEL = {
329
- openai: "gpt-4o-mini",
330
- anthropic: "claude-sonnet-4-6",
331
- gemini: "gemini-2.5-flash",
332
- ollama: "llama3.1",
333
- demo: "demo"
334
- };
335
- var PROVIDER_ENV_KEY = {
336
- openai: "OPENAI_API_KEY",
337
- anthropic: "ANTHROPIC_API_KEY",
338
- gemini: "GEMINI_API_KEY",
339
- ollama: null,
340
- demo: null
341
- };
342
- function adapterCall(provider, prefix = "process.env") {
343
- const model = PROVIDER_DEFAULT_MODEL[provider];
344
- if (provider === "demo") return `demoAdapter()`;
345
- if (provider === "ollama") return `ollama({ model: '${model}' })`;
346
- const envKey = PROVIDER_ENV_KEY[provider];
347
- return `${PROVIDER_IMPORT[provider]}({ apiKey: ${prefix}.${envKey} ?? '', model: '${model}' })`;
1122
+ async function loadPlugins(options = {}) {
1123
+ const {
1124
+ specs = [],
1125
+ pluginDirs = [],
1126
+ cwd = process.cwd(),
1127
+ autoDiscoverUserDir = true,
1128
+ onError = (spec, err) => process.stderr.write(
1129
+ `[agentskit] plugin "${spec}" failed to load: ${err instanceof Error ? err.message : String(err)}
1130
+ `
1131
+ ),
1132
+ log = () => {
1133
+ }
1134
+ } = options;
1135
+ const resolvedSpecs = [...specs];
1136
+ const discoveryDirs = [...pluginDirs];
1137
+ if (autoDiscoverUserDir) discoveryDirs.push(join(homedir(), ".agentskit", "plugins"));
1138
+ for (const dir of discoveryDirs) {
1139
+ const discovered = await discoverPluginsInDir(dir);
1140
+ resolvedSpecs.push(...discovered);
1141
+ }
1142
+ const plugins = [];
1143
+ for (const spec of resolvedSpecs) {
1144
+ try {
1145
+ const plugin = await loadPluginFromSpec(spec, cwd, log);
1146
+ if (plugin) plugins.push(plugin);
1147
+ } catch (err) {
1148
+ onError(spec, err);
1149
+ }
1150
+ }
1151
+ return mergePluginsIntoBundle(plugins);
1152
+ }
1153
+ async function discoverPluginsInDir(dir) {
1154
+ try {
1155
+ const entries = await readdir(dir);
1156
+ const absolutes = entries.filter((name) => /\.(m?js|ts)$/i.test(name)).map((name) => join(dir, name));
1157
+ const validated = [];
1158
+ for (const p of absolutes) {
1159
+ try {
1160
+ const s = await stat(p);
1161
+ if (s.isFile()) validated.push(p);
1162
+ } catch {
1163
+ }
1164
+ }
1165
+ return validated;
1166
+ } catch {
1167
+ return [];
1168
+ }
1169
+ }
1170
+ async function loadPluginFromSpec(spec, cwd, log) {
1171
+ const isPath = spec.startsWith("./") || spec.startsWith("../") || isAbsolute(spec);
1172
+ const importTarget = isPath ? pathToFileURL(resolve(cwd, spec)).href : spec;
1173
+ const mod = await import(importTarget);
1174
+ const exported = mod.default ?? mod.plugin ?? mod;
1175
+ const sourcePath = isPath ? resolve(cwd, spec) : void 0;
1176
+ const ctx = {
1177
+ cwd,
1178
+ sourcePath,
1179
+ log: (msg) => log(`[${spec}] ${msg}`)
1180
+ };
1181
+ if (typeof exported === "function") {
1182
+ const factory = exported;
1183
+ return await factory(ctx);
1184
+ }
1185
+ if (exported && typeof exported === "object" && "name" in exported) {
1186
+ const plugin = exported;
1187
+ if (plugin.init) await plugin.init(ctx);
1188
+ return plugin;
1189
+ }
1190
+ throw new Error(
1191
+ "Module did not export a Plugin \u2014 expected default export to be a Plugin object or a PluginFactory function."
1192
+ );
1193
+ }
1194
+ function mergePluginsIntoBundle(plugins) {
1195
+ const bundle = {
1196
+ plugins,
1197
+ slashCommands: [],
1198
+ tools: [],
1199
+ skills: [],
1200
+ providers: {},
1201
+ hooks: [],
1202
+ mcpServers: []
1203
+ };
1204
+ for (const plugin of plugins) {
1205
+ if (plugin.slashCommands) bundle.slashCommands.push(...plugin.slashCommands);
1206
+ if (plugin.tools) bundle.tools.push(...plugin.tools);
1207
+ if (plugin.skills) bundle.skills.push(...plugin.skills);
1208
+ if (plugin.hooks) bundle.hooks.push(...plugin.hooks);
1209
+ if (plugin.mcpServers) bundle.mcpServers.push(...plugin.mcpServers);
1210
+ if (plugin.providers) {
1211
+ for (const [name, factory] of Object.entries(plugin.providers)) {
1212
+ bundle.providers[name] = factory;
1213
+ }
1214
+ }
1215
+ }
1216
+ return bundle;
1217
+ }
1218
+ var McpClient = class {
1219
+ constructor(spec, onError = (err) => process.stderr.write(
1220
+ `[agentskit] mcp[${spec.name}] error: ${err instanceof Error ? err.message : String(err)}
1221
+ `
1222
+ )) {
1223
+ this.spec = spec;
1224
+ this.onError = onError;
1225
+ this.buffer = "";
1226
+ this.nextId = 1;
1227
+ this.pending = /* @__PURE__ */ new Map();
1228
+ this.disposed = false;
1229
+ }
1230
+ async start() {
1231
+ if (this.child) return;
1232
+ const child = spawn(this.spec.command, this.spec.args ?? [], {
1233
+ env: { ...process.env, ...this.spec.env ?? {} },
1234
+ stdio: ["pipe", "pipe", "pipe"]
1235
+ });
1236
+ this.child = child;
1237
+ child.stdout.on("data", (chunk) => this.onStdout(chunk.toString()));
1238
+ child.stderr.on("data", (chunk) => {
1239
+ process.stderr.write(`[mcp ${this.spec.name}] ${chunk}`);
1240
+ });
1241
+ child.on("error", (err) => this.onError(err));
1242
+ child.on("close", () => {
1243
+ this.disposed = true;
1244
+ for (const pending of this.pending.values()) {
1245
+ pending({ jsonrpc: "2.0", id: 0, error: { code: -1, message: "server closed" } });
1246
+ }
1247
+ this.pending.clear();
1248
+ });
1249
+ await this.request("initialize", {
1250
+ protocolVersion: "2024-11-05",
1251
+ capabilities: {},
1252
+ clientInfo: { name: "agentskit", version: "0" }
1253
+ });
1254
+ }
1255
+ async listTools() {
1256
+ const res = await this.request("tools/list", {});
1257
+ const tools = res.tools ?? [];
1258
+ return tools;
1259
+ }
1260
+ async callTool(name, args) {
1261
+ return await this.request("tools/call", { name, arguments: args });
1262
+ }
1263
+ dispose() {
1264
+ if (this.disposed) return;
1265
+ this.disposed = true;
1266
+ this.child?.kill("SIGTERM");
1267
+ }
1268
+ request(method, params) {
1269
+ return new Promise((resolvePromise, rejectPromise) => {
1270
+ if (!this.child || this.disposed) {
1271
+ rejectPromise(new Error(`mcp server ${this.spec.name} not running`));
1272
+ return;
1273
+ }
1274
+ const id = this.nextId++;
1275
+ const timeoutMs = this.spec.timeout ?? 1e4;
1276
+ const timer = setTimeout(() => {
1277
+ this.pending.delete(id);
1278
+ rejectPromise(new Error(`mcp ${this.spec.name}.${method} timed out`));
1279
+ }, timeoutMs);
1280
+ this.pending.set(id, (res) => {
1281
+ clearTimeout(timer);
1282
+ if (res.error) {
1283
+ rejectPromise(new Error(`mcp ${method} failed: ${res.error.message}`));
1284
+ return;
1285
+ }
1286
+ resolvePromise(res.result);
1287
+ });
1288
+ const message = JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n";
1289
+ this.child.stdin.write(message);
1290
+ });
1291
+ }
1292
+ onStdout(chunk) {
1293
+ this.buffer += chunk;
1294
+ let newlineIndex = this.buffer.indexOf("\n");
1295
+ while (newlineIndex !== -1) {
1296
+ const line = this.buffer.slice(0, newlineIndex).trim();
1297
+ this.buffer = this.buffer.slice(newlineIndex + 1);
1298
+ if (line) {
1299
+ try {
1300
+ const parsed = JSON.parse(line);
1301
+ const pending = this.pending.get(Number(parsed.id));
1302
+ if (pending) {
1303
+ this.pending.delete(Number(parsed.id));
1304
+ pending(parsed);
1305
+ }
1306
+ } catch (err) {
1307
+ this.onError(err);
1308
+ }
1309
+ }
1310
+ newlineIndex = this.buffer.indexOf("\n");
1311
+ }
1312
+ }
1313
+ };
1314
+
1315
+ // src/extensibility/mcp/bridge.ts
1316
+ async function bridgeMcpServers(specs) {
1317
+ const clients = [];
1318
+ const tools = [];
1319
+ for (const spec of specs) {
1320
+ const client = new McpClient(spec);
1321
+ try {
1322
+ await client.start();
1323
+ const mcpTools = await client.listTools();
1324
+ for (const mcpTool of mcpTools) {
1325
+ tools.push(mcpToolToDefinition(spec.name, client, mcpTool));
1326
+ }
1327
+ clients.push(client);
1328
+ } catch (err) {
1329
+ process.stderr.write(
1330
+ `[agentskit] mcp server "${spec.name}" failed: ${err instanceof Error ? err.message : String(err)}
1331
+ `
1332
+ );
1333
+ client.dispose();
1334
+ }
1335
+ }
1336
+ return { clients, tools };
1337
+ }
1338
+ function mcpToolToDefinition(serverName, client, tool) {
1339
+ return {
1340
+ name: `${serverName}__${tool.name}`,
1341
+ description: tool.description ?? `MCP tool ${tool.name} from ${serverName}`,
1342
+ schema: tool.inputSchema ?? { type: "object", properties: {} },
1343
+ execute: async (args) => {
1344
+ return await client.callTool(tool.name, args);
1345
+ }
1346
+ };
1347
+ }
1348
+ function disposeMcpClients(clients) {
1349
+ for (const client of clients) client.dispose();
1350
+ }
1351
+ function formatEvent(event) {
1352
+ switch (event.type) {
1353
+ case "agent:step":
1354
+ return `[step ${event.step}] ${event.action}`;
1355
+ case "llm:start":
1356
+ return `[llm] start (${event.messageCount} messages)`;
1357
+ case "llm:end": {
1358
+ const preview = event.content.length > 100 ? event.content.slice(0, 100) + "..." : event.content;
1359
+ return `[llm] done (${event.durationMs}ms) "${preview}"`;
1360
+ }
1361
+ case "tool:start":
1362
+ return `[tool] ${event.name} ${JSON.stringify(event.args)}`;
1363
+ case "tool:end":
1364
+ return `[tool] ${event.name} done (${event.durationMs}ms)`;
1365
+ case "error":
1366
+ return `[error] ${event.error.message}`;
1367
+ default:
1368
+ return `[${event.type}]`;
1369
+ }
1370
+ }
1371
+ async function runAgent(task, options) {
1372
+ if (options.skill && options.skills) {
1373
+ process.stderr.write("Error: --skill and --skills are mutually exclusive. Use one or the other.\n");
1374
+ process.exit(1);
1375
+ }
1376
+ const { adapter } = resolveChatProvider({
1377
+ provider: options.provider,
1378
+ model: options.model,
1379
+ apiKey: options.apiKey,
1380
+ baseUrl: options.baseUrl
1381
+ });
1382
+ const tools = resolveTools(options.tools);
1383
+ const skill = options.skills ? resolveSkills(options.skills) : resolveSkill(options.skill);
1384
+ const memory = options.memory ? resolveMemory(options.memoryBackend, options.memory) : void 0;
1385
+ const observers = [];
1386
+ if (options.verbose) {
1387
+ observers.push({
1388
+ name: "cli-verbose",
1389
+ on(event) {
1390
+ process.stderr.write(formatEvent(event) + "\n");
1391
+ }
1392
+ });
1393
+ }
1394
+ const runtime = createRuntime({
1395
+ adapter,
1396
+ tools,
1397
+ memory,
1398
+ systemPrompt: options.systemPrompt,
1399
+ maxSteps: options.maxSteps ? parseInt(options.maxSteps, 10) : void 0,
1400
+ observers
1401
+ });
1402
+ const result = await runtime.run(task, {
1403
+ skill: skill ?? void 0
1404
+ });
1405
+ process.stdout.write(result.content + "\n");
1406
+ }
1407
+ var PROVIDER_IMPORT = {
1408
+ openai: "openai",
1409
+ anthropic: "anthropic",
1410
+ gemini: "gemini",
1411
+ ollama: "ollama"
1412
+ };
1413
+ var PROVIDER_DEFAULT_MODEL = {
1414
+ openai: "gpt-4o-mini",
1415
+ anthropic: "claude-sonnet-4-6",
1416
+ gemini: "gemini-2.5-flash",
1417
+ ollama: "llama3.1",
1418
+ demo: "demo"
1419
+ };
1420
+ var PROVIDER_ENV_KEY = {
1421
+ openai: "OPENAI_API_KEY",
1422
+ anthropic: "ANTHROPIC_API_KEY",
1423
+ gemini: "GEMINI_API_KEY",
1424
+ ollama: null,
1425
+ demo: null
1426
+ };
1427
+ function adapterCall(provider, prefix = "process.env") {
1428
+ const model = PROVIDER_DEFAULT_MODEL[provider];
1429
+ if (provider === "demo") return `demoAdapter()`;
1430
+ if (provider === "ollama") return `ollama({ model: '${model}' })`;
1431
+ const envKey = PROVIDER_ENV_KEY[provider];
1432
+ return `${PROVIDER_IMPORT[provider]}({ apiKey: ${prefix}.${envKey} ?? '', model: '${model}' })`;
348
1433
  }
349
1434
  function viteAdapterCall(provider) {
350
1435
  if (provider === "demo") return `demoAdapter()`;
@@ -792,62 +1877,6 @@ async function writeStarterProject(options) {
792
1877
  })
793
1878
  );
794
1879
  }
795
- function formatEvent(event) {
796
- switch (event.type) {
797
- case "agent:step":
798
- return `[step ${event.step}] ${event.action}`;
799
- case "llm:start":
800
- return `[llm] start (${event.messageCount} messages)`;
801
- case "llm:end": {
802
- const preview = event.content.length > 100 ? event.content.slice(0, 100) + "..." : event.content;
803
- return `[llm] done (${event.durationMs}ms) "${preview}"`;
804
- }
805
- case "tool:start":
806
- return `[tool] ${event.name} ${JSON.stringify(event.args)}`;
807
- case "tool:end":
808
- return `[tool] ${event.name} done (${event.durationMs}ms)`;
809
- case "error":
810
- return `[error] ${event.error.message}`;
811
- default:
812
- return `[${event.type}]`;
813
- }
814
- }
815
- async function runAgent(task, options) {
816
- if (options.skill && options.skills) {
817
- process.stderr.write("Error: --skill and --skills are mutually exclusive. Use one or the other.\n");
818
- process.exit(1);
819
- }
820
- const { adapter } = resolveChatProvider({
821
- provider: options.provider,
822
- model: options.model,
823
- apiKey: options.apiKey,
824
- baseUrl: options.baseUrl
825
- });
826
- const tools = resolveTools(options.tools);
827
- const skill = options.skills ? resolveSkills(options.skills) : resolveSkill(options.skill);
828
- const memory = options.memory ? resolveMemory(options.memoryBackend, options.memory) : void 0;
829
- const observers = [];
830
- if (options.verbose) {
831
- observers.push({
832
- name: "cli-verbose",
833
- on(event) {
834
- process.stderr.write(formatEvent(event) + "\n");
835
- }
836
- });
837
- }
838
- const runtime = createRuntime({
839
- adapter,
840
- tools,
841
- memory,
842
- systemPrompt: options.systemPrompt,
843
- maxSteps: options.maxSteps ? parseInt(options.maxSteps, 10) : void 0,
844
- observers
845
- });
846
- const result = await runtime.run(task, {
847
- skill: skill ?? void 0
848
- });
849
- process.stdout.write(result.content + "\n");
850
- }
851
1880
  var PROVIDER_ENV_KEYS = {
852
1881
  openai: "OPENAI_API_KEY",
853
1882
  anthropic: "ANTHROPIC_API_KEY",
@@ -907,8 +1936,8 @@ async function checkPnpm() {
907
1936
  };
908
1937
  }
909
1938
  async function checkPackageJson() {
910
- const path4 = join(process.cwd(), "package.json");
911
- if (!existsSync(path4)) {
1939
+ const path5 = join(process.cwd(), "package.json");
1940
+ if (!existsSync(path5)) {
912
1941
  return {
913
1942
  status: "warn",
914
1943
  name: "package.json",
@@ -917,7 +1946,7 @@ async function checkPackageJson() {
917
1946
  };
918
1947
  }
919
1948
  try {
920
- const pkg = JSON.parse(await readFile(path4, "utf8"));
1949
+ const pkg = JSON.parse(await readFile(path5, "utf8"));
921
1950
  const deps = {
922
1951
  ...pkg.dependencies ?? {},
923
1952
  ...pkg.devDependencies ?? {}
@@ -1179,11 +2208,11 @@ function startDev(options) {
1179
2208
  }
1180
2209
  });
1181
2210
  };
1182
- const restart = (path4) => {
2211
+ const restart = (path5) => {
1183
2212
  if (restartTimer) clearTimeout(restartTimer);
1184
2213
  restartTimer = setTimeout(() => {
1185
2214
  restartTimer = void 0;
1186
- banner(`\u21BB change detected \u2014 ${path4}`, "yellow");
2215
+ banner(`\u21BB change detected \u2014 ${path5}`, "yellow");
1187
2216
  if (child && !child.killed && child.exitCode === null) {
1188
2217
  child.kill("SIGTERM");
1189
2218
  }
@@ -1284,132 +2313,205 @@ async function startTunnel(options) {
1284
2313
  requests: () => requests
1285
2314
  };
1286
2315
  }
1287
- async function runInteractiveInit(defaults = {}) {
1288
- process.stdout.write(`
1289
- ${kleur3.bold().green("\u25B2")} ${kleur3.bold("agentskit init")}
1290
- `);
1291
- process.stdout.write(kleur3.dim(" Generate a starter project \u2014 answer five questions.\n\n"));
1292
- try {
1293
- const targetDir = await input({
1294
- message: "Project directory:",
1295
- default: defaults.dir ?? "agentskit-app",
1296
- validate: (value) => {
1297
- if (!value.trim()) return "A directory name is required.";
1298
- const abs = path.resolve(process.cwd(), value);
1299
- if (existsSync(abs)) return `${value} already exists. Pick a different name.`;
1300
- return true;
1301
- }
1302
- });
1303
- const template = await select({
1304
- message: "Template:",
1305
- default: defaults.template ?? "react",
1306
- choices: [
1307
- { name: "React chat (Vite + browser)", value: "react", description: "Streaming UI with @agentskit/react" },
1308
- { name: "Ink chat (terminal UI)", value: "ink", description: "Same chat but in your terminal" },
1309
- { name: "Runtime (headless agent, no UI)", value: "runtime", description: "Autonomous task \u2192 result" },
1310
- { name: "Multi-agent (planner + delegates)", value: "multi-agent", description: "Supervisor pattern, ready to extend" }
1311
- ]
1312
- });
1313
- const provider = await select({
1314
- message: "LLM provider:",
1315
- default: "demo",
1316
- choices: [
1317
- { name: "Demo (no API key \u2014 deterministic stub)", value: "demo" },
1318
- { name: "OpenAI", value: "openai" },
1319
- { name: "Anthropic", value: "anthropic" },
1320
- { name: "Gemini", value: "gemini" },
1321
- { name: "Ollama (local, no key)", value: "ollama" }
1322
- ]
2316
+
2317
+ // src/extensibility/rag/embedders.ts
2318
+ function createOpenAiEmbedder(config) {
2319
+ const model = config.model ?? "text-embedding-3-small";
2320
+ const baseUrl = (config.baseUrl ?? "https://api.openai.com").replace(/\/$/, "");
2321
+ return async (text) => {
2322
+ const res = await fetch(`${baseUrl}/v1/embeddings`, {
2323
+ method: "POST",
2324
+ headers: {
2325
+ "content-type": "application/json",
2326
+ authorization: `Bearer ${config.apiKey}`
2327
+ },
2328
+ body: JSON.stringify({ model, input: text })
1323
2329
  });
1324
- let tools = [];
1325
- if (template !== "react") {
1326
- tools = await checkbox({
1327
- message: "Tools (space to toggle, enter to confirm):",
1328
- choices: [
1329
- { name: "web_search", value: "web_search" },
1330
- { name: "filesystem", value: "filesystem" },
1331
- { name: "shell", value: "shell" }
1332
- ]
1333
- });
2330
+ if (!res.ok) {
2331
+ const body = await res.text().catch(() => "");
2332
+ throw new Error(`embedder ${model} HTTP ${res.status}: ${body}`);
1334
2333
  }
1335
- const memory = await select({
1336
- message: "Memory backend:",
1337
- default: "none",
1338
- choices: [
1339
- { name: "None (stateless)", value: "none" },
1340
- { name: "File (JSON on disk)", value: "file" },
1341
- { name: "SQLite (better-sqlite3)", value: "sqlite" }
1342
- ]
1343
- });
1344
- const packageManager = await select({
1345
- message: "Package manager:",
1346
- default: "pnpm",
1347
- choices: [
1348
- { name: "pnpm", value: "pnpm" },
1349
- { name: "npm", value: "npm" },
1350
- { name: "yarn", value: "yarn" },
1351
- { name: "bun", value: "bun" }
1352
- ]
1353
- });
1354
- process.stdout.write("\n" + kleur3.dim(" Summary:\n"));
1355
- process.stdout.write(kleur3.dim(` dir ${targetDir}
1356
- `));
1357
- process.stdout.write(kleur3.dim(` template ${template}
1358
- `));
1359
- process.stdout.write(kleur3.dim(` provider ${provider}
1360
- `));
1361
- if (tools.length) process.stdout.write(kleur3.dim(` tools ${tools.join(", ")}
1362
- `));
1363
- process.stdout.write(kleur3.dim(` memory ${memory}
1364
- `));
1365
- process.stdout.write(kleur3.dim(` pm ${packageManager}
1366
-
1367
- `));
1368
- const proceed = await confirm({ message: "Generate?", default: true });
1369
- if (!proceed) {
1370
- process.stdout.write(kleur3.yellow("Cancelled.\n"));
1371
- return { cancelled: true, options: { targetDir, template, provider, tools, memory, packageManager } };
1372
- }
1373
- return {
1374
- cancelled: false,
1375
- options: {
1376
- targetDir: path.resolve(process.cwd(), targetDir),
1377
- template,
1378
- provider,
1379
- tools,
1380
- memory,
1381
- packageManager
1382
- }
1383
- };
1384
- } catch (err) {
1385
- if (err.name === "ExitPromptError") {
1386
- process.stdout.write(kleur3.yellow("\nCancelled.\n"));
1387
- return { cancelled: true, options: { targetDir: "", template: "react" } };
1388
- }
1389
- throw err;
2334
+ const json = await res.json();
2335
+ const first = json.data?.[0]?.embedding;
2336
+ if (!first) throw new Error(`embedder ${model}: response missing data[0].embedding`);
2337
+ return first;
2338
+ };
2339
+ }
2340
+ function resolveEmbedder(config) {
2341
+ const embedder = config.embedder;
2342
+ const provider = embedder?.provider ?? "openai";
2343
+ if (provider !== "openai") {
2344
+ throw new Error(`Unsupported RAG embedder provider: ${provider}. Only "openai" is built-in.`);
2345
+ }
2346
+ const apiKey = embedder?.apiKey ?? process.env.OPENAI_API_KEY ?? process.env.OPENROUTER_API_KEY;
2347
+ if (!apiKey) {
2348
+ throw new Error("RAG embedder needs an API key (config.rag.embedder.apiKey or OPENAI_API_KEY env).");
1390
2349
  }
2350
+ return createOpenAiEmbedder({
2351
+ apiKey,
2352
+ model: embedder?.model,
2353
+ baseUrl: embedder?.baseUrl
2354
+ });
1391
2355
  }
1392
- function printNextSteps(options) {
1393
- const dir = path.relative(process.cwd(), options.targetDir) || ".";
1394
- const pm = options.packageManager ?? "pnpm";
1395
- const installCmd = pm === "npm" ? "npm install" : `${pm} install`;
1396
- const runCmd = pm === "npm" ? "npm run dev" : `${pm} dev`;
1397
- process.stdout.write("\n" + kleur3.green("\u2713 Created starter at ") + kleur3.bold(dir) + "\n\n");
1398
- process.stdout.write(kleur3.bold("Next steps:\n\n"));
1399
- process.stdout.write(` ${kleur3.cyan("cd")} ${dir}
1400
- `);
1401
- process.stdout.write(` ${kleur3.cyan(installCmd)}
1402
- `);
1403
- if (options.provider && options.provider !== "demo" && options.provider !== "ollama") {
1404
- process.stdout.write(
1405
- ` ${kleur3.cyan("cp")} .env.example .env ${kleur3.dim("# add your API key")}
1406
- `
1407
- );
2356
+ function buildRagFromConfig(options) {
2357
+ const cwd = options.cwd ?? process.cwd();
2358
+ const dir = resolve(cwd, options.config.dir ?? "./.agentskit-rag");
2359
+ const store = fileVectorMemory({ path: `${dir}/store.json` });
2360
+ const embed = options.embedder ?? resolveEmbedder(options.config);
2361
+ return createRAG({
2362
+ embed,
2363
+ store,
2364
+ chunkSize: options.config.chunkSize,
2365
+ topK: options.config.topK
2366
+ });
2367
+ }
2368
+ async function indexSources(rag, config, cwd) {
2369
+ const root = cwd ?? process.cwd();
2370
+ const sources = config.sources ?? [];
2371
+ const absolutePaths = [];
2372
+ for (const pattern of sources) {
2373
+ for await (const match of glob(pattern, { cwd: root })) {
2374
+ absolutePaths.push(resolve(root, match));
2375
+ }
1408
2376
  }
1409
- process.stdout.write(` ${kleur3.cyan(runCmd)}
2377
+ const documents = await Promise.all(
2378
+ absolutePaths.map(async (path5) => ({
2379
+ id: path5,
2380
+ content: await readFile(path5, "utf8"),
2381
+ source: path5
2382
+ }))
2383
+ );
2384
+ if (documents.length > 0) await rag.ingest(documents);
2385
+ return { documentCount: documents.length, sources: absolutePaths };
2386
+ }
1410
2387
 
2388
+ // src/commands/shared.ts
2389
+ function mergeWithConfig(options, config) {
2390
+ if (!config) return options;
2391
+ const d = config.defaults ?? {};
2392
+ const resolvedApiKey = options.apiKey ?? (d.apiKeyEnv ? process.env[d.apiKeyEnv] : void 0) ?? d.apiKey;
2393
+ return {
2394
+ ...options,
2395
+ provider: options.provider !== "demo" ? options.provider : d.provider ?? options.provider,
2396
+ model: options.model ?? d.model,
2397
+ apiKey: resolvedApiKey,
2398
+ baseUrl: options.baseUrl ?? d.baseUrl,
2399
+ tools: options.tools ?? d.tools,
2400
+ skill: options.skill ?? d.skill,
2401
+ system: options.system ?? d.system,
2402
+ memoryBackend: options.memoryBackend ?? d.memoryBackend
2403
+ };
2404
+ }
2405
+
2406
+ // src/commands/chat.ts
2407
+ function registerChatCommand(program) {
2408
+ 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").option(
2409
+ "--plugin-dir <dir>",
2410
+ "Extra directory to auto-discover plugin modules from (repeatable)",
2411
+ (value, prev = []) => [...prev, value],
2412
+ []
2413
+ ).option(
2414
+ "--mode <mode>",
2415
+ "Permission mode: default | plan | acceptEdits | bypassPermissions"
2416
+ ).action(async (options) => {
2417
+ if (options.listSessions) {
2418
+ const sessions = listSessions();
2419
+ if (sessions.length === 0) {
2420
+ process.stdout.write("No saved sessions for this directory.\n");
2421
+ return;
2422
+ }
2423
+ for (const s of sessions) {
2424
+ const { id, updatedAt, messageCount, preview, model, label, forkedFrom } = s.metadata;
2425
+ const display = label ? `${label} (${id})` : id;
2426
+ const forkNote = forkedFrom ? ` \u2190 fork ${forkedFrom}` : "";
2427
+ process.stdout.write(
2428
+ `${display} ${updatedAt} msgs=${messageCount}${model ? ` model=${model}` : ""}${forkNote}
2429
+ ${preview}
2430
+ `
2431
+ );
2432
+ }
2433
+ return;
2434
+ }
2435
+ const config = options.config !== false ? await loadConfig() : void 0;
2436
+ const merged = mergeWithConfig(options, config);
2437
+ const session = resolveSession({
2438
+ explicitPath: options.memory,
2439
+ forceNew: Boolean(options.new),
2440
+ resumeId: options.resume
2441
+ });
2442
+ if (!session.isNew && !options.memory) {
2443
+ process.stdout.write(
2444
+ `Resuming session ${session.id}. Start fresh with --new or list with --list-sessions.
2445
+ `
2446
+ );
2447
+ }
2448
+ const pluginBundle = await loadPlugins({
2449
+ specs: config?.plugins ?? [],
2450
+ pluginDirs: options.pluginDir ?? []
2451
+ });
2452
+ const configHooks = configHooksToHandlers(config?.hooks);
2453
+ const hookHandlers = [...configHooks, ...pluginBundle.hooks];
2454
+ const configMcpSpecs = Object.entries(config?.mcp?.servers ?? {}).map(([name, spec]) => ({
2455
+ name,
2456
+ command: spec.command,
2457
+ args: spec.args,
2458
+ env: spec.env,
2459
+ timeout: spec.timeout
2460
+ }));
2461
+ const allMcpSpecs = [...configMcpSpecs, ...pluginBundle.mcpServers];
2462
+ const { clients: mcpClients, tools: mcpTools } = await bridgeMcpServers(allMcpSpecs);
2463
+ const policyMode = options.mode ?? config?.permissions?.mode ?? "default";
2464
+ const permissionPolicy = {
2465
+ mode: policyMode,
2466
+ rules: (config?.permissions?.rules ?? []).map((r) => ({
2467
+ tool: r.tool,
2468
+ action: r.action,
2469
+ scope: r.scope
2470
+ }))
2471
+ };
2472
+ const chatOptions = {
2473
+ apiKey: merged.apiKey ?? options.apiKey,
2474
+ baseUrl: merged.baseUrl ?? options.baseUrl,
2475
+ provider: merged.provider,
2476
+ model: merged.model,
2477
+ system: merged.system ?? options.system,
2478
+ memoryPath: session.file,
2479
+ sessionId: session.id,
2480
+ tools: merged.tools ?? options.tools,
2481
+ skill: merged.skill ?? options.skill,
2482
+ memoryBackend: merged.memoryBackend ?? options.memoryBackend,
2483
+ agentsKitConfig: config,
2484
+ slashCommands: pluginBundle.slashCommands,
2485
+ extraTools: [...pluginBundle.tools, ...mcpTools],
2486
+ extraSkills: pluginBundle.skills,
2487
+ hookHandlers,
2488
+ permissionPolicy
2489
+ };
2490
+ process.stdout.write(`${renderChatHeader(chatOptions)}
1411
2491
  `);
1412
- process.stdout.write(kleur3.dim(" Docs: https://www.agentskit.io/docs\n\n"));
2492
+ const instance = render(React2.createElement(ChatApp, chatOptions));
2493
+ try {
2494
+ await instance.waitUntilExit();
2495
+ } finally {
2496
+ disposeMcpClients(mcpClients);
2497
+ }
2498
+ if (options.memory) {
2499
+ process.stdout.write(
2500
+ `
2501
+ Session saved to ${session.file}. Resume with --memory ${session.file}
2502
+ `
2503
+ );
2504
+ } else {
2505
+ process.stdout.write(
2506
+ `
2507
+ Session saved. Resume with:
2508
+ agentskit chat --resume ${session.id}
2509
+ Or start fresh with:
2510
+ agentskit chat --new
2511
+ `
2512
+ );
2513
+ }
2514
+ });
1413
2515
  }
1414
2516
  function RunApp({ task, options }) {
1415
2517
  const [status, setStatus] = useState("running");
@@ -1510,38 +2612,8 @@ function RunApp({ task, options }) {
1510
2612
  ] });
1511
2613
  }
1512
2614
 
1513
- // src/commands.ts
1514
- function mergeWithConfig(options, config) {
1515
- if (!config) return options;
1516
- return {
1517
- ...options,
1518
- // Config defaults — only apply if CLI flag wasn't set
1519
- provider: options.provider !== "demo" ? options.provider : config.defaults?.provider ?? options.provider,
1520
- model: options.model ?? config.defaults?.model
1521
- };
1522
- }
1523
- function createCli() {
1524
- const program = new Command();
1525
- program.name("agentskit").description("AgentsKit CLI for chat demos and project bootstrapping.");
1526
- 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) => {
1527
- const config = options.config !== false ? await loadConfig() : void 0;
1528
- const merged = mergeWithConfig(options, config);
1529
- const chatOptions = {
1530
- apiKey: merged.apiKey ?? options.apiKey,
1531
- baseUrl: merged.baseUrl ?? options.baseUrl,
1532
- provider: merged.provider,
1533
- model: merged.model,
1534
- system: options.system,
1535
- memoryPath: options.memory,
1536
- tools: options.tools,
1537
- skill: options.skill,
1538
- memoryBackend: options.memoryBackend,
1539
- agentsKitConfig: config
1540
- };
1541
- process.stdout.write(`${renderChatHeader(chatOptions)}
1542
- `);
1543
- render(React3.createElement(ChatApp, chatOptions));
1544
- });
2615
+ // src/commands/run.ts
2616
+ function registerRunCommand(program) {
1545
2617
  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) => {
1546
2618
  const task = options.task ?? positionalTask;
1547
2619
  if (!task) {
@@ -1551,7 +2623,7 @@ function createCli() {
1551
2623
  const config = options.config !== false ? await loadConfig() : void 0;
1552
2624
  const merged = mergeWithConfig(options, config);
1553
2625
  if (options.pretty) {
1554
- render(React3.createElement(RunApp, { task, options }));
2626
+ render(React2.createElement(RunApp, { task, options }));
1555
2627
  } else {
1556
2628
  try {
1557
2629
  await runAgent(task, { ...options, provider: merged.provider, model: merged.model });
@@ -1562,6 +2634,137 @@ function createCli() {
1562
2634
  }
1563
2635
  }
1564
2636
  });
2637
+ }
2638
+ async function runInteractiveInit(defaults = {}) {
2639
+ process.stdout.write(`
2640
+ ${kleur3.bold().green("\u25B2")} ${kleur3.bold("agentskit init")}
2641
+ `);
2642
+ process.stdout.write(kleur3.dim(" Generate a starter project \u2014 answer five questions.\n\n"));
2643
+ try {
2644
+ const targetDir = await input({
2645
+ message: "Project directory:",
2646
+ default: defaults.dir ?? "agentskit-app",
2647
+ validate: (value) => {
2648
+ if (!value.trim()) return "A directory name is required.";
2649
+ const abs = path.resolve(process.cwd(), value);
2650
+ if (existsSync(abs)) return `${value} already exists. Pick a different name.`;
2651
+ return true;
2652
+ }
2653
+ });
2654
+ const template = await select({
2655
+ message: "Template:",
2656
+ default: defaults.template ?? "react",
2657
+ choices: [
2658
+ { name: "React chat (Vite + browser)", value: "react", description: "Streaming UI with @agentskit/react" },
2659
+ { name: "Ink chat (terminal UI)", value: "ink", description: "Same chat but in your terminal" },
2660
+ { name: "Runtime (headless agent, no UI)", value: "runtime", description: "Autonomous task \u2192 result" },
2661
+ { name: "Multi-agent (planner + delegates)", value: "multi-agent", description: "Supervisor pattern, ready to extend" }
2662
+ ]
2663
+ });
2664
+ const provider = await select({
2665
+ message: "LLM provider:",
2666
+ default: "demo",
2667
+ choices: [
2668
+ { name: "Demo (no API key \u2014 deterministic stub)", value: "demo" },
2669
+ { name: "OpenAI", value: "openai" },
2670
+ { name: "Anthropic", value: "anthropic" },
2671
+ { name: "Gemini", value: "gemini" },
2672
+ { name: "Ollama (local, no key)", value: "ollama" }
2673
+ ]
2674
+ });
2675
+ let tools = [];
2676
+ if (template !== "react") {
2677
+ tools = await checkbox({
2678
+ message: "Tools (space to toggle, enter to confirm):",
2679
+ choices: [
2680
+ { name: "web_search", value: "web_search" },
2681
+ { name: "filesystem", value: "filesystem" },
2682
+ { name: "shell", value: "shell" }
2683
+ ]
2684
+ });
2685
+ }
2686
+ const memory = await select({
2687
+ message: "Memory backend:",
2688
+ default: "none",
2689
+ choices: [
2690
+ { name: "None (stateless)", value: "none" },
2691
+ { name: "File (JSON on disk)", value: "file" },
2692
+ { name: "SQLite (better-sqlite3)", value: "sqlite" }
2693
+ ]
2694
+ });
2695
+ const packageManager = await select({
2696
+ message: "Package manager:",
2697
+ default: "pnpm",
2698
+ choices: [
2699
+ { name: "pnpm", value: "pnpm" },
2700
+ { name: "npm", value: "npm" },
2701
+ { name: "yarn", value: "yarn" },
2702
+ { name: "bun", value: "bun" }
2703
+ ]
2704
+ });
2705
+ process.stdout.write("\n" + kleur3.dim(" Summary:\n"));
2706
+ process.stdout.write(kleur3.dim(` dir ${targetDir}
2707
+ `));
2708
+ process.stdout.write(kleur3.dim(` template ${template}
2709
+ `));
2710
+ process.stdout.write(kleur3.dim(` provider ${provider}
2711
+ `));
2712
+ if (tools.length) process.stdout.write(kleur3.dim(` tools ${tools.join(", ")}
2713
+ `));
2714
+ process.stdout.write(kleur3.dim(` memory ${memory}
2715
+ `));
2716
+ process.stdout.write(kleur3.dim(` pm ${packageManager}
2717
+
2718
+ `));
2719
+ const proceed = await confirm({ message: "Generate?", default: true });
2720
+ if (!proceed) {
2721
+ process.stdout.write(kleur3.yellow("Cancelled.\n"));
2722
+ return { cancelled: true, options: { targetDir, template, provider, tools, memory, packageManager } };
2723
+ }
2724
+ return {
2725
+ cancelled: false,
2726
+ options: {
2727
+ targetDir: path.resolve(process.cwd(), targetDir),
2728
+ template,
2729
+ provider,
2730
+ tools,
2731
+ memory,
2732
+ packageManager
2733
+ }
2734
+ };
2735
+ } catch (err) {
2736
+ if (err.name === "ExitPromptError") {
2737
+ process.stdout.write(kleur3.yellow("\nCancelled.\n"));
2738
+ return { cancelled: true, options: { targetDir: "", template: "react" } };
2739
+ }
2740
+ throw err;
2741
+ }
2742
+ }
2743
+ function printNextSteps(options) {
2744
+ const dir = path.relative(process.cwd(), options.targetDir) || ".";
2745
+ const pm = options.packageManager ?? "pnpm";
2746
+ const installCmd = pm === "npm" ? "npm install" : `${pm} install`;
2747
+ const runCmd = pm === "npm" ? "npm run dev" : `${pm} dev`;
2748
+ process.stdout.write("\n" + kleur3.green("\u2713 Created starter at ") + kleur3.bold(dir) + "\n\n");
2749
+ process.stdout.write(kleur3.bold("Next steps:\n\n"));
2750
+ process.stdout.write(` ${kleur3.cyan("cd")} ${dir}
2751
+ `);
2752
+ process.stdout.write(` ${kleur3.cyan(installCmd)}
2753
+ `);
2754
+ if (options.provider && options.provider !== "demo" && options.provider !== "ollama") {
2755
+ process.stdout.write(
2756
+ ` ${kleur3.cyan("cp")} .env.example .env ${kleur3.dim("# add your API key")}
2757
+ `
2758
+ );
2759
+ }
2760
+ process.stdout.write(` ${kleur3.cyan(runCmd)}
2761
+
2762
+ `);
2763
+ process.stdout.write(kleur3.dim(" Docs: https://www.agentskit.io/docs\n\n"));
2764
+ }
2765
+
2766
+ // src/commands/init.ts
2767
+ function registerInitCommand(program) {
1565
2768
  program.command("init").description("Generate a starter project. Run with no flags for interactive mode.").option("--template <template>", "Starter template (react|ink|runtime|multi-agent)").option("--dir <directory>", "Target directory", "agentskit-app").option("--provider <provider>", "LLM provider (openai|anthropic|gemini|ollama|demo)").option("--tools <tools>", "Comma-separated tools (web_search,filesystem,shell)").option("--memory <backend>", "Memory backend (none|file|sqlite)").option("--pm <packageManager>", "Package manager (pnpm|npm|yarn|bun)").option("-y, --yes", "Skip interactive prompts; use flag values + defaults").action(async (rawOptions) => {
1566
2769
  const isCi = !process.stdout.isTTY || rawOptions.yes || rawOptions.template;
1567
2770
  let resolved;
@@ -1595,6 +2798,10 @@ function createCli() {
1595
2798
  printNextSteps(resolved);
1596
2799
  }
1597
2800
  });
2801
+ }
2802
+
2803
+ // src/commands/doctor.ts
2804
+ function registerDoctorCommand(program) {
1598
2805
  program.command("doctor").description("Diagnose your AgentsKit environment.").option("--no-network", "Skip provider reachability checks").option(
1599
2806
  "--providers <providers>",
1600
2807
  "Comma-separated providers to check (default: openai,anthropic,gemini,ollama)"
@@ -1611,6 +2818,10 @@ function createCli() {
1611
2818
  }
1612
2819
  if (report.fail > 0) process.exit(1);
1613
2820
  });
2821
+ }
2822
+
2823
+ // src/commands/dev.ts
2824
+ function registerDevCommand(program) {
1614
2825
  program.command("dev [entry]").description("Run an entry file with hot-reload on file changes.").option("--watch <globs>", "Comma-separated glob patterns to watch").option("--ignore <globs>", "Comma-separated glob patterns to ignore").option("--debounce <ms>", "Debounce window before restart", "200").action(async (positional, options) => {
1615
2826
  const entry = positional ?? "src/index.ts";
1616
2827
  const watch = options.watch ? options.watch.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
@@ -1629,6 +2840,55 @@ function createCli() {
1629
2840
  process.exit(1);
1630
2841
  }
1631
2842
  });
2843
+ }
2844
+ function registerConfigCommand(program) {
2845
+ program.command("config").description("Show or scaffold the AgentsKit config.").argument(
2846
+ "[action]",
2847
+ 'Action: "init" to create a template, "show" to print the merged config.',
2848
+ "show"
2849
+ ).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) => {
2850
+ const isLocal = Boolean(options.local);
2851
+ const targetPath = isLocal ? path.join(process.cwd(), ".agentskit.config.json") : path.join(homedir(), ".agentskit", "config.json");
2852
+ if (action === "show") {
2853
+ const config = await loadConfig();
2854
+ process.stdout.write(JSON.stringify(config ?? {}, null, 2) + "\n");
2855
+ return;
2856
+ }
2857
+ if (action !== "init") {
2858
+ process.stderr.write(`Unknown action: ${action}. Use "init" or "show".
2859
+ `);
2860
+ process.exit(2);
2861
+ }
2862
+ if (existsSync(targetPath) && !options.force) {
2863
+ process.stderr.write(
2864
+ `Config already exists at ${targetPath}. Re-run with --force to overwrite.
2865
+ `
2866
+ );
2867
+ process.exit(1);
2868
+ }
2869
+ const template = {
2870
+ defaults: {
2871
+ provider: "openai",
2872
+ baseUrl: "https://openrouter.ai/api",
2873
+ apiKeyEnv: "OPENROUTER_API_KEY",
2874
+ model: "openai/gpt-oss-120b:free",
2875
+ tools: "web_search,fetch_url"
2876
+ }
2877
+ };
2878
+ mkdirSync(path.dirname(targetPath), { recursive: true });
2879
+ writeFileSync(targetPath, JSON.stringify(template, null, 2) + "\n");
2880
+ process.stdout.write(
2881
+ `Wrote ${targetPath}
2882
+ Edit it to taste, then run:
2883
+ agentskit chat
2884
+ (flags on the CLI still win over config values.)
2885
+ `
2886
+ );
2887
+ });
2888
+ }
2889
+
2890
+ // src/commands/tunnel.ts
2891
+ function registerTunnelCommand(program) {
1632
2892
  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) => {
1633
2893
  const portNum = Number(port);
1634
2894
  if (Number.isNaN(portNum) || portNum < 1 || portNum > 65535) {
@@ -1649,9 +2909,62 @@ function createCli() {
1649
2909
  process.exit(1);
1650
2910
  }
1651
2911
  });
2912
+ }
2913
+
2914
+ // src/commands/rag.ts
2915
+ function registerRagCommand(program) {
2916
+ const rag = program.command("rag").description("Retrieval-augmented generation utilities.");
2917
+ rag.command("index").description("Index files referenced by config.rag.sources into the vector store.").option(
2918
+ "--source <glob>",
2919
+ "Glob to index (overrides config.rag.sources; repeatable)",
2920
+ (value, prev = []) => [...prev, value],
2921
+ []
2922
+ ).action(async (options) => {
2923
+ const config = await loadConfig();
2924
+ const rawConfig = config?.rag;
2925
+ const overrideSources = options.source;
2926
+ const ragConfig = {
2927
+ ...rawConfig ?? {},
2928
+ sources: overrideSources.length > 0 ? overrideSources : rawConfig?.sources ?? []
2929
+ };
2930
+ if (!ragConfig.sources || ragConfig.sources.length === 0) {
2931
+ process.stderr.write("No RAG sources configured. Set config.rag.sources or pass --source <glob>.\n");
2932
+ process.exit(1);
2933
+ }
2934
+ try {
2935
+ const instance = buildRagFromConfig({ config: ragConfig });
2936
+ const result = await indexSources(instance, ragConfig);
2937
+ process.stdout.write(
2938
+ `Indexed ${result.documentCount} document${result.documentCount === 1 ? "" : "s"}.
2939
+ `
2940
+ );
2941
+ for (const source of result.sources) {
2942
+ process.stdout.write(` \u2022 ${source}
2943
+ `);
2944
+ }
2945
+ } catch (err) {
2946
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
2947
+ `);
2948
+ process.exit(1);
2949
+ }
2950
+ });
2951
+ }
2952
+
2953
+ // src/commands/index.ts
2954
+ function createCli() {
2955
+ const program = new Command();
2956
+ program.name("agentskit").description("AgentsKit CLI for chat demos and project bootstrapping.");
2957
+ registerChatCommand(program);
2958
+ registerRunCommand(program);
2959
+ registerInitCommand(program);
2960
+ registerDoctorCommand(program);
2961
+ registerDevCommand(program);
2962
+ registerConfigCommand(program);
2963
+ registerTunnelCommand(program);
2964
+ registerRagCommand(program);
1652
2965
  return program;
1653
2966
  }
1654
2967
 
1655
- export { ChatApp, createCli, loadConfig, renderChatHeader, renderReport, resolveChatProvider, runAgent, runDoctor, startDev, startTunnel, writeStarterProject };
1656
- //# sourceMappingURL=chunk-CCPJYGHP.js.map
1657
- //# sourceMappingURL=chunk-CCPJYGHP.js.map
2968
+ export { ChatApp, HookDispatcher, McpClient, applyPolicyToTool, applyPolicyToTools, bridgeMcpServers, buildRagFromConfig, computeCost, configHooksToHandlers, createCli, createOpenAiEmbedder, defaultPolicy, derivePreview, disposeMcpClients, evaluatePolicy, findLatestSession, findSession, forkSession, generateSessionId, getPricing, indexSources, listSessions, loadConfig, loadPlugins, mergePluginsIntoBundle, registerPricing, renameSession, renderChatHeader, renderReport, resolveChatProvider, resolveSession, runAgent, runDoctor, sessionFilePath, startDev, startTunnel, writeSessionMeta, writeStarterProject };
2969
+ //# sourceMappingURL=chunk-72XFU2X2.js.map
2970
+ //# sourceMappingURL=chunk-72XFU2X2.js.map