@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.
- package/dist/index.js +1938 -428
- 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((
|
|
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((
|
|
2239
|
-
proc.on("exit", (code) =>
|
|
2240
|
-
proc.on("error", () =>
|
|
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((
|
|
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
|
-
|
|
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((
|
|
2560
|
-
proc.on("exit", () =>
|
|
2561
|
-
proc.on("error", () =>
|
|
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
|
-
|
|
2642
|
-
for (const bin of
|
|
2691
|
+
if (_rgCache.resolved) return _rgCache.value;
|
|
2692
|
+
for (const bin of rgCandidates()) {
|
|
2643
2693
|
try {
|
|
2644
|
-
const exited = await new Promise((
|
|
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", () =>
|
|
2648
|
-
p.on("exit", (code) =>
|
|
2697
|
+
p.on("error", () => resolve21(null));
|
|
2698
|
+
p.on("exit", (code) => resolve21(code));
|
|
2649
2699
|
} catch {
|
|
2650
|
-
|
|
2700
|
+
resolve21(null);
|
|
2651
2701
|
}
|
|
2652
2702
|
});
|
|
2653
|
-
if (exited === 0)
|
|
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((
|
|
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
|
-
|
|
2725
|
+
resolve21({ stdout, stderr: stderr + (e?.message ?? ""), exitCode: -1 });
|
|
2672
2726
|
});
|
|
2673
2727
|
p.on("exit", (code) => {
|
|
2674
|
-
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
-
(
|
|
4298
|
+
(resolve21) => setTimeout(() => resolve21("timeout"), timeoutMs)
|
|
3927
4299
|
);
|
|
3928
4300
|
const processPromise = (async () => {
|
|
3929
|
-
const exitCode2 = await new Promise((
|
|
3930
|
-
proc.on("exit", (code) =>
|
|
3931
|
-
proc.on("error", () =>
|
|
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
|
|
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 =
|
|
4122
|
-
const rel =
|
|
4123
|
-
if (rel.startsWith("..") ||
|
|
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
|
|
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
|
|
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 =
|
|
4316
|
-
const rel =
|
|
4317
|
-
if (rel.startsWith("..") ||
|
|
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 =
|
|
4375
|
-
|
|
5053
|
+
const backupPath = join11(
|
|
5054
|
+
homedir6(),
|
|
4376
5055
|
".msapling",
|
|
4377
5056
|
"backups",
|
|
4378
5057
|
`${filename}.backup-${stamp}-${suffix}.bak`
|
|
4379
5058
|
);
|
|
4380
|
-
await mkdir3(
|
|
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
|
|
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
|
|
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 =
|
|
4548
|
-
const rel =
|
|
4549
|
-
if (rel.startsWith("..") ||
|
|
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 =
|
|
4618
|
-
|
|
5296
|
+
const backupPath = join12(
|
|
5297
|
+
homedir7(),
|
|
4619
5298
|
".msapling",
|
|
4620
5299
|
"backups",
|
|
4621
5300
|
`${filename}.backup-${stamp}-${suffix}.bak`
|
|
4622
5301
|
);
|
|
4623
|
-
await mkdir4(
|
|
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
|
|
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
|
|
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 =
|
|
4657
|
-
const rel =
|
|
4658
|
-
if (rel.startsWith("..") ||
|
|
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 =
|
|
4668
|
-
const dstPath =
|
|
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:
|
|
4680
|
-
const { join:
|
|
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 =
|
|
4685
|
-
|
|
5363
|
+
const backupPath = join41(
|
|
5364
|
+
homedir25(),
|
|
4686
5365
|
".msapling",
|
|
4687
5366
|
"backups",
|
|
4688
5367
|
`${filename}.backup-${stamp}-${suffix}.bak`
|
|
4689
5368
|
);
|
|
4690
|
-
await mkdir10(
|
|
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 =
|
|
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) &&
|
|
5437
|
+
if (existsSync10(absDst) && statSync4(absDst).isDirectory() && !srcIsDir) {
|
|
4759
5438
|
const srcName = absSrc.split(/[\\/]/).pop();
|
|
4760
|
-
absDst =
|
|
4761
|
-
const rel2 =
|
|
4762
|
-
if (rel2.startsWith("..") ||
|
|
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 (!
|
|
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
|
|
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 =
|
|
4878
|
-
const rel =
|
|
4879
|
-
if (rel.startsWith("..") ||
|
|
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 ===
|
|
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:
|
|
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 =
|
|
4924
|
-
await mkdir10(
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
5089
|
-
const isOutside = rel.startsWith("..") ||
|
|
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 =
|
|
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 =
|
|
5115
|
-
const isOutside = rel.startsWith("..") ||
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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((
|
|
6040
|
+
await new Promise((resolve21, reject) => {
|
|
5303
6041
|
try {
|
|
5304
|
-
const proc =
|
|
5305
|
-
proc.on("exit", () =>
|
|
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 {
|
|
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
|
-
|
|
5420
|
-
|
|
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
|
-
|
|
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
|
|
6402
|
+
import { homedir as homedir8 } from "os";
|
|
5665
6403
|
import { readFile as readFile9 } from "fs/promises";
|
|
5666
|
-
import { join as
|
|
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 =
|
|
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((
|
|
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((
|
|
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 === "
|
|
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 =
|
|
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
|
|
6166
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
7133
|
+
const home = homedir9();
|
|
6204
7134
|
if (!home) return null;
|
|
6205
|
-
const userDir =
|
|
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((
|
|
6787
|
-
release3 =
|
|
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
|
|
6826
|
-
import { homedir as
|
|
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 =
|
|
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
|
|
6996
|
-
import { chmodSync, existsSync as
|
|
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 =
|
|
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 (
|
|
8085
|
+
if (existsSync14(filePath)) unlinkSync(filePath);
|
|
7020
8086
|
} catch {
|
|
7021
8087
|
}
|
|
7022
8088
|
}
|
|
7023
8089
|
async function loadToken(baseDir) {
|
|
7024
|
-
const filePath =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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:
|
|
7085
|
-
const key =
|
|
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 =
|
|
7096
|
-
const refPath =
|
|
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 =
|
|
7108
|
-
if (!
|
|
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 =
|
|
7111
|
-
if (!
|
|
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
|
|
7136
|
-
import { existsSync as
|
|
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 =
|
|
7142
|
-
const indexPath =
|
|
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 (!
|
|
8210
|
+
if (!existsSync15(objectPath)) {
|
|
7145
8211
|
await writeFile7(objectPath, content, "utf8");
|
|
7146
8212
|
}
|
|
7147
8213
|
let index = {};
|
|
7148
|
-
if (
|
|
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 =
|
|
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 =
|
|
7166
|
-
if (!
|
|
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 (!
|
|
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 =
|
|
7179
|
-
if (!
|
|
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
|
|
7191
|
-
import { existsSync as
|
|
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
|
|
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 =
|
|
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 (!
|
|
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 (
|
|
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 =
|
|
7242
|
-
if (!
|
|
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 =
|
|
8340
|
+
const path2 = join21(baseDir, "history", "shell_history.json");
|
|
7275
8341
|
let release3;
|
|
7276
8342
|
try {
|
|
7277
|
-
if (!
|
|
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 (
|
|
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 =
|
|
7308
|
-
if (!
|
|
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 (
|
|
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 =
|
|
7321
|
-
const corruptBackupPath =
|
|
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
|
|
7357
|
-
import { existsSync as
|
|
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
|
|
8425
|
+
import { randomBytes as randomBytes12 } from "crypto";
|
|
7360
8426
|
async function savePermissions(baseDir, permissionsMutex, permissions) {
|
|
7361
|
-
const path2 =
|
|
8427
|
+
const path2 = join22(baseDir, "vault", "permissions.json");
|
|
7362
8428
|
let release3;
|
|
7363
8429
|
try {
|
|
7364
|
-
if (!
|
|
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 (
|
|
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 =
|
|
7395
|
-
if (!
|
|
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 (
|
|
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 =
|
|
7408
|
-
const corruptBackupPath =
|
|
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
|
|
7444
|
-
import { homedir as
|
|
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
|
|
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 =
|
|
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(
|
|
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 =
|
|
7569
|
-
const backupPath =
|
|
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
|
|
7601
|
-
import { join as
|
|
7602
|
-
import { existsSync as
|
|
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
|
|
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 =
|
|
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 (!
|
|
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 ||
|
|
7687
|
-
const userPath =
|
|
7688
|
-
const projectPath =
|
|
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
|
|
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 =
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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", () =>
|
|
7898
|
-
stdout.on("error", () =>
|
|
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((
|
|
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
|
-
|
|
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((
|
|
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((
|
|
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
|
|
8613
|
-
import { join as
|
|
8614
|
-
import { existsSync as
|
|
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 =
|
|
9736
|
+
const settingsPath = join25(homedir14(), ".msapling", "settings.json");
|
|
8641
9737
|
try {
|
|
8642
|
-
if (!
|
|
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((
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
9398
|
-
import { basename as basename2, resolve as
|
|
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 =
|
|
9407
|
-
const resolved =
|
|
9408
|
-
const rel =
|
|
9409
|
-
if (rel === ".." || rel.startsWith(`..${sep3}`) || rel.startsWith("../") ||
|
|
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 (!
|
|
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
|
|
9612
|
-
import { join as
|
|
9613
|
-
import { existsSync as
|
|
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 (
|
|
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 =
|
|
9631
|
-
if (!
|
|
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 =
|
|
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
|
|
9917
|
-
return i < 9 ? String(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 +
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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
|
|
10066
|
-
import { existsSync as
|
|
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 =
|
|
10081
|
-
if (
|
|
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
|
|
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 (
|
|
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
|
|
11415
|
+
import { existsSync as existsSync25 } from "fs";
|
|
10222
11416
|
import { readFile as readFile22 } from "fs/promises";
|
|
10223
|
-
import { join as
|
|
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 =
|
|
10229
|
-
if (
|
|
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
|
|
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
|
|
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 =
|
|
10348
|
-
if (
|
|
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 =
|
|
11550
|
+
domains = readdirSync3(root);
|
|
10357
11551
|
} catch {
|
|
10358
11552
|
return out;
|
|
10359
11553
|
}
|
|
10360
11554
|
for (const domain of domains) {
|
|
10361
|
-
const dir =
|
|
11555
|
+
const dir = join29(root, domain);
|
|
10362
11556
|
let s;
|
|
10363
11557
|
try {
|
|
10364
|
-
s =
|
|
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 =
|
|
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:
|
|
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
|
|
10467
|
-
import { join as
|
|
10468
|
-
import { mkdirSync as
|
|
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 =
|
|
10588
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
10837
|
-
import { homedir as
|
|
10838
|
-
import { existsSync as
|
|
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 =
|
|
12035
|
+
const settingsPath = join31(homedir17(), ".msapling", "settings.json");
|
|
10842
12036
|
let existing = {};
|
|
10843
12037
|
try {
|
|
10844
|
-
if (
|
|
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(
|
|
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.
|
|
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-
|
|
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
|
|
10946
|
-
import { existsSync as
|
|
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 =
|
|
10952
|
-
if (!
|
|
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
|
|
10994
|
-
import { join as
|
|
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 =
|
|
12238
|
+
outputPath = join33(homedir18(), `msapling-export-${timestamp}.json`);
|
|
11045
12239
|
content = buildJsonExport(history);
|
|
11046
12240
|
} else if (arg === "markdown" || arg === "md") {
|
|
11047
|
-
outputPath =
|
|
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 =
|
|
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
|
|
11255
|
-
import { join as
|
|
11256
|
-
import { existsSync as
|
|
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
|
|
12453
|
+
return join34(homedir19(), ".msapling", "notes.json");
|
|
11260
12454
|
}
|
|
11261
12455
|
async function readNotes(filePath = getNotesFilePath()) {
|
|
11262
12456
|
try {
|
|
11263
|
-
if (!
|
|
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 =
|
|
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
|
|
11420
|
-
import { join as
|
|
11421
|
-
import { existsSync as
|
|
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 ||
|
|
12617
|
+
return process.env.HOME || process.env.USERPROFILE || homedir20();
|
|
11424
12618
|
}
|
|
11425
12619
|
function stylesDir() {
|
|
11426
|
-
return
|
|
12620
|
+
return join35(resolveHome(), ".msapling", "output-styles");
|
|
11427
12621
|
}
|
|
11428
12622
|
function activeFile() {
|
|
11429
|
-
return
|
|
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 (!
|
|
12644
|
+
if (!existsSync30(dir)) return [];
|
|
11451
12645
|
const out = [];
|
|
11452
|
-
for (const entry of
|
|
12646
|
+
for (const entry of readdirSync4(dir)) {
|
|
11453
12647
|
if (extname3(entry).toLowerCase() !== ".md") continue;
|
|
11454
|
-
const full =
|
|
12648
|
+
const full = join35(dir, entry);
|
|
11455
12649
|
try {
|
|
11456
|
-
const text =
|
|
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 (!
|
|
11483
|
-
return
|
|
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 (!
|
|
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 (!
|
|
11504
|
-
const target =
|
|
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
|
|
14325
|
+
import { existsSync as existsSync33 } from "fs";
|
|
12871
14326
|
import { readFile as readFile29 } from "fs/promises";
|
|
12872
|
-
import { homedir as
|
|
12873
|
-
import { join as
|
|
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 =
|
|
12878
|
-
if (!
|
|
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
|
|
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((
|
|
14584
|
+
return new Promise((resolve21) => {
|
|
13130
14585
|
try {
|
|
13131
|
-
const child =
|
|
14586
|
+
const child = spawn11(cmd, args2, { stdio: "ignore", detached: true });
|
|
13132
14587
|
child.unref();
|
|
13133
14588
|
} catch {
|
|
13134
14589
|
}
|
|
13135
|
-
|
|
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((
|
|
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((
|
|
14806
|
+
return new Promise((resolve21) => rl.question(question, resolve21));
|
|
13352
14807
|
}
|
|
13353
14808
|
function promptDefault(rl, question, defaultVal) {
|
|
13354
14809
|
return new Promise(
|
|
13355
|
-
(
|
|
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((
|
|
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
|
|
13737
|
-
import { join as
|
|
13738
|
-
import { existsSync as
|
|
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 =
|
|
13762
|
-
if (!
|
|
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 =
|
|
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 || !
|
|
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 =
|
|
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 =
|
|
15417
|
+
const testDir = join38(tmpdir(), `msapling-longpath-test-${Date.now()}`);
|
|
13963
15418
|
const longDirName = "A".repeat(260);
|
|
13964
|
-
const testPath =
|
|
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(
|
|
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
|
|
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
|
|
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((
|
|
15626
|
+
return new Promise((resolve21) => {
|
|
14172
15627
|
let p;
|
|
14173
15628
|
const timeout = setTimeout(() => {
|
|
14174
15629
|
if (p) p.kill();
|
|
14175
|
-
|
|
15630
|
+
resolve21({ stdout: "", stderr: "Command timed out after 30s", exit_code: -1 });
|
|
14176
15631
|
}, 3e4);
|
|
14177
15632
|
try {
|
|
14178
|
-
p =
|
|
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
|
-
|
|
15648
|
+
resolve21({ stdout, stderr: stderr + (e?.message ?? ""), exit_code: -1 });
|
|
14194
15649
|
});
|
|
14195
15650
|
p.on("exit", (code) => {
|
|
14196
15651
|
clearTimeout(timeout);
|
|
14197
|
-
|
|
15652
|
+
resolve21({ stdout, stderr, exit_code: code });
|
|
14198
15653
|
});
|
|
14199
15654
|
} catch (e) {
|
|
14200
15655
|
clearTimeout(timeout);
|
|
14201
|
-
|
|
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 =
|
|
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(
|
|
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 ?
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
15642
|
-
import { join as
|
|
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 =
|
|
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 =
|
|
17114
|
+
const full = join39(dir, name);
|
|
15660
17115
|
let s;
|
|
15661
17116
|
try {
|
|
15662
|
-
s =
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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((
|
|
16244
|
-
this._drainResolve = () =>
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
|
16875
|
-
import { join as
|
|
16876
|
-
import { existsSync as
|
|
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
|
|
16879
|
-
var STATE_PATH =
|
|
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 (!
|
|
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 (!
|
|
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 =
|
|
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 =
|
|
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((
|
|
18459
|
+
await new Promise((resolve21, reject) => {
|
|
17005
18460
|
proc.on("close", (code) => {
|
|
17006
|
-
if (code === 0 || code === null)
|
|
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:
|
|
18500
|
+
const { existsSync: existsSync35 } = await import("fs");
|
|
17046
18501
|
const { readFile: readFile30 } = await import("fs/promises");
|
|
17047
|
-
if (
|
|
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
|
|
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:
|
|
17135
|
-
const { join:
|
|
17136
|
-
const userSettingsPath =
|
|
17137
|
-
if (
|
|
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
|
-
|
|
17290
|
-
|
|
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
|
-
|
|
17362
|
-
|
|
17363
|
-
|
|
17364
|
-
|
|
17365
|
-
|
|
17366
|
-
|
|
17367
|
-
|
|
17368
|
-
|
|
17369
|
-
|
|
17370
|
-
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
17374
|
-
|
|
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
|
|
19009
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
17509
19010
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
17510
|
-
import { dirname as dirname6, join as
|
|
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(
|
|
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,
|