@mtreeai/msapling-cli 2.3.6-beta.45 → 2.3.6-beta.47

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.
Files changed (2) hide show
  1. package/dist/index.js +1938 -428
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1017,6 +1017,21 @@ var init_src = __esm({
1017
1017
  const data = overviewData;
1018
1018
  return this.mapUser(data, billingData);
1019
1019
  }
1020
+ /**
1021
+ * CLI-PARITY-P0-3: Web search via the backend proxy (POST /api/web/search).
1022
+ *
1023
+ * Provider API keys (Tavily/Serper) stay server-side; the CLI only ever sees
1024
+ * normalised result rows. Mirrors the WebSearchTool surface so the agent can
1025
+ * search the live web without local credentials. Backend fails open with an
1026
+ * empty result set + provider "none" rather than 500 when no provider is
1027
+ * available, so callers can degrade gracefully.
1028
+ */
1029
+ async webSearch(query, maxResults = 5) {
1030
+ return await this.request("/api/web/search", {
1031
+ method: "POST",
1032
+ body: JSON.stringify({ query, max_results: maxResults })
1033
+ });
1034
+ }
1020
1035
  async getHistory(chatId) {
1021
1036
  const data = await this.request(`/api/projects/chat/${chatId}/history`);
1022
1037
  return data.messages.map((m) => ({
@@ -1268,6 +1283,30 @@ var init_src = __esm({
1268
1283
  const data = await this.request(`/api/projects/${encodeURIComponent(projectId)}/chats`);
1269
1284
  return Array.isArray(data) ? data : data.chats ?? [];
1270
1285
  }
1286
+ // P0-4 (CLI-RESUME-01): list the user's recent chats across ALL projects for
1287
+ // `/resume` + `msapling --continue`. The /api/projects/ tree keys projects by
1288
+ // NAME and nests each project's `chats` array ({id, title, model, created_at,
1289
+ // updated_at?}). We flatten the tree, stamp each chat with its project name,
1290
+ // and sort by recency (updated_at, then created_at) so the most-recently
1291
+ // touched chat is first. Backend doesn't expose a flat "recent chats" route,
1292
+ // so this client-side flatten is the canonical path (mirrors chat.ts).
1293
+ async listRecentChats(limit = 20) {
1294
+ const data = await this.request("/api/projects/");
1295
+ const flat = [];
1296
+ for (const [projectName, bucket] of Object.entries(data.projects ?? {})) {
1297
+ for (const chat of bucket?.chats ?? []) {
1298
+ if (!chat?.id) continue;
1299
+ flat.push({ ...chat, project: projectName });
1300
+ }
1301
+ }
1302
+ const recencyKey = (c) => {
1303
+ const raw = c.updated_at ?? c.created_at;
1304
+ const t = raw ? Date.parse(raw) : NaN;
1305
+ return Number.isNaN(t) ? 0 : t;
1306
+ };
1307
+ flat.sort((a, b) => recencyKey(b) - recencyKey(a));
1308
+ return flat.slice(0, Math.max(1, limit));
1309
+ }
1271
1310
  // CLI-CHAT-CREATE-01 (Iter 34): create a new chat in a project. Per LAB
1272
1311
  // projects.py:365 — POST /api/projects/chat/new with
1273
1312
  // {project_name, slot_label?, chat_name?, model?, client_type?}.
@@ -2155,7 +2194,7 @@ var init_RunCommandTool = __esm({
2155
2194
  this.activeCommands++;
2156
2195
  return;
2157
2196
  }
2158
- return new Promise((resolve20) => this.queue.push(resolve20));
2197
+ return new Promise((resolve21) => this.queue.push(resolve21));
2159
2198
  }
2160
2199
  static releaseLock() {
2161
2200
  if (this.queue.length > 0) {
@@ -2235,9 +2274,9 @@ var init_RunCommandTool = __esm({
2235
2274
  const chunks = { stdout: [], stderr: [] };
2236
2275
  proc.stdout?.on("data", (chunk) => chunks.stdout.push(chunk));
2237
2276
  proc.stderr?.on("data", (chunk) => chunks.stderr.push(chunk));
2238
- const exitCode = await new Promise((resolve20) => {
2239
- proc.on("exit", (code) => resolve20(code ?? 1));
2240
- proc.on("error", () => resolve20(1));
2277
+ const exitCode = await new Promise((resolve21) => {
2278
+ proc.on("exit", (code) => resolve21(code ?? 1));
2279
+ proc.on("error", () => resolve21(1));
2241
2280
  });
2242
2281
  const stdout = Buffer.concat(chunks.stdout).toString("utf-8");
2243
2282
  const stderr = Buffer.concat(chunks.stderr).toString("utf-8");
@@ -2395,7 +2434,7 @@ var init_src2 = __esm({
2395
2434
  const message = `Content-Length: ${Buffer.byteLength(content, "utf8")}\r
2396
2435
  \r
2397
2436
  ${content}`;
2398
- return new Promise((resolve20, reject) => {
2437
+ return new Promise((resolve21, reject) => {
2399
2438
  const timeoutHandle = setTimeout(() => {
2400
2439
  this.pendingRequests.delete(id);
2401
2440
  reject(new Error(`LSP request timeout after ${timeoutMs}ms: ${method}`));
@@ -2403,7 +2442,7 @@ ${content}`;
2403
2442
  this.pendingRequests.set(id, {
2404
2443
  resolve: (response) => {
2405
2444
  clearTimeout(timeoutHandle);
2406
- resolve20(response);
2445
+ resolve21(response);
2407
2446
  },
2408
2447
  reject: (error) => {
2409
2448
  clearTimeout(timeoutHandle);
@@ -2556,9 +2595,9 @@ var init_SubShellTool = __esm({
2556
2595
  }
2557
2596
  throw e;
2558
2597
  }
2559
- await new Promise((resolve20) => {
2560
- proc.on("exit", () => resolve20());
2561
- proc.on("error", () => resolve20());
2598
+ await new Promise((resolve21) => {
2599
+ proc.on("exit", () => resolve21());
2600
+ proc.on("error", () => resolve21());
2562
2601
  });
2563
2602
  return { content: `Successfully launched separate window for ${args2.worker_id}` };
2564
2603
  }
@@ -2637,27 +2676,42 @@ async function* walkFiles(root, current = root) {
2637
2676
  }
2638
2677
  }
2639
2678
  }
2679
+ function rgCandidates() {
2680
+ if (process.platform === "win32") {
2681
+ return [
2682
+ "rg.exe",
2683
+ "rg",
2684
+ "C:\\Program Files\\ripgrep\\rg.exe",
2685
+ "C:\\ProgramData\\chocolatey\\bin\\rg.exe"
2686
+ ];
2687
+ }
2688
+ return ["rg", "/usr/bin/rg", "/usr/local/bin/rg", "/opt/homebrew/bin/rg"];
2689
+ }
2640
2690
  async function findRg() {
2641
- const candidates = ["rg", "C:\\Program Files\\ripgrep\\rg.exe"];
2642
- for (const bin of candidates) {
2691
+ if (_rgCache.resolved) return _rgCache.value;
2692
+ for (const bin of rgCandidates()) {
2643
2693
  try {
2644
- const exited = await new Promise((resolve20) => {
2694
+ const exited = await new Promise((resolve21) => {
2645
2695
  try {
2646
2696
  const p = spawn4(bin, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
2647
- p.on("error", () => resolve20(null));
2648
- p.on("exit", (code) => resolve20(code));
2697
+ p.on("error", () => resolve21(null));
2698
+ p.on("exit", (code) => resolve21(code));
2649
2699
  } catch {
2650
- resolve20(null);
2700
+ resolve21(null);
2651
2701
  }
2652
2702
  });
2653
- if (exited === 0) return bin;
2703
+ if (exited === 0) {
2704
+ _rgCache = { resolved: true, value: bin };
2705
+ return bin;
2706
+ }
2654
2707
  } catch {
2655
2708
  }
2656
2709
  }
2710
+ _rgCache = { resolved: true, value: null };
2657
2711
  return null;
2658
2712
  }
2659
2713
  function runRg(bin, args2) {
2660
- return new Promise((resolve20) => {
2714
+ return new Promise((resolve21) => {
2661
2715
  const p = spawn4(bin, args2, { stdio: ["ignore", "pipe", "pipe"] });
2662
2716
  let stdout = "";
2663
2717
  let stderr = "";
@@ -2668,17 +2722,24 @@ function runRg(bin, args2) {
2668
2722
  stderr += d.toString("utf8");
2669
2723
  });
2670
2724
  p.on("error", (e) => {
2671
- resolve20({ stdout, stderr: stderr + (e?.message ?? ""), exitCode: -1 });
2725
+ resolve21({ stdout, stderr: stderr + (e?.message ?? ""), exitCode: -1 });
2672
2726
  });
2673
2727
  p.on("exit", (code) => {
2674
- resolve20({ stdout, stderr, exitCode: code });
2728
+ resolve21({ stdout, stderr, exitCode: code });
2675
2729
  });
2676
2730
  });
2677
2731
  }
2678
- async function nodeGrepFallback(pattern, searchRoot, maxMatches, caseSensitive) {
2732
+ async function nodeGrepFallback(pattern, searchRoot, maxMatches, caseSensitive, include, exclude) {
2679
2733
  const regex = new RegExp(pattern, caseSensitive ? "" : "i");
2680
2734
  const matches2 = [];
2735
+ const relPaths = [];
2681
2736
  for await (const relPath of walkFiles(searchRoot)) {
2737
+ if (include && !include.test(relPath)) continue;
2738
+ if (exclude && exclude.test(relPath)) continue;
2739
+ relPaths.push(relPath);
2740
+ }
2741
+ relPaths.sort();
2742
+ for (const relPath of relPaths) {
2682
2743
  if (matches2.length >= maxMatches) break;
2683
2744
  const fullPath = join6(searchRoot, relPath);
2684
2745
  try {
@@ -2698,7 +2759,7 @@ async function nodeGrepFallback(pattern, searchRoot, maxMatches, caseSensitive)
2698
2759
  }
2699
2760
  return matches2;
2700
2761
  }
2701
- var MAX_GLOB_RESULTS, MAX_GREP_MATCHES, SKIP_DIRS, GlobFilesTool, GrepSearchTool;
2762
+ var MAX_GLOB_RESULTS, MAX_GREP_MATCHES, SKIP_DIRS, GlobFilesTool, _rgCache, GrepSearchTool;
2702
2763
  var init_SearchTools = __esm({
2703
2764
  "../core/src/tools/SearchTools.ts"() {
2704
2765
  "use strict";
@@ -2786,9 +2847,13 @@ var init_SearchTools = __esm({
2786
2847
  }
2787
2848
  }
2788
2849
  };
2850
+ _rgCache = {
2851
+ resolved: false,
2852
+ value: null
2853
+ };
2789
2854
  GrepSearchTool = class extends BaseTool {
2790
2855
  name = "grep_search";
2791
- description = "Search file contents for a regex pattern. Returns matching lines with file path and line number. Uses ripgrep (rg) when available, falls back to a built-in scanner otherwise.";
2856
+ description = 'Search file contents for a regex pattern. Returns matching lines with file path and line number, sorted deterministically. Uses ripgrep (rg) when available \u2014 fast on large repos, respects .gitignore \u2014 and falls back to a built-in scanner otherwise. Optional include/exclude glob filters narrow which files are searched (e.g. include "*.ts", exclude "*.test.ts").';
2792
2857
  parameters = {
2793
2858
  type: "object",
2794
2859
  required: ["pattern"],
@@ -2804,6 +2869,14 @@ var init_SearchTools = __esm({
2804
2869
  case_sensitive: {
2805
2870
  type: "boolean",
2806
2871
  description: "Whether the search is case-sensitive (default: false)."
2872
+ },
2873
+ include: {
2874
+ type: "string",
2875
+ description: 'Optional glob of files to include (e.g. "*.ts", "src/**/*.tsx"). Passed to ripgrep as --glob.'
2876
+ },
2877
+ exclude: {
2878
+ type: "string",
2879
+ description: 'Optional glob of files to exclude (e.g. "*.test.ts", "dist/**"). Passed to ripgrep as a negated --glob.'
2807
2880
  }
2808
2881
  }
2809
2882
  };
@@ -2831,6 +2904,8 @@ var init_SearchTools = __esm({
2831
2904
  };
2832
2905
  }
2833
2906
  const caseSensitive = args2?.case_sensitive === true;
2907
+ const include = typeof args2?.include === "string" && args2.include.trim() ? args2.include.trim() : void 0;
2908
+ const exclude = typeof args2?.exclude === "string" && args2.exclude.trim() ? args2.exclude.trim() : void 0;
2834
2909
  const rg = await findRg();
2835
2910
  if (rg) {
2836
2911
  try {
@@ -2838,9 +2913,14 @@ var init_SearchTools = __esm({
2838
2913
  "--line-number",
2839
2914
  `--max-count=${MAX_GREP_MATCHES}`,
2840
2915
  "--no-heading",
2841
- "--color=never"
2916
+ "--color=never",
2917
+ // Deterministic output: sort by file path so repeated queries return
2918
+ // identical ordering (ripgrep parallelises by default → non-stable).
2919
+ "--sort=path"
2842
2920
  ];
2843
2921
  if (!caseSensitive) rgArgs.push("--ignore-case");
2922
+ if (include) rgArgs.push("--glob", include);
2923
+ if (exclude) rgArgs.push("--glob", `!${exclude}`);
2844
2924
  rgArgs.push("--", pattern, searchPath);
2845
2925
  const { stdout, stderr, exitCode } = await runRg(rg, rgArgs);
2846
2926
  if (exitCode !== 0 && exitCode !== 1) {
@@ -2860,15 +2940,28 @@ var init_SearchTools = __esm({
2860
2940
  }
2861
2941
  try {
2862
2942
  const isFile = statSync(searchPath).isFile();
2943
+ const includeRe = include ? globToRegExp(include) : void 0;
2944
+ const excludeRe = exclude ? globToRegExp(exclude) : void 0;
2863
2945
  let matches2;
2864
2946
  if (isFile) {
2865
- const content = await readFile4(searchPath, "utf8");
2866
- const regex = new RegExp(pattern, caseSensitive ? "" : "i");
2867
- const lines = content.split("\n");
2868
2947
  const relPath = args2?.path ?? searchPath;
2869
- matches2 = lines.flatMap((line, idx) => regex.test(line) ? [`${relPath}:${idx + 1}:${line}`] : []).slice(0, MAX_GREP_MATCHES);
2948
+ if (includeRe && !includeRe.test(String(relPath)) || excludeRe && excludeRe.test(String(relPath))) {
2949
+ matches2 = [];
2950
+ } else {
2951
+ const content = await readFile4(searchPath, "utf8");
2952
+ const regex = new RegExp(pattern, caseSensitive ? "" : "i");
2953
+ const lines = content.split("\n");
2954
+ matches2 = lines.flatMap((line, idx) => regex.test(line) ? [`${relPath}:${idx + 1}:${line}`] : []).slice(0, MAX_GREP_MATCHES);
2955
+ }
2870
2956
  } else {
2871
- matches2 = await nodeGrepFallback(pattern, searchPath, MAX_GREP_MATCHES, caseSensitive);
2957
+ matches2 = await nodeGrepFallback(
2958
+ pattern,
2959
+ searchPath,
2960
+ MAX_GREP_MATCHES,
2961
+ caseSensitive,
2962
+ includeRe,
2963
+ excludeRe
2964
+ );
2872
2965
  }
2873
2966
  if (matches2.length === 0) return { content: "No matches found." };
2874
2967
  const suffix = matches2.length >= MAX_GREP_MATCHES ? `
@@ -3361,13 +3454,164 @@ Pre-edit content backed up to: ${backedUpTo}`;
3361
3454
  }
3362
3455
  });
3363
3456
 
3457
+ // ../core/src/agent/namedAgents.ts
3458
+ import { readdirSync as readdirSync2, statSync as statSync3, readFileSync as readFileSync2 } from "fs";
3459
+ import { join as join9 } from "path";
3460
+ function normalizeToolName(raw) {
3461
+ const trimmed = raw.trim();
3462
+ if (!trimmed) return "";
3463
+ const lower = trimmed.toLowerCase();
3464
+ const key = lower.replace(/[^a-z0-9]/g, "");
3465
+ if (TOOL_NAME_ALIASES[key]) return TOOL_NAME_ALIASES[key];
3466
+ return lower;
3467
+ }
3468
+ function discoverAgentFiles(cwd, homeDir) {
3469
+ const seen = /* @__PURE__ */ new Map();
3470
+ for (const scope of [
3471
+ { dir: join9(homeDir, ".msapling", "agents"), scope: "user" },
3472
+ { dir: join9(cwd, ".msapling", "agents"), scope: "project" }
3473
+ ]) {
3474
+ const entries = safeReaddir(scope.dir);
3475
+ for (const fname of entries) {
3476
+ if (!fname.toLowerCase().endsWith(".md")) continue;
3477
+ const path2 = join9(scope.dir, fname);
3478
+ if (!isRegularFile(path2)) continue;
3479
+ const base = fname.slice(0, -".md".length);
3480
+ seen.set(base, { name: base, path: path2, scope: scope.scope });
3481
+ }
3482
+ }
3483
+ return [...seen.values()].sort((a, b) => a.name.localeCompare(b.name));
3484
+ }
3485
+ function safeReaddir(dir) {
3486
+ try {
3487
+ return readdirSync2(dir);
3488
+ } catch {
3489
+ return [];
3490
+ }
3491
+ }
3492
+ function isRegularFile(p) {
3493
+ try {
3494
+ return statSync3(p).isFile();
3495
+ } catch {
3496
+ return false;
3497
+ }
3498
+ }
3499
+ function parseToolsValue(raw) {
3500
+ if (raw === void 0) return void 0;
3501
+ let s = raw.trim();
3502
+ if (s === "") return void 0;
3503
+ if (s === "*" || s.toLowerCase() === "all") return void 0;
3504
+ if (s.startsWith("[") && s.endsWith("]")) s = s.slice(1, -1);
3505
+ const parts = s.split(",").map((t) => t.trim().replace(/^['"]|['"]$/g, "")).map((t) => normalizeToolName(t)).filter(Boolean);
3506
+ return [...new Set(parts)];
3507
+ }
3508
+ function parseAgentFile(text, fallbackName) {
3509
+ const fm = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
3510
+ if (!fm) {
3511
+ const body = text.trim();
3512
+ const firstLine = body.split(/\r?\n/).find((l) => l.trim()) ?? "";
3513
+ return {
3514
+ name: fallbackName.toLowerCase(),
3515
+ description: firstLine.replace(/^#\s+/, "").trim() || `(agent: ${fallbackName})`,
3516
+ systemPrompt: body
3517
+ };
3518
+ }
3519
+ const meta = fm[1];
3520
+ const systemPrompt = text.slice(fm[0].length).trim();
3521
+ const scalar = (key) => {
3522
+ const m = meta.match(new RegExp(`^${key}[ \\t]*:[ \\t]*(.+)$`, "m"));
3523
+ return m ? m[1].trim().replace(/^['"]|['"]$/g, "") : void 0;
3524
+ };
3525
+ const name = (scalar("name") ?? fallbackName).toLowerCase();
3526
+ const description = scalar("description") ?? `(agent: ${name})`;
3527
+ const model = scalar("model");
3528
+ let tools;
3529
+ const inlineTools = scalar("tools");
3530
+ const blockMatch = meta.match(/^tools[ \t]*:[ \t]*\n((?:[ \t]*-[ \t]*.+\n?)+)/m);
3531
+ if (blockMatch && (inlineTools === void 0 || inlineTools === "")) {
3532
+ const items = blockMatch[1].split(/\r?\n/).map((l) => l.replace(/^[ \t]*-[ \t]*/, "").trim().replace(/^['"]|['"]$/g, "")).map((t) => normalizeToolName(t)).filter(Boolean);
3533
+ tools = [...new Set(items)];
3534
+ } else {
3535
+ tools = parseToolsValue(inlineTools);
3536
+ }
3537
+ return { name, description, model, tools, systemPrompt };
3538
+ }
3539
+ function loadNamedAgents(cwd, homeDir) {
3540
+ const out = [];
3541
+ for (const file of discoverAgentFiles(cwd, homeDir)) {
3542
+ let text;
3543
+ try {
3544
+ text = readFileSync2(file.path, "utf8");
3545
+ } catch {
3546
+ continue;
3547
+ }
3548
+ const parsed = parseAgentFile(text, file.name);
3549
+ out.push({
3550
+ name: parsed.name,
3551
+ description: parsed.description,
3552
+ model: parsed.model,
3553
+ tools: parsed.tools,
3554
+ systemPrompt: parsed.systemPrompt,
3555
+ path: file.path,
3556
+ scope: file.scope
3557
+ });
3558
+ }
3559
+ const byName = /* @__PURE__ */ new Map();
3560
+ for (const a of out) {
3561
+ const prev = byName.get(a.name);
3562
+ if (!prev || prev.scope === "user" && a.scope === "project") {
3563
+ byName.set(a.name, a);
3564
+ }
3565
+ }
3566
+ return [...byName.values()].sort((a, b) => a.name.localeCompare(b.name));
3567
+ }
3568
+ function findNamedAgent(name, cwd, homeDir) {
3569
+ const target = name.trim().toLowerCase();
3570
+ return loadNamedAgents(cwd, homeDir).find((a) => a.name === target) ?? null;
3571
+ }
3572
+ var TOOL_NAME_ALIASES;
3573
+ var init_namedAgents = __esm({
3574
+ "../core/src/agent/namedAgents.ts"() {
3575
+ "use strict";
3576
+ init_esm_shims();
3577
+ TOOL_NAME_ALIASES = {
3578
+ read: "read_file",
3579
+ write: "write_file",
3580
+ edit: "edit_file",
3581
+ multiedit: "multi_edit_file",
3582
+ patch: "patch_file",
3583
+ move: "move_file",
3584
+ delete: "delete_file",
3585
+ bash: "bash_command",
3586
+ shell: "bash_command",
3587
+ run: "run_command",
3588
+ glob: "glob_files",
3589
+ grep: "grep_search",
3590
+ ls: "list_directory",
3591
+ list: "list_directory",
3592
+ webfetch: "web_fetch",
3593
+ websearch: "web_search",
3594
+ fetch: "web_fetch",
3595
+ search: "web_search",
3596
+ task: "dispatch_agent",
3597
+ dispatch: "dispatch_agent",
3598
+ todowrite: "todo_write",
3599
+ todoread: "todo_read",
3600
+ notebookread: "notebook_read",
3601
+ notebookedit: "notebook_edit_cell"
3602
+ };
3603
+ }
3604
+ });
3605
+
3364
3606
  // ../core/src/tools/DispatchAgentTool.ts
3607
+ import { homedir as homedir5 } from "os";
3365
3608
  var SUB_AGENT_TIMEOUT_MS, MAX_PROMPT_CHARS, MAX_RESPONSE_CHARS, DEFAULT_SUB_AGENT_MODEL, DispatchAgentTool;
3366
3609
  var init_DispatchAgentTool = __esm({
3367
3610
  "../core/src/tools/DispatchAgentTool.ts"() {
3368
3611
  "use strict";
3369
3612
  init_esm_shims();
3370
3613
  init_BaseTool();
3614
+ init_namedAgents();
3371
3615
  SUB_AGENT_TIMEOUT_MS = 12e4;
3372
3616
  MAX_PROMPT_CHARS = 8e3;
3373
3617
  MAX_RESPONSE_CHARS = 16e3;
@@ -3383,6 +3627,10 @@ var init_DispatchAgentTool = __esm({
3383
3627
  type: "string",
3384
3628
  description: `The full self-contained prompt for the sub-agent. Include all necessary context \u2014 the sub-agent does not have access to prior conversation history. Maximum ${MAX_PROMPT_CHARS} characters.`
3385
3629
  },
3630
+ agent: {
3631
+ type: "string",
3632
+ description: "Optional NAMED subagent to run (defined in .msapling/agents/<name>.md). The named agent supplies its own system prompt, model, and tool allowlist \u2014 unlike the anonymous dispatch path it CAN run tools (gated by the permission system). Run /agents to list available named agents. When omitted, an anonymous read/reasoning sub-agent runs."
3633
+ },
3386
3634
  description: {
3387
3635
  type: "string",
3388
3636
  description: 'Short human-readable label for this sub-task (e.g. "Summarise auth.ts"). Appears in the session log alongside the result.'
@@ -3400,11 +3648,27 @@ var init_DispatchAgentTool = __esm({
3400
3648
  client;
3401
3649
  parentChatId;
3402
3650
  projectRoot;
3651
+ onSubagentStop;
3652
+ runNamedAgent;
3403
3653
  constructor(options) {
3404
3654
  super();
3405
3655
  this.client = options.client;
3406
3656
  this.parentChatId = options.parentChatId ?? void 0;
3407
3657
  this.projectRoot = options.projectRoot;
3658
+ this.onSubagentStop = options.onSubagentStop;
3659
+ this.runNamedAgent = options.runNamedAgent;
3660
+ }
3661
+ /** Resolve the user home dir, preferring env overrides so test isolation
3662
+ * (HOME / USERPROFILE) is honored exactly like outputStyle.ts. */
3663
+ resolveHome() {
3664
+ return process.env.HOME || process.env.USERPROFILE || homedir5();
3665
+ }
3666
+ /** Fire the subagent-stop callback defensively — it must never throw. */
3667
+ signalStop(info) {
3668
+ try {
3669
+ this.onSubagentStop?.(info);
3670
+ } catch {
3671
+ }
3408
3672
  }
3409
3673
  async execute(args2, _projectRoot) {
3410
3674
  if (!args2?.prompt || typeof args2.prompt !== "string" || args2.prompt.trim() === "") {
@@ -3414,9 +3678,19 @@ var init_DispatchAgentTool = __esm({
3414
3678
  };
3415
3679
  }
3416
3680
  const prompt4 = args2.prompt.trim().slice(0, MAX_PROMPT_CHARS);
3417
- const model = typeof args2.model === "string" && args2.model.trim() ? args2.model.trim() : DEFAULT_SUB_AGENT_MODEL;
3418
3681
  const chatId = typeof args2.chat_id === "string" && args2.chat_id.trim() ? args2.chat_id.trim() : this.parentChatId;
3419
- const description = typeof args2.description === "string" && args2.description.trim() ? args2.description.trim() : "sub-task";
3682
+ let named = null;
3683
+ if (typeof args2.agent === "string" && args2.agent.trim()) {
3684
+ named = findNamedAgent(args2.agent.trim(), this.projectRoot, this.resolveHome());
3685
+ if (!named) {
3686
+ return {
3687
+ content: `dispatch_agent error: no named agent "${args2.agent.trim()}" found in .msapling/agents/. Run /agents to list available agents.`,
3688
+ isError: true
3689
+ };
3690
+ }
3691
+ }
3692
+ const model = typeof args2.model === "string" && args2.model.trim() ? args2.model.trim() : named?.model && named.model.trim() ? named.model.trim() : DEFAULT_SUB_AGENT_MODEL;
3693
+ const description = typeof args2.description === "string" && args2.description.trim() ? args2.description.trim() : named ? `agent:${named.name}` : "sub-task";
3420
3694
  let response = "";
3421
3695
  const timeoutPromise = new Promise(
3422
3696
  (_, reject) => setTimeout(
@@ -3425,10 +3699,28 @@ var init_DispatchAgentTool = __esm({
3425
3699
  )
3426
3700
  );
3427
3701
  const streamPromise = (async () => {
3702
+ if (named && this.runNamedAgent) {
3703
+ await this.runNamedAgent({
3704
+ agent: named,
3705
+ prompt: prompt4,
3706
+ model,
3707
+ chatId,
3708
+ onContent: (chunk) => {
3709
+ response += chunk;
3710
+ }
3711
+ });
3712
+ return;
3713
+ }
3714
+ const effectivePrompt = named ? `[System prompt for agent "${named.name}"]
3715
+ ${named.systemPrompt}
3716
+
3717
+ ---
3718
+
3719
+ ${prompt4}`.slice(0, MAX_PROMPT_CHARS) : prompt4;
3428
3720
  const stream = this.client.streamChat({
3429
- prompt: prompt4,
3721
+ prompt: effectivePrompt,
3430
3722
  model,
3431
- // No tools — sub-agent is read/reasoning only.
3723
+ // No tools — sub-agent is read/reasoning only on this path.
3432
3724
  tools: [],
3433
3725
  ...chatId ? { chat_id: chatId } : {},
3434
3726
  // Forward project root as context so the backend can enrich the prompt
@@ -3444,12 +3736,14 @@ var init_DispatchAgentTool = __esm({
3444
3736
  try {
3445
3737
  await Promise.race([streamPromise, timeoutPromise]);
3446
3738
  } catch (e) {
3739
+ this.signalStop({ description, ok: false, chars: response.length, error: e?.message });
3447
3740
  return {
3448
3741
  content: `dispatch_agent error [${description}]: ${e.message}`,
3449
3742
  isError: true
3450
3743
  };
3451
3744
  }
3452
3745
  if (!response.trim()) {
3746
+ this.signalStop({ description, ok: false, chars: 0, error: "empty response" });
3453
3747
  return {
3454
3748
  content: `dispatch_agent [${description}]: Sub-agent returned an empty response.`,
3455
3749
  isError: true
@@ -3461,6 +3755,7 @@ var init_DispatchAgentTool = __esm({
3461
3755
 
3462
3756
  ... [TRUNCATED: Sub-agent response exceeded ${MAX_RESPONSE_CHARS} characters]`;
3463
3757
  }
3758
+ this.signalStop({ description, ok: true, chars: finalResponse.length });
3464
3759
  return {
3465
3760
  content: `[dispatch_agent: ${description}]
3466
3761
 
@@ -3776,6 +4071,83 @@ Content-Type: ${contentType}
3776
4071
  }
3777
4072
  });
3778
4073
 
4074
+ // ../core/src/tools/WebSearchTool.ts
4075
+ var DEFAULT_MAX_RESULTS, MAX_RESULTS_CEILING, MAX_SNIPPET_CHARS, WebSearchTool;
4076
+ var init_WebSearchTool = __esm({
4077
+ "../core/src/tools/WebSearchTool.ts"() {
4078
+ "use strict";
4079
+ init_esm_shims();
4080
+ init_BaseTool();
4081
+ DEFAULT_MAX_RESULTS = 5;
4082
+ MAX_RESULTS_CEILING = 10;
4083
+ MAX_SNIPPET_CHARS = 500;
4084
+ WebSearchTool = class extends BaseTool {
4085
+ name = "web_search";
4086
+ description = "Search the live web for current information and return a ranked list of result titles, URLs, and snippets. Use this to find documentation, recent events, library versions, error explanations, or any topic where you need up-to-date sources you do not already have a URL for. Pair with web_fetch to read a specific result in full. Runs through the MSapling backend so no API keys are needed client-side.";
4087
+ parameters = {
4088
+ type: "object",
4089
+ required: ["query"],
4090
+ properties: {
4091
+ query: {
4092
+ type: "string",
4093
+ description: "The search query. Be specific for better results."
4094
+ },
4095
+ max_results: {
4096
+ type: "number",
4097
+ description: `Maximum number of results to return. Default: ${DEFAULT_MAX_RESULTS}. Max: ${MAX_RESULTS_CEILING}.`
4098
+ }
4099
+ }
4100
+ };
4101
+ client;
4102
+ constructor(options) {
4103
+ super();
4104
+ this.client = options.client;
4105
+ }
4106
+ async execute(args2, _projectRoot) {
4107
+ const rawQuery = args2?.query;
4108
+ if (!rawQuery || typeof rawQuery !== "string" || rawQuery.trim() === "") {
4109
+ return {
4110
+ content: 'Error: web_search requires a non-empty "query" argument.',
4111
+ isError: true
4112
+ };
4113
+ }
4114
+ const query = rawQuery.trim();
4115
+ let maxResults = DEFAULT_MAX_RESULTS;
4116
+ if (typeof args2?.max_results === "number" && args2.max_results > 0) {
4117
+ maxResults = Math.min(Math.floor(args2.max_results), MAX_RESULTS_CEILING);
4118
+ }
4119
+ let resp;
4120
+ try {
4121
+ resp = await this.client.webSearch(query, maxResults);
4122
+ } catch (e) {
4123
+ return {
4124
+ content: `Error: web_search failed \u2014 ${e?.message ?? String(e)}`,
4125
+ isError: true
4126
+ };
4127
+ }
4128
+ const results = Array.isArray(resp?.results) ? resp.results : [];
4129
+ if (results.length === 0) {
4130
+ return {
4131
+ content: `[web_search] No results for "${query}" (provider: ${resp?.provider ?? "none"}). Try rephrasing the query or use web_fetch if you already have a URL.`
4132
+ };
4133
+ }
4134
+ const confidence = results[0]?.confidence ?? 0;
4135
+ const confidenceLabel = confidence >= 0.9 ? "high" : "low (scraped)";
4136
+ const header = `[web_search: ${results.length} result(s) for "${query}" \u2014 provider: ${resp.provider}, confidence: ${confidenceLabel}]
4137
+
4138
+ `;
4139
+ const body = results.map((r, i) => {
4140
+ const snippet = (r.snippet || "").slice(0, MAX_SNIPPET_CHARS);
4141
+ return `${i + 1}. ${r.title || "(untitled)"}
4142
+ ${r.url || ""}
4143
+ ${snippet}`;
4144
+ }).join("\n\n");
4145
+ return { content: header + body };
4146
+ }
4147
+ };
4148
+ }
4149
+ });
4150
+
3779
4151
  // ../core/src/tools/BashTool.ts
3780
4152
  import { resolve as resolve7, normalize as normalize6, relative as relative7, isAbsolute as isAbsolute7 } from "path";
3781
4153
  import { spawn as spawn5 } from "child_process";
@@ -3923,12 +4295,12 @@ Command: ${command}`,
3923
4295
  proc.stdout?.on("data", (chunk) => chunks.stdout.push(chunk));
3924
4296
  proc.stderr?.on("data", (chunk) => chunks.stderr.push(chunk));
3925
4297
  const timeoutPromise = new Promise(
3926
- (resolve20) => setTimeout(() => resolve20("timeout"), timeoutMs)
4298
+ (resolve21) => setTimeout(() => resolve21("timeout"), timeoutMs)
3927
4299
  );
3928
4300
  const processPromise = (async () => {
3929
- const exitCode2 = await new Promise((resolve20) => {
3930
- proc.on("exit", (code) => resolve20(code ?? 1));
3931
- proc.on("error", () => resolve20(1));
4301
+ const exitCode2 = await new Promise((resolve21) => {
4302
+ proc.on("exit", (code) => resolve21(code ?? 1));
4303
+ proc.on("error", () => resolve21(1));
3932
4304
  });
3933
4305
  const stdout2 = Buffer.concat(chunks.stdout).toString("utf-8");
3934
4306
  const stderr2 = Buffer.concat(chunks.stderr).toString("utf-8");
@@ -3962,8 +4334,315 @@ ${stderr}`;
3962
4334
  }
3963
4335
  });
3964
4336
 
4337
+ // ../core/src/tools/BackgroundShellTool.ts
4338
+ import { resolve as resolve8, normalize as normalize7, relative as relative8, isAbsolute as isAbsolute8 } from "path";
4339
+ import { spawn as spawn6 } from "child_process";
4340
+ function formatBackgroundShells(shells) {
4341
+ if (shells.length === 0) return "No background shells.";
4342
+ return shells.map((s) => {
4343
+ const dur = ((s.endedAt ?? Date.now()) - s.startedAt) / 1e3;
4344
+ const exit = s.exitCode !== void 0 && s.exitCode !== null ? ` exit=${s.exitCode}` : "";
4345
+ const pid = s.pid !== void 0 ? ` pid=${s.pid}` : "";
4346
+ return `[${s.id}] ${s.status.toUpperCase()}${pid}${exit} ${dur.toFixed(1)}s $ ${s.command}`;
4347
+ }).join("\n");
4348
+ }
4349
+ function resolveCwd(projectRoot, rawArg) {
4350
+ let cwd = projectRoot;
4351
+ if (typeof rawArg === "string" && rawArg.trim()) {
4352
+ const raw = rawArg.trim();
4353
+ const resolved = isAbsolute8(raw) ? normalize7(raw) : resolve8(projectRoot, raw);
4354
+ const rel = relative8(projectRoot, resolved);
4355
+ if (rel.startsWith("..") || isAbsolute8(rel)) {
4356
+ return { error: `Security Block: cwd "${raw}" resolves outside the project root.` };
4357
+ }
4358
+ cwd = resolved;
4359
+ }
4360
+ return { cwd };
4361
+ }
4362
+ var MAX_BUFFER_BYTES, MAX_FINISHED_RETAINED, BackgroundShellRegistryImpl, BackgroundShellRegistry, BashBackgroundTool, ListBackgroundShellsTool, ReadBackgroundShellTool, KillBackgroundShellTool;
4363
+ var init_BackgroundShellTool = __esm({
4364
+ "../core/src/tools/BackgroundShellTool.ts"() {
4365
+ "use strict";
4366
+ init_esm_shims();
4367
+ init_BaseTool();
4368
+ init_BashTool();
4369
+ MAX_BUFFER_BYTES = 256 * 1024;
4370
+ MAX_FINISHED_RETAINED = 50;
4371
+ BackgroundShellRegistryImpl = class {
4372
+ shells = /* @__PURE__ */ new Map();
4373
+ counter = 0;
4374
+ /** Monotonic, human-readable id: bg-1, bg-2, … */
4375
+ nextId() {
4376
+ this.counter += 1;
4377
+ return `bg-${this.counter}`;
4378
+ }
4379
+ /**
4380
+ * Register an already-spawned child process and start buffering its output.
4381
+ * Returns the assigned shell id.
4382
+ */
4383
+ register(proc, command, cwd) {
4384
+ const id = this.nextId();
4385
+ const entry = {
4386
+ id,
4387
+ command,
4388
+ cwd,
4389
+ status: "running",
4390
+ pid: proc.pid,
4391
+ startedAt: Date.now(),
4392
+ proc,
4393
+ buffer: "",
4394
+ bufferTruncated: false
4395
+ };
4396
+ this.shells.set(id, entry);
4397
+ const append = (chunk) => {
4398
+ entry.buffer += chunk.toString("utf-8");
4399
+ if (entry.buffer.length > MAX_BUFFER_BYTES) {
4400
+ entry.buffer = entry.buffer.slice(entry.buffer.length - MAX_BUFFER_BYTES);
4401
+ entry.bufferTruncated = true;
4402
+ }
4403
+ };
4404
+ proc.stdout?.on("data", append);
4405
+ proc.stderr?.on("data", append);
4406
+ proc.on("error", (err) => {
4407
+ if (entry.status === "running") {
4408
+ entry.status = "error";
4409
+ entry.endedAt = Date.now();
4410
+ entry.buffer += `
4411
+ [spawn error: ${err.message}]`;
4412
+ }
4413
+ this.reapFinished();
4414
+ });
4415
+ proc.on("exit", (code, signal) => {
4416
+ if (entry.status === "running") {
4417
+ entry.status = "exited";
4418
+ }
4419
+ entry.exitCode = code;
4420
+ entry.endedAt = Date.now();
4421
+ if (signal) entry.buffer += `
4422
+ [terminated by signal ${signal}]`;
4423
+ this.reapFinished();
4424
+ });
4425
+ return id;
4426
+ }
4427
+ /** Evict the oldest finished shells once we exceed the retention cap. */
4428
+ reapFinished() {
4429
+ const finished = [...this.shells.values()].filter((s) => s.status !== "running").sort((a, b) => (a.endedAt ?? 0) - (b.endedAt ?? 0));
4430
+ let excess = finished.length - MAX_FINISHED_RETAINED;
4431
+ for (let i = 0; i < finished.length && excess > 0; i++, excess--) {
4432
+ this.shells.delete(finished[i].id);
4433
+ }
4434
+ }
4435
+ /** Public view of all tracked shells, newest first. */
4436
+ list() {
4437
+ return [...this.shells.values()].sort((a, b) => b.startedAt - a.startedAt).map((s) => this.toInfo(s));
4438
+ }
4439
+ get(id) {
4440
+ const s = this.shells.get(id);
4441
+ return s ? this.toInfo(s) : void 0;
4442
+ }
4443
+ /**
4444
+ * Read the buffered output of a shell. `lines` (optional) tails the last N
4445
+ * lines. Returns null if the id is unknown.
4446
+ */
4447
+ readOutput(id, lines) {
4448
+ const s = this.shells.get(id);
4449
+ if (!s) return null;
4450
+ let output = s.buffer;
4451
+ if (lines && lines > 0) {
4452
+ const all = output.split("\n");
4453
+ output = all.slice(-lines).join("\n");
4454
+ }
4455
+ return { output, truncated: s.bufferTruncated };
4456
+ }
4457
+ /**
4458
+ * Terminate a running shell. Returns:
4459
+ * - 'killed' on success
4460
+ * - 'not-found' if the id is unknown
4461
+ * - 'already' if the shell had already finished
4462
+ */
4463
+ kill(id) {
4464
+ const s = this.shells.get(id);
4465
+ if (!s) return "not-found";
4466
+ if (s.status !== "running") return "already";
4467
+ s.status = "killed";
4468
+ s.endedAt = Date.now();
4469
+ try {
4470
+ s.proc.kill();
4471
+ } catch {
4472
+ }
4473
+ return "killed";
4474
+ }
4475
+ /** Kill every running shell (used on CLI shutdown). */
4476
+ killAll() {
4477
+ for (const s of this.shells.values()) {
4478
+ if (s.status === "running") {
4479
+ s.status = "killed";
4480
+ s.endedAt = Date.now();
4481
+ try {
4482
+ s.proc.kill();
4483
+ } catch {
4484
+ }
4485
+ }
4486
+ }
4487
+ }
4488
+ /** Test-only: drop all tracked shells (does not kill — caller's responsibility). */
4489
+ _reset() {
4490
+ this.shells.clear();
4491
+ this.counter = 0;
4492
+ }
4493
+ toInfo(s) {
4494
+ return {
4495
+ id: s.id,
4496
+ command: s.command,
4497
+ cwd: s.cwd,
4498
+ status: s.status,
4499
+ pid: s.pid,
4500
+ exitCode: s.exitCode,
4501
+ startedAt: s.startedAt,
4502
+ endedAt: s.endedAt
4503
+ };
4504
+ }
4505
+ };
4506
+ BackgroundShellRegistry = new BackgroundShellRegistryImpl();
4507
+ BashBackgroundTool = class extends BaseTool {
4508
+ name = "bash_background";
4509
+ description = "Launch a shell command in the BACKGROUND and return immediately with a shell id. Use for long-running processes (dev servers, watchers, `tail -f`) that should keep running while you continue working. Inspect output with read_background_shell, list with list_background_shells, and stop with kill_background_shell. Runs via bash (Unix) / cmd (Windows) so shell operators work. For commands that must finish before you proceed, use bash_command instead.";
4510
+ parameters = {
4511
+ type: "object",
4512
+ required: ["command"],
4513
+ properties: {
4514
+ command: {
4515
+ type: "string",
4516
+ description: "The shell command to run in the background. May contain pipes, redirects, and other shell operators."
4517
+ },
4518
+ cwd: {
4519
+ type: "string",
4520
+ description: "Optional working directory (relative to project root). Defaults to the project root."
4521
+ }
4522
+ }
4523
+ };
4524
+ async execute(args2, projectRoot) {
4525
+ if (!args2?.command || typeof args2.command !== "string" || args2.command.trim() === "") {
4526
+ return {
4527
+ content: 'Error: bash_background requires a non-empty "command" argument.',
4528
+ isError: true
4529
+ };
4530
+ }
4531
+ const command = args2.command;
4532
+ const block = BashTool.checkBlocklist(command);
4533
+ if (block.blocked) {
4534
+ return {
4535
+ content: `Security Block: command rejected \u2014 ${block.reason}.
4536
+ Command: ${command}`,
4537
+ isError: true
4538
+ };
4539
+ }
4540
+ const cwdResult = resolveCwd(projectRoot, args2.cwd);
4541
+ if ("error" in cwdResult) {
4542
+ return { content: cwdResult.error, isError: true };
4543
+ }
4544
+ const cwd = cwdResult.cwd;
4545
+ const isWindows = process.platform === "win32";
4546
+ const shellArgv = isWindows ? ["cmd", "/c", command] : ["bash", "-c", command];
4547
+ let proc;
4548
+ try {
4549
+ proc = spawn6(shellArgv[0], shellArgv.slice(1), {
4550
+ cwd,
4551
+ stdio: ["ignore", "pipe", "pipe"],
4552
+ shell: false
4553
+ });
4554
+ } catch (e) {
4555
+ return { content: `Error: failed to spawn background shell: ${e.message}`, isError: true };
4556
+ }
4557
+ const id = BackgroundShellRegistry.register(proc, command, cwd);
4558
+ return {
4559
+ content: `Started background shell ${id} (pid ${proc.pid ?? "?"}).
4560
+ Command: ${command}
4561
+ Use read_background_shell with shell_id="${id}" to read its output, or kill_background_shell to stop it.`
4562
+ };
4563
+ }
4564
+ };
4565
+ ListBackgroundShellsTool = class extends BaseTool {
4566
+ name = "list_background_shells";
4567
+ description = "List all background shells started in this session (running and finished), with their id, status, pid, exit code, and command.";
4568
+ parameters = {
4569
+ type: "object",
4570
+ properties: {}
4571
+ };
4572
+ async execute() {
4573
+ return { content: formatBackgroundShells(BackgroundShellRegistry.list()) };
4574
+ }
4575
+ };
4576
+ ReadBackgroundShellTool = class extends BaseTool {
4577
+ name = "read_background_shell";
4578
+ description = "Read the buffered output of a background shell by its id. Optionally tail only the last N lines.";
4579
+ parameters = {
4580
+ type: "object",
4581
+ required: ["shell_id"],
4582
+ properties: {
4583
+ shell_id: {
4584
+ type: "string",
4585
+ description: 'The background shell id (e.g. "bg-1") returned by bash_background.'
4586
+ },
4587
+ lines: {
4588
+ type: "number",
4589
+ description: "Optional: return only the last N lines of buffered output."
4590
+ }
4591
+ }
4592
+ };
4593
+ async execute(args2) {
4594
+ const id = typeof args2?.shell_id === "string" ? args2.shell_id.trim() : "";
4595
+ if (!id) {
4596
+ return { content: 'Error: read_background_shell requires a "shell_id" argument.', isError: true };
4597
+ }
4598
+ const lines = typeof args2?.lines === "number" && args2.lines > 0 ? Math.floor(args2.lines) : void 0;
4599
+ const read = BackgroundShellRegistry.readOutput(id, lines);
4600
+ if (!read) {
4601
+ return { content: `Error: no background shell with id "${id}".`, isError: true };
4602
+ }
4603
+ const info = BackgroundShellRegistry.get(id);
4604
+ const header = `[${info.id}] ${info.status.toUpperCase()}` + (info.exitCode !== void 0 && info.exitCode !== null ? ` exit=${info.exitCode}` : "") + ` $ ${info.command}
4605
+ `;
4606
+ const body = read.output.trim() === "" ? "(no output yet)" : read.output;
4607
+ const truncNote = read.truncated ? "\n... [output buffer truncated to most recent 256 KB]" : "";
4608
+ return { content: header + body + truncNote };
4609
+ }
4610
+ };
4611
+ KillBackgroundShellTool = class extends BaseTool {
4612
+ name = "kill_background_shell";
4613
+ description = "Terminate a running background shell by its id.";
4614
+ parameters = {
4615
+ type: "object",
4616
+ required: ["shell_id"],
4617
+ properties: {
4618
+ shell_id: {
4619
+ type: "string",
4620
+ description: 'The background shell id (e.g. "bg-1") to terminate.'
4621
+ }
4622
+ }
4623
+ };
4624
+ async execute(args2) {
4625
+ const id = typeof args2?.shell_id === "string" ? args2.shell_id.trim() : "";
4626
+ if (!id) {
4627
+ return { content: 'Error: kill_background_shell requires a "shell_id" argument.', isError: true };
4628
+ }
4629
+ const result = BackgroundShellRegistry.kill(id);
4630
+ switch (result) {
4631
+ case "killed":
4632
+ return { content: `Killed background shell ${id}.` };
4633
+ case "already":
4634
+ return { content: `Background shell ${id} had already finished.` };
4635
+ case "not-found":
4636
+ default:
4637
+ return { content: `Error: no background shell with id "${id}".`, isError: true };
4638
+ }
4639
+ }
4640
+ };
4641
+ }
4642
+ });
4643
+
3965
4644
  // ../core/src/tools/NotebookReadTool.ts
3966
- import { resolve as resolve8, normalize as normalize7, relative as relative8, isAbsolute as isAbsolute8, extname } from "path";
4645
+ import { resolve as resolve9, normalize as normalize8, relative as relative9, isAbsolute as isAbsolute9, extname } from "path";
3967
4646
  import { readFile as readFile6 } from "fs/promises";
3968
4647
  import { existsSync as existsSync7 } from "fs";
3969
4648
  function joinSource(source) {
@@ -4118,9 +4797,9 @@ var init_NotebookReadTool = __esm({
4118
4797
  isError: true
4119
4798
  };
4120
4799
  }
4121
- const absPath = isAbsolute8(rawPath) ? normalize7(rawPath) : resolve8(projectRoot, rawPath);
4122
- const rel = relative8(projectRoot, absPath);
4123
- if (rel.startsWith("..") || isAbsolute8(rel)) {
4800
+ const absPath = isAbsolute9(rawPath) ? normalize8(rawPath) : resolve9(projectRoot, rawPath);
4801
+ const rel = relative9(projectRoot, absPath);
4802
+ if (rel.startsWith("..") || isAbsolute9(rel)) {
4124
4803
  return {
4125
4804
  content: `Security Block: path "${rawPath}" resolves outside the project root.`,
4126
4805
  isError: true
@@ -4188,10 +4867,10 @@ _(Note: notebook has ${nb.cells.length} cells; only first ${MAX_CELLS} shown.)_`
4188
4867
  });
4189
4868
 
4190
4869
  // ../core/src/tools/NotebookEditTool.ts
4191
- import { resolve as resolve9, normalize as normalize8, relative as relative9, isAbsolute as isAbsolute9, extname as extname2, join as join10 } from "path";
4870
+ import { resolve as resolve10, normalize as normalize9, relative as relative10, isAbsolute as isAbsolute10, extname as extname2, join as join11 } from "path";
4192
4871
  import { readFile as readFile7, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
4193
4872
  import { existsSync as existsSync8 } from "fs";
4194
- import { homedir as homedir5 } from "os";
4873
+ import { homedir as homedir6 } from "os";
4195
4874
  import { randomBytes as randomBytes5 } from "crypto";
4196
4875
  function normaliseSource(source) {
4197
4876
  if (source === "") return [];
@@ -4312,9 +4991,9 @@ var init_NotebookEditTool = __esm({
4312
4991
  isError: true
4313
4992
  };
4314
4993
  }
4315
- const absPath = isAbsolute9(rawPath) ? normalize8(rawPath) : resolve9(projectRoot, rawPath);
4316
- const rel = relative9(projectRoot, absPath);
4317
- if (rel.startsWith("..") || isAbsolute9(rel)) {
4994
+ const absPath = isAbsolute10(rawPath) ? normalize9(rawPath) : resolve10(projectRoot, rawPath);
4995
+ const rel = relative10(projectRoot, absPath);
4996
+ if (rel.startsWith("..") || isAbsolute10(rel)) {
4318
4997
  return {
4319
4998
  content: `Security Block: path "${rawPath}" resolves outside the project root.`,
4320
4999
  isError: true
@@ -4371,13 +5050,13 @@ var init_NotebookEditTool = __esm({
4371
5050
  const filename = absPath.split(/[\\/]/).pop() ?? "notebook.ipynb";
4372
5051
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4373
5052
  const suffix = randomBytes5(4).toString("hex");
4374
- const backupPath = join10(
4375
- homedir5(),
5053
+ const backupPath = join11(
5054
+ homedir6(),
4376
5055
  ".msapling",
4377
5056
  "backups",
4378
5057
  `${filename}.backup-${stamp}-${suffix}.bak`
4379
5058
  );
4380
- await mkdir3(join10(homedir5(), ".msapling", "backups"), { recursive: true });
5059
+ await mkdir3(join11(homedir6(), ".msapling", "backups"), { recursive: true });
4381
5060
  await writeFile3(backupPath, rawJson, "utf8");
4382
5061
  backedUpTo = backupPath;
4383
5062
  } catch {
@@ -4447,10 +5126,10 @@ Backup: ${backedUpTo}`;
4447
5126
  });
4448
5127
 
4449
5128
  // ../core/src/tools/MultiEditFileTool.ts
4450
- import { resolve as resolve10, normalize as normalize9, relative as relative10, isAbsolute as isAbsolute10, join as join11 } from "path";
5129
+ import { resolve as resolve11, normalize as normalize10, relative as relative11, isAbsolute as isAbsolute11, join as join12 } from "path";
4451
5130
  import { readFile as readFile8, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
4452
5131
  import { existsSync as existsSync9 } from "fs";
4453
- import { homedir as homedir6 } from "os";
5132
+ import { homedir as homedir7 } from "os";
4454
5133
  import { randomBytes as randomBytes6 } from "crypto";
4455
5134
  var MAX_EDITS, MultiEditFileTool;
4456
5135
  var init_MultiEditFileTool = __esm({
@@ -4544,9 +5223,9 @@ var init_MultiEditFileTool = __esm({
4544
5223
  };
4545
5224
  }
4546
5225
  }
4547
- const normalizedTarget = isAbsolute10(args2.path) ? normalize9(args2.path) : resolve10(projectRoot, args2.path.trim());
4548
- const rel = relative10(projectRoot, normalizedTarget);
4549
- if (rel.startsWith("..") || isAbsolute10(rel)) {
5226
+ const normalizedTarget = isAbsolute11(args2.path) ? normalize10(args2.path) : resolve11(projectRoot, args2.path.trim());
5227
+ const rel = relative11(projectRoot, normalizedTarget);
5228
+ if (rel.startsWith("..") || isAbsolute11(rel)) {
4550
5229
  return {
4551
5230
  content: `Security Block: path "${args2.path}" resolves outside the project root.`,
4552
5231
  isError: true
@@ -4614,13 +5293,13 @@ No changes were written (atomic: all-or-nothing).`,
4614
5293
  const filename = resolvedTarget.split(/[\\/]/).pop() ?? "file";
4615
5294
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4616
5295
  const suffix = randomBytes6(4).toString("hex");
4617
- const backupPath = join11(
4618
- homedir6(),
5296
+ const backupPath = join12(
5297
+ homedir7(),
4619
5298
  ".msapling",
4620
5299
  "backups",
4621
5300
  `${filename}.backup-${stamp}-${suffix}.bak`
4622
5301
  );
4623
- await mkdir4(join11(homedir6(), ".msapling", "backups"), { recursive: true });
5302
+ await mkdir4(join12(homedir7(), ".msapling", "backups"), { recursive: true });
4624
5303
  await writeFile4(backupPath, originalContent, "utf8");
4625
5304
  backedUpTo = backupPath;
4626
5305
  } catch {
@@ -4648,14 +5327,14 @@ Pre-edit content backed up to: ${backedUpTo}`;
4648
5327
  });
4649
5328
 
4650
5329
  // ../core/src/tools/MoveFileTool.ts
4651
- import { resolve as resolve11, normalize as normalize10, relative as relative11, isAbsolute as isAbsolute11, dirname as dirname2 } from "path";
5330
+ import { resolve as resolve12, normalize as normalize11, relative as relative12, isAbsolute as isAbsolute12, dirname as dirname2 } from "path";
4652
5331
  import { rename, mkdir as mkdir5, copyFile, rm, stat as stat2, readdir as readdir2 } from "fs/promises";
4653
- import { existsSync as existsSync10, statSync as statSync3 } from "fs";
5332
+ import { existsSync as existsSync10, statSync as statSync4 } from "fs";
4654
5333
  import { randomBytes as randomBytes7 } from "crypto";
4655
5334
  function containedPath(p, root) {
4656
- const abs = isAbsolute11(p) ? normalize10(p) : resolve11(root, p.trim());
4657
- const rel = relative11(root, abs);
4658
- if (rel.startsWith("..") || isAbsolute11(rel)) {
5335
+ const abs = isAbsolute12(p) ? normalize11(p) : resolve12(root, p.trim());
5336
+ const rel = relative12(root, abs);
5337
+ if (rel.startsWith("..") || isAbsolute12(rel)) {
4659
5338
  return { ok: false, reason: `path "${p}" resolves outside the project root` };
4660
5339
  }
4661
5340
  return { ok: true, abs };
@@ -4664,8 +5343,8 @@ async function copyDir(src, dst) {
4664
5343
  await mkdir5(dst, { recursive: true });
4665
5344
  const entries = await readdir2(src, { withFileTypes: true });
4666
5345
  for (const entry of entries) {
4667
- const srcPath = resolve11(src, entry.name);
4668
- const dstPath = resolve11(dst, entry.name);
5346
+ const srcPath = resolve12(src, entry.name);
5347
+ const dstPath = resolve12(dst, entry.name);
4669
5348
  if (entry.isDirectory()) {
4670
5349
  await copyDir(srcPath, dstPath);
4671
5350
  } else {
@@ -4676,18 +5355,18 @@ async function copyDir(src, dst) {
4676
5355
  async function backupFile(absPath) {
4677
5356
  try {
4678
5357
  const { readFile: readFile30, writeFile: writeFile19, mkdir: mkdir10 } = await import("fs/promises");
4679
- const { homedir: homedir22 } = await import("os");
4680
- const { join: join39 } = await import("path");
5358
+ const { homedir: homedir25 } = await import("os");
5359
+ const { join: join41 } = await import("path");
4681
5360
  const filename = absPath.split(/[\\/]/).pop() ?? "file";
4682
5361
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4683
5362
  const suffix = randomBytes7(4).toString("hex");
4684
- const backupPath = join39(
4685
- homedir22(),
5363
+ const backupPath = join41(
5364
+ homedir25(),
4686
5365
  ".msapling",
4687
5366
  "backups",
4688
5367
  `${filename}.backup-${stamp}-${suffix}.bak`
4689
5368
  );
4690
- await mkdir10(join39(homedir22(), ".msapling", "backups"), { recursive: true });
5369
+ await mkdir10(join41(homedir25(), ".msapling", "backups"), { recursive: true });
4691
5370
  const content = await readFile30(absPath, "utf8");
4692
5371
  await writeFile19(backupPath, content, "utf8");
4693
5372
  return backupPath;
@@ -4747,7 +5426,7 @@ var init_MoveFileTool = __esm({
4747
5426
  isError: true
4748
5427
  };
4749
5428
  }
4750
- const srcStat = statSync3(absSrc);
5429
+ const srcStat = statSync4(absSrc);
4751
5430
  const srcIsDir = srcStat.isDirectory();
4752
5431
  let rawDst = args2.destination.trim();
4753
5432
  const dstPrelimCheck = containedPath(rawDst, projectRoot);
@@ -4755,11 +5434,11 @@ var init_MoveFileTool = __esm({
4755
5434
  return { content: `Security Block: ${dstPrelimCheck.reason}.`, isError: true };
4756
5435
  }
4757
5436
  let absDst = dstPrelimCheck.abs;
4758
- if (existsSync10(absDst) && statSync3(absDst).isDirectory() && !srcIsDir) {
5437
+ if (existsSync10(absDst) && statSync4(absDst).isDirectory() && !srcIsDir) {
4759
5438
  const srcName = absSrc.split(/[\\/]/).pop();
4760
- absDst = resolve11(absDst, srcName);
4761
- const rel2 = relative11(projectRoot, absDst);
4762
- if (rel2.startsWith("..") || isAbsolute11(rel2)) {
5439
+ absDst = resolve12(absDst, srcName);
5440
+ const rel2 = relative12(projectRoot, absDst);
5441
+ if (rel2.startsWith("..") || isAbsolute12(rel2)) {
4763
5442
  return {
4764
5443
  content: `Security Block: adjusted destination "${absDst}" resolves outside the project root.`,
4765
5444
  isError: true
@@ -4780,7 +5459,7 @@ var init_MoveFileTool = __esm({
4780
5459
  isError: true
4781
5460
  };
4782
5461
  }
4783
- if (!statSync3(absDst).isDirectory()) {
5462
+ if (!statSync4(absDst).isDirectory()) {
4784
5463
  backedUpTo = await backupFile(absDst);
4785
5464
  }
4786
5465
  }
@@ -4839,7 +5518,7 @@ Overwritten destination backed up to: ${backedUpTo}`;
4839
5518
  });
4840
5519
 
4841
5520
  // ../core/src/tools/DeleteFileTool.ts
4842
- import { resolve as resolve12, normalize as normalize11, relative as relative12, isAbsolute as isAbsolute12, join as join13 } from "path";
5521
+ import { resolve as resolve13, normalize as normalize12, relative as relative13, isAbsolute as isAbsolute13, join as join14 } from "path";
4843
5522
  import { rm as rm2, stat as stat3 } from "fs/promises";
4844
5523
  import { randomBytes as randomBytes8 } from "crypto";
4845
5524
  var DeleteFileTool;
@@ -4874,15 +5553,15 @@ var init_DeleteFileTool = __esm({
4874
5553
  }
4875
5554
  const rawPath = args2.path.trim();
4876
5555
  const recursive = args2.recursive === true;
4877
- const abs = isAbsolute12(rawPath) ? normalize11(rawPath) : resolve12(projectRoot, rawPath);
4878
- const rel = relative12(projectRoot, abs);
4879
- if (rel.startsWith("..") || isAbsolute12(rel)) {
5556
+ const abs = isAbsolute13(rawPath) ? normalize12(rawPath) : resolve13(projectRoot, rawPath);
5557
+ const rel = relative13(projectRoot, abs);
5558
+ if (rel.startsWith("..") || isAbsolute13(rel)) {
4880
5559
  return {
4881
5560
  content: `Security Block: path "${rawPath}" resolves outside the project root.`,
4882
5561
  isError: true
4883
5562
  };
4884
5563
  }
4885
- if (rel === "" || abs === normalize11(projectRoot)) {
5564
+ if (rel === "" || abs === normalize12(projectRoot)) {
4886
5565
  return {
4887
5566
  content: "Security Block: refusing to delete the project root directory.",
4888
5567
  isError: true
@@ -4915,13 +5594,13 @@ var init_DeleteFileTool = __esm({
4915
5594
  if (isFile) {
4916
5595
  try {
4917
5596
  const { readFile: readFile30, writeFile: writeFile19, mkdir: mkdir10 } = await import("fs/promises");
4918
- const { homedir: homedir22 } = await import("os");
5597
+ const { homedir: homedir25 } = await import("os");
4919
5598
  const existingContent = await readFile30(abs, "utf8");
4920
5599
  const filename = abs.split(/[\\/]/).pop() ?? "file";
4921
5600
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4922
5601
  const suffix = randomBytes8(4).toString("hex");
4923
- const backupPath = join13(homedir22(), ".msapling", "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
4924
- await mkdir10(join13(homedir22(), ".msapling", "backups"), { recursive: true });
5602
+ const backupPath = join14(homedir25(), ".msapling", "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
5603
+ await mkdir10(join14(homedir25(), ".msapling", "backups"), { recursive: true });
4925
5604
  await writeFile19(backupPath, existingContent, "utf8");
4926
5605
  backedUpTo = backupPath;
4927
5606
  } catch {
@@ -4953,6 +5632,65 @@ Backup: ${backedUpTo}`;
4953
5632
  }
4954
5633
  });
4955
5634
 
5635
+ // ../core/src/tools/PlanModeTools.ts
5636
+ var MAX_PLAN_CHARS, EnterPlanModeTool, ExitPlanModeTool;
5637
+ var init_PlanModeTools = __esm({
5638
+ "../core/src/tools/PlanModeTools.ts"() {
5639
+ "use strict";
5640
+ init_esm_shims();
5641
+ init_BaseTool();
5642
+ MAX_PLAN_CHARS = 2e4;
5643
+ EnterPlanModeTool = class extends BaseTool {
5644
+ name = "enter_plan_mode";
5645
+ description = "Switch into Plan Mode for a complex task that needs exploration before any changes. In Plan Mode you may ONLY use read-only tools (read_file, grep, glob, list_directory, web_fetch, web_search, etc.) \u2014 file writes and shell commands are blocked. Use this when the task is non-trivial and you should design an approach first. When your plan is ready, call exit_plan_mode to present it for the user's approval before executing.";
5646
+ parameters = {
5647
+ type: "object",
5648
+ properties: {}
5649
+ };
5650
+ async execute(_args, _projectRoot) {
5651
+ return {
5652
+ content: "Entered Plan Mode. This is a READ-ONLY exploration and design phase.\n1. Thoroughly explore the codebase to understand existing patterns.\n2. Identify similar features and architectural approaches.\n3. Consider multiple approaches and their trade-offs.\n4. Design a concrete implementation strategy.\n5. When ready, call exit_plan_mode with your finished plan for approval.\nDo NOT write or edit files or run commands until the plan is approved."
5653
+ };
5654
+ }
5655
+ };
5656
+ ExitPlanModeTool = class _ExitPlanModeTool extends BaseTool {
5657
+ name = "exit_plan_mode";
5658
+ description = "Present your finished implementation plan to the user for approval and exit Plan Mode. Only call this once you have a concrete plan. The plan is shown to the user; if they approve, Plan Mode is lifted and you may begin making changes. If they reject, you remain in Plan Mode and should revise the plan based on their feedback. Pass the full plan as Markdown in `plan`.";
5659
+ parameters = {
5660
+ type: "object",
5661
+ required: ["plan"],
5662
+ properties: {
5663
+ plan: {
5664
+ type: "string",
5665
+ description: "The full implementation plan, formatted as Markdown. Describe the steps you will take, the files you will change, and any trade-offs."
5666
+ }
5667
+ }
5668
+ };
5669
+ /** Validate the plan argument. Returns an error string or null when valid. */
5670
+ static validatePlan(args2) {
5671
+ const plan = args2?.plan;
5672
+ if (!plan || typeof plan !== "string" || plan.trim() === "") {
5673
+ return 'Error: exit_plan_mode requires a non-empty "plan" argument (the implementation plan to present for approval).';
5674
+ }
5675
+ if (plan.length > MAX_PLAN_CHARS) {
5676
+ return `Error: plan is too long (${plan.length} chars; max ${MAX_PLAN_CHARS}). Summarize the plan.`;
5677
+ }
5678
+ return null;
5679
+ }
5680
+ async execute(args2, _projectRoot) {
5681
+ const err = _ExitPlanModeTool.validatePlan(args2);
5682
+ if (err) return { content: err, isError: true };
5683
+ return {
5684
+ content: `Plan recorded. (No interactive approval surface available \u2014 use /mode default or /mode acceptEdits to leave Plan Mode and begin execution.)
5685
+
5686
+ ## Plan
5687
+ ${String(args2.plan)}`
5688
+ };
5689
+ }
5690
+ };
5691
+ }
5692
+ });
5693
+
4956
5694
  // ../core/src/MDrive.ts
4957
5695
  import { createHash as createHash2 } from "crypto";
4958
5696
  var MDriveService;
@@ -5004,7 +5742,7 @@ var init_MDrive = __esm({
5004
5742
  });
5005
5743
 
5006
5744
  // ../core/src/Sandbox.ts
5007
- import { resolve as resolve13, normalize as normalize12, relative as relative13, isAbsolute as isAbsolute13 } from "path";
5745
+ import { resolve as resolve14, normalize as normalize13, relative as relative14, isAbsolute as isAbsolute14 } from "path";
5008
5746
  import { realpathSync } from "fs";
5009
5747
  import { realpath as realpath2 } from "fs/promises";
5010
5748
  import { createHash as createHash3 } from "crypto";
@@ -5063,7 +5801,7 @@ var init_Sandbox = __esm({
5063
5801
  "sc"
5064
5802
  ]);
5065
5803
  constructor(projectRoot, opts) {
5066
- this.projectRoot = resolve13(projectRoot);
5804
+ this.projectRoot = resolve14(projectRoot);
5067
5805
  this.realpathSyncFn = opts?.realpathSync ?? realpathSync;
5068
5806
  }
5069
5807
  setPermissions(state) {
@@ -5078,15 +5816,15 @@ var init_Sandbox = __esm({
5078
5816
  return hasher.digest("hex");
5079
5817
  }
5080
5818
  isPathSafe(targetPath) {
5081
- const normalizedTarget = isAbsolute13(targetPath) ? normalize12(targetPath) : resolve13(this.projectRoot, targetPath);
5819
+ const normalizedTarget = isAbsolute14(targetPath) ? normalize13(targetPath) : resolve14(this.projectRoot, targetPath);
5082
5820
  let resolvedTarget = normalizedTarget;
5083
5821
  try {
5084
5822
  resolvedTarget = this.realpathSyncFn(normalizedTarget);
5085
5823
  } catch {
5086
5824
  resolvedTarget = normalizedTarget;
5087
5825
  }
5088
- const rel = relative13(this.projectRoot, resolvedTarget);
5089
- const isOutside = rel.startsWith("..") || isAbsolute13(rel);
5826
+ const rel = relative14(this.projectRoot, resolvedTarget);
5827
+ const isOutside = rel.startsWith("..") || isAbsolute14(rel);
5090
5828
  if (isOutside) {
5091
5829
  if (this.permissions.trustedPaths.includes(resolvedTarget) || this.permissions.trustedPaths.includes(normalizedTarget)) {
5092
5830
  return { safe: true, absolutePath: resolvedTarget };
@@ -5104,15 +5842,15 @@ var init_Sandbox = __esm({
5104
5842
  * isPathSafe (sync) is kept for cold/boot-time paths and existing tests.
5105
5843
  */
5106
5844
  async isPathSafeAsync(targetPath) {
5107
- const normalizedTarget = isAbsolute13(targetPath) ? normalize12(targetPath) : resolve13(this.projectRoot, targetPath);
5845
+ const normalizedTarget = isAbsolute14(targetPath) ? normalize13(targetPath) : resolve14(this.projectRoot, targetPath);
5108
5846
  let resolvedTarget = normalizedTarget;
5109
5847
  try {
5110
5848
  resolvedTarget = await realpath2(normalizedTarget);
5111
5849
  } catch {
5112
5850
  resolvedTarget = normalizedTarget;
5113
5851
  }
5114
- const rel = relative13(this.projectRoot, resolvedTarget);
5115
- const isOutside = rel.startsWith("..") || isAbsolute13(rel);
5852
+ const rel = relative14(this.projectRoot, resolvedTarget);
5853
+ const isOutside = rel.startsWith("..") || isAbsolute14(rel);
5116
5854
  if (isOutside) {
5117
5855
  if (this.permissions.trustedPaths.includes(resolvedTarget) || this.permissions.trustedPaths.includes(normalizedTarget)) {
5118
5856
  return { safe: true, absolutePath: resolvedTarget };
@@ -5144,7 +5882,7 @@ var init_Sandbox = __esm({
5144
5882
  }
5145
5883
  const parsed = parse(commandLine);
5146
5884
  const tokens = parsed.filter((t) => typeof t === "string");
5147
- const normalize13 = (raw) => {
5885
+ const normalize14 = (raw) => {
5148
5886
  let b = raw.toLowerCase();
5149
5887
  const slash = Math.max(b.lastIndexOf("/"), b.lastIndexOf("\\"));
5150
5888
  if (slash >= 0) b = b.slice(slash + 1);
@@ -5152,7 +5890,7 @@ var init_Sandbox = __esm({
5152
5890
  return b;
5153
5891
  };
5154
5892
  for (const tok of tokens) {
5155
- const b = normalize13(tok);
5893
+ const b = normalize14(tok);
5156
5894
  if (_Sandbox.DANGEROUS_BINARIES.has(b)) {
5157
5895
  return { status: "blocked", reason: `Binary '${b}' is explicitly forbidden.`, hash };
5158
5896
  }
@@ -5160,7 +5898,7 @@ var init_Sandbox = __esm({
5160
5898
  if (tokens.length === 0) {
5161
5899
  return { status: "safe", hash };
5162
5900
  }
5163
- const binary = normalize13(tokens[0]);
5901
+ const binary = normalize14(tokens[0]);
5164
5902
  const sub = tokens.length > 1 ? tokens[1].toLowerCase() : "";
5165
5903
  if (binary === "git") {
5166
5904
  const destructiveGit = /* @__PURE__ */ new Set(["push", "reset", "clean", "rebase", "merge", "force-push"]);
@@ -5259,7 +5997,7 @@ var init_Sandbox = __esm({
5259
5997
  });
5260
5998
 
5261
5999
  // ../core/src/Voice.ts
5262
- import { spawn as spawn6 } from "child_process";
6000
+ import { spawn as spawn7 } from "child_process";
5263
6001
  function buildTtsInvocation(text, rate) {
5264
6002
  const psCommand = `
5265
6003
  Add-Type -AssemblyName System.Speech;
@@ -5299,10 +6037,10 @@ var init_Voice = __esm({
5299
6037
  const { command, args: args2, env } = buildTtsInvocation(text, rate);
5300
6038
  try {
5301
6039
  if (process.platform === "win32") {
5302
- await new Promise((resolve20, reject) => {
6040
+ await new Promise((resolve21, reject) => {
5303
6041
  try {
5304
- const proc = spawn6(command, args2, { env });
5305
- proc.on("exit", () => resolve20());
6042
+ const proc = spawn7(command, args2, { env });
6043
+ proc.on("exit", () => resolve21());
5306
6044
  proc.on("error", reject);
5307
6045
  } catch (e) {
5308
6046
  reject(e);
@@ -5399,7 +6137,7 @@ var init_ShadowService = __esm({
5399
6137
  });
5400
6138
 
5401
6139
  // ../core/src/Hooks.ts
5402
- import { spawn as spawn7 } from "child_process";
6140
+ import { exec } from "child_process";
5403
6141
  function matches(entry, ctx) {
5404
6142
  if (!entry.matcher) return true;
5405
6143
  let re;
@@ -5416,62 +6154,50 @@ function matches(entry, ctx) {
5416
6154
  async function runOne(entry, ctx) {
5417
6155
  const timeoutMs = entry.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
5418
6156
  const command = entry.command;
5419
- return new Promise((resolve20) => {
5420
- const isWindows = process.platform === "win32";
5421
- const child = spawn7(isWindows ? "cmd.exe" : "sh", isWindows ? ["/c", command] : ["-c", command], {
5422
- cwd: ctx.cwd ?? process.cwd(),
5423
- stdio: ["pipe", "pipe", "pipe"]
5424
- });
5425
- let stdout = "";
5426
- let stderr = "";
6157
+ const env = { ...process.env, MSAPLING_HOOK_PAYLOAD: JSON.stringify({ ...ctx }) };
6158
+ return new Promise((resolve21) => {
5427
6159
  let timedOut = false;
5428
6160
  let settled = false;
5429
- const finish = (exitCode) => {
6161
+ const finish = (exitCode, stdout, stderr) => {
5430
6162
  if (settled) return;
5431
6163
  settled = true;
5432
6164
  clearTimeout(killer);
5433
- const blocked = !!entry.blocking && (exitCode === null || exitCode !== 0);
5434
- resolve20({ command, exitCode, stdout, stderr, timedOut, blocked });
6165
+ const blocked = !NON_BLOCKING_EVENTS.has(ctx.event) && !!entry.blocking && (exitCode === null || exitCode !== 0);
6166
+ resolve21({ command, exitCode, stdout, stderr, timedOut, blocked });
5435
6167
  };
6168
+ const child = exec(command, {
6169
+ cwd: ctx.cwd ?? process.cwd(),
6170
+ env,
6171
+ maxBuffer: MAX_OUTPUT_BYTES3
6172
+ }, (err, stdout, stderr) => {
6173
+ const rawCode = err ? err.code : 0;
6174
+ const exitCode = typeof rawCode === "number" ? rawCode : null;
6175
+ finish(exitCode, stdout.slice(0, MAX_OUTPUT_BYTES3), stderr.slice(0, MAX_OUTPUT_BYTES3));
6176
+ });
5436
6177
  const killer = setTimeout(() => {
5437
6178
  timedOut = true;
5438
6179
  try {
5439
6180
  child.kill("SIGKILL");
5440
6181
  } catch {
5441
6182
  }
5442
- finish(null);
6183
+ finish(null, "", "");
5443
6184
  }, timeoutMs);
5444
- child.stdout.on("data", (b) => {
5445
- if (stdout.length < MAX_OUTPUT_BYTES3) stdout += b.toString("utf8");
5446
- });
5447
- child.stderr.on("data", (b) => {
5448
- if (stderr.length < MAX_OUTPUT_BYTES3) stderr += b.toString("utf8");
5449
- });
5450
- child.on("error", (e) => {
5451
- stderr += `
5452
- [hook spawn error] ${e.message}`;
5453
- finish(null);
5454
- });
5455
- child.on("close", (code) => finish(code));
5456
- try {
5457
- child.stdin.write(JSON.stringify({ ...ctx }) + "\n");
5458
- child.stdin.end();
5459
- } catch (e) {
5460
- stderr += `
5461
- [hook stdin error] ${e?.message}`;
5462
- try {
5463
- child.kill();
5464
- } catch {
5465
- }
5466
- finish(null);
5467
- }
5468
6185
  });
5469
6186
  }
5470
- var DEFAULT_TIMEOUT_MS3, MAX_OUTPUT_BYTES3, HookRunner;
6187
+ var NON_BLOCKING_EVENTS, DEFAULT_TIMEOUT_MS3, MAX_OUTPUT_BYTES3, HookRunner;
5471
6188
  var init_Hooks = __esm({
5472
6189
  "../core/src/Hooks.ts"() {
5473
6190
  "use strict";
5474
6191
  init_esm_shims();
6192
+ NON_BLOCKING_EVENTS = /* @__PURE__ */ new Set([
6193
+ "post-tool-use",
6194
+ "session-start",
6195
+ "session-end",
6196
+ "stop",
6197
+ "subagent-stop",
6198
+ "pre-compact",
6199
+ "notification"
6200
+ ]);
5475
6201
  DEFAULT_TIMEOUT_MS3 = 5e3;
5476
6202
  MAX_OUTPUT_BYTES3 = 32e3;
5477
6203
  HookRunner = class {
@@ -5499,6 +6225,18 @@ var init_Hooks = __esm({
5499
6225
  }
5500
6226
  return outcomes;
5501
6227
  }
6228
+ /**
6229
+ * P1-7a: fire an observation-only lifecycle event without awaiting or
6230
+ * blocking the caller. Errors are swallowed so a misbehaving hook can never
6231
+ * break session start/stop/teardown. Returns immediately; the hook chain
6232
+ * runs in the background. No-op if no hooks are registered for the event.
6233
+ */
6234
+ fireForget(ctx) {
6235
+ const entries = this.hooks[ctx.event];
6236
+ if (!entries || entries.length === 0) return;
6237
+ void this.fire(ctx).catch(() => {
6238
+ });
6239
+ }
5502
6240
  /** Convenience: did any outcome block? */
5503
6241
  static anyBlocked(outcomes) {
5504
6242
  return outcomes.find((o) => o.blocked) ?? null;
@@ -5661,9 +6399,9 @@ var init_specs = __esm({
5661
6399
 
5662
6400
  // ../core/src/governor/ResourceGovernor.ts
5663
6401
  import { freemem as freemem2 } from "os";
5664
- import { homedir as homedir7 } from "os";
6402
+ import { homedir as homedir8 } from "os";
5665
6403
  import { readFile as readFile9 } from "fs/promises";
5666
- import { join as join14 } from "path";
6404
+ import { join as join15 } from "path";
5667
6405
  function determineTier(specs) {
5668
6406
  const memGB = specs.memory.totalGB;
5669
6407
  const cores = specs.cpu.cores;
@@ -5678,7 +6416,7 @@ function recommendLimits(specs) {
5678
6416
  }
5679
6417
  async function readConfigOverrides() {
5680
6418
  try {
5681
- const configPath = join14(homedir7(), ".msapling", "config.json");
6419
+ const configPath = join15(homedir8(), ".msapling", "config.json");
5682
6420
  const content = await readFile9(configPath, "utf-8");
5683
6421
  const config = JSON.parse(content);
5684
6422
  return config.limits ?? null;
@@ -5759,7 +6497,7 @@ var init_ResourceGovernor = __esm({
5759
6497
  this.activeAgents++;
5760
6498
  return;
5761
6499
  }
5762
- await new Promise((resolve20) => this.agentWaiters.push(resolve20));
6500
+ await new Promise((resolve21) => this.agentWaiters.push(resolve21));
5763
6501
  this.activeAgents++;
5764
6502
  }
5765
6503
  /**
@@ -5778,7 +6516,7 @@ var init_ResourceGovernor = __esm({
5778
6516
  this.activeTool++;
5779
6517
  return;
5780
6518
  }
5781
- await new Promise((resolve20) => this.toolWaiters.push(resolve20));
6519
+ await new Promise((resolve21) => this.toolWaiters.push(resolve21));
5782
6520
  this.activeTool++;
5783
6521
  }
5784
6522
  /**
@@ -5834,21 +6572,29 @@ var init_ToolExecutor = __esm({
5834
6572
  init_PatchFileTool();
5835
6573
  init_DispatchAgentTool();
5836
6574
  init_WebFetchTool();
6575
+ init_WebSearchTool();
5837
6576
  init_BashTool();
6577
+ init_BackgroundShellTool();
5838
6578
  init_NotebookReadTool();
5839
6579
  init_NotebookEditTool();
5840
6580
  init_MultiEditFileTool();
5841
6581
  init_MoveFileTool();
5842
6582
  init_DeleteFileTool();
6583
+ init_PlanModeTools();
5843
6584
  init_MDrive();
5844
6585
  init_Sandbox();
5845
6586
  init_Voice();
5846
6587
  init_ShadowService();
5847
6588
  init_Hooks();
5848
6589
  init_ResourceGovernor();
5849
- APPROVAL_GATED = /* @__PURE__ */ new Set(["run_command", "bash_command", "edit_file", "write_file", "patch_file", "multi_edit_file", "notebook_edit_cell", "move_file", "delete_file", "open_sub_shell"]);
6590
+ APPROVAL_GATED = /* @__PURE__ */ new Set(["run_command", "bash_command", "bash_background", "edit_file", "write_file", "patch_file", "multi_edit_file", "notebook_edit_cell", "move_file", "delete_file", "open_sub_shell"]);
5850
6591
  ToolExecutor = class {
5851
6592
  tools = /* @__PURE__ */ new Map();
6593
+ /** Backend client — retained so named-subagent dispatch can open its own
6594
+ * scoped stream (CLI-PARITY-P1-6). */
6595
+ client;
6596
+ /** Project root — retained for named-subagent dispatch context. */
6597
+ projectRoot;
5852
6598
  sandbox;
5853
6599
  mdrive;
5854
6600
  voice;
@@ -5875,7 +6621,16 @@ var init_ToolExecutor = __esm({
5875
6621
  sessionTrust = /* @__PURE__ */ new Set();
5876
6622
  mcpRegistry = null;
5877
6623
  hooks = null;
6624
+ /**
6625
+ * CLI-PARITY-P0-2: Notifier fired whenever a tool flips the permission mode
6626
+ * (enter_plan_mode / exit_plan_mode). Lets the CLI keep its React `mode`
6627
+ * state + footer in sync with a model-driven mode change. Optional — when
6628
+ * unset the mode still flips internally; only the UI display would lag.
6629
+ */
6630
+ onModeChange = null;
5878
6631
  constructor(client, projectRoot) {
6632
+ this.client = client;
6633
+ this.projectRoot = projectRoot;
5879
6634
  this.sandbox = new Sandbox(projectRoot);
5880
6635
  this.mdrive = new MDriveService(client);
5881
6636
  this.voice = new VoiceService();
@@ -5894,15 +6649,37 @@ var init_ToolExecutor = __esm({
5894
6649
  this.registerTool(new PatchFileTool());
5895
6650
  this.registerTool(new DispatchAgentTool({
5896
6651
  client,
5897
- projectRoot
6652
+ projectRoot,
6653
+ // P1-7a (CLI-HOOKS-LIFECYCLE-01): fire the subagent-stop lifecycle hook
6654
+ // when a dispatched sub-agent finishes. Observation-only / non-blocking.
6655
+ onSubagentStop: (info) => {
6656
+ this.hooks?.fireForget({
6657
+ event: "subagent-stop",
6658
+ tool: info.description,
6659
+ payload: info,
6660
+ cwd: projectRoot
6661
+ });
6662
+ },
6663
+ // CLI-PARITY-P1-6: named-subagent runner. Runs a named agent with its own
6664
+ // system prompt + model + TOOL ALLOWLIST. Tools execute back through THIS
6665
+ // executor (so the existing permission/approval system gates writes), but
6666
+ // only the agent's allowlisted tools are advertised + permitted.
6667
+ runNamedAgent: (req) => this.runNamedSubagent(req)
5898
6668
  }));
5899
6669
  this.registerTool(new WebFetchTool());
6670
+ this.registerTool(new WebSearchTool({ client }));
5900
6671
  this.registerTool(new BashTool());
6672
+ this.registerTool(new BashBackgroundTool());
6673
+ this.registerTool(new ListBackgroundShellsTool());
6674
+ this.registerTool(new ReadBackgroundShellTool());
6675
+ this.registerTool(new KillBackgroundShellTool());
5901
6676
  this.registerTool(new NotebookReadTool());
5902
6677
  this.registerTool(new NotebookEditTool());
5903
6678
  this.registerTool(new MultiEditFileTool());
5904
6679
  this.registerTool(new MoveFileTool());
5905
6680
  this.registerTool(new DeleteFileTool());
6681
+ this.registerTool(new EnterPlanModeTool());
6682
+ this.registerTool(new ExitPlanModeTool());
5906
6683
  }
5907
6684
  setToolsEnabled(enabled) {
5908
6685
  this.toolsEnabled = enabled;
@@ -5913,6 +6690,24 @@ var init_ToolExecutor = __esm({
5913
6690
  getMode() {
5914
6691
  return this.mode;
5915
6692
  }
6693
+ /**
6694
+ * CLI-PARITY-P0-2: Register a callback invoked when a tool changes the
6695
+ * permission mode (enter_plan_mode / exit_plan_mode), so the CLI can mirror
6696
+ * the new mode into its React state. Does not affect the internal mode.
6697
+ */
6698
+ setOnModeChange(cb) {
6699
+ this.onModeChange = cb;
6700
+ }
6701
+ /** Internal: flip the mode AND notify any registered listener. */
6702
+ applyModeChange(mode) {
6703
+ this.mode = mode;
6704
+ if (this.onModeChange) {
6705
+ try {
6706
+ this.onModeChange(mode);
6707
+ } catch {
6708
+ }
6709
+ }
6710
+ }
5916
6711
  setApprovalCallback(cb) {
5917
6712
  this.approvalCallback = cb;
5918
6713
  }
@@ -5957,6 +6752,61 @@ var init_ToolExecutor = __esm({
5957
6752
  registerTool(tool) {
5958
6753
  this.tools.set(tool.name, tool);
5959
6754
  }
6755
+ /**
6756
+ * CLI-PARITY-P0-2: enter_plan_mode handler. Flips the executor into read-only
6757
+ * `plan` mode (idempotent) and returns the research instructions. No approval:
6758
+ * entering plan mode only tightens permissions.
6759
+ */
6760
+ async handleEnterPlanMode() {
6761
+ if (this.mode !== "plan") {
6762
+ this.applyModeChange("plan");
6763
+ }
6764
+ const tool = this.tools.get("enter_plan_mode");
6765
+ return tool.execute({}, "");
6766
+ }
6767
+ /**
6768
+ * CLI-PARITY-P0-2: exit_plan_mode handler. Validates the drafted plan,
6769
+ * presents it through the existing approval UI (approvalCallback), and on
6770
+ * approve flips `plan` → default/acceptEdits so execution can proceed. On deny
6771
+ * the executor stays in plan mode and the model is told to revise.
6772
+ */
6773
+ async handleExitPlanMode(args2) {
6774
+ const err = ExitPlanModeTool.validatePlan(args2);
6775
+ if (err) return { content: err, isError: true };
6776
+ if (this.mode !== "plan") {
6777
+ return {
6778
+ content: "You are not in Plan Mode, so there is nothing to exit. Continue with implementation."
6779
+ };
6780
+ }
6781
+ const plan = String(args2.plan);
6782
+ if (!this.approvalCallback) {
6783
+ this.applyModeChange("default");
6784
+ return {
6785
+ content: `No interactive approval surface available \u2014 Plan Mode lifted (now in default mode). Proceed with the plan.
6786
+
6787
+ ## Approved Plan
6788
+ ${plan}`
6789
+ };
6790
+ }
6791
+ const decision = await this.approvalCallback({
6792
+ tool: "exit_plan_mode",
6793
+ command: plan,
6794
+ reason: "The agent has finished planning and wants to start executing this plan. Approve to leave Plan Mode."
6795
+ });
6796
+ if (decision === "no") {
6797
+ return {
6798
+ content: "User rejected the plan. You remain in Plan Mode. Ask the user for specific feedback and revise the plan, then call exit_plan_mode again."
6799
+ };
6800
+ }
6801
+ const targetMode = decision === "always" ? "acceptEdits" : "default";
6802
+ this.applyModeChange(targetMode);
6803
+ return {
6804
+ content: `User approved the plan. Plan Mode lifted (now in ${targetMode} mode). You can now start executing. Begin with the first step.
6805
+
6806
+ ## Approved Plan
6807
+ ${plan}`
6808
+ };
6809
+ }
5960
6810
  async execute(toolName, args2, projectRoot) {
5961
6811
  const tool = this.tools.get(toolName);
5962
6812
  if (!tool) {
@@ -5972,6 +6822,12 @@ var init_ToolExecutor = __esm({
5972
6822
  }
5973
6823
  return { content: `Error: unknown tool: ${toolName}`, isError: true };
5974
6824
  }
6825
+ if (toolName === "enter_plan_mode") {
6826
+ return this.handleEnterPlanMode();
6827
+ }
6828
+ if (toolName === "exit_plan_mode") {
6829
+ return this.handleExitPlanMode(args2);
6830
+ }
5975
6831
  if (this.mode === "plan") {
5976
6832
  if (APPROVAL_GATED.has(toolName)) {
5977
6833
  return {
@@ -6008,13 +6864,13 @@ ${blocker.stderr || "(empty)"}`,
6008
6864
  return { content: `Security Block: ${check2.reason}`, isError: true };
6009
6865
  }
6010
6866
  }
6011
- if (toolName === "run_command" || toolName === "bash_command") {
6867
+ if (toolName === "run_command" || toolName === "bash_command" || toolName === "bash_background") {
6012
6868
  if (!args2.command) {
6013
6869
  return { content: `Error: ${toolName} requires a command argument`, isError: true };
6014
6870
  }
6015
6871
  const analysis = this.sandbox.analyzeCommand(args2.command);
6016
6872
  if (analysis.status === "blocked") {
6017
- const staleCmdKey = toolName === "bash_command" ? `bash_command:${(args2.command || "").trim().replace(/\s+/g, " ")}${args2.cwd ? `:cwd=${args2.cwd}` : ""}` : `run_command:${(args2.command || "").trim()}`;
6873
+ const staleCmdKey = toolName === "run_command" ? `run_command:${(args2.command || "").trim()}` : `${toolName}:${(args2.command || "").trim().replace(/\s+/g, " ")}${args2.cwd ? `:cwd=${args2.cwd}` : ""}`;
6018
6874
  if (this.trustStore?.has(staleCmdKey)) {
6019
6875
  this.trustStore.delete(staleCmdKey).catch(() => {
6020
6876
  });
@@ -6029,10 +6885,10 @@ ${blocker.stderr || "(empty)"}`,
6029
6885
  let cmdKey = "";
6030
6886
  if (toolName === "run_command") {
6031
6887
  cmdKey = `run_command:${(args2.command || "").trim()}`;
6032
- } else if (toolName === "bash_command") {
6888
+ } else if (toolName === "bash_command" || toolName === "bash_background") {
6033
6889
  const normalizedCmd = (args2.command || "").trim().replace(/\s+/g, " ");
6034
6890
  const cwdSuffix = args2.cwd ? `:cwd=${args2.cwd}` : "";
6035
- cmdKey = `bash_command:${normalizedCmd}${cwdSuffix}`;
6891
+ cmdKey = `${toolName}:${normalizedCmd}${cwdSuffix}`;
6036
6892
  } else {
6037
6893
  cmdKey = `${toolName}:${(args2.path || args2.instruction || "").trim()}`;
6038
6894
  }
@@ -6056,7 +6912,7 @@ ${blocker.stderr || "(empty)"}`,
6056
6912
  }
6057
6913
  }
6058
6914
  }
6059
- if (toolName === "run_command" || toolName === "bash_command" || toolName === "edit_file" || toolName === "write_file" || toolName === "patch_file" || toolName === "multi_edit_file" || toolName === "notebook_edit_cell" || toolName === "move_file" || toolName === "delete_file") {
6915
+ if (toolName === "run_command" || toolName === "bash_command" || toolName === "bash_background" || toolName === "edit_file" || toolName === "write_file" || toolName === "patch_file" || toolName === "multi_edit_file" || toolName === "notebook_edit_cell" || toolName === "move_file" || toolName === "delete_file") {
6060
6916
  const audit = await this.shadow.verifyAction(
6061
6917
  JSON.stringify({ tool: toolName, args: args2 }),
6062
6918
  `Execution context: ${projectRoot}`
@@ -6117,6 +6973,80 @@ Please approve the diff in the UI to sync this change locally.`
6117
6973
  }));
6118
6974
  return [...builtin, ...mcp];
6119
6975
  }
6976
+ /**
6977
+ * CLI-PARITY-P1-6: tool schemas filtered to a named agent's allowlist.
6978
+ *
6979
+ * - `undefined` allowlist → inherit the FULL builtin+MCP set (getToolSchemas).
6980
+ * - `[]` → no tools (read/reason-only agent).
6981
+ * - `['read_file', …]` → ONLY those tools (silently drops names that
6982
+ * don't resolve to a registered tool).
6983
+ *
6984
+ * `dispatch_agent` is always excluded from a subagent's view so a named agent
6985
+ * cannot recursively spawn further named agents (avoids unbounded fan-out).
6986
+ */
6987
+ getToolSchemasForAllowlist(allow) {
6988
+ const all = this.getToolSchemas();
6989
+ if (allow === void 0) {
6990
+ return all.filter((s) => s.name !== "dispatch_agent");
6991
+ }
6992
+ const allowSet = new Set(allow);
6993
+ return all.filter((s) => s.name !== "dispatch_agent" && allowSet.has(s.name));
6994
+ }
6995
+ /**
6996
+ * CLI-PARITY-P1-6: run a NAMED subagent to completion.
6997
+ *
6998
+ * Drives a bounded agentic loop against the backend using the agent's system
6999
+ * prompt + model, advertising ONLY the agent's allowlisted tools. Tool calls
7000
+ * execute back through `this.execute(...)`, so the SAME permission/approval +
7001
+ * shadow-audit gates apply — the allowlist narrows WHICH tools the agent may
7002
+ * request; the permission system still governs WHETHER a given write runs.
7003
+ *
7004
+ * This is the mechanism that closes the "subagents are read-only" gap: a named
7005
+ * agent whose frontmatter grants `write_file`/`edit_file`/`bash_command` can
7006
+ * use them (subject to approval), whereas the anonymous dispatch path cannot.
7007
+ */
7008
+ async runNamedSubagent(req) {
7009
+ const { agent, prompt: prompt4, model, chatId, onContent } = req;
7010
+ const schemas = this.getToolSchemasForAllowlist(agent.tools);
7011
+ const framedPrompt = `[You are the "${agent.name}" subagent. Operate strictly within this role.]
7012
+ ${agent.systemPrompt}
7013
+
7014
+ ---
7015
+
7016
+ ${prompt4}`;
7017
+ const MAX_SUBAGENT_ROUNDS = 12;
7018
+ const queue = [framedPrompt];
7019
+ let rounds = 0;
7020
+ while (queue.length > 0 && rounds < MAX_SUBAGENT_ROUNDS) {
7021
+ const current = queue.shift();
7022
+ rounds++;
7023
+ const stream = this.client.streamChat({
7024
+ ...chatId ? { chat_id: chatId } : {},
7025
+ prompt: current,
7026
+ model,
7027
+ tools: schemas,
7028
+ project_root: this.projectRoot,
7029
+ mode: this.mode
7030
+ });
7031
+ for await (const chunk of stream) {
7032
+ if (chunk.content) onContent(chunk.content);
7033
+ if (chunk.tool_use) {
7034
+ const name = chunk.tool_use.name;
7035
+ if (agent.tools !== void 0 && !agent.tools.includes(name)) {
7036
+ queue.push(
7037
+ `[TOOL_RESULT]: ${JSON.stringify({
7038
+ content: `Tool "${name}" is not in the "${agent.name}" agent's allowlist; refused.`,
7039
+ isError: true
7040
+ })}`
7041
+ );
7042
+ continue;
7043
+ }
7044
+ const result = await this.execute(name, chunk.tool_use.args, this.projectRoot);
7045
+ queue.push(`[TOOL_RESULT]: ${JSON.stringify(result)}`);
7046
+ }
7047
+ }
7048
+ }
7049
+ }
6120
7050
  };
6121
7051
  }
6122
7052
  });
@@ -6162,8 +7092,8 @@ var init_Safety = __esm({
6162
7092
  });
6163
7093
 
6164
7094
  // ../core/src/ProjectConfig.ts
6165
- import { homedir as homedir8 } from "os";
6166
- import { join as join15, dirname as dirname3, parse as parsePath } from "path";
7095
+ import { homedir as homedir9 } from "os";
7096
+ import { join as join16, dirname as dirname3, parse as parsePath } from "path";
6167
7097
  import { existsSync as existsSync11 } from "fs";
6168
7098
  import { readFile as readFile11 } from "fs/promises";
6169
7099
  async function readIfExists(path2) {
@@ -6177,7 +7107,7 @@ async function readIfExists(path2) {
6177
7107
  }
6178
7108
  async function findInDir(dir) {
6179
7109
  for (const filename of FILENAMES) {
6180
- const path2 = join15(dir, filename);
7110
+ const path2 = join16(dir, filename);
6181
7111
  const content = await readIfExists(path2);
6182
7112
  if (content !== null) {
6183
7113
  return { path: path2, filename, content };
@@ -6200,9 +7130,9 @@ async function findProjectConfig(start) {
6200
7130
  return null;
6201
7131
  }
6202
7132
  async function findUserConfig() {
6203
- const home = homedir8();
7133
+ const home = homedir9();
6204
7134
  if (!home) return null;
6205
- const userDir = join15(home, ".msapling");
7135
+ const userDir = join16(home, ".msapling");
6206
7136
  return findInDir(userDir);
6207
7137
  }
6208
7138
  function buildCombined(user, project) {
@@ -6396,6 +7326,14 @@ var init_Agent = __esm({
6396
7326
  getMode() {
6397
7327
  return this.executor.getMode();
6398
7328
  }
7329
+ /**
7330
+ * CLI-PARITY-P0-2: Register a listener for model-driven mode changes
7331
+ * (enter_plan_mode / exit_plan_mode) so the CLI can mirror the new mode into
7332
+ * its own state + footer.
7333
+ */
7334
+ setOnModeChange(cb) {
7335
+ this.executor.setOnModeChange(cb);
7336
+ }
6399
7337
  setApprovalCallback(cb) {
6400
7338
  this.executor.setApprovalCallback(cb);
6401
7339
  }
@@ -6406,6 +7344,19 @@ var init_Agent = __esm({
6406
7344
  this.executor.setHookRunner(runner);
6407
7345
  this.hooks = runner;
6408
7346
  }
7347
+ /** Expose the active HookRunner (e.g. for App-level session-start/end). */
7348
+ getHookRunner() {
7349
+ return this.hooks;
7350
+ }
7351
+ /**
7352
+ * P1-7a: fire an observation-only lifecycle hook event without blocking.
7353
+ * No-op when no HookRunner is wired. `name` is the descriptive string used
7354
+ * for matcher evaluation (e.g. the subagent description); `payload` is the
7355
+ * JSON event body delivered on the hook's stdin.
7356
+ */
7357
+ fireLifecycleHook(event, payload, name = "") {
7358
+ this.hooks?.fireForget({ event, tool: name, payload, cwd: this.projectRoot });
7359
+ }
6409
7360
  /**
6410
7361
  * CLI-PARITY-37: Wire a pre-loaded TrustStore so "always" approval decisions
6411
7362
  * are persisted across CLI restarts. Must be called after `store.load()`.
@@ -6465,6 +7416,11 @@ var init_Agent = __esm({
6465
7416
  `
6466
7417
  [Agent: context budget reached \u2014 auto-compacting conversation (${this.contextBudget.summary()})]`
6467
7418
  );
7419
+ this.fireLifecycleHook(
7420
+ "pre-compact",
7421
+ { chat_id: chatId, reason: "auto", budget: this.contextBudget.summary() },
7422
+ "auto"
7423
+ );
6468
7424
  rounds++;
6469
7425
  const compactionStream = this.client.streamChat({
6470
7426
  chat_id: chatId,
@@ -6547,6 +7503,7 @@ ${next}`;
6547
7503
  if (rounds >= MAX_WORKER_TURN_DEPTH) {
6548
7504
  throw new Error("runWorkerTurn exceeded MAX_DEPTH");
6549
7505
  }
7506
+ this.fireLifecycleHook("stop", { chat_id: chatId, rounds }, "");
6550
7507
  return streamUsage;
6551
7508
  }
6552
7509
  async run(prompt4, model) {
@@ -6783,8 +7740,8 @@ var init_Mutex = __esm({
6783
7740
  */
6784
7741
  acquire() {
6785
7742
  let release3;
6786
- const next = new Promise((resolve20) => {
6787
- release3 = resolve20;
7743
+ const next = new Promise((resolve21) => {
7744
+ release3 = resolve21;
6788
7745
  });
6789
7746
  const entry = this._queue.then(() => release3);
6790
7747
  this._queue = this._queue.then(() => next);
@@ -6822,8 +7779,8 @@ var init_lockfile = __esm({
6822
7779
  });
6823
7780
 
6824
7781
  // ../core/src/TrustStore.ts
6825
- import { join as join16, dirname as dirname4 } from "path";
6826
- import { homedir as homedir9, platform as platform3 } from "os";
7782
+ import { join as join17, dirname as dirname4 } from "path";
7783
+ import { homedir as homedir10, platform as platform3 } from "os";
6827
7784
  import { existsSync as existsSync12, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
6828
7785
  import { readFile as readFile12, writeFile as writeFile5, rename as rename2, chmod } from "fs/promises";
6829
7786
  import { randomBytes as randomBytes9 } from "crypto";
@@ -6834,7 +7791,7 @@ var init_TrustStore = __esm({
6834
7791
  init_esm_shims();
6835
7792
  init_Mutex();
6836
7793
  init_lockfile();
6837
- USER_SETTINGS_PATH = join16(homedir9(), ".msapling", "settings.json");
7794
+ USER_SETTINGS_PATH = join17(homedir10(), ".msapling", "settings.json");
6838
7795
  TrustStore = class {
6839
7796
  /** Current in-memory set of trusted `tool:command` keys. */
6840
7797
  trusted = /* @__PURE__ */ new Set();
@@ -6991,9 +7948,118 @@ var init_TrustStore = __esm({
6991
7948
  }
6992
7949
  });
6993
7950
 
7951
+ // ../core/src/BackupIndex.ts
7952
+ import { join as join18 } from "path";
7953
+ import { homedir as homedir11 } from "os";
7954
+ import {
7955
+ readFileSync as readFileSync3,
7956
+ appendFileSync as appendFileSync2,
7957
+ existsSync as existsSync13,
7958
+ mkdirSync as mkdirSync3,
7959
+ copyFileSync
7960
+ } from "fs";
7961
+ import { randomBytes as randomBytes10 } from "crypto";
7962
+ function _setBackupDirOverride(dir) {
7963
+ _backupDirOverride = dir;
7964
+ }
7965
+ function getBackupDir() {
7966
+ return _backupDirOverride ?? join18(homedir11(), ".msapling", "backups");
7967
+ }
7968
+ function getManifestPath() {
7969
+ return join18(getBackupDir(), "manifest.jsonl");
7970
+ }
7971
+ function ensureBackupDir() {
7972
+ const dir = getBackupDir();
7973
+ if (!existsSync13(dir)) {
7974
+ mkdirSync3(dir, { recursive: true });
7975
+ }
7976
+ }
7977
+ function openTurn(label) {
7978
+ ensureBackupDir();
7979
+ _turnCounter += 1;
7980
+ const turnId = `turn-${Date.now()}-${randomBytes10(4).toString("hex")}`;
7981
+ const entry = {
7982
+ turnId,
7983
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
7984
+ label: label ?? `Turn ${_turnCounter}`,
7985
+ backups: []
7986
+ };
7987
+ _openTurns.set(turnId, entry);
7988
+ return turnId;
7989
+ }
7990
+ function recordBackup(turnId, originalPath, backupPath) {
7991
+ const entry = _openTurns.get(turnId);
7992
+ if (!entry) return;
7993
+ entry.backups.push({ originalPath, backupPath });
7994
+ }
7995
+ function closeTurn(turnId) {
7996
+ const entry = _openTurns.get(turnId);
7997
+ if (!entry) return;
7998
+ _openTurns.delete(turnId);
7999
+ entry.closedAt = (/* @__PURE__ */ new Date()).toISOString();
8000
+ try {
8001
+ ensureBackupDir();
8002
+ appendFileSync2(getManifestPath(), JSON.stringify(entry) + "\n", "utf8");
8003
+ } catch {
8004
+ }
8005
+ }
8006
+ function listCheckpoints() {
8007
+ const mp = getManifestPath();
8008
+ if (!existsSync13(mp)) return [];
8009
+ let raw;
8010
+ try {
8011
+ raw = readFileSync3(mp, "utf8");
8012
+ } catch {
8013
+ return [];
8014
+ }
8015
+ const entries = [];
8016
+ for (const line of raw.split("\n")) {
8017
+ const trimmed = line.trim();
8018
+ if (!trimmed) continue;
8019
+ try {
8020
+ entries.push(JSON.parse(trimmed));
8021
+ } catch {
8022
+ }
8023
+ }
8024
+ return entries.reverse().slice(0, MAX_CHECKPOINTS);
8025
+ }
8026
+ function restoreCheckpoint(entry) {
8027
+ const results = [];
8028
+ for (const rec of entry.backups) {
8029
+ if (!existsSync13(rec.backupPath)) {
8030
+ results.push(`Skip (backup file missing): ${rec.originalPath}`);
8031
+ continue;
8032
+ }
8033
+ try {
8034
+ copyFileSync(rec.backupPath, rec.originalPath);
8035
+ results.push(`Restored: ${rec.originalPath}`);
8036
+ } catch (e) {
8037
+ results.push(`Error restoring ${rec.originalPath}: ${e.message}`);
8038
+ }
8039
+ }
8040
+ return results;
8041
+ }
8042
+ function manifestPath() {
8043
+ return getManifestPath();
8044
+ }
8045
+ function backupDir() {
8046
+ return getBackupDir();
8047
+ }
8048
+ var MAX_CHECKPOINTS, _backupDirOverride, _openTurns, _turnCounter;
8049
+ var init_BackupIndex = __esm({
8050
+ "../core/src/BackupIndex.ts"() {
8051
+ "use strict";
8052
+ init_esm_shims();
8053
+ MAX_CHECKPOINTS = 50;
8054
+ _backupDirOverride = null;
8055
+ _openTurns = /* @__PURE__ */ new Map();
8056
+ _turnCounter = 0;
8057
+ }
8058
+ });
8059
+
6994
8060
  // ../core/src/Storage/vault.ts
6995
- import { join as join17 } from "path";
6996
- import { chmodSync, existsSync as existsSync13, renameSync as renameSync2, unlinkSync } from "fs";
8061
+ import { join as join19 } from "path";
8062
+ import { chmodSync, existsSync as existsSync14, renameSync as renameSync2, unlinkSync } from "fs";
6997
8063
  import { writeFile as writeFile6, readFile as readFile13 } from "fs/promises";
6998
8064
  import { createHash as createHash4 } from "crypto";
6999
8065
  async function getKeytar() {
@@ -7007,7 +8073,7 @@ async function getKeytar() {
7007
8073
  return _keytar;
7008
8074
  }
7009
8075
  async function saveToken(baseDir, token) {
7010
- const filePath = join17(baseDir, "vault", "token");
8076
+ const filePath = join19(baseDir, "vault", "token");
7011
8077
  try {
7012
8078
  const kt = await getKeytar();
7013
8079
  if (!kt) throw new Error("keyring not loadable");
@@ -7016,12 +8082,12 @@ async function saveToken(baseDir, token) {
7016
8082
  throw new KeychainUnavailableError(e instanceof Error ? e.message : String(e));
7017
8083
  }
7018
8084
  try {
7019
- if (existsSync13(filePath)) unlinkSync(filePath);
8085
+ if (existsSync14(filePath)) unlinkSync(filePath);
7020
8086
  } catch {
7021
8087
  }
7022
8088
  }
7023
8089
  async function loadToken(baseDir) {
7024
- const filePath = join17(baseDir, "vault", "token");
8090
+ const filePath = join19(baseDir, "vault", "token");
7025
8091
  let kt = null;
7026
8092
  try {
7027
8093
  kt = await getKeytar();
@@ -7032,7 +8098,7 @@ async function loadToken(baseDir) {
7032
8098
  kt = null;
7033
8099
  console.debug(`Keychain unavailable (${e instanceof Error ? e.message : String(e)})`);
7034
8100
  }
7035
- if (existsSync13(filePath)) {
8101
+ if (existsSync14(filePath)) {
7036
8102
  const legacyToken = (await readFile13(filePath, "utf8")).trim();
7037
8103
  if (!legacyToken) return null;
7038
8104
  if (kt) {
@@ -7051,7 +8117,7 @@ async function loadToken(baseDir) {
7051
8117
  return null;
7052
8118
  }
7053
8119
  async function clearToken(baseDir) {
7054
- const filePath = join17(baseDir, "vault", "token");
8120
+ const filePath = join19(baseDir, "vault", "token");
7055
8121
  try {
7056
8122
  const kt = await getKeytar();
7057
8123
  if (kt) await kt.deletePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
@@ -7059,7 +8125,7 @@ async function clearToken(baseDir) {
7059
8125
  console.debug(`Keychain unavailable for deletion (${e instanceof Error ? e.message : String(e)})`);
7060
8126
  }
7061
8127
  try {
7062
- if (existsSync13(filePath)) {
8128
+ if (existsSync14(filePath)) {
7063
8129
  const fs3 = await import("fs/promises");
7064
8130
  await fs3.unlink(filePath);
7065
8131
  }
@@ -7081,8 +8147,8 @@ async function getOrCreateJournalKey() {
7081
8147
  const buf = Buffer.from(existing, "base64");
7082
8148
  if (buf.length === 32) return buf;
7083
8149
  }
7084
- const { randomBytes: randomBytes15 } = await import("crypto");
7085
- const key = randomBytes15(32);
8150
+ const { randomBytes: randomBytes16 } = await import("crypto");
8151
+ const key = randomBytes16(32);
7086
8152
  await kt.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_JOURNAL_ACCOUNT, key.toString("base64"));
7087
8153
  return key;
7088
8154
  } catch (e) {
@@ -7092,8 +8158,8 @@ async function getOrCreateJournalKey() {
7092
8158
  }
7093
8159
  async function writeVaultRef(baseDir, label, value) {
7094
8160
  const hash = createHash4("sha256").update(value, "utf8").digest("hex");
7095
- const objectPath = join17(baseDir, "vault", "objects", hash);
7096
- const refPath = join17(baseDir, "vault", "refs", label);
8161
+ const objectPath = join19(baseDir, "vault", "objects", hash);
8162
+ const refPath = join19(baseDir, "vault", "refs", label);
7097
8163
  const refTmp = `${refPath}.tmp`;
7098
8164
  await writeFile6(objectPath, value, "utf8");
7099
8165
  if (process.platform !== "win32") {
@@ -7104,11 +8170,11 @@ async function writeVaultRef(baseDir, label, value) {
7104
8170
  return hash;
7105
8171
  }
7106
8172
  async function readVaultRef(baseDir, label) {
7107
- const refPath = join17(baseDir, "vault", "refs", label);
7108
- if (!existsSync13(refPath)) return null;
8173
+ const refPath = join19(baseDir, "vault", "refs", label);
8174
+ if (!existsSync14(refPath)) return null;
7109
8175
  const hash = (await readFile13(refPath, "utf8")).trim();
7110
- const objectPath = join17(baseDir, "vault", "objects", hash);
7111
- if (!existsSync13(objectPath)) return null;
8176
+ const objectPath = join19(baseDir, "vault", "objects", hash);
8177
+ if (!existsSync14(objectPath)) return null;
7112
8178
  return readFile13(objectPath, "utf8");
7113
8179
  }
7114
8180
  var _keytar, KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, KeychainUnavailableError, KEYCHAIN_JOURNAL_ACCOUNT;
@@ -7132,20 +8198,20 @@ var init_vault = __esm({
7132
8198
  });
7133
8199
 
7134
8200
  // ../core/src/Storage/recipes.ts
7135
- import { join as join18 } from "path";
7136
- import { existsSync as existsSync14, renameSync as renameSync3 } from "fs";
8201
+ import { join as join20 } from "path";
8202
+ import { existsSync as existsSync15, renameSync as renameSync3 } from "fs";
7137
8203
  import { writeFile as writeFile7, readFile as readFile14 } from "fs/promises";
7138
8204
  import { createHash as createHash5 } from "crypto";
7139
8205
  async function registerRecipe(baseDir, name, content) {
7140
8206
  const hash = createHash5("sha256").update(content, "utf8").digest("hex");
7141
- const objectPath = join18(baseDir, "cache", "recipes", "objects", hash);
7142
- const indexPath = join18(baseDir, "cache", "recipes", "index.json");
8207
+ const objectPath = join20(baseDir, "cache", "recipes", "objects", hash);
8208
+ const indexPath = join20(baseDir, "cache", "recipes", "index.json");
7143
8209
  const indexTmp = `${indexPath}.tmp`;
7144
- if (!existsSync14(objectPath)) {
8210
+ if (!existsSync15(objectPath)) {
7145
8211
  await writeFile7(objectPath, content, "utf8");
7146
8212
  }
7147
8213
  let index = {};
7148
- if (existsSync14(indexPath)) {
8214
+ if (existsSync15(indexPath)) {
7149
8215
  try {
7150
8216
  index = JSON.parse(await readFile14(indexPath, "utf8"));
7151
8217
  } catch {
@@ -7158,15 +8224,15 @@ async function registerRecipe(baseDir, name, content) {
7158
8224
  return hash;
7159
8225
  }
7160
8226
  async function resolveRecipe(baseDir, nameOrRef) {
7161
- const indexPath = join18(baseDir, "cache", "recipes", "index.json");
8227
+ const indexPath = join20(baseDir, "cache", "recipes", "index.json");
7162
8228
  const atIdx = nameOrRef.indexOf("@");
7163
8229
  if (atIdx !== -1) {
7164
8230
  const hash2 = nameOrRef.slice(atIdx + 1);
7165
- const objectPath2 = join18(baseDir, "cache", "recipes", "objects", hash2);
7166
- if (!existsSync14(objectPath2)) return null;
8231
+ const objectPath2 = join20(baseDir, "cache", "recipes", "objects", hash2);
8232
+ if (!existsSync15(objectPath2)) return null;
7167
8233
  return { hash: hash2, content: await readFile14(objectPath2, "utf8") };
7168
8234
  }
7169
- if (!existsSync14(indexPath)) return null;
8235
+ if (!existsSync15(indexPath)) return null;
7170
8236
  let index;
7171
8237
  try {
7172
8238
  index = JSON.parse(await readFile14(indexPath, "utf8"));
@@ -7175,8 +8241,8 @@ async function resolveRecipe(baseDir, nameOrRef) {
7175
8241
  }
7176
8242
  const hash = index[nameOrRef];
7177
8243
  if (!hash) return null;
7178
- const objectPath = join18(baseDir, "cache", "recipes", "objects", hash);
7179
- if (!existsSync14(objectPath)) return null;
8244
+ const objectPath = join20(baseDir, "cache", "recipes", "objects", hash);
8245
+ if (!existsSync15(objectPath)) return null;
7180
8246
  return { hash, content: await readFile14(objectPath, "utf8") };
7181
8247
  }
7182
8248
  var init_recipes = __esm({
@@ -7187,25 +8253,25 @@ var init_recipes = __esm({
7187
8253
  });
7188
8254
 
7189
8255
  // ../core/src/Storage/history.ts
7190
- import { join as join19, basename } from "path";
7191
- import { existsSync as existsSync15, renameSync as renameSync4, writeFileSync as writeFileSync3 } from "fs";
8256
+ import { join as join21, basename } from "path";
8257
+ import { existsSync as existsSync16, renameSync as renameSync4, writeFileSync as writeFileSync3 } from "fs";
7192
8258
  import { writeFile as writeFile8, readFile as readFile15, appendFile } from "fs/promises";
7193
- import { createHash as createHash6, randomBytes as randomBytes10 } from "crypto";
8259
+ import { createHash as createHash6, randomBytes as randomBytes11 } from "crypto";
7194
8260
  function hashLine(line) {
7195
8261
  return createHash6("sha256").update(line, "utf8").digest("hex");
7196
8262
  }
7197
8263
  async function appendHistoryEntry(baseDir, historyMutex, content) {
7198
- const path2 = join19(baseDir, "history", "shell_history.jsonl");
8264
+ const path2 = join21(baseDir, "history", "shell_history.jsonl");
7199
8265
  return historyMutex.run(async () => {
7200
8266
  let release3 = null;
7201
8267
  try {
7202
- if (!existsSync15(path2)) {
8268
+ if (!existsSync16(path2)) {
7203
8269
  await writeFile8(path2, "", "utf8");
7204
8270
  }
7205
8271
  release3 = await lock(path2, { retries: 5, retryWait: 50 });
7206
8272
  let prevHash = null;
7207
8273
  let seq = 1;
7208
- if (existsSync15(path2)) {
8274
+ if (existsSync16(path2)) {
7209
8275
  const raw = (await readFile15(path2, "utf8")).trimEnd();
7210
8276
  if (raw.length > 0) {
7211
8277
  const lines = raw.split("\n");
@@ -7238,8 +8304,8 @@ async function appendHistoryEntry(baseDir, historyMutex, content) {
7238
8304
  });
7239
8305
  }
7240
8306
  async function loadHistoryEntries(baseDir) {
7241
- const path2 = join19(baseDir, "history", "shell_history.jsonl");
7242
- if (!existsSync15(path2)) return [];
8307
+ const path2 = join21(baseDir, "history", "shell_history.jsonl");
8308
+ if (!existsSync16(path2)) return [];
7243
8309
  const raw = await readFile15(path2, "utf8");
7244
8310
  const entries = [];
7245
8311
  for (const line of raw.split("\n")) {
@@ -7271,10 +8337,10 @@ async function verifyHistory(baseDir) {
7271
8337
  return breaks.length === 0 ? { ok: true } : { ok: false, breaks };
7272
8338
  }
7273
8339
  async function saveHistory(baseDir, historyMutex, history) {
7274
- const path2 = join19(baseDir, "history", "shell_history.json");
8340
+ const path2 = join21(baseDir, "history", "shell_history.json");
7275
8341
  let release3;
7276
8342
  try {
7277
- if (!existsSync15(path2)) writeFileSync3(path2, "[]", "utf8");
8343
+ if (!existsSync16(path2)) writeFileSync3(path2, "[]", "utf8");
7278
8344
  release3 = await lock(path2, { realpath: false, retries: 5, retryWait: 50 });
7279
8345
  await historyMutex.run(async () => {
7280
8346
  const tmpPath = `${path2}.tmp`;
@@ -7284,7 +8350,7 @@ async function saveHistory(baseDir, historyMutex, history) {
7284
8350
  renameSync4(tmpPath, path2);
7285
8351
  } catch (e) {
7286
8352
  try {
7287
- if (existsSync15(tmpPath)) {
8353
+ if (existsSync16(tmpPath)) {
7288
8354
  const fs3 = await import("fs/promises");
7289
8355
  await fs3.unlink(tmpPath);
7290
8356
  }
@@ -7304,21 +8370,21 @@ async function saveHistory(baseDir, historyMutex, history) {
7304
8370
  }
7305
8371
  }
7306
8372
  async function loadHistory(baseDir, historyMutex) {
7307
- const path2 = join19(baseDir, "history", "shell_history.json");
7308
- if (!existsSync15(path2)) return [];
8373
+ const path2 = join21(baseDir, "history", "shell_history.json");
8374
+ if (!existsSync16(path2)) return [];
7309
8375
  let release3;
7310
8376
  try {
7311
8377
  release3 = await lock(path2, { realpath: false, retries: 5, retryWait: 50 });
7312
8378
  return historyMutex.run(async () => {
7313
- if (existsSync15(path2)) {
8379
+ if (existsSync16(path2)) {
7314
8380
  const text = await readFile15(path2, "utf8");
7315
8381
  try {
7316
8382
  return JSON.parse(text);
7317
8383
  } catch (parseErr) {
7318
8384
  const filename = basename(path2) || "shell_history.json";
7319
8385
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7320
- const suffix = randomBytes10(4).toString("hex");
7321
- const corruptBackupPath = join19(
8386
+ const suffix = randomBytes11(4).toString("hex");
8387
+ const corruptBackupPath = join21(
7322
8388
  baseDir,
7323
8389
  "history",
7324
8390
  `${filename}.corrupt.${stamp}-${suffix}.bak`
@@ -7353,15 +8419,15 @@ var init_history = __esm({
7353
8419
  });
7354
8420
 
7355
8421
  // ../core/src/Storage/permissions.ts
7356
- import { join as join20 } from "path";
7357
- import { existsSync as existsSync16, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "fs";
8422
+ import { join as join22 } from "path";
8423
+ import { existsSync as existsSync17, renameSync as renameSync5, writeFileSync as writeFileSync4 } from "fs";
7358
8424
  import { writeFile as writeFile9, readFile as readFile16 } from "fs/promises";
7359
- import { randomBytes as randomBytes11 } from "crypto";
8425
+ import { randomBytes as randomBytes12 } from "crypto";
7360
8426
  async function savePermissions(baseDir, permissionsMutex, permissions) {
7361
- const path2 = join20(baseDir, "vault", "permissions.json");
8427
+ const path2 = join22(baseDir, "vault", "permissions.json");
7362
8428
  let release3;
7363
8429
  try {
7364
- if (!existsSync16(path2)) writeFileSync4(path2, "{}", "utf8");
8430
+ if (!existsSync17(path2)) writeFileSync4(path2, "{}", "utf8");
7365
8431
  release3 = await lock(path2, { realpath: false, retries: 5, retryWait: 50 });
7366
8432
  await permissionsMutex.run(async () => {
7367
8433
  const tmpPath = `${path2}.tmp`;
@@ -7371,7 +8437,7 @@ async function savePermissions(baseDir, permissionsMutex, permissions) {
7371
8437
  renameSync5(tmpPath, path2);
7372
8438
  } catch (e) {
7373
8439
  try {
7374
- if (existsSync16(tmpPath)) {
8440
+ if (existsSync17(tmpPath)) {
7375
8441
  const fs3 = await import("fs/promises");
7376
8442
  await fs3.unlink(tmpPath);
7377
8443
  }
@@ -7391,21 +8457,21 @@ async function savePermissions(baseDir, permissionsMutex, permissions) {
7391
8457
  }
7392
8458
  }
7393
8459
  async function loadPermissions(baseDir, permissionsMutex) {
7394
- const path2 = join20(baseDir, "vault", "permissions.json");
7395
- if (!existsSync16(path2)) return { trustedCommands: [], trustedPaths: [] };
8460
+ const path2 = join22(baseDir, "vault", "permissions.json");
8461
+ if (!existsSync17(path2)) return { trustedCommands: [], trustedPaths: [] };
7396
8462
  let release3;
7397
8463
  try {
7398
8464
  release3 = await lock(path2, { realpath: false, retries: 5, retryWait: 50 });
7399
8465
  return permissionsMutex.run(async () => {
7400
- if (existsSync16(path2)) {
8466
+ if (existsSync17(path2)) {
7401
8467
  const text = await readFile16(path2, "utf8");
7402
8468
  try {
7403
8469
  return JSON.parse(text);
7404
8470
  } catch (parseErr) {
7405
8471
  const filename = path2.split("/").pop() || "permissions.json";
7406
8472
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7407
- const suffix = randomBytes11(4).toString("hex");
7408
- const corruptBackupPath = join20(
8473
+ const suffix = randomBytes12(4).toString("hex");
8474
+ const corruptBackupPath = join22(
7409
8475
  baseDir,
7410
8476
  "vault",
7411
8477
  `${filename}.corrupt.${stamp}-${suffix}.bak`
@@ -7440,11 +8506,11 @@ var init_permissions = __esm({
7440
8506
  });
7441
8507
 
7442
8508
  // ../core/src/Storage.ts
7443
- import { join as join21 } from "path";
7444
- import { homedir as homedir10 } from "os";
8509
+ import { join as join23 } from "path";
8510
+ import { homedir as homedir12 } from "os";
7445
8511
  import { chmodSync as chmodSync2 } from "fs";
7446
8512
  import { mkdir as mkdir6, writeFile as writeFile10 } from "fs/promises";
7447
- import { randomBytes as randomBytes12 } from "crypto";
8513
+ import { randomBytes as randomBytes13 } from "crypto";
7448
8514
  var StorageManager;
7449
8515
  var init_Storage = __esm({
7450
8516
  "../core/src/Storage.ts"() {
@@ -7471,7 +8537,7 @@ var init_Storage = __esm({
7471
8537
  */
7472
8538
  _ready;
7473
8539
  constructor() {
7474
- this.baseDir = join21(homedir10(), ".msapling");
8540
+ this.baseDir = join23(homedir12(), ".msapling");
7475
8541
  this._ready = this.ensureDirs();
7476
8542
  }
7477
8543
  async ensureDirs() {
@@ -7488,7 +8554,7 @@ var init_Storage = __esm({
7488
8554
  "cache/recipes/objects"
7489
8555
  ];
7490
8556
  for (const sub of subdirs) {
7491
- await mkdir6(join21(this.baseDir, sub), { recursive: true });
8557
+ await mkdir6(join23(this.baseDir, sub), { recursive: true });
7492
8558
  }
7493
8559
  if (process.platform !== "win32") {
7494
8560
  chmodSync2(this.baseDir, 448);
@@ -7565,8 +8631,8 @@ var init_Storage = __esm({
7565
8631
  await this._ready;
7566
8632
  const filename = filePath.split("/").pop() || "file";
7567
8633
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7568
- const suffix = randomBytes12(4).toString("hex");
7569
- const backupPath = join21(this.baseDir, "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
8634
+ const suffix = randomBytes13(4).toString("hex");
8635
+ const backupPath = join23(this.baseDir, "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
7570
8636
  await writeFile10(backupPath, content, "utf8");
7571
8637
  if (process.platform !== "win32") {
7572
8638
  chmodSync2(backupPath, 384);
@@ -7597,15 +8663,15 @@ var init_journalCrypto = __esm({
7597
8663
  });
7598
8664
 
7599
8665
  // ../core/src/Settings.ts
7600
- import { homedir as homedir11 } from "os";
7601
- import { join as join22 } from "path";
7602
- import { existsSync as existsSync17 } from "fs";
8666
+ import { homedir as homedir13 } from "os";
8667
+ import { join as join24 } from "path";
8668
+ import { existsSync as existsSync18 } from "fs";
7603
8669
  import * as fs from "fs";
7604
8670
  import { readFile as readFile17 } from "fs/promises";
7605
- import { randomBytes as randomBytes13 } from "crypto";
8671
+ import { randomBytes as randomBytes14 } from "crypto";
7606
8672
  function backupStaleFile(p) {
7607
8673
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7608
- const suffix = randomBytes13(4).toString("hex");
8674
+ const suffix = randomBytes14(4).toString("hex");
7609
8675
  fs.renameSync(p, `${p}.broken-${stamp}-${suffix}`);
7610
8676
  }
7611
8677
  function ensureConfigDir(p) {
@@ -7628,7 +8694,7 @@ function ensureConfigDir(p) {
7628
8694
  }
7629
8695
  async function readJson(path2) {
7630
8696
  try {
7631
- if (!existsSync17(path2)) return null;
8697
+ if (!existsSync18(path2)) return null;
7632
8698
  const text = await readFile17(path2, "utf8");
7633
8699
  if (!text.trim()) return null;
7634
8700
  return JSON.parse(text);
@@ -7683,9 +8749,9 @@ function mergeSettings(base, override) {
7683
8749
  }
7684
8750
  async function loadSettings(cwd = process.cwd(), env = process.env, warn) {
7685
8751
  const sources = [];
7686
- const home = env.HOME || env.USERPROFILE || process.env.HOME || process.env.USERPROFILE || homedir11() || ".";
7687
- const userPath = join22(home, ".msapling", "settings.json");
7688
- const projectPath = join22(cwd, ".msapling", "settings.json");
8752
+ const home = env.HOME || env.USERPROFILE || process.env.HOME || process.env.USERPROFILE || homedir13() || ".";
8753
+ const userPath = join24(home, ".msapling", "settings.json");
8754
+ const projectPath = join24(cwd, ".msapling", "settings.json");
7689
8755
  const [user, project] = await Promise.all([readJson(userPath), readJson(projectPath)]);
7690
8756
  if (user) sources.push(userPath);
7691
8757
  if (project) sources.push(projectPath);
@@ -7738,7 +8804,7 @@ var init_Settings = __esm({
7738
8804
  });
7739
8805
 
7740
8806
  // ../core/src/mcp/client.ts
7741
- import { spawn as spawn8 } from "child_process";
8807
+ import { spawn as spawn9 } from "child_process";
7742
8808
  var PROTOCOL_VERSION, CLIENT_INFO, MCPClientError, MCPClient, MCPRegistry;
7743
8809
  var init_client = __esm({
7744
8810
  "../core/src/mcp/client.ts"() {
@@ -7774,7 +8840,7 @@ var init_client = __esm({
7774
8840
  if (this.proc) return;
7775
8841
  const env = { ...process.env, ...this.config.env ?? {} };
7776
8842
  try {
7777
- this.proc = spawn8(this.config.command, this.config.args ?? [], {
8843
+ this.proc = spawn9(this.config.command, this.config.args ?? [], {
7778
8844
  stdio: ["pipe", "pipe", "pipe"],
7779
8845
  env
7780
8846
  });
@@ -7848,7 +8914,7 @@ var init_client = __esm({
7848
8914
  if (!this.proc) throw new MCPClientError(`MCP server "${this.name}" not started`);
7849
8915
  const id = this.nextId++;
7850
8916
  const frame = { jsonrpc: "2.0", id, method, params };
7851
- return new Promise((resolve20, reject) => {
8917
+ return new Promise((resolve21, reject) => {
7852
8918
  const timer = setTimeout(() => {
7853
8919
  this.pending.delete(id);
7854
8920
  reject(new MCPClientError(`MCP request ${method} timed out after ${timeoutMs}ms`));
@@ -7856,7 +8922,7 @@ var init_client = __esm({
7856
8922
  this.pending.set(id, {
7857
8923
  resolve: (v) => {
7858
8924
  clearTimeout(timer);
7859
- resolve20(v);
8925
+ resolve21(v);
7860
8926
  },
7861
8927
  reject: (e) => {
7862
8928
  clearTimeout(timer);
@@ -7883,7 +8949,7 @@ var init_client = __esm({
7883
8949
  if (!this.proc?.stdout) return;
7884
8950
  const stdout = this.proc.stdout;
7885
8951
  const decoder = new TextDecoder();
7886
- return new Promise((resolve20) => {
8952
+ return new Promise((resolve21) => {
7887
8953
  stdout.on("data", (chunk) => {
7888
8954
  this.buffer += decoder.decode(chunk, { stream: true });
7889
8955
  let idx;
@@ -7894,8 +8960,8 @@ var init_client = __esm({
7894
8960
  this.handleFrame(line);
7895
8961
  }
7896
8962
  });
7897
- stdout.on("end", () => resolve20());
7898
- stdout.on("error", () => resolve20());
8963
+ stdout.on("end", () => resolve21());
8964
+ stdout.on("error", () => resolve21());
7899
8965
  });
7900
8966
  }
7901
8967
  handleFrame(line) {
@@ -8035,16 +9101,23 @@ __export(src_exports2, {
8035
9101
  APPROVAL_GATED: () => APPROVAL_GATED,
8036
9102
  Agent: () => Agent,
8037
9103
  AsyncMutex: () => AsyncMutex,
9104
+ BackgroundShellRegistry: () => BackgroundShellRegistry,
9105
+ BashBackgroundTool: () => BashBackgroundTool,
8038
9106
  BashTool: () => BashTool,
8039
9107
  ContextBudget: () => ContextBudget,
8040
9108
  DEFAULT_SETTINGS: () => DEFAULT_SETTINGS,
8041
9109
  DeleteFileTool: () => DeleteFileTool,
8042
9110
  DispatchAgentTool: () => DispatchAgentTool,
9111
+ EnterPlanModeTool: () => EnterPlanModeTool,
9112
+ ExitPlanModeTool: () => ExitPlanModeTool,
8043
9113
  GlobFilesTool: () => GlobFilesTool,
8044
9114
  GrepSearchTool: () => GrepSearchTool,
8045
9115
  HookRunner: () => HookRunner,
8046
9116
  KeychainUnavailableError: () => KeychainUnavailableError,
9117
+ KillBackgroundShellTool: () => KillBackgroundShellTool,
9118
+ ListBackgroundShellsTool: () => ListBackgroundShellsTool,
8047
9119
  ListDirectoryTool: () => ListDirectoryTool,
9120
+ MAX_PLAN_CHARS: () => MAX_PLAN_CHARS,
8048
9121
  MCPClient: () => MCPClient,
8049
9122
  MCPClientError: () => MCPClientError,
8050
9123
  MCPRegistry: () => MCPRegistry,
@@ -8053,25 +9126,43 @@ __export(src_exports2, {
8053
9126
  NotebookEditTool: () => NotebookEditTool,
8054
9127
  NotebookReadTool: () => NotebookReadTool,
8055
9128
  PatchFileTool: () => PatchFileTool,
9129
+ ReadBackgroundShellTool: () => ReadBackgroundShellTool,
8056
9130
  StorageManager: () => StorageManager,
8057
9131
  SwarmManager: () => SwarmManager,
9132
+ TOOL_NAME_ALIASES: () => TOOL_NAME_ALIASES,
8058
9133
  TodoReadTool: () => TodoReadTool,
8059
9134
  TodoStore: () => TodoStore,
8060
9135
  TodoWriteTool: () => TodoWriteTool,
8061
9136
  ToolExecutor: () => ToolExecutor,
8062
9137
  TrustStore: () => TrustStore,
8063
9138
  WebFetchTool: () => WebFetchTool,
9139
+ WebSearchTool: () => WebSearchTool,
8064
9140
  WriteFileTool: () => WriteFileTool,
9141
+ _setBackupDirOverride: () => _setBackupDirOverride,
9142
+ backupDir: () => backupDir,
8065
9143
  buildCompactionPrompt: () => buildCompactionPrompt,
8066
9144
  buildHwContext: () => buildHwContext,
9145
+ closeTurn: () => closeTurn,
9146
+ discoverAgentFiles: () => discoverAgentFiles,
8067
9147
  ensureConfigDir: () => ensureConfigDir,
9148
+ findNamedAgent: () => findNamedAgent,
9149
+ formatBackgroundShells: () => formatBackgroundShells,
8068
9150
  formatCell: () => formatCell,
8069
9151
  formatNotebookHeader: () => formatNotebookHeader,
8070
9152
  formatTodos: () => formatTodos,
8071
9153
  getOrCreateJournalKey: () => getOrCreateJournalKey,
8072
9154
  initJournalEncryption: () => initJournalEncryption,
9155
+ listCheckpoints: () => listCheckpoints,
9156
+ loadNamedAgents: () => loadNamedAgents,
8073
9157
  loadProjectConfig: () => loadProjectConfig,
8074
9158
  loadSettings: () => loadSettings,
9159
+ manifestPath: () => manifestPath,
9160
+ normalizeToolName: () => normalizeToolName,
9161
+ openTurn: () => openTurn,
9162
+ parseAgentFile: () => parseAgentFile,
9163
+ parseToolsValue: () => parseToolsValue,
9164
+ recordBackup: () => recordBackup,
9165
+ restoreCheckpoint: () => restoreCheckpoint,
8075
9166
  takeSnapshot: () => takeSnapshot
8076
9167
  });
8077
9168
  var init_src3 = __esm({
@@ -8080,9 +9171,11 @@ var init_src3 = __esm({
8080
9171
  init_esm_shims();
8081
9172
  init_ToolExecutor();
8082
9173
  init_Agent();
9174
+ init_namedAgents();
8083
9175
  init_HardwareMonitor();
8084
9176
  init_TrustStore();
8085
9177
  init_ContextBudget();
9178
+ init_BackupIndex();
8086
9179
  init_Storage();
8087
9180
  init_Storage();
8088
9181
  init_journalCrypto();
@@ -8099,7 +9192,10 @@ var init_src3 = __esm({
8099
9192
  init_TodoTools();
8100
9193
  init_DispatchAgentTool();
8101
9194
  init_WebFetchTool();
9195
+ init_WebSearchTool();
9196
+ init_PlanModeTools();
8102
9197
  init_BashTool();
9198
+ init_BackgroundShellTool();
8103
9199
  init_NotebookReadTool();
8104
9200
  init_NotebookEditTool();
8105
9201
  init_MultiEditFileTool();
@@ -8124,7 +9220,7 @@ function setRawModeGuarded(stdin, mode) {
8124
9220
  }
8125
9221
  }
8126
9222
  async function promptPassword(prompt4) {
8127
- return new Promise((resolve20) => {
9223
+ return new Promise((resolve21) => {
8128
9224
  const stdin = process.stdin;
8129
9225
  const stdout = process.stdout;
8130
9226
  stdout.write(prompt4);
@@ -8138,7 +9234,7 @@ async function promptPassword(prompt4) {
8138
9234
  setRawModeGuarded(stdin, wasRaw);
8139
9235
  stdin.removeListener("data", onData);
8140
9236
  stdout.write("\n");
8141
- resolve20(value);
9237
+ resolve21(value);
8142
9238
  };
8143
9239
  const onData = (chunk) => {
8144
9240
  try {
@@ -8240,7 +9336,7 @@ async function loginWithGithubDevice(context) {
8240
9336
  const deadline = Date.now() + expires_in * 1e3;
8241
9337
  let githubToken = null;
8242
9338
  while (Date.now() < deadline) {
8243
- await new Promise((resolve20) => setTimeout(resolve20, pollMs));
9339
+ await new Promise((resolve21) => setTimeout(resolve21, pollMs));
8244
9340
  let tokenResp;
8245
9341
  try {
8246
9342
  tokenResp = await fetch(GITHUB_TOKEN_URL, {
@@ -8264,7 +9360,7 @@ async function loginWithGithubDevice(context) {
8264
9360
  if (tokenData.error === "authorization_pending") continue;
8265
9361
  if (tokenData.error === "slow_down") {
8266
9362
  pollMs += 5e3;
8267
- await new Promise((resolve20) => setTimeout(resolve20, 5e3));
9363
+ await new Promise((resolve21) => setTimeout(resolve21, 5e3));
8268
9364
  continue;
8269
9365
  }
8270
9366
  context.addMessage("system", `GitHub auth error: ${tokenData.error_description || tokenData.error}`);
@@ -8609,9 +9705,9 @@ var init_unlock = __esm({
8609
9705
  });
8610
9706
 
8611
9707
  // src/commands/doctor.ts
8612
- import { homedir as homedir12 } from "os";
8613
- import { join as join23 } from "path";
8614
- import { existsSync as existsSync19 } from "fs";
9708
+ import { homedir as homedir14 } from "os";
9709
+ import { join as join25 } from "path";
9710
+ import { existsSync as existsSync20 } from "fs";
8615
9711
  import { readFile as readFile18 } from "fs/promises";
8616
9712
  async function checkApiHealth(client) {
8617
9713
  try {
@@ -8637,9 +9733,9 @@ async function checkAuthStatus(client) {
8637
9733
  }
8638
9734
  }
8639
9735
  async function checkSettingsFile() {
8640
- const settingsPath = join23(homedir12(), ".msapling", "settings.json");
9736
+ const settingsPath = join25(homedir14(), ".msapling", "settings.json");
8641
9737
  try {
8642
- if (!existsSync19(settingsPath)) {
9738
+ if (!existsSync20(settingsPath)) {
8643
9739
  return { ok: false, message: `Not found: ${settingsPath}` };
8644
9740
  }
8645
9741
  const text = await readFile18(settingsPath, "utf8");
@@ -9037,6 +10133,104 @@ var init_chat = __esm({
9037
10133
  }
9038
10134
  });
9039
10135
 
10136
+ // src/commands/resume.ts
10137
+ var resume_exports = {};
10138
+ __export(resume_exports, {
10139
+ rehydrateChat: () => rehydrateChat,
10140
+ resolveRecentChat: () => resolveRecentChat,
10141
+ resumeCommand: () => resumeCommand
10142
+ });
10143
+ function labelFor2(i) {
10144
+ return i < 9 ? String(i + 1) : LETTERS2[i - 9] ?? `#${i + 1}`;
10145
+ }
10146
+ function resolveRecentChat(arg, chats) {
10147
+ const trimmed = arg.trim();
10148
+ if (!trimmed) return { kind: "error", message: "empty argument" };
10149
+ const idHit = chats.find((c) => c.id === trimmed);
10150
+ if (idHit) return { kind: "ok", chat: idHit };
10151
+ if (/^\d+$/.test(trimmed)) {
10152
+ const n = parseInt(trimmed, 10);
10153
+ if (n >= 1 && n <= chats.length) return { kind: "ok", chat: chats[n - 1] };
10154
+ return { kind: "error", message: `no chat at index ${n} (have ${chats.length})` };
10155
+ }
10156
+ if (/^[a-zA-Z]$/.test(trimmed)) {
10157
+ const idx = 9 + LETTERS2.indexOf(trimmed.toLowerCase());
10158
+ if (idx >= 9 && idx < chats.length) return { kind: "ok", chat: chats[idx] };
10159
+ }
10160
+ return {
10161
+ kind: "error",
10162
+ message: `no recent chat matches '${trimmed}'. Run /resume (no args) for the list.`
10163
+ };
10164
+ }
10165
+ async function rehydrateChat(chat, context) {
10166
+ context.clearHistory();
10167
+ context.setActiveChatId(chat.id);
10168
+ let messages = [];
10169
+ try {
10170
+ messages = await context.client.getHistory(chat.id);
10171
+ } catch (e) {
10172
+ context.addMessage("error", `Resumed ${chat.id} but failed to load history: ${e?.message ?? e}`);
10173
+ }
10174
+ const label = chat.title ?? chat.id;
10175
+ const where = chat.project ? ` in '${chat.project}'` : "";
10176
+ context.addMessage("system", `Resumed chat "${label}"${where} (${messages.length} message(s) replayed).`);
10177
+ for (const m of messages) {
10178
+ const role = m.role === "user" ? "user" : m.role === "assistant" ? "assistant" : "system";
10179
+ context.addMessage(role, m.content);
10180
+ }
10181
+ try {
10182
+ await context.refreshOverview();
10183
+ } catch {
10184
+ }
10185
+ return messages.length;
10186
+ }
10187
+ async function listAndResume(args2, context) {
10188
+ let chats;
10189
+ try {
10190
+ chats = await context.client.listRecentChats(35);
10191
+ } catch (e) {
10192
+ context.addMessage("error", `Failed to list recent chats: ${e?.message ?? e}`);
10193
+ return;
10194
+ }
10195
+ if (chats.length === 0) {
10196
+ context.addMessage("system", "No prior chats found. Send a message to start one.");
10197
+ return;
10198
+ }
10199
+ const arg = args2.join(" ").trim();
10200
+ if (!arg) {
10201
+ context.addMessage("system", "Recent chats \u2014 /resume <number|letter|id> to reopen:");
10202
+ chats.forEach((c, i) => {
10203
+ const star = c.id === context.activeChatId ? " \u2605" : "";
10204
+ const project = c.project ? ` (${c.project})` : "";
10205
+ const model = c.model ? ` [${c.model}]` : "";
10206
+ context.addMessage("system", ` ${labelFor2(i)}. ${c.title ?? c.id}${project}${model}${star}`);
10207
+ });
10208
+ return;
10209
+ }
10210
+ const resolved = resolveRecentChat(arg, chats);
10211
+ if (resolved.kind === "error") {
10212
+ context.addMessage("error", resolved.message);
10213
+ return;
10214
+ }
10215
+ await rehydrateChat(resolved.chat, context);
10216
+ }
10217
+ var LETTERS2, resumeCommand;
10218
+ var init_resume = __esm({
10219
+ "src/commands/resume.ts"() {
10220
+ "use strict";
10221
+ init_esm_shims();
10222
+ LETTERS2 = "abcdefghijklmnopqrstuvwxyz";
10223
+ resumeCommand = {
10224
+ name: "resume",
10225
+ aliases: ["continue"],
10226
+ args: "[number|letter|id]",
10227
+ description: "List recent chats and reopen one \u2014 replays its history into the REPL",
10228
+ category: "chat",
10229
+ handler: async (args2, context) => listAndResume(args2, context)
10230
+ };
10231
+ }
10232
+ });
10233
+
9040
10234
  // src/commands/broadcast.ts
9041
10235
  var broadcastCommand;
9042
10236
  var init_broadcast = __esm({
@@ -9216,7 +10410,7 @@ function setRawModeGuarded2(stdin, mode) {
9216
10410
  }
9217
10411
  }
9218
10412
  async function promptSecret(prompt4) {
9219
- return new Promise((resolve20) => {
10413
+ return new Promise((resolve21) => {
9220
10414
  const stdin = process.stdin;
9221
10415
  const stdout = process.stdout;
9222
10416
  stdout.write(prompt4);
@@ -9229,12 +10423,12 @@ async function promptSecret(prompt4) {
9229
10423
  setRawModeGuarded2(stdin, wasRaw);
9230
10424
  stdin.removeListener("data", onData);
9231
10425
  stdout.write("\n");
9232
- resolve20(secret);
10426
+ resolve21(secret);
9233
10427
  } else if (char === "") {
9234
10428
  setRawModeGuarded2(stdin, wasRaw);
9235
10429
  stdin.removeListener("data", onData);
9236
10430
  stdout.write("\n");
9237
- resolve20("");
10431
+ resolve21("");
9238
10432
  } else if (char === "\x7F" || char === "\b") {
9239
10433
  secret = secret.slice(0, -1);
9240
10434
  } else if (char >= " " && char <= "~") {
@@ -9394,8 +10588,8 @@ var init_memories = __esm({
9394
10588
 
9395
10589
  // src/commands/mdrive.ts
9396
10590
  import { readFile as readFile19, writeFile as writeFile11 } from "fs/promises";
9397
- import { existsSync as existsSync20 } from "fs";
9398
- import { basename as basename2, resolve as resolve14, relative as relative14, isAbsolute as isAbsolute14, sep as sep3 } from "path";
10591
+ import { existsSync as existsSync21 } from "fs";
10592
+ import { basename as basename2, resolve as resolve15, relative as relative15, isAbsolute as isAbsolute15, sep as sep3 } from "path";
9399
10593
  function formatBytes(b) {
9400
10594
  if (!b) return "0";
9401
10595
  if (b < 1024) return `${b}`;
@@ -9403,10 +10597,10 @@ function formatBytes(b) {
9403
10597
  return `${(b / 1024 / 1024).toFixed(1)}M`;
9404
10598
  }
9405
10599
  function containLocalPath(targetPath, root = process.cwd()) {
9406
- const resolvedRoot = resolve14(root);
9407
- const resolved = isAbsolute14(targetPath) ? resolve14(targetPath) : resolve14(resolvedRoot, targetPath);
9408
- const rel = relative14(resolvedRoot, resolved);
9409
- if (rel === ".." || rel.startsWith(`..${sep3}`) || rel.startsWith("../") || isAbsolute14(rel)) {
10600
+ const resolvedRoot = resolve15(root);
10601
+ const resolved = isAbsolute15(targetPath) ? resolve15(targetPath) : resolve15(resolvedRoot, targetPath);
10602
+ const rel = relative15(resolvedRoot, resolved);
10603
+ if (rel === ".." || rel.startsWith(`..${sep3}`) || rel.startsWith("../") || isAbsolute15(rel)) {
9410
10604
  return null;
9411
10605
  }
9412
10606
  return resolved;
@@ -9487,7 +10681,7 @@ var init_mdrive = __esm({
9487
10681
  context.addMessage("error", `Refusing to read local file outside the working directory: ${local}`);
9488
10682
  return;
9489
10683
  }
9490
- if (!existsSync20(absLocal)) {
10684
+ if (!existsSync21(absLocal)) {
9491
10685
  context.addMessage("error", `Local file not found: ${absLocal}`);
9492
10686
  return;
9493
10687
  }
@@ -9608,14 +10802,14 @@ var init_clear = __esm({
9608
10802
  });
9609
10803
 
9610
10804
  // src/commands/mode.ts
9611
- import { homedir as homedir13 } from "os";
9612
- import { join as join24 } from "path";
9613
- import { existsSync as existsSync21 } from "fs";
10805
+ import { homedir as homedir15 } from "os";
10806
+ import { join as join26 } from "path";
10807
+ import { existsSync as existsSync22 } from "fs";
9614
10808
  import { readFile as readFile20, writeFile as writeFile12, mkdir as mkdir7 } from "fs/promises";
9615
10809
  async function persistApprovalMode(mode, ttlMs) {
9616
10810
  try {
9617
10811
  let existing = {};
9618
- if (existsSync21(SETTINGS_PATH)) {
10812
+ if (existsSync22(SETTINGS_PATH)) {
9619
10813
  const text = await readFile20(SETTINGS_PATH, "utf8");
9620
10814
  if (text.trim()) {
9621
10815
  existing = JSON.parse(text);
@@ -9627,8 +10821,8 @@ async function persistApprovalMode(mode, ttlMs) {
9627
10821
  ...ttlMs && { ttlMs }
9628
10822
  };
9629
10823
  existing.approvalMode = entry;
9630
- const settingsDir = join24(homedir13(), ".msapling");
9631
- if (!existsSync21(settingsDir)) {
10824
+ const settingsDir = join26(homedir15(), ".msapling");
10825
+ if (!existsSync22(settingsDir)) {
9632
10826
  await mkdir7(settingsDir, { recursive: true });
9633
10827
  }
9634
10828
  await writeFile12(SETTINGS_PATH, JSON.stringify(existing, null, 2), "utf8");
@@ -9640,7 +10834,7 @@ var init_mode = __esm({
9640
10834
  "src/commands/mode.ts"() {
9641
10835
  "use strict";
9642
10836
  init_esm_shims();
9643
- SETTINGS_PATH = join24(homedir13(), ".msapling", "settings.json");
10837
+ SETTINGS_PATH = join26(homedir15(), ".msapling", "settings.json");
9644
10838
  modeCommand = {
9645
10839
  name: "mode",
9646
10840
  args: "[default|plan|acceptEdits|bypassPermissions] [...options]",
@@ -9913,8 +11107,8 @@ var init_memory = __esm({
9913
11107
  });
9914
11108
 
9915
11109
  // src/commands/project.ts
9916
- function labelFor2(i) {
9917
- return i < 9 ? String(i + 1) : LETTERS2[i - 9] ?? `#${i + 1}`;
11110
+ function labelFor3(i) {
11111
+ return i < 9 ? String(i + 1) : LETTERS3[i - 9] ?? `#${i + 1}`;
9918
11112
  }
9919
11113
  function resolveProject(arg, list2) {
9920
11114
  const trimmed = arg.trim();
@@ -9927,7 +11121,7 @@ function resolveProject(arg, list2) {
9927
11121
  return { kind: "error", message: `no project at index ${n} (have ${list2.length})` };
9928
11122
  }
9929
11123
  if (/^[a-zA-Z]$/.test(trimmed)) {
9930
- const idx = 9 + LETTERS2.indexOf(trimmed.toLowerCase());
11124
+ const idx = 9 + LETTERS3.indexOf(trimmed.toLowerCase());
9931
11125
  if (idx >= 9 && idx < list2.length) return { kind: "ok", id: list2[idx].id };
9932
11126
  }
9933
11127
  const lower = trimmed.toLowerCase();
@@ -9941,12 +11135,12 @@ function resolveProject(arg, list2) {
9941
11135
  }
9942
11136
  return { kind: "error", message: `no project matches '${trimmed}'. Run /project (no args) for the list.` };
9943
11137
  }
9944
- var LETTERS2, projectCommand;
11138
+ var LETTERS3, projectCommand;
9945
11139
  var init_project = __esm({
9946
11140
  "src/commands/project.ts"() {
9947
11141
  "use strict";
9948
11142
  init_esm_shims();
9949
- LETTERS2 = "abcdefghijklmnopqrstuvwxyz";
11143
+ LETTERS3 = "abcdefghijklmnopqrstuvwxyz";
9950
11144
  projectCommand = {
9951
11145
  name: "project",
9952
11146
  args: "[number|letter|name|id|config]",
@@ -10005,7 +11199,7 @@ var init_project = __esm({
10005
11199
  }
10006
11200
  context.addMessage("system", "Available Projects:");
10007
11201
  projects.forEach((p, i) => {
10008
- const label = labelFor2(i);
11202
+ const label = labelFor3(i);
10009
11203
  const star = p.id === currentId ? " \u2605" : "";
10010
11204
  const usage = p.chat_count != null && p.chat_limit != null ? ` (${p.chat_count}/${p.chat_limit} chats)` : "";
10011
11205
  context.addMessage("system", ` ${label}. ${p.name ?? p.id}${usage}${star}`);
@@ -10062,8 +11256,8 @@ var init_compact = __esm({
10062
11256
  });
10063
11257
 
10064
11258
  // src/commands/init.ts
10065
- import { join as join25 } from "path";
10066
- import { existsSync as existsSync22 } from "fs";
11259
+ import { join as join27 } from "path";
11260
+ import { existsSync as existsSync23 } from "fs";
10067
11261
  import { writeFile as writeFile13 } from "fs/promises";
10068
11262
  var initCommand;
10069
11263
  var init_init = __esm({
@@ -10077,8 +11271,8 @@ var init_init = __esm({
10077
11271
  handler: async (args2, context) => {
10078
11272
  try {
10079
11273
  const cwd = process.cwd();
10080
- const path2 = join25(cwd, "MSAPLING.md");
10081
- if (existsSync22(path2)) {
11274
+ const path2 = join27(cwd, "MSAPLING.md");
11275
+ if (existsSync23(path2)) {
10082
11276
  context.addMessage("error", "MSAPLING.md already exists in current directory.");
10083
11277
  return;
10084
11278
  }
@@ -10104,7 +11298,7 @@ var init_init = __esm({
10104
11298
  });
10105
11299
 
10106
11300
  // src/commands/review.ts
10107
- import { existsSync as existsSync23 } from "fs";
11301
+ import { existsSync as existsSync24 } from "fs";
10108
11302
  import { readFile as readFile21 } from "fs/promises";
10109
11303
  var reviewCommand;
10110
11304
  var init_review = __esm({
@@ -10124,7 +11318,7 @@ var init_review = __esm({
10124
11318
  }
10125
11319
  let content = "";
10126
11320
  try {
10127
- if (existsSync23(target)) {
11321
+ if (existsSync24(target)) {
10128
11322
  content = await readFile21(target, "utf8");
10129
11323
  } else {
10130
11324
  content = `Review target: ${target}`;
@@ -10218,15 +11412,15 @@ var init_swarm = __esm({
10218
11412
 
10219
11413
  // src/commands/recipe.ts
10220
11414
  import { parse as parseYaml } from "yaml";
10221
- import { existsSync as existsSync24 } from "fs";
11415
+ import { existsSync as existsSync25 } from "fs";
10222
11416
  import { readFile as readFile22 } from "fs/promises";
10223
- import { join as join26 } from "path";
11417
+ import { join as join28 } from "path";
10224
11418
  function findRecipe(name, cwd) {
10225
11419
  for (const dir of RECIPE_DIRS) {
10226
11420
  for (const suffix of NAME_SUFFIXES) {
10227
11421
  for (const ext of FILE_EXTS) {
10228
- const p = join26(cwd, dir, `${name}${suffix}${ext}`);
10229
- if (existsSync24(p)) return p;
11422
+ const p = join28(cwd, dir, `${name}${suffix}${ext}`);
11423
+ if (existsSync25(p)) return p;
10230
11424
  }
10231
11425
  }
10232
11426
  }
@@ -10339,13 +11533,13 @@ ${rendered}` : rendered;
10339
11533
  });
10340
11534
 
10341
11535
  // src/commands/skill.ts
10342
- import { existsSync as existsSync25, readdirSync as readdirSync2, statSync as statSync5 } from "fs";
11536
+ import { existsSync as existsSync26, readdirSync as readdirSync3, statSync as statSync7 } from "fs";
10343
11537
  import { readFile as readFile23 } from "fs/promises";
10344
- import { join as join27, resolve as resolve15 } from "path";
11538
+ import { join as join29, resolve as resolve16 } from "path";
10345
11539
  function findSkillsRoot(cwd) {
10346
11540
  for (const candidate of SKILLS_DIRS) {
10347
- const full = resolve15(cwd, candidate);
10348
- if (existsSync25(full) && statSync5(full).isDirectory()) return full;
11541
+ const full = resolve16(cwd, candidate);
11542
+ if (existsSync26(full) && statSync7(full).isDirectory()) return full;
10349
11543
  }
10350
11544
  return null;
10351
11545
  }
@@ -10353,28 +11547,28 @@ function listAllSkills(root) {
10353
11547
  const out = [];
10354
11548
  let domains;
10355
11549
  try {
10356
- domains = readdirSync2(root);
11550
+ domains = readdirSync3(root);
10357
11551
  } catch {
10358
11552
  return out;
10359
11553
  }
10360
11554
  for (const domain of domains) {
10361
- const dir = join27(root, domain);
11555
+ const dir = join29(root, domain);
10362
11556
  let s;
10363
11557
  try {
10364
- s = statSync5(dir);
11558
+ s = statSync7(dir);
10365
11559
  } catch {
10366
11560
  continue;
10367
11561
  }
10368
11562
  if (!s.isDirectory()) continue;
10369
11563
  let files;
10370
11564
  try {
10371
- files = readdirSync2(dir);
11565
+ files = readdirSync3(dir);
10372
11566
  } catch {
10373
11567
  continue;
10374
11568
  }
10375
11569
  for (const f of files) {
10376
11570
  if (!f.endsWith(".md")) continue;
10377
- out.push({ domain, name: f.slice(0, -3), path: join27(dir, f) });
11571
+ out.push({ domain, name: f.slice(0, -3), path: join29(dir, f) });
10378
11572
  }
10379
11573
  }
10380
11574
  return out.sort(
@@ -10463,9 +11657,9 @@ ${prompt4}`;
10463
11657
  });
10464
11658
 
10465
11659
  // src/commands/benchmark.ts
10466
- import { homedir as homedir14 } from "os";
10467
- import { join as join28 } from "path";
10468
- import { mkdirSync as mkdirSync4 } from "fs";
11660
+ import { homedir as homedir16 } from "os";
11661
+ import { join as join30 } from "path";
11662
+ import { mkdirSync as mkdirSync5 } from "fs";
10469
11663
  import * as fs2 from "fs";
10470
11664
  function parseArgs(args2) {
10471
11665
  let models = null;
@@ -10584,10 +11778,10 @@ HW at start: ${hw.cores}-core ${hw.platform} | CPU ${hw.cpuPct}% | RAM ${hw.ramP
10584
11778
  `[HW at run time: CPU ${hwAtEnd.cpuPct}% / RAM ${hwAtEnd.ramPct}% | ${hw.ramGiB} GiB RAM, ${hw.cores} cores]`
10585
11779
  );
10586
11780
  try {
10587
- const dir = join28(homedir14(), ".msapling", "benchmarks");
10588
- mkdirSync4(dir, { recursive: true });
11781
+ const dir = join30(homedir16(), ".msapling", "benchmarks");
11782
+ mkdirSync5(dir, { recursive: true });
10589
11783
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 16);
10590
- const file = join28(dir, `${ts}.json`);
11784
+ const file = join30(dir, `${ts}.json`);
10591
11785
  const run = {
10592
11786
  ts: (/* @__PURE__ */ new Date()).toISOString(),
10593
11787
  rounds,
@@ -10616,7 +11810,7 @@ var init_hooks = __esm({
10616
11810
  init_src3();
10617
11811
  hooksCommand = {
10618
11812
  name: "hooks",
10619
- description: "List configured lifecycle hooks (pre-tool-use, post-tool-use, user-prompt-submit)",
11813
+ description: "List configured lifecycle hooks (pre/post-tool-use, user-prompt-submit, session-start/end, stop, subagent-stop, pre-compact, notification)",
10620
11814
  category: "debug",
10621
11815
  handler: async (args2, context) => {
10622
11816
  const sub = args2[0]?.toLowerCase();
@@ -10833,22 +12027,22 @@ var init_theme = __esm({
10833
12027
  });
10834
12028
 
10835
12029
  // src/commands/theme.ts
10836
- import { join as join29 } from "path";
10837
- import { homedir as homedir15 } from "os";
10838
- import { existsSync as existsSync26 } from "fs";
12030
+ import { join as join31 } from "path";
12031
+ import { homedir as homedir17 } from "os";
12032
+ import { existsSync as existsSync27 } from "fs";
10839
12033
  import { readFile as readFile24, writeFile as writeFile14 } from "fs/promises";
10840
12034
  async function persistTheme(storage, themeName) {
10841
- const settingsPath = join29(homedir15(), ".msapling", "settings.json");
12035
+ const settingsPath = join31(homedir17(), ".msapling", "settings.json");
10842
12036
  let existing = {};
10843
12037
  try {
10844
- if (existsSync26(settingsPath)) {
12038
+ if (existsSync27(settingsPath)) {
10845
12039
  const text = await readFile24(settingsPath, "utf8");
10846
12040
  if (text.trim()) existing = JSON.parse(text);
10847
12041
  }
10848
12042
  } catch {
10849
12043
  }
10850
12044
  existing["theme"] = themeName;
10851
- ensureConfigDir(join29(homedir15(), ".msapling"));
12045
+ ensureConfigDir(join31(homedir17(), ".msapling"));
10852
12046
  await writeFile14(settingsPath, JSON.stringify(existing, null, 2), "utf8");
10853
12047
  }
10854
12048
  var VALID_THEMES, themeCommand;
@@ -10920,7 +12114,7 @@ var init_version = __esm({
10920
12114
  description: "Show version information for CLI and core packages",
10921
12115
  category: "debug",
10922
12116
  handler: async (_args, context) => {
10923
- const cliVersion = true ? "2.3.6-beta.45" : "(dev)";
12117
+ const cliVersion = true ? "2.3.6-beta.47" : "(dev)";
10924
12118
  const coreVersion = true ? "2.3.6-beta.43" : "(dev)";
10925
12119
  const runtime = process.version;
10926
12120
  context.addMessage("system", "MSapling Version Info");
@@ -10929,7 +12123,7 @@ var init_version = __esm({
10929
12123
  context.addMessage("system", row2("Core (@msapling/core)", coreVersion));
10930
12124
  context.addMessage("system", row2("Runtime (Node/Bun)", runtime));
10931
12125
  try {
10932
- const ts = "2026-06-20T07:16:00.342Z";
12126
+ const ts = "2026-06-20T08:11:43.445Z";
10933
12127
  if (ts && ts !== "__BUILD_TIMESTAMP__") {
10934
12128
  context.addMessage("system", row2("Build Timestamp", ts));
10935
12129
  }
@@ -10942,14 +12136,14 @@ var init_version = __esm({
10942
12136
  });
10943
12137
 
10944
12138
  // src/commands/feedback.ts
10945
- import { join as join30 } from "path";
10946
- import { existsSync as existsSync27 } from "fs";
12139
+ import { join as join32 } from "path";
12140
+ import { existsSync as existsSync28 } from "fs";
10947
12141
  import { readFile as readFile25 } from "fs/promises";
10948
12142
  async function readCliVersion() {
10949
12143
  try {
10950
12144
  const baseDir = typeof __dirname !== "undefined" ? __dirname : process.cwd();
10951
- const pkgPath = join30(baseDir, "..", "..", "package.json");
10952
- if (!existsSync27(pkgPath)) return "unknown";
12145
+ const pkgPath = join32(baseDir, "..", "..", "package.json");
12146
+ if (!existsSync28(pkgPath)) return "unknown";
10953
12147
  const text = await readFile25(pkgPath, "utf8");
10954
12148
  const json = JSON.parse(text);
10955
12149
  return json.version ?? "unknown";
@@ -10990,8 +12184,8 @@ var init_feedback = __esm({
10990
12184
  });
10991
12185
 
10992
12186
  // src/commands/export.ts
10993
- import { homedir as homedir16 } from "os";
10994
- import { join as join31 } from "path";
12187
+ import { homedir as homedir18 } from "os";
12188
+ import { join as join33 } from "path";
10995
12189
  import { writeFile as writeFile15, mkdir as mkdir8 } from "fs/promises";
10996
12190
  function formatTimestamp(date) {
10997
12191
  return date.toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
@@ -11041,10 +12235,10 @@ var init_export = __esm({
11041
12235
  let outputPath;
11042
12236
  let content;
11043
12237
  if (arg === "" || arg === "json") {
11044
- outputPath = join31(homedir16(), `msapling-export-${timestamp}.json`);
12238
+ outputPath = join33(homedir18(), `msapling-export-${timestamp}.json`);
11045
12239
  content = buildJsonExport(history);
11046
12240
  } else if (arg === "markdown" || arg === "md") {
11047
- outputPath = join31(homedir16(), `msapling-export-${timestamp}.md`);
12241
+ outputPath = join33(homedir18(), `msapling-export-${timestamp}.md`);
11048
12242
  content = buildMarkdownExport(history);
11049
12243
  } else {
11050
12244
  outputPath = arg;
@@ -11056,7 +12250,7 @@ var init_export = __esm({
11056
12250
  }
11057
12251
  }
11058
12252
  try {
11059
- const dir = join31(outputPath, "..");
12253
+ const dir = join33(outputPath, "..");
11060
12254
  await mkdir8(dir, { recursive: true });
11061
12255
  await writeFile15(outputPath, content, "utf8");
11062
12256
  context.addMessage("system", `Exported to: ${outputPath}`);
@@ -11251,16 +12445,16 @@ var init_plan = __esm({
11251
12445
  });
11252
12446
 
11253
12447
  // src/commands/note.ts
11254
- import { homedir as homedir17 } from "os";
11255
- import { join as join32 } from "path";
11256
- import { existsSync as existsSync28 } from "fs";
12448
+ import { homedir as homedir19 } from "os";
12449
+ import { join as join34 } from "path";
12450
+ import { existsSync as existsSync29 } from "fs";
11257
12451
  import { readFile as readFile26, writeFile as writeFile16 } from "fs/promises";
11258
12452
  function getNotesFilePath() {
11259
- return join32(homedir17(), ".msapling", "notes.json");
12453
+ return join34(homedir19(), ".msapling", "notes.json");
11260
12454
  }
11261
12455
  async function readNotes(filePath = getNotesFilePath()) {
11262
12456
  try {
11263
- if (!existsSync28(filePath)) return [];
12457
+ if (!existsSync29(filePath)) return [];
11264
12458
  const raw = await readFile26(filePath, "utf8");
11265
12459
  const parsed = JSON.parse(raw);
11266
12460
  if (!Array.isArray(parsed)) return [];
@@ -11270,7 +12464,7 @@ async function readNotes(filePath = getNotesFilePath()) {
11270
12464
  }
11271
12465
  }
11272
12466
  async function writeNotes(notes, filePath = getNotesFilePath()) {
11273
- const dir = join32(homedir17(), ".msapling");
12467
+ const dir = join34(homedir19(), ".msapling");
11274
12468
  ensureConfigDir(dir);
11275
12469
  await writeFile16(filePath, JSON.stringify(notes, null, 2), "utf8");
11276
12470
  }
@@ -11416,17 +12610,17 @@ var init_todo = __esm({
11416
12610
  });
11417
12611
 
11418
12612
  // src/commands/outputStyle.ts
11419
- import { homedir as homedir18 } from "os";
11420
- import { join as join33, basename as basename3, extname as extname3 } from "path";
11421
- import { existsSync as existsSync29, mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync5 } from "fs";
12613
+ import { homedir as homedir20 } from "os";
12614
+ import { join as join35, basename as basename3, extname as extname3 } from "path";
12615
+ import { existsSync as existsSync30, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
11422
12616
  function resolveHome() {
11423
- return process.env.HOME || process.env.USERPROFILE || homedir18();
12617
+ return process.env.HOME || process.env.USERPROFILE || homedir20();
11424
12618
  }
11425
12619
  function stylesDir() {
11426
- return join33(resolveHome(), ".msapling", "output-styles");
12620
+ return join35(resolveHome(), ".msapling", "output-styles");
11427
12621
  }
11428
12622
  function activeFile() {
11429
- return join33(stylesDir(), ".active");
12623
+ return join35(stylesDir(), ".active");
11430
12624
  }
11431
12625
  function parseStyleFile(text) {
11432
12626
  const fm = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
@@ -11447,13 +12641,13 @@ function parseStyleFile(text) {
11447
12641
  }
11448
12642
  function listUserStyles() {
11449
12643
  const dir = stylesDir();
11450
- if (!existsSync29(dir)) return [];
12644
+ if (!existsSync30(dir)) return [];
11451
12645
  const out = [];
11452
- for (const entry of readdirSync3(dir)) {
12646
+ for (const entry of readdirSync4(dir)) {
11453
12647
  if (extname3(entry).toLowerCase() !== ".md") continue;
11454
- const full = join33(dir, entry);
12648
+ const full = join35(dir, entry);
11455
12649
  try {
11456
- const text = readFileSync2(full, "utf8");
12650
+ const text = readFileSync4(full, "utf8");
11457
12651
  const { description, body } = parseStyleFile(text);
11458
12652
  out.push({
11459
12653
  name: basename3(entry, ".md"),
@@ -11479,15 +12673,15 @@ function findStyle(name) {
11479
12673
  function getActiveStyleName() {
11480
12674
  try {
11481
12675
  const f = activeFile();
11482
- if (!existsSync29(f)) return "default";
11483
- return readFileSync2(f, "utf8").trim() || "default";
12676
+ if (!existsSync30(f)) return "default";
12677
+ return readFileSync4(f, "utf8").trim() || "default";
11484
12678
  } catch {
11485
12679
  return "default";
11486
12680
  }
11487
12681
  }
11488
12682
  function setActiveStyleName(name) {
11489
12683
  const dir = stylesDir();
11490
- if (!existsSync29(dir)) mkdirSync5(dir, { recursive: true });
12684
+ if (!existsSync30(dir)) mkdirSync6(dir, { recursive: true });
11491
12685
  writeFileSync5(activeFile(), `${name}
11492
12686
  `, "utf8");
11493
12687
  }
@@ -11500,8 +12694,8 @@ function createUserStyle(name, description, body) {
11500
12694
  throw new Error(`Invalid style name "${name}" \u2014 use letters, digits, _ and - only.`);
11501
12695
  }
11502
12696
  const dir = stylesDir();
11503
- if (!existsSync29(dir)) mkdirSync5(dir, { recursive: true });
11504
- const target = join33(dir, `${name}.md`);
12697
+ if (!existsSync30(dir)) mkdirSync6(dir, { recursive: true });
12698
+ const target = join35(dir, `${name}.md`);
11505
12699
  const frontmatter = `---
11506
12700
  description: ${description.replace(/\n/g, " ")}
11507
12701
  ---
@@ -12665,6 +13859,259 @@ Note: server may return a [server notice] caveat for v1-scaffold features.`
12665
13859
  }
12666
13860
  });
12667
13861
 
13862
+ // src/commands/rewind.ts
13863
+ var rewindCommand;
13864
+ var init_rewind = __esm({
13865
+ "src/commands/rewind.ts"() {
13866
+ "use strict";
13867
+ init_esm_shims();
13868
+ init_src3();
13869
+ rewindCommand = {
13870
+ name: "rewind",
13871
+ aliases: ["checkpoint", "restore"],
13872
+ args: "[n] [--confirm]",
13873
+ description: "List file-edit checkpoints or restore files to a prior snapshot. Usage: /rewind (list) \xB7 /rewind <n> (preview) \xB7 /rewind <n> --confirm (restore).",
13874
+ category: "chat",
13875
+ handler(args2, context) {
13876
+ const checkpoints = listCheckpoints();
13877
+ if (args2.length === 0) {
13878
+ if (checkpoints.length === 0) {
13879
+ context.addMessage(
13880
+ "system",
13881
+ "No checkpoints found. File edits made by the agent will be listed here."
13882
+ );
13883
+ return;
13884
+ }
13885
+ const lines = [
13886
+ `${checkpoints.length} checkpoint(s) \u2014 most recent first:`,
13887
+ ""
13888
+ ];
13889
+ checkpoints.forEach((cp, idx) => {
13890
+ const num = idx + 1;
13891
+ const fileCount = cp.backups.length;
13892
+ const when = new Date(cp.timestamp).toLocaleString();
13893
+ lines.push(
13894
+ ` ${num}. [${when}] ${cp.label} (${fileCount} file${fileCount !== 1 ? "s" : ""})`
13895
+ );
13896
+ });
13897
+ lines.push("");
13898
+ lines.push("To preview a restore: /rewind <n>");
13899
+ lines.push("To restore: /rewind <n> --confirm");
13900
+ context.addMessage("system", lines.join("\n"));
13901
+ return;
13902
+ }
13903
+ const positional = args2.filter((a) => !a.startsWith("-"));
13904
+ const hasConfirm = args2.includes("--confirm") || args2.includes("-y") || args2.includes("--yes");
13905
+ if (positional.length === 0) {
13906
+ context.addMessage("system", "Usage: /rewind <n> [--confirm]");
13907
+ return;
13908
+ }
13909
+ const rawN = positional[0];
13910
+ const n = parseInt(rawN, 10);
13911
+ if (isNaN(n) || n < 1) {
13912
+ context.addMessage(
13913
+ "error",
13914
+ `Invalid checkpoint number: "${rawN}". Run /rewind to list available checkpoints.`
13915
+ );
13916
+ return;
13917
+ }
13918
+ if (checkpoints.length === 0) {
13919
+ context.addMessage("system", "No checkpoints available to restore.");
13920
+ return;
13921
+ }
13922
+ if (n > checkpoints.length) {
13923
+ context.addMessage(
13924
+ "error",
13925
+ `Checkpoint #${n} does not exist. ${checkpoints.length} checkpoint(s) available. Run /rewind to list them.`
13926
+ );
13927
+ return;
13928
+ }
13929
+ const target = checkpoints[n - 1];
13930
+ if (!hasConfirm) {
13931
+ const when = new Date(target.timestamp).toLocaleString();
13932
+ const lines = [
13933
+ `Checkpoint #${n}: ${target.label} [${when}]`,
13934
+ ""
13935
+ ];
13936
+ if (target.backups.length === 0) {
13937
+ lines.push(" (no file edits recorded in this checkpoint)");
13938
+ } else {
13939
+ lines.push("Files that will be reverted:");
13940
+ for (const rec of target.backups) {
13941
+ lines.push(` - ${rec.originalPath}`);
13942
+ }
13943
+ }
13944
+ lines.push("");
13945
+ lines.push(`To restore these files, run: /rewind ${n} --confirm`);
13946
+ lines.push(
13947
+ "Note: only file contents are restored (chat history is NOT truncated)."
13948
+ );
13949
+ context.addMessage("system", lines.join("\n"));
13950
+ return;
13951
+ }
13952
+ if (target.backups.length === 0) {
13953
+ context.addMessage(
13954
+ "system",
13955
+ `Checkpoint #${n} has no file edits \u2014 nothing to restore.`
13956
+ );
13957
+ return;
13958
+ }
13959
+ const results = restoreCheckpoint(target);
13960
+ const restored = results.filter((r) => r.startsWith("Restored:"));
13961
+ const skipped = results.filter((r) => r.startsWith("Skip"));
13962
+ const errors = results.filter((r) => r.startsWith("Error"));
13963
+ const summary = [
13964
+ `Rewind to checkpoint #${n} complete.`,
13965
+ "",
13966
+ ...results
13967
+ ];
13968
+ if (errors.length > 0) {
13969
+ summary.push("");
13970
+ summary.push(
13971
+ `${errors.length} error(s) occurred. Check that the backup files in ~/.msapling/backups/ are accessible.`
13972
+ );
13973
+ }
13974
+ if (restored.length > 0 && errors.length === 0) {
13975
+ summary.push("");
13976
+ summary.push(
13977
+ `${restored.length} file(s) restored successfully. Note: chat history was NOT truncated (follow-up: REWIND-FOLLOW-UP-01).`
13978
+ );
13979
+ }
13980
+ context.addMessage("system", summary.join("\n"));
13981
+ }
13982
+ };
13983
+ }
13984
+ });
13985
+
13986
+ // src/commands/bashes.ts
13987
+ var bashesCommand;
13988
+ var init_bashes = __esm({
13989
+ "src/commands/bashes.ts"() {
13990
+ "use strict";
13991
+ init_esm_shims();
13992
+ init_src3();
13993
+ bashesCommand = {
13994
+ name: "bashes",
13995
+ aliases: ["bg", "background"],
13996
+ args: "[read <id> [n] | kill <id|all> | <id>]",
13997
+ description: "List, inspect, or kill background shells",
13998
+ category: "debug",
13999
+ handler(args2, context) {
14000
+ const sub = (args2[0] ?? "").toLowerCase();
14001
+ if (sub === "kill") {
14002
+ const target = args2[1] ?? "";
14003
+ if (!target) {
14004
+ context.addMessage("error", "Usage: /bashes kill <id|all>");
14005
+ return;
14006
+ }
14007
+ if (target.toLowerCase() === "all") {
14008
+ const running = BackgroundShellRegistry.list().filter((s) => s.status === "running");
14009
+ BackgroundShellRegistry.killAll();
14010
+ context.addMessage("system", `Killed ${running.length} running background shell(s).`);
14011
+ return;
14012
+ }
14013
+ const result = BackgroundShellRegistry.kill(target);
14014
+ if (result === "killed") context.addMessage("system", `Killed background shell ${target}.`);
14015
+ else if (result === "already")
14016
+ context.addMessage("system", `Background shell ${target} had already finished.`);
14017
+ else context.addMessage("error", `No background shell with id "${target}".`);
14018
+ return;
14019
+ }
14020
+ const explicitRead = sub === "read";
14021
+ const id = explicitRead ? args2[1] : args2[0];
14022
+ if (id) {
14023
+ const lineArg = explicitRead ? args2[2] : args2[1];
14024
+ const lines = lineArg && /^\d+$/.test(lineArg) ? parseInt(lineArg, 10) : void 0;
14025
+ const read = BackgroundShellRegistry.readOutput(id, lines);
14026
+ const info = BackgroundShellRegistry.get(id);
14027
+ if (!read || !info) {
14028
+ context.addMessage("error", `No background shell with id "${id}".`);
14029
+ return;
14030
+ }
14031
+ const header = `[${info.id}] ${info.status.toUpperCase()}` + (info.exitCode !== void 0 && info.exitCode !== null ? ` exit=${info.exitCode}` : "") + ` $ ${info.command}
14032
+ `;
14033
+ const body = read.output.trim() === "" ? "(no output yet)" : read.output;
14034
+ const trunc = read.truncated ? "\n... [output buffer truncated to most recent 256 KB]" : "";
14035
+ context.addMessage("system", header + body + trunc);
14036
+ return;
14037
+ }
14038
+ context.addMessage("system", formatBackgroundShells(BackgroundShellRegistry.list()));
14039
+ }
14040
+ };
14041
+ }
14042
+ });
14043
+
14044
+ // src/commands/agents.ts
14045
+ import { homedir as homedir21 } from "os";
14046
+ function resolveHome2() {
14047
+ return process.env.HOME || process.env.USERPROFILE || homedir21();
14048
+ }
14049
+ function describeTools(agent) {
14050
+ if (agent.tools === void 0) return "all (inherits full tool set)";
14051
+ if (agent.tools.length === 0) return "none (read/reason only)";
14052
+ return agent.tools.join(", ");
14053
+ }
14054
+ var agentsCommand;
14055
+ var init_agents = __esm({
14056
+ "src/commands/agents.ts"() {
14057
+ "use strict";
14058
+ init_esm_shims();
14059
+ init_src3();
14060
+ agentsCommand = {
14061
+ name: "agents",
14062
+ aliases: ["agent"],
14063
+ args: "[name]",
14064
+ description: "List named subagents (.msapling/agents/*.md), or show one. Named agents have their own system prompt, model, and tool allowlist.",
14065
+ category: "swarm",
14066
+ handler: (args2, context) => {
14067
+ const cwd = process.cwd();
14068
+ const home = resolveHome2();
14069
+ if (args2.length >= 1 && args2[0] !== "list" && args2[0] !== "ls") {
14070
+ const ref = args2[0];
14071
+ const agent = findNamedAgent(ref, cwd, home);
14072
+ if (!agent) {
14073
+ context.addMessage(
14074
+ "error",
14075
+ `No named agent "${ref}". Run /agents (no args) to list available agents.`
14076
+ );
14077
+ return;
14078
+ }
14079
+ context.addMessage("system", `Agent: ${agent.name} [${agent.scope}]`);
14080
+ context.addMessage("system", ` description: ${agent.description}`);
14081
+ context.addMessage("system", ` model: ${agent.model ?? "(inherit session model)"}`);
14082
+ context.addMessage("system", ` tools: ${describeTools(agent)}`);
14083
+ context.addMessage("system", ` source: ${agent.path}`);
14084
+ context.addMessage("system", " \u2500\u2500\u2500 system prompt \u2500\u2500\u2500");
14085
+ context.addMessage("system", agent.systemPrompt || "(empty)");
14086
+ context.addMessage(
14087
+ "system",
14088
+ `Run it: the model can call dispatch_agent with agent="${agent.name}".`
14089
+ );
14090
+ return;
14091
+ }
14092
+ const agents = loadNamedAgents(cwd, home);
14093
+ if (agents.length === 0) {
14094
+ context.addMessage(
14095
+ "system",
14096
+ "No named agents found. Create one at .msapling/agents/<name>.md (project) or ~/.msapling/agents/<name>.md (global). Frontmatter: name, description, model (optional), tools (optional allowlist); body = the system prompt."
14097
+ );
14098
+ return;
14099
+ }
14100
+ context.addMessage("system", `Named subagents (${agents.length}):`);
14101
+ for (const a of agents) {
14102
+ const model = a.model ? ` model=${a.model}` : "";
14103
+ const toolCount = a.tools === void 0 ? "tools=all" : `tools=${a.tools.length}`;
14104
+ context.addMessage(
14105
+ "system",
14106
+ ` ${a.name.padEnd(20)} [${a.scope}] ${toolCount}${model} \u2014 ${a.description}`
14107
+ );
14108
+ }
14109
+ context.addMessage("system", "Run /agents <name> to see an agent in full.");
14110
+ }
14111
+ };
14112
+ }
14113
+ });
14114
+
12668
14115
  // src/commands/index.ts
12669
14116
  var commands_exports = {};
12670
14117
  __export(commands_exports, {
@@ -12689,6 +14136,7 @@ var init_commands = __esm({
12689
14136
  init_exit();
12690
14137
  init_help();
12691
14138
  init_chat();
14139
+ init_resume();
12692
14140
  init_broadcast();
12693
14141
  init_ollama();
12694
14142
  init_keys();
@@ -12735,6 +14183,9 @@ var init_commands = __esm({
12735
14183
  init_diag();
12736
14184
  init_parallel();
12737
14185
  init_remoteAgent();
14186
+ init_rewind();
14187
+ init_bashes();
14188
+ init_agents();
12738
14189
  commands = [
12739
14190
  loginCommand,
12740
14191
  logoutCommand,
@@ -12752,6 +14203,7 @@ var init_commands = __esm({
12752
14203
  helpCommand,
12753
14204
  chatCommand,
12754
14205
  chatsCommand,
14206
+ resumeCommand,
12755
14207
  broadcastCommand,
12756
14208
  ollamaCommand,
12757
14209
  keysCommand,
@@ -12797,7 +14249,10 @@ var init_commands = __esm({
12797
14249
  syncCommand,
12798
14250
  diagCommand,
12799
14251
  parallelCommand,
12800
- remoteAgentCommand
14252
+ remoteAgentCommand,
14253
+ rewindCommand,
14254
+ bashesCommand,
14255
+ agentsCommand
12801
14256
  ];
12802
14257
  }
12803
14258
  });
@@ -12867,15 +14322,15 @@ var exec_exports = {};
12867
14322
  __export(exec_exports, {
12868
14323
  runExec: () => runExec
12869
14324
  });
12870
- import { existsSync as existsSync32 } from "fs";
14325
+ import { existsSync as existsSync33 } from "fs";
12871
14326
  import { readFile as readFile29 } from "fs/promises";
12872
- import { homedir as homedir20 } from "os";
12873
- import { join as join35 } from "path";
14327
+ import { homedir as homedir23 } from "os";
14328
+ import { join as join37 } from "path";
12874
14329
  async function loadPersistedSettings() {
12875
14330
  const out = { mode: "default", theme: null };
12876
14331
  try {
12877
- const p = join35(homedir20(), ".msapling", "settings.json");
12878
- if (!existsSync32(p)) return out;
14332
+ const p = join37(homedir23(), ".msapling", "settings.json");
14333
+ if (!existsSync33(p)) return out;
12879
14334
  const raw = JSON.parse(await readFile29(p, "utf8"));
12880
14335
  const parsed = parseApprovalMode(raw, Date.now());
12881
14336
  if (parsed.kind === "ok") out.mode = parsed.mode;
@@ -13111,7 +14566,7 @@ var init_format = __esm({
13111
14566
  });
13112
14567
 
13113
14568
  // src/commands/billing/open-browser.ts
13114
- import { spawn as spawn10 } from "child_process";
14569
+ import { spawn as spawn11 } from "child_process";
13115
14570
  async function openBrowser(url) {
13116
14571
  const platform5 = process.platform;
13117
14572
  let cmd;
@@ -13126,13 +14581,13 @@ async function openBrowser(url) {
13126
14581
  cmd = "xdg-open";
13127
14582
  args2 = [url];
13128
14583
  }
13129
- return new Promise((resolve20) => {
14584
+ return new Promise((resolve21) => {
13130
14585
  try {
13131
- const child = spawn10(cmd, args2, { stdio: "ignore", detached: true });
14586
+ const child = spawn11(cmd, args2, { stdio: "ignore", detached: true });
13132
14587
  child.unref();
13133
14588
  } catch {
13134
14589
  }
13135
- resolve20();
14590
+ resolve21();
13136
14591
  });
13137
14592
  }
13138
14593
  var init_open_browser = __esm({
@@ -13222,7 +14677,7 @@ var init_checkout = __esm({
13222
14677
  // src/commands/billing/sub.ts
13223
14678
  import * as readline from "readline";
13224
14679
  function prompt(rl, question) {
13225
- return new Promise((resolve20) => rl.question(question, resolve20));
14680
+ return new Promise((resolve21) => rl.question(question, resolve21));
13226
14681
  }
13227
14682
  async function runSub(argv) {
13228
14683
  const subCmd = argv[0] ?? "";
@@ -13348,11 +14803,11 @@ var init_sub = __esm({
13348
14803
  // src/commands/billing/topup.ts
13349
14804
  import * as readline2 from "readline";
13350
14805
  function prompt2(rl, question) {
13351
- return new Promise((resolve20) => rl.question(question, resolve20));
14806
+ return new Promise((resolve21) => rl.question(question, resolve21));
13352
14807
  }
13353
14808
  function promptDefault(rl, question, defaultVal) {
13354
14809
  return new Promise(
13355
- (resolve20) => rl.question(`${question} [${defaultVal}]: `, (ans) => resolve20(ans.trim() || defaultVal))
14810
+ (resolve21) => rl.question(`${question} [${defaultVal}]: `, (ans) => resolve21(ans.trim() || defaultVal))
13356
14811
  );
13357
14812
  }
13358
14813
  async function runTopup(argv) {
@@ -13487,7 +14942,7 @@ var init_redeem = __esm({
13487
14942
  // src/commands/billing/gift.ts
13488
14943
  import * as readline3 from "readline";
13489
14944
  function prompt3(rl, question) {
13490
- return new Promise((resolve20) => rl.question(question, resolve20));
14945
+ return new Promise((resolve21) => rl.question(question, resolve21));
13491
14946
  }
13492
14947
  async function runGift(argv) {
13493
14948
  const subCmd = argv[0] ?? "";
@@ -13733,11 +15188,11 @@ var doctor_exports = {};
13733
15188
  __export(doctor_exports, {
13734
15189
  runDoctor: () => runDoctor
13735
15190
  });
13736
- import { homedir as homedir21, platform as platform4, tmpdir } from "os";
13737
- import { join as join36 } from "path";
13738
- import { existsSync as existsSync33, statSync as statSync6 } from "fs";
15191
+ import { homedir as homedir24, platform as platform4, tmpdir } from "os";
15192
+ import { join as join38 } from "path";
15193
+ import { existsSync as existsSync34, statSync as statSync8 } from "fs";
13739
15194
  import { readdir as readdir3, mkdir as mkdir9, rm as rm3 } from "fs/promises";
13740
- import { exec } from "child_process";
15195
+ import { exec as exec2 } from "child_process";
13741
15196
  import { promisify } from "util";
13742
15197
  async function checkNodeVersion() {
13743
15198
  const version = process.version;
@@ -13758,8 +15213,8 @@ async function checkNodeVersion() {
13758
15213
  };
13759
15214
  }
13760
15215
  async function checkConfigDir() {
13761
- const configDir = join36(homedir21(), ".msapling");
13762
- if (!existsSync33(configDir)) {
15216
+ const configDir = join38(homedir24(), ".msapling");
15217
+ if (!existsSync34(configDir)) {
13763
15218
  return {
13764
15219
  name: "Config directory",
13765
15220
  status: "WARN",
@@ -13767,7 +15222,7 @@ async function checkConfigDir() {
13767
15222
  remediation: `mkdir -p "${configDir}" && chmod 700 "${configDir}"`
13768
15223
  };
13769
15224
  }
13770
- const stats = statSync6(configDir);
15225
+ const stats = statSync8(configDir);
13771
15226
  if (!stats.isDirectory()) {
13772
15227
  return {
13773
15228
  name: "Config directory",
@@ -13828,7 +15283,7 @@ async function checkPathConflicts() {
13828
15283
  const timedOutDirs = [];
13829
15284
  const DIR_TIMEOUT_MS = 1500;
13830
15285
  for (const dir of paths) {
13831
- if (!dir || !existsSync33(dir)) continue;
15286
+ if (!dir || !existsSync34(dir)) continue;
13832
15287
  try {
13833
15288
  const files = await Promise.race([
13834
15289
  readdir3(dir),
@@ -13841,7 +15296,7 @@ async function checkPathConflicts() {
13841
15296
  ]);
13842
15297
  for (const file of files) {
13843
15298
  if (file === "msapling" || file === "msapling.exe" || file === "msapling.py") {
13844
- const fullPath = join36(dir, file);
15299
+ const fullPath = join38(dir, file);
13845
15300
  conflicts.push(fullPath);
13846
15301
  }
13847
15302
  }
@@ -13959,9 +15414,9 @@ async function checkTokenValidity() {
13959
15414
  }
13960
15415
  async function checkOsSpecific() {
13961
15416
  if (platform4() === "win32") {
13962
- const testDir = join36(tmpdir(), `msapling-longpath-test-${Date.now()}`);
15417
+ const testDir = join38(tmpdir(), `msapling-longpath-test-${Date.now()}`);
13963
15418
  const longDirName = "A".repeat(260);
13964
- const testPath = join36(testDir, longDirName);
15419
+ const testPath = join38(testDir, longDirName);
13965
15420
  try {
13966
15421
  await mkdir9(testDir, { recursive: true });
13967
15422
  try {
@@ -14115,7 +15570,7 @@ var init_doctor2 = __esm({
14115
15570
  "use strict";
14116
15571
  init_esm_shims();
14117
15572
  init_doctorRedact();
14118
- execAsync = promisify(exec);
15573
+ execAsync = promisify(exec2);
14119
15574
  }
14120
15575
  });
14121
15576
 
@@ -14161,21 +15616,21 @@ var init_registry_merger = __esm({
14161
15616
  });
14162
15617
 
14163
15618
  // ../core/src/mcp/local_tools.ts
14164
- import { spawn as spawn11 } from "child_process";
15619
+ import { spawn as spawn12 } from "child_process";
14165
15620
  import { readdir as readdir4, stat as stat4, realpath as realpath3 } from "fs/promises";
14166
- import { resolve as resolve17 } from "path";
15621
+ import { resolve as resolve18 } from "path";
14167
15622
  function asResult(text, isError = false) {
14168
15623
  return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
14169
15624
  }
14170
15625
  async function runCommand(command, cwd) {
14171
- return new Promise((resolve20) => {
15626
+ return new Promise((resolve21) => {
14172
15627
  let p;
14173
15628
  const timeout = setTimeout(() => {
14174
15629
  if (p) p.kill();
14175
- resolve20({ stdout: "", stderr: "Command timed out after 30s", exit_code: -1 });
15630
+ resolve21({ stdout: "", stderr: "Command timed out after 30s", exit_code: -1 });
14176
15631
  }, 3e4);
14177
15632
  try {
14178
- p = spawn11("sh", ["-c", command], {
15633
+ p = spawn12("sh", ["-c", command], {
14179
15634
  cwd: cwd || process.cwd(),
14180
15635
  stdio: ["ignore", "pipe", "pipe"],
14181
15636
  timeout: 3e4
@@ -14190,15 +15645,15 @@ async function runCommand(command, cwd) {
14190
15645
  });
14191
15646
  p.on("error", (e) => {
14192
15647
  clearTimeout(timeout);
14193
- resolve20({ stdout, stderr: stderr + (e?.message ?? ""), exit_code: -1 });
15648
+ resolve21({ stdout, stderr: stderr + (e?.message ?? ""), exit_code: -1 });
14194
15649
  });
14195
15650
  p.on("exit", (code) => {
14196
15651
  clearTimeout(timeout);
14197
- resolve20({ stdout, stderr, exit_code: code });
15652
+ resolve21({ stdout, stderr, exit_code: code });
14198
15653
  });
14199
15654
  } catch (e) {
14200
15655
  clearTimeout(timeout);
14201
- resolve20({
15656
+ resolve21({
14202
15657
  stdout: "",
14203
15658
  stderr: e?.message ?? "Failed to spawn process",
14204
15659
  exit_code: -1
@@ -14265,7 +15720,7 @@ async function callLocalTool(name, args2, projectRoot) {
14265
15720
  const command = String(args2.command ?? "");
14266
15721
  let cwd = projectRoot;
14267
15722
  if (args2.cwd) {
14268
- cwd = resolve17(projectRoot, String(args2.cwd));
15723
+ cwd = resolve18(projectRoot, String(args2.cwd));
14269
15724
  try {
14270
15725
  const resolvedCwd = await realpath3(cwd);
14271
15726
  const resolvedRoot = await realpath3(projectRoot);
@@ -14302,7 +15757,7 @@ ${res.stderr}`
14302
15757
  return asResult("path is required", true);
14303
15758
  }
14304
15759
  try {
14305
- const resolvedPath = await realpath3(resolve17(projectRoot, pathArg));
15760
+ const resolvedPath = await realpath3(resolve18(projectRoot, pathArg));
14306
15761
  const resolvedRoot = await realpath3(projectRoot);
14307
15762
  if (!resolvedPath.startsWith(resolvedRoot)) {
14308
15763
  return asResult("Error: path attempts to escape project root", true);
@@ -14321,7 +15776,7 @@ ${res.stderr}`
14321
15776
  }
14322
15777
  case "local_glob": {
14323
15778
  const pattern = String(args2.pattern ?? "");
14324
- let cwd = args2.cwd ? resolve17(projectRoot, String(args2.cwd)) : projectRoot;
15779
+ let cwd = args2.cwd ? resolve18(projectRoot, String(args2.cwd)) : projectRoot;
14325
15780
  if (!pattern) {
14326
15781
  return asResult("pattern is required", true);
14327
15782
  }
@@ -14350,7 +15805,7 @@ ${res.stderr}`
14350
15805
  }
14351
15806
  if (path2) {
14352
15807
  try {
14353
- const resolvedPath = await realpath3(resolve17(projectRoot, path2));
15808
+ const resolvedPath = await realpath3(resolve18(projectRoot, path2));
14354
15809
  const resolvedRoot = await realpath3(projectRoot);
14355
15810
  if (!resolvedPath.startsWith(resolvedRoot)) {
14356
15811
  return asResult("Error: path attempts to escape project root", true);
@@ -14370,7 +15825,7 @@ ${res.stderr}`
14370
15825
  return asResult("cwd is required", true);
14371
15826
  }
14372
15827
  try {
14373
- const resolvedCwd = await realpath3(resolve17(projectRoot, cwdArg));
15828
+ const resolvedCwd = await realpath3(resolve18(projectRoot, cwdArg));
14374
15829
  const resolvedRoot = await realpath3(projectRoot);
14375
15830
  if (!resolvedCwd.startsWith(resolvedRoot)) {
14376
15831
  return asResult("Error: cwd attempts to escape project root", true);
@@ -14396,7 +15851,7 @@ ${status2.porcelain || "(clean)"}`
14396
15851
  return asResult("cwd is required", true);
14397
15852
  }
14398
15853
  try {
14399
- const resolvedCwd = await realpath3(resolve17(projectRoot, cwdArg));
15854
+ const resolvedCwd = await realpath3(resolve18(projectRoot, cwdArg));
14400
15855
  const resolvedRoot = await realpath3(projectRoot);
14401
15856
  if (!resolvedCwd.startsWith(resolvedRoot)) {
14402
15857
  return asResult("Error: cwd attempts to escape project root", true);
@@ -14590,13 +16045,13 @@ var init_base = __esm({
14590
16045
  editLength++;
14591
16046
  };
14592
16047
  if (callback) {
14593
- (function exec2() {
16048
+ (function exec3() {
14594
16049
  setTimeout(function() {
14595
16050
  if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
14596
16051
  return callback(void 0);
14597
16052
  }
14598
16053
  if (!execEditLength()) {
14599
- exec2();
16054
+ exec3();
14600
16055
  }
14601
16056
  }, 0);
14602
16057
  })();
@@ -15638,8 +17093,8 @@ var init_libesm = __esm({
15638
17093
  });
15639
17094
 
15640
17095
  // ../core/src/mcp/catalog.ts
15641
- import { readdirSync as readdirSync4, readFileSync as readFileSync3, statSync as statSync7 } from "fs";
15642
- import { join as join37, relative as relative15 } from "path";
17096
+ import { readdirSync as readdirSync5, readFileSync as readFileSync5, statSync as statSync9 } from "fs";
17097
+ import { join as join39, relative as relative16 } from "path";
15643
17098
  function buildFileTree(root, maxFiles) {
15644
17099
  const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "build", "dist", ".venv", "venv", ".next", "__pycache__", ".dart_tool", ".bun", "target"]);
15645
17100
  const SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java", ".kt", ".swift", ".dart", ".rb", ".cs", ".cpp", ".c", ".h", ".md", ".json", ".yaml", ".yml", ".toml", ".sh", ".sql"]);
@@ -15649,17 +17104,17 @@ function buildFileTree(root, maxFiles) {
15649
17104
  const dir = queue.shift();
15650
17105
  let entries;
15651
17106
  try {
15652
- entries = readdirSync4(dir);
17107
+ entries = readdirSync5(dir);
15653
17108
  } catch {
15654
17109
  continue;
15655
17110
  }
15656
17111
  for (const name of entries) {
15657
17112
  if (out.length >= maxFiles) break;
15658
17113
  if (SKIP_DIRS2.has(name)) continue;
15659
- const full = join37(dir, name);
17114
+ const full = join39(dir, name);
15660
17115
  let s;
15661
17116
  try {
15662
- s = statSync7(full);
17117
+ s = statSync9(full);
15663
17118
  } catch {
15664
17119
  continue;
15665
17120
  }
@@ -15680,12 +17135,12 @@ function readFilesAsContext(root, files, maxKB) {
15680
17135
  for (const f of files) {
15681
17136
  let body;
15682
17137
  try {
15683
- body = readFileSync3(f, "utf8");
17138
+ body = readFileSync5(f, "utf8");
15684
17139
  } catch {
15685
17140
  continue;
15686
17141
  }
15687
17142
  if (body.length > cap) body = body.slice(0, cap) + "\n[...truncated]";
15688
- const rel = relative15(root, f).replace(/\\/g, "/");
17143
+ const rel = relative16(root, f).replace(/\\/g, "/");
15689
17144
  parts.push(`### ${rel}
15690
17145
 
15691
17146
  \`\`\`
@@ -15887,7 +17342,7 @@ var init_types = __esm({
15887
17342
  });
15888
17343
 
15889
17344
  // ../core/src/mcp/handlers.ts
15890
- import { resolve as resolve19 } from "path";
17345
+ import { resolve as resolve20 } from "path";
15891
17346
  async function callTool(name, args2, client, getIsProCached) {
15892
17347
  switch (name) {
15893
17348
  case "msapling_chat": {
@@ -15985,7 +17440,7 @@ ${r.response ?? ""}`;
15985
17440
  return asResult2(JSON.stringify(result));
15986
17441
  }
15987
17442
  case "msapling_project_context": {
15988
- const root = resolve19(String(args2.path ?? "."));
17443
+ const root = resolve20(String(args2.path ?? "."));
15989
17444
  const maxFiles = Number.isFinite(args2.max_files) ? Number(args2.max_files) : 30;
15990
17445
  const maxKB = Number.isFinite(args2.max_file_size_kb) ? Number(args2.max_file_size_kb) : 50;
15991
17446
  const files = buildFileTree(root, maxFiles);
@@ -16240,13 +17695,13 @@ var init_server = __esm({
16240
17695
  if (inflight === 0) {
16241
17696
  return [];
16242
17697
  }
16243
- const forcedResponses = await new Promise((resolve20) => {
16244
- this._drainResolve = () => resolve20([]);
17698
+ const forcedResponses = await new Promise((resolve21) => {
17699
+ this._drainResolve = () => resolve21([]);
16245
17700
  setTimeout(() => {
16246
17701
  this._drainResolve = null;
16247
17702
  const remaining = Array.from(this._inflightCalls.values());
16248
17703
  if (remaining.length === 0) {
16249
- resolve20([]);
17704
+ resolve21([]);
16250
17705
  return;
16251
17706
  }
16252
17707
  process.stderr.write(
@@ -16262,7 +17717,7 @@ var init_server = __esm({
16262
17717
  }
16263
17718
  }));
16264
17719
  this._inflightCalls.clear();
16265
- resolve20(errorResponses);
17720
+ resolve21(errorResponses);
16266
17721
  }, DRAIN_TIMEOUT_MS);
16267
17722
  });
16268
17723
  return forcedResponses;
@@ -16462,7 +17917,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
16462
17917
  var Header = () => /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [
16463
17918
  /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
16464
17919
  "\u25CF MSapling CLI v",
16465
- "2.3.6-beta.45"
17920
+ "2.3.6-beta.47"
16466
17921
  ] }),
16467
17922
  /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Platinum Tier Architecture" }) })
16468
17923
  ] });
@@ -16867,19 +18322,19 @@ function createIdleAwarePoll(pollingIntervalRef, lastActivityRef, client, setUse
16867
18322
  init_esm_shims();
16868
18323
  init_commands();
16869
18324
  init_plan();
16870
- import { spawn as spawn9 } from "child_process";
18325
+ import { spawn as spawn10 } from "child_process";
16871
18326
 
16872
18327
  // src/state/persistentState.ts
16873
18328
  init_esm_shims();
16874
- import { homedir as homedir19 } from "os";
16875
- import { join as join34, dirname as dirname5 } from "path";
16876
- import { existsSync as existsSync30, mkdirSync as mkdirSync6 } from "fs";
18329
+ import { homedir as homedir22 } from "os";
18330
+ import { join as join36, dirname as dirname5 } from "path";
18331
+ import { existsSync as existsSync31, mkdirSync as mkdirSync7 } from "fs";
16877
18332
  import { readFile as readFile27, writeFile as writeFile17, rename as rename3 } from "fs/promises";
16878
- import { randomBytes as randomBytes14 } from "crypto";
16879
- var STATE_PATH = join34(homedir19(), ".msapling", "state.json");
18333
+ import { randomBytes as randomBytes15 } from "crypto";
18334
+ var STATE_PATH = join36(homedir22(), ".msapling", "state.json");
16880
18335
  async function loadPersistentState(statePath = STATE_PATH) {
16881
18336
  try {
16882
- if (!existsSync30(statePath)) return { version: 1 };
18337
+ if (!existsSync31(statePath)) return { version: 1 };
16883
18338
  const text = await readFile27(statePath, "utf8");
16884
18339
  const parsed = JSON.parse(text);
16885
18340
  if (parsed.version !== 1) return { version: 1 };
@@ -16895,7 +18350,7 @@ async function loadPersistentState(statePath = STATE_PATH) {
16895
18350
  async function savePersistentState(state, statePath = STATE_PATH) {
16896
18351
  try {
16897
18352
  const dir = dirname5(statePath);
16898
- if (!existsSync30(dir)) mkdirSync6(dir, { recursive: true });
18353
+ if (!existsSync31(dir)) mkdirSync7(dir, { recursive: true });
16899
18354
  const existing = await loadPersistentState(statePath);
16900
18355
  const merged = {
16901
18356
  version: 1,
@@ -16903,7 +18358,7 @@ async function savePersistentState(state, statePath = STATE_PATH) {
16903
18358
  lastChatId: state.lastChatId ?? existing.lastChatId
16904
18359
  };
16905
18360
  const pid = process.pid;
16906
- const rand = randomBytes14(4).toString("hex");
18361
+ const rand = randomBytes15(4).toString("hex");
16907
18362
  const tmp = `${statePath}.tmp.${pid}.${rand}`;
16908
18363
  await writeFile17(tmp, JSON.stringify(merged, null, 2), "utf8");
16909
18364
  await rename3(tmp, statePath);
@@ -16990,7 +18445,7 @@ ${prompt4}` : prompt4;
16990
18445
  ctx.addMessage("system", "\u26A0 Local shell command executed without MCP/tool-level safety controls. Ensure command is trusted.");
16991
18446
  try {
16992
18447
  const shellArgv = process.platform === "win32" ? ["cmd.exe", "/c", execCmd] : ["sh", "-c", execCmd];
16993
- const proc = spawn9(shellArgv[0], shellArgv.slice(1), {
18448
+ const proc = spawn10(shellArgv[0], shellArgv.slice(1), {
16994
18449
  stdio: ["inherit", "pipe", "pipe"]
16995
18450
  });
16996
18451
  let stdout = "";
@@ -17001,9 +18456,9 @@ ${prompt4}` : prompt4;
17001
18456
  if (proc.stderr) proc.stderr.on("data", (chunk) => {
17002
18457
  stderr += chunk.toString();
17003
18458
  });
17004
- await new Promise((resolve20, reject) => {
18459
+ await new Promise((resolve21, reject) => {
17005
18460
  proc.on("close", (code) => {
17006
- if (code === 0 || code === null) resolve20();
18461
+ if (code === 0 || code === null) resolve21();
17007
18462
  else reject(new Error(`Process exited with code ${code}`));
17008
18463
  });
17009
18464
  proc.on("error", reject);
@@ -17042,9 +18497,9 @@ ${prompt4}` : prompt4;
17042
18497
  for (const mention of fileMentions) {
17043
18498
  const filePath = mention.slice(1);
17044
18499
  try {
17045
- const { existsSync: existsSync34 } = await import("fs");
18500
+ const { existsSync: existsSync35 } = await import("fs");
17046
18501
  const { readFile: readFile30 } = await import("fs/promises");
17047
- if (existsSync34(filePath)) {
18502
+ if (existsSync35(filePath)) {
17048
18503
  const content = await readFile30(filePath, "utf8");
17049
18504
  const MAX_LEN = 32768;
17050
18505
  const truncated = content.length > MAX_LEN ? content.slice(0, MAX_LEN) + "\n...[TRUNCATED]" : content;
@@ -17106,7 +18561,7 @@ init_src3();
17106
18561
  init_src();
17107
18562
  init_parseApprovalMode();
17108
18563
  import { readFile as readFile28 } from "fs/promises";
17109
- import { existsSync as existsSync31 } from "fs";
18564
+ import { existsSync as existsSync32 } from "fs";
17110
18565
  async function initSession(ctx) {
17111
18566
  try {
17112
18567
  const journalEncrypted = await initJournalEncryption();
@@ -17131,10 +18586,10 @@ async function initSession(ctx) {
17131
18586
  ctx.setShellEscapeEnabled(settings.shellEscapeEnabled !== false);
17132
18587
  }
17133
18588
  try {
17134
- const { homedir: homedir22 } = await import("os");
17135
- const { join: join39 } = await import("path");
17136
- const userSettingsPath = join39(homedir22(), ".msapling", "settings.json");
17137
- if (existsSync31(userSettingsPath)) {
18589
+ const { homedir: homedir25 } = await import("os");
18590
+ const { join: join41 } = await import("path");
18591
+ const userSettingsPath = join41(homedir25(), ".msapling", "settings.json");
18592
+ if (existsSync32(userSettingsPath)) {
17138
18593
  const userText = await readFile28(userSettingsPath, "utf8");
17139
18594
  let parsed;
17140
18595
  try {
@@ -17263,7 +18718,7 @@ function useTerminalResize() {
17263
18718
 
17264
18719
  // src/App.tsx
17265
18720
  import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
17266
- var App = ({ compact: compact2 = false }) => {
18721
+ var App = ({ compact: compact2 = false, continueSession: continueSession2 = false }) => {
17267
18722
  const [user, setUser] = useState4(null);
17268
18723
  const [input, setInput] = useState4("");
17269
18724
  const [history, setHistory] = useState4([]);
@@ -17285,18 +18740,32 @@ var App = ({ compact: compact2 = false }) => {
17285
18740
  const { columns: termResizeCols, rows: termResizeRows } = useTerminalResize();
17286
18741
  const storage = useRef(new StorageManager()).current;
17287
18742
  const client = useRef(new MSaplingClient()).current;
18743
+ const agentRef = useRef(null);
17288
18744
  const requestApproval = useCallback2((request) => {
17289
- return new Promise((resolve20) => {
17290
- setPendingApproval({ request, resolve: resolve20 });
18745
+ agentRef.current?.fireLifecycleHook(
18746
+ "notification",
18747
+ { kind: "approval-request", tool: request.tool, command: request.command, reason: request.reason },
18748
+ request.tool
18749
+ );
18750
+ return new Promise((resolve21) => {
18751
+ setPendingApproval({ request, resolve: resolve21 });
17291
18752
  });
17292
18753
  }, []);
17293
18754
  const agent = useRef(new Agent(client, process.cwd(), requestApproval)).current;
18755
+ agentRef.current = agent;
17294
18756
  const trustStore = useRef(new TrustStore()).current;
17295
18757
  const lastActivityRef = useRef(Date.now());
17296
18758
  const pollingIntervalRef = useRef(null);
17297
18759
  useEffect4(() => {
17298
18760
  agent.setApprovalCallback(requestApproval);
17299
18761
  }, [agent, requestApproval]);
18762
+ useEffect4(() => {
18763
+ agent.setOnModeChange((m) => {
18764
+ setModeState(m);
18765
+ addMessage("system", `Mode changed to: ${m} (via plan-mode tool)`);
18766
+ });
18767
+ return () => agent.setOnModeChange(null);
18768
+ }, [agent]);
17300
18769
  const resolveApproval = useCallback2((decision) => {
17301
18770
  setPendingApproval((current) => {
17302
18771
  current?.resolve(decision);
@@ -17358,21 +18827,53 @@ var App = ({ compact: compact2 = false }) => {
17358
18827
  }
17359
18828
  }, [client, activeChatId, activeProjectId, setProjectId, handle401]);
17360
18829
  useEffect4(() => {
17361
- initSession({
17362
- agent,
17363
- client,
17364
- storage,
17365
- trustStore,
17366
- setActiveModel,
17367
- setMode,
17368
- setStatus,
17369
- addMessage,
17370
- refreshOverview,
17371
- setShellEscapeEnabled,
17372
- // CLI-PORT-STATE-04: restore persisted project/chat on cold start.
17373
- setProjectId,
17374
- setActiveChatId
17375
- });
18830
+ (async () => {
18831
+ await initSession({
18832
+ agent,
18833
+ client,
18834
+ storage,
18835
+ trustStore,
18836
+ setActiveModel,
18837
+ setMode,
18838
+ setStatus,
18839
+ addMessage,
18840
+ refreshOverview,
18841
+ setShellEscapeEnabled,
18842
+ // CLI-PORT-STATE-04: restore persisted project/chat on cold start.
18843
+ setProjectId,
18844
+ setActiveChatId
18845
+ });
18846
+ if (continueSession2) {
18847
+ try {
18848
+ const { rehydrateChat: rehydrateChat2 } = await Promise.resolve().then(() => (init_resume(), resume_exports));
18849
+ const recent = await client.listRecentChats(1);
18850
+ if (recent.length > 0) {
18851
+ await rehydrateChat2(recent[0], {
18852
+ client,
18853
+ setActiveChatId,
18854
+ clearHistory,
18855
+ addMessage,
18856
+ refreshOverview
18857
+ });
18858
+ } else {
18859
+ addMessage("system", "--continue: no prior chats to resume.");
18860
+ }
18861
+ } catch (e) {
18862
+ addMessage("system", `--continue: could not resume most-recent chat: ${e?.message ?? e}`);
18863
+ }
18864
+ }
18865
+ agent.fireLifecycleHook("session-start", {
18866
+ cwd: process.cwd(),
18867
+ compact: compact2,
18868
+ continued: continueSession2,
18869
+ chat_id: activeChatId
18870
+ });
18871
+ })();
18872
+ }, []);
18873
+ useEffect4(() => {
18874
+ return () => {
18875
+ agent.fireLifecycleHook("session-end", { cwd: process.cwd() });
18876
+ };
17376
18877
  }, []);
17377
18878
  useEffect4(() => {
17378
18879
  if (!user) return;
@@ -17505,14 +19006,14 @@ var App = ({ compact: compact2 = false }) => {
17505
19006
 
17506
19007
  // src/runtime/bootstrap.ts
17507
19008
  init_esm_shims();
17508
- import { readFileSync as readFileSync4 } from "fs";
19009
+ import { readFileSync as readFileSync6 } from "fs";
17509
19010
  import { fileURLToPath as fileURLToPath2 } from "url";
17510
- import { dirname as dirname6, join as join38 } from "path";
19011
+ import { dirname as dirname6, join as join40 } from "path";
17511
19012
  function readCliVersion2() {
17512
19013
  const here = dirname6(fileURLToPath2(import.meta.url));
17513
19014
  for (const rel of ["../package.json", "../../package.json"]) {
17514
19015
  try {
17515
- const pkg = JSON.parse(readFileSync4(join38(here, rel), "utf8"));
19016
+ const pkg = JSON.parse(readFileSync6(join40(here, rel), "utf8"));
17516
19017
  if (pkg.name && pkg.version) {
17517
19018
  return { name: pkg.name, version: pkg.version };
17518
19019
  }
@@ -17531,6 +19032,7 @@ function handleCliArgs(args2) {
17531
19032
  console.log("msapling \u2014 MSapling CLI (React/Ink)");
17532
19033
  console.log("Usage: msapling start interactive REPL");
17533
19034
  console.log(" msapling --compact start REPL in compact mode (no footer, thin separators)");
19035
+ console.log(" msapling --continue resume your most-recent chat (alias: -c); replays its history");
17534
19036
  console.log(' msapling --exec "<cmd>" run one slash command non-interactively and exit');
17535
19037
  console.log(" msapling mcp serve run as MCP stdio server (Claude Code / Cursor / Windsurf integration)");
17536
19038
  console.log(" msapling doctor run diagnostic health checks");
@@ -17617,6 +19119,7 @@ function handleCliArgs(args2) {
17617
19119
  }
17618
19120
 
17619
19121
  // src/index.tsx
19122
+ init_src3();
17620
19123
  import { jsx as jsx9 } from "react/jsx-runtime";
17621
19124
  var index_default = App;
17622
19125
  function restoreTerminalMode() {
@@ -17642,12 +19145,19 @@ msapling: fatal ${label}: ${msg}`);
17642
19145
  if (!process.env.NODE_ENV?.includes("test")) {
17643
19146
  process.on("uncaughtException", (err) => handleFatal("uncaught exception", err));
17644
19147
  process.on("unhandledRejection", (reason) => handleFatal("unhandled rejection", reason));
19148
+ process.on("exit", () => {
19149
+ try {
19150
+ BackgroundShellRegistry.killAll();
19151
+ } catch {
19152
+ }
19153
+ });
17645
19154
  }
17646
19155
  var args = process.argv.slice(2);
17647
19156
  var compact = args.includes("--compact");
19157
+ var continueSession = args.includes("--continue") || args.includes("-c");
17648
19158
  var shouldRenderRepl = handleCliArgs(args);
17649
19159
  if (shouldRenderRepl && !process.env.NODE_ENV?.includes("test")) {
17650
- render(/* @__PURE__ */ jsx9(App, { compact }));
19160
+ render(/* @__PURE__ */ jsx9(App, { compact, continueSession }));
17651
19161
  }
17652
19162
  export {
17653
19163
  App,