@mtreeai/msapling-cli 2.3.6-beta.5 → 2.3.6-beta.7
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 +1171 -159
- package/package.json +55 -55
package/dist/index.js
CHANGED
|
@@ -272,7 +272,11 @@ var init_src = __esm({
|
|
|
272
272
|
try {
|
|
273
273
|
const parts = token.split(".");
|
|
274
274
|
if (parts.length !== 3) return null;
|
|
275
|
-
|
|
275
|
+
let b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
276
|
+
const padLen = (4 - b64.length % 4) % 4;
|
|
277
|
+
b64 += "=".repeat(padLen);
|
|
278
|
+
const decoded = typeof atob === "function" ? atob(b64) : Buffer.from(b64, "base64").toString("binary");
|
|
279
|
+
const payload = JSON.parse(decoded);
|
|
276
280
|
return payload;
|
|
277
281
|
} catch {
|
|
278
282
|
return null;
|
|
@@ -374,8 +378,9 @@ var init_src = __esm({
|
|
|
374
378
|
*/
|
|
375
379
|
async chatOnce(prompt, model, chatId) {
|
|
376
380
|
let acc = "";
|
|
377
|
-
for await (const chunk of this.streamChat({ prompt, model, chat_id: chatId })) {
|
|
378
|
-
if (chunk.
|
|
381
|
+
for await (const chunk of this.streamChat({ content: prompt, model, chat_id: chatId ?? "" })) {
|
|
382
|
+
if (chunk.delta) acc += chunk.delta;
|
|
383
|
+
else if (chunk.content) acc += chunk.content;
|
|
379
384
|
}
|
|
380
385
|
return acc;
|
|
381
386
|
}
|
|
@@ -455,6 +460,33 @@ var init_src = __esm({
|
|
|
455
460
|
});
|
|
456
461
|
return data.hash;
|
|
457
462
|
}
|
|
463
|
+
async getActiveTasks() {
|
|
464
|
+
return this.request("/api/task-monitor/active");
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Wakeup Management (LAB-CHAT-PARITY-04)
|
|
468
|
+
*/
|
|
469
|
+
async listWakeups(params = {}) {
|
|
470
|
+
const queryParams = new URLSearchParams(params);
|
|
471
|
+
return this.request(`/api/wakeups/?${queryParams.toString()}`);
|
|
472
|
+
}
|
|
473
|
+
async createWakeup(data) {
|
|
474
|
+
return this.request("/api/wakeups/", {
|
|
475
|
+
method: "POST",
|
|
476
|
+
body: JSON.stringify(data)
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
async cancelWakeup(id) {
|
|
480
|
+
await this.request(`/api/wakeups/${id}`, {
|
|
481
|
+
method: "DELETE"
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
/**
|
|
485
|
+
* Orchestration & Fleet Status
|
|
486
|
+
*/
|
|
487
|
+
async getFleetStatus() {
|
|
488
|
+
return this.request("/api/orchestration/status");
|
|
489
|
+
}
|
|
458
490
|
async proposeEdit(params) {
|
|
459
491
|
return await this.request("/api/mdrive/ram/blocks/propose", {
|
|
460
492
|
method: "POST",
|
|
@@ -462,6 +494,9 @@ var init_src = __esm({
|
|
|
462
494
|
});
|
|
463
495
|
}
|
|
464
496
|
async *streamChat(params) {
|
|
497
|
+
if (!params.frame_id) {
|
|
498
|
+
params = { ...params, frame_id: "earth_surface" };
|
|
499
|
+
}
|
|
465
500
|
const controller = new AbortController();
|
|
466
501
|
const timeout = setTimeout(() => controller.abort(), 3e4);
|
|
467
502
|
try {
|
|
@@ -487,22 +522,34 @@ var init_src = __esm({
|
|
|
487
522
|
if (!response.body) throw new Error("No response body");
|
|
488
523
|
const reader = response.body.getReader();
|
|
489
524
|
const decoder = new TextDecoder();
|
|
525
|
+
let pending = "";
|
|
526
|
+
const yieldLine = function* (raw) {
|
|
527
|
+
const line = raw.replace(/\r$/, "").trim();
|
|
528
|
+
if (!line) return;
|
|
529
|
+
try {
|
|
530
|
+
yield JSON.parse(line);
|
|
531
|
+
} catch {
|
|
532
|
+
if (line.startsWith("{")) {
|
|
533
|
+
console.warn(`[Stream] Dropped malformed JSON: ${line}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
};
|
|
490
537
|
while (true) {
|
|
491
538
|
const { done, value } = await reader.read();
|
|
492
539
|
if (done) break;
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
} catch (e) {
|
|
500
|
-
if (line.startsWith("{")) {
|
|
501
|
-
console.warn(`[Stream] Dropped partial JSON: ${line}`);
|
|
502
|
-
}
|
|
503
|
-
}
|
|
540
|
+
pending += decoder.decode(value, { stream: true });
|
|
541
|
+
let nlIdx;
|
|
542
|
+
while ((nlIdx = pending.indexOf("\n")) !== -1) {
|
|
543
|
+
const raw = pending.slice(0, nlIdx);
|
|
544
|
+
pending = pending.slice(nlIdx + 1);
|
|
545
|
+
yield* yieldLine(raw);
|
|
504
546
|
}
|
|
505
547
|
}
|
|
548
|
+
pending += decoder.decode();
|
|
549
|
+
if (pending.length > 0) {
|
|
550
|
+
yield* yieldLine(pending);
|
|
551
|
+
pending = "";
|
|
552
|
+
}
|
|
506
553
|
} catch (e) {
|
|
507
554
|
if (e.name === "AbortError") {
|
|
508
555
|
throw new MSaplingError("Request timed out after 30s.", 408, "timeout");
|
|
@@ -3493,18 +3540,18 @@ async function copyDir(src, dst) {
|
|
|
3493
3540
|
async function backupFile(absPath) {
|
|
3494
3541
|
try {
|
|
3495
3542
|
const { readFile: readFile23, writeFile: writeFile12, mkdir: mkdir9 } = await import("fs/promises");
|
|
3496
|
-
const { homedir:
|
|
3497
|
-
const { join:
|
|
3543
|
+
const { homedir: homedir17 } = await import("os");
|
|
3544
|
+
const { join: join31 } = await import("path");
|
|
3498
3545
|
const filename = absPath.split(/[\\/]/).pop() ?? "file";
|
|
3499
3546
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3500
3547
|
const suffix = randomBytes5(4).toString("hex");
|
|
3501
|
-
const backupPath =
|
|
3502
|
-
|
|
3548
|
+
const backupPath = join31(
|
|
3549
|
+
homedir17(),
|
|
3503
3550
|
".msapling",
|
|
3504
3551
|
"backups",
|
|
3505
3552
|
`${filename}.backup-${stamp}-${suffix}.bak`
|
|
3506
3553
|
);
|
|
3507
|
-
await mkdir9(
|
|
3554
|
+
await mkdir9(join31(homedir17(), ".msapling", "backups"), { recursive: true });
|
|
3508
3555
|
const content = await readFile23(absPath, "utf8");
|
|
3509
3556
|
await writeFile12(backupPath, content, "utf8");
|
|
3510
3557
|
return backupPath;
|
|
@@ -3732,13 +3779,13 @@ var init_DeleteFileTool = __esm({
|
|
|
3732
3779
|
if (isFile) {
|
|
3733
3780
|
try {
|
|
3734
3781
|
const { readFile: readFile23, writeFile: writeFile12, mkdir: mkdir9 } = await import("fs/promises");
|
|
3735
|
-
const { homedir:
|
|
3782
|
+
const { homedir: homedir17 } = await import("os");
|
|
3736
3783
|
const existingContent = await readFile23(abs, "utf8");
|
|
3737
3784
|
const filename = abs.split(/[\\/]/).pop() ?? "file";
|
|
3738
3785
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
3739
3786
|
const suffix = randomBytes6(4).toString("hex");
|
|
3740
|
-
const backupPath = join11(
|
|
3741
|
-
await mkdir9(join11(
|
|
3787
|
+
const backupPath = join11(homedir17(), ".msapling", "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
|
|
3788
|
+
await mkdir9(join11(homedir17(), ".msapling", "backups"), { recursive: true });
|
|
3742
3789
|
await writeFile12(backupPath, existingContent, "utf8");
|
|
3743
3790
|
backedUpTo = backupPath;
|
|
3744
3791
|
} catch {
|
|
@@ -3960,12 +4007,81 @@ var init_Sandbox = __esm({
|
|
|
3960
4007
|
}
|
|
3961
4008
|
return { status: "safe", hash };
|
|
3962
4009
|
}
|
|
3963
|
-
|
|
3964
|
-
|
|
4010
|
+
/**
|
|
4011
|
+
* CLI-AUDIT-AUTO-44: curate PATH for sandboxed subprocesses.
|
|
4012
|
+
*
|
|
4013
|
+
* Previously `getRestrictedEnv` forwarded `process.env.PATH` verbatim. That
|
|
4014
|
+
* means any directory injected onto the user's PATH (npm-global, ~/.local/bin,
|
|
4015
|
+
* a malicious package's postinstall step adding /tmp/evil) inherits straight
|
|
4016
|
+
* into every sandboxed tool spawn. Containment is then trivially defeated:
|
|
4017
|
+
* "git" might resolve to /tmp/evil/git.
|
|
4018
|
+
*
|
|
4019
|
+
* Strategy:
|
|
4020
|
+
* - Default to a small allowlist of canonical system dirs (matches POSIX
|
|
4021
|
+
* `/etc/login.defs` style + common Windows system roots).
|
|
4022
|
+
* - Filter the caller's PATH against the allowlist, preserving order so
|
|
4023
|
+
* that legitimate /usr/local/bin/git still wins over /usr/bin/git.
|
|
4024
|
+
* - Drop world-writable / home-relative entries (`/tmp`, `~`, `.`, ``).
|
|
4025
|
+
* - Caller can override with an explicit `pathOverride` argument (e.g.
|
|
4026
|
+
* boot-time validation, tests).
|
|
4027
|
+
*/
|
|
4028
|
+
static SAFE_PATH_ENTRIES = /* @__PURE__ */ new Set([
|
|
4029
|
+
// POSIX
|
|
4030
|
+
"/usr/local/sbin",
|
|
4031
|
+
"/usr/local/bin",
|
|
4032
|
+
"/usr/sbin",
|
|
4033
|
+
"/usr/bin",
|
|
4034
|
+
"/sbin",
|
|
4035
|
+
"/bin",
|
|
4036
|
+
// macOS extras
|
|
4037
|
+
"/opt/homebrew/bin",
|
|
4038
|
+
"/opt/homebrew/sbin",
|
|
4039
|
+
"/opt/local/bin",
|
|
4040
|
+
// Windows
|
|
4041
|
+
"C:\\Windows\\System32",
|
|
4042
|
+
"C:\\Windows",
|
|
4043
|
+
"C:\\Windows\\System32\\Wbem",
|
|
4044
|
+
"C:\\Windows\\System32\\WindowsPowerShell\\v1.0"
|
|
4045
|
+
]);
|
|
4046
|
+
/**
|
|
4047
|
+
* Decide whether a single PATH entry is acceptable inside the sandbox.
|
|
4048
|
+
* Exposed (static) for unit testing.
|
|
4049
|
+
*/
|
|
4050
|
+
static isSafePathEntry(entry) {
|
|
4051
|
+
if (!entry) return false;
|
|
4052
|
+
const e = entry.trim();
|
|
4053
|
+
if (!e) return false;
|
|
4054
|
+
if (e === "." || e === "..") return false;
|
|
4055
|
+
if (e.startsWith("~")) return false;
|
|
4056
|
+
if (e.startsWith("/tmp") || e.startsWith("/var/tmp")) return false;
|
|
4057
|
+
if (process.platform === "win32") {
|
|
4058
|
+
const lower = e.toLowerCase();
|
|
4059
|
+
for (const safe of _Sandbox.SAFE_PATH_ENTRIES) {
|
|
4060
|
+
if (safe.toLowerCase() === lower) return true;
|
|
4061
|
+
}
|
|
4062
|
+
return false;
|
|
4063
|
+
}
|
|
4064
|
+
return _Sandbox.SAFE_PATH_ENTRIES.has(e);
|
|
4065
|
+
}
|
|
4066
|
+
/**
|
|
4067
|
+
* Build a curated PATH from the host environment, retaining only entries that
|
|
4068
|
+
* pass `isSafePathEntry`. Order is preserved. If nothing matches, fall back
|
|
4069
|
+
* to a minimal platform default so subprocess spawn never sees an empty PATH.
|
|
4070
|
+
*/
|
|
4071
|
+
static curatePath(rawPath) {
|
|
4072
|
+
const sep3 = process.platform === "win32" ? ";" : ":";
|
|
4073
|
+
const entries = (rawPath ?? "").split(sep3);
|
|
4074
|
+
const kept = entries.filter((e) => _Sandbox.isSafePathEntry(e));
|
|
4075
|
+
if (kept.length > 0) return kept.join(sep3);
|
|
4076
|
+
return process.platform === "win32" ? "C:\\Windows\\System32;C:\\Windows" : "/usr/local/bin:/usr/bin:/bin";
|
|
4077
|
+
}
|
|
4078
|
+
getRestrictedEnv(pathOverride) {
|
|
4079
|
+
const safeKeys = ["LANG", "LC_ALL", "NODE_ENV", "BUN_ENV"];
|
|
3965
4080
|
const filteredEnv = {};
|
|
3966
4081
|
for (const key of safeKeys) {
|
|
3967
4082
|
if (process.env[key]) filteredEnv[key] = process.env[key];
|
|
3968
4083
|
}
|
|
4084
|
+
filteredEnv["PATH"] = pathOverride ?? _Sandbox.curatePath(process.env.PATH);
|
|
3969
4085
|
filteredEnv["MSAPLING_SANDBOX"] = "true";
|
|
3970
4086
|
return filteredEnv;
|
|
3971
4087
|
}
|
|
@@ -4389,6 +4505,35 @@ ${blocker.stderr || "(empty)"}`,
|
|
|
4389
4505
|
};
|
|
4390
4506
|
}
|
|
4391
4507
|
}
|
|
4508
|
+
if (args2.path) {
|
|
4509
|
+
const check = this.sandbox.isPathSafe(args2.path);
|
|
4510
|
+
if (!check.safe) {
|
|
4511
|
+
await this.voice.speak(`Security block detected`, "urgent");
|
|
4512
|
+
const staleKey = `${toolName}:${(args2.path || "").trim()}`;
|
|
4513
|
+
if (this.trustStore?.has(staleKey)) {
|
|
4514
|
+
this.trustStore.delete(staleKey).catch(() => {
|
|
4515
|
+
});
|
|
4516
|
+
}
|
|
4517
|
+
return { content: `Security Block: ${check.reason}`, isError: true };
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
if (toolName === "run_command" || toolName === "bash_command") {
|
|
4521
|
+
if (!args2.command) {
|
|
4522
|
+
return { content: `Error: ${toolName} requires a command argument`, isError: true };
|
|
4523
|
+
}
|
|
4524
|
+
const analysis = this.sandbox.analyzeCommand(args2.command);
|
|
4525
|
+
if (analysis.status === "blocked") {
|
|
4526
|
+
const staleCmdKey = toolName === "bash_command" ? `bash_command:${(args2.command || "").trim().replace(/\s+/g, " ")}${args2.cwd ? `:cwd=${args2.cwd}` : ""}` : `run_command:${(args2.command || "").trim()}`;
|
|
4527
|
+
if (this.trustStore?.has(staleCmdKey)) {
|
|
4528
|
+
this.trustStore.delete(staleCmdKey).catch(() => {
|
|
4529
|
+
});
|
|
4530
|
+
}
|
|
4531
|
+
return { content: `Security Block: ${analysis.reason}`, isError: true };
|
|
4532
|
+
}
|
|
4533
|
+
if (analysis.status === "dangerous" && this.mode !== "bypassPermissions") {
|
|
4534
|
+
return { content: `Dangerous Command Blocked: ${analysis.reason}. Use manual approval or trust hash ${analysis.hash}.`, isError: true };
|
|
4535
|
+
}
|
|
4536
|
+
}
|
|
4392
4537
|
if (this.needsApproval(toolName) && this.approvalCallback) {
|
|
4393
4538
|
let cmdKey = "";
|
|
4394
4539
|
if (toolName === "run_command") {
|
|
@@ -4400,9 +4545,6 @@ ${blocker.stderr || "(empty)"}`,
|
|
|
4400
4545
|
} else {
|
|
4401
4546
|
cmdKey = `${toolName}:${(args2.path || args2.instruction || "").trim()}`;
|
|
4402
4547
|
}
|
|
4403
|
-
if ((toolName === "run_command" || toolName === "bash_command") && !args2.command) {
|
|
4404
|
-
return { content: `Error: ${toolName} requires a command argument`, isError: true };
|
|
4405
|
-
}
|
|
4406
4548
|
const alreadyTrusted = this.trustStore ? this.trustStore.has(cmdKey) : this.sessionTrust.has(cmdKey);
|
|
4407
4549
|
if (!alreadyTrusted) {
|
|
4408
4550
|
const decision = await this.approvalCallback({
|
|
@@ -4449,22 +4591,6 @@ ${blocker.stderr || "(empty)"}`,
|
|
|
4449
4591
|
Please approve the diff in the UI to sync this change locally.`
|
|
4450
4592
|
};
|
|
4451
4593
|
}
|
|
4452
|
-
if (args2.path) {
|
|
4453
|
-
const check = this.sandbox.isPathSafe(args2.path);
|
|
4454
|
-
if (!check.safe) {
|
|
4455
|
-
await this.voice.speak(`Security block detected`, "urgent");
|
|
4456
|
-
return { content: `Security Block: ${check.reason}`, isError: true };
|
|
4457
|
-
}
|
|
4458
|
-
}
|
|
4459
|
-
if (toolName === "run_command" || toolName === "bash_command") {
|
|
4460
|
-
const analysis = this.sandbox.analyzeCommand(args2.command);
|
|
4461
|
-
if (analysis.status === "blocked") {
|
|
4462
|
-
return { content: `Security Block: ${analysis.reason}`, isError: true };
|
|
4463
|
-
}
|
|
4464
|
-
if (analysis.status === "dangerous" && this.mode !== "bypassPermissions") {
|
|
4465
|
-
return { content: `Dangerous Command Blocked: ${analysis.reason}. Use manual approval or trust hash ${analysis.hash}.`, isError: true };
|
|
4466
|
-
}
|
|
4467
|
-
}
|
|
4468
4594
|
const result = await tool.execute(args2, projectRoot);
|
|
4469
4595
|
if (this.hooks) {
|
|
4470
4596
|
this.hooks.fire({
|
|
@@ -5011,9 +5137,9 @@ var init_Mutex = __esm({
|
|
|
5011
5137
|
|
|
5012
5138
|
// ../core/src/TrustStore.ts
|
|
5013
5139
|
import { join as join13 } from "path";
|
|
5014
|
-
import { homedir as homedir6 } from "os";
|
|
5140
|
+
import { homedir as homedir6, platform as platform2 } from "os";
|
|
5015
5141
|
import { existsSync as existsSync11, mkdirSync } from "fs";
|
|
5016
|
-
import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
|
|
5142
|
+
import { readFile as readFile10, writeFile as writeFile5, chmod } from "fs/promises";
|
|
5017
5143
|
var USER_SETTINGS_PATH, TrustStore;
|
|
5018
5144
|
var init_TrustStore = __esm({
|
|
5019
5145
|
"../core/src/TrustStore.ts"() {
|
|
@@ -5053,6 +5179,12 @@ var init_TrustStore = __esm({
|
|
|
5053
5179
|
const dir = join13(homedir6(), ".msapling");
|
|
5054
5180
|
if (!existsSync11(dir)) mkdirSync(dir, { recursive: true });
|
|
5055
5181
|
await writeFile5(this.settingsPath, JSON.stringify(settings, null, 2), "utf8");
|
|
5182
|
+
if (platform2() !== "win32") {
|
|
5183
|
+
try {
|
|
5184
|
+
await chmod(this.settingsPath, 384);
|
|
5185
|
+
} catch {
|
|
5186
|
+
}
|
|
5187
|
+
}
|
|
5056
5188
|
}
|
|
5057
5189
|
// ── Public API ─────────────────────────────────────────────────────────────
|
|
5058
5190
|
/**
|
|
@@ -5130,7 +5262,7 @@ var require_polyfills = __commonJS({
|
|
|
5130
5262
|
var constants = __require("constants");
|
|
5131
5263
|
var origCwd = process.cwd;
|
|
5132
5264
|
var cwd = null;
|
|
5133
|
-
var
|
|
5265
|
+
var platform4 = process.env.GRACEFUL_FS_PLATFORM || process.platform;
|
|
5134
5266
|
process.cwd = function() {
|
|
5135
5267
|
if (!cwd)
|
|
5136
5268
|
cwd = origCwd.call(process);
|
|
@@ -5189,7 +5321,7 @@ var require_polyfills = __commonJS({
|
|
|
5189
5321
|
fs3.lchownSync = function() {
|
|
5190
5322
|
};
|
|
5191
5323
|
}
|
|
5192
|
-
if (
|
|
5324
|
+
if (platform4 === "win32") {
|
|
5193
5325
|
fs3.rename = typeof fs3.rename !== "function" ? fs3.rename : (function(fs$rename) {
|
|
5194
5326
|
function rename2(from, to, cb) {
|
|
5195
5327
|
var start = Date.now();
|
|
@@ -5656,8 +5788,8 @@ var require_graceful_fs = __commonJS({
|
|
|
5656
5788
|
}
|
|
5657
5789
|
var fs$appendFile = fs4.appendFile;
|
|
5658
5790
|
if (fs$appendFile)
|
|
5659
|
-
fs4.appendFile =
|
|
5660
|
-
function
|
|
5791
|
+
fs4.appendFile = appendFile2;
|
|
5792
|
+
function appendFile2(path2, data, options, cb) {
|
|
5661
5793
|
if (typeof options === "function")
|
|
5662
5794
|
cb = options, options = null;
|
|
5663
5795
|
return go$appendFile(path2, data, options, cb);
|
|
@@ -6730,7 +6862,7 @@ var init_keytar = __esm({
|
|
|
6730
6862
|
|
|
6731
6863
|
// node-file:<repo>\node_modules\.bun\keytar@7.9.0\node_modules\keytar\build\Release\keytar.node
|
|
6732
6864
|
var require_keytar = __commonJS({
|
|
6733
|
-
"node-file:
|
|
6865
|
+
"node-file:F:\\MForest\\projects\\MSapling_CLI\\node_modules\\.bun\\keytar@7.9.0\\node_modules\\keytar\\build\\Release\\keytar.node"(exports, module) {
|
|
6734
6866
|
"use strict";
|
|
6735
6867
|
init_esm_shims();
|
|
6736
6868
|
init_keytar();
|
|
@@ -6784,9 +6916,12 @@ var require_keytar2 = __commonJS({
|
|
|
6784
6916
|
// ../core/src/Storage.ts
|
|
6785
6917
|
import { join as join14 } from "path";
|
|
6786
6918
|
import { homedir as homedir7 } from "os";
|
|
6787
|
-
import { chmodSync, existsSync as existsSync12, renameSync, unlinkSync } from "fs";
|
|
6788
|
-
import { mkdir as mkdir6, writeFile as writeFile6, readFile as readFile11 } from "fs/promises";
|
|
6789
|
-
import { randomBytes as randomBytes7 } from "crypto";
|
|
6919
|
+
import { chmodSync, existsSync as existsSync12, renameSync, unlinkSync, writeFileSync } from "fs";
|
|
6920
|
+
import { mkdir as mkdir6, writeFile as writeFile6, readFile as readFile11, appendFile } from "fs/promises";
|
|
6921
|
+
import { randomBytes as randomBytes7, createHash as createHash3 } from "crypto";
|
|
6922
|
+
function hashLine(line) {
|
|
6923
|
+
return createHash3("sha256").update(line, "utf8").digest("hex");
|
|
6924
|
+
}
|
|
6790
6925
|
var lockfile, keytar, StorageManager;
|
|
6791
6926
|
var init_Storage = __esm({
|
|
6792
6927
|
"../core/src/Storage.ts"() {
|
|
@@ -6819,7 +6954,18 @@ var init_Storage = __esm({
|
|
|
6819
6954
|
async ensureDirs() {
|
|
6820
6955
|
try {
|
|
6821
6956
|
await mkdir6(this.baseDir, { recursive: true });
|
|
6822
|
-
const subdirs = [
|
|
6957
|
+
const subdirs = [
|
|
6958
|
+
"history",
|
|
6959
|
+
"backups",
|
|
6960
|
+
"cache",
|
|
6961
|
+
// CLI-ARCH-CONTENT-HASH-VAULT-01: content-addressed vault layout
|
|
6962
|
+
"vault",
|
|
6963
|
+
"vault/objects",
|
|
6964
|
+
"vault/refs",
|
|
6965
|
+
// CLI-ARCH-RECIPE-AT-HASH-URI-01: recipe hash cache
|
|
6966
|
+
"cache/recipes",
|
|
6967
|
+
"cache/recipes/objects"
|
|
6968
|
+
];
|
|
6823
6969
|
for (const sub of subdirs) {
|
|
6824
6970
|
const path2 = join14(this.baseDir, sub);
|
|
6825
6971
|
await mkdir6(path2, { recursive: true });
|
|
@@ -6895,6 +7041,230 @@ var init_Storage = __esm({
|
|
|
6895
7041
|
console.warn(`Failed to delete token file: ${e}`);
|
|
6896
7042
|
}
|
|
6897
7043
|
}
|
|
7044
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
7045
|
+
// CLI-ARCH-CONTENT-HASH-VAULT-01 — content-addressed vault helpers
|
|
7046
|
+
//
|
|
7047
|
+
// Every vault value is a content-addressed blob stored under
|
|
7048
|
+
// ~/.msapling/vault/objects/<sha256-hex>
|
|
7049
|
+
// A human-readable ref file under
|
|
7050
|
+
// ~/.msapling/vault/refs/<label>
|
|
7051
|
+
// holds the sha256-hex of the current value.
|
|
7052
|
+
//
|
|
7053
|
+
// Updating a value: write new object → atomically rewrite ref.
|
|
7054
|
+
// Old objects are GC'd separately (retention window TBD — see roadmap).
|
|
7055
|
+
// The keytar account name stores the sha256-hex so the plaintext label
|
|
7056
|
+
// remains opaque on disk (label → hash is in keytar; hash → blob is on disk).
|
|
7057
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
7058
|
+
/**
|
|
7059
|
+
* Write a value into the content-addressed object store and atomically
|
|
7060
|
+
* update the named ref. Returns the sha256 hex of the stored object.
|
|
7061
|
+
*
|
|
7062
|
+
* WAL-PATTERN: write to objects/<hash> (idempotent — same content = same
|
|
7063
|
+
* path), then rename refs/<label>.tmp → refs/<label>.
|
|
7064
|
+
*/
|
|
7065
|
+
async writeVaultRef(label, value) {
|
|
7066
|
+
await this._ready;
|
|
7067
|
+
const hash = createHash3("sha256").update(value, "utf8").digest("hex");
|
|
7068
|
+
const objectPath = join14(this.baseDir, "vault", "objects", hash);
|
|
7069
|
+
const refPath = join14(this.baseDir, "vault", "refs", label);
|
|
7070
|
+
const refTmp = `${refPath}.tmp`;
|
|
7071
|
+
await writeFile6(objectPath, value, "utf8");
|
|
7072
|
+
if (process.platform !== "win32") {
|
|
7073
|
+
chmodSync(objectPath, 384);
|
|
7074
|
+
}
|
|
7075
|
+
await writeFile6(refTmp, hash, "utf8");
|
|
7076
|
+
renameSync(refTmp, refPath);
|
|
7077
|
+
return hash;
|
|
7078
|
+
}
|
|
7079
|
+
/**
|
|
7080
|
+
* Read a vault value by label. Returns null if the ref or its object is
|
|
7081
|
+
* missing (bootstrap / first-run case).
|
|
7082
|
+
*/
|
|
7083
|
+
async readVaultRef(label) {
|
|
7084
|
+
await this._ready;
|
|
7085
|
+
const refPath = join14(this.baseDir, "vault", "refs", label);
|
|
7086
|
+
if (!existsSync12(refPath)) return null;
|
|
7087
|
+
const hash = (await readFile11(refPath, "utf8")).trim();
|
|
7088
|
+
const objectPath = join14(this.baseDir, "vault", "objects", hash);
|
|
7089
|
+
if (!existsSync12(objectPath)) return null;
|
|
7090
|
+
return readFile11(objectPath, "utf8");
|
|
7091
|
+
}
|
|
7092
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
7093
|
+
// CLI-ARCH-RECIPE-AT-HASH-URI-01 — recipe hash registry helpers
|
|
7094
|
+
//
|
|
7095
|
+
// On first load of a recipe file, its content is sha256-hashed and the
|
|
7096
|
+
// mapping { name → hash } is stored in
|
|
7097
|
+
// ~/.msapling/cache/recipes/index.json
|
|
7098
|
+
// The immutable blob is written to
|
|
7099
|
+
// ~/.msapling/cache/recipes/objects/<sha256>
|
|
7100
|
+
//
|
|
7101
|
+
// Invocations reference recipes as `recipe@<sha256>` URIs. The /recipe
|
|
7102
|
+
// command resolves a bare name through the index and logs the hash for
|
|
7103
|
+
// traceability.
|
|
7104
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
7105
|
+
/**
|
|
7106
|
+
* Register a recipe file in the local hash index. Idempotent — if the
|
|
7107
|
+
* content hash already exists the call is a no-op (returns existing hash).
|
|
7108
|
+
* Returns the sha256 hex for use in log messages / URI construction.
|
|
7109
|
+
*/
|
|
7110
|
+
async registerRecipe(name, content) {
|
|
7111
|
+
await this._ready;
|
|
7112
|
+
const hash = createHash3("sha256").update(content, "utf8").digest("hex");
|
|
7113
|
+
const objectPath = join14(this.baseDir, "cache", "recipes", "objects", hash);
|
|
7114
|
+
const indexPath = join14(this.baseDir, "cache", "recipes", "index.json");
|
|
7115
|
+
const indexTmp = `${indexPath}.tmp`;
|
|
7116
|
+
if (!existsSync12(objectPath)) {
|
|
7117
|
+
await writeFile6(objectPath, content, "utf8");
|
|
7118
|
+
}
|
|
7119
|
+
let index = {};
|
|
7120
|
+
if (existsSync12(indexPath)) {
|
|
7121
|
+
try {
|
|
7122
|
+
index = JSON.parse(await readFile11(indexPath, "utf8"));
|
|
7123
|
+
} catch {
|
|
7124
|
+
index = {};
|
|
7125
|
+
}
|
|
7126
|
+
}
|
|
7127
|
+
index[name] = hash;
|
|
7128
|
+
await writeFile6(indexTmp, JSON.stringify(index, null, 2), "utf8");
|
|
7129
|
+
renameSync(indexTmp, indexPath);
|
|
7130
|
+
return hash;
|
|
7131
|
+
}
|
|
7132
|
+
/**
|
|
7133
|
+
* Resolve a recipe name or `recipe@<hash>` URI to its cached content.
|
|
7134
|
+
* Returns { hash, content } or null if not found in the local cache.
|
|
7135
|
+
*/
|
|
7136
|
+
async resolveRecipe(nameOrRef) {
|
|
7137
|
+
await this._ready;
|
|
7138
|
+
const indexPath = join14(this.baseDir, "cache", "recipes", "index.json");
|
|
7139
|
+
const atIdx = nameOrRef.indexOf("@");
|
|
7140
|
+
if (atIdx !== -1) {
|
|
7141
|
+
const hash2 = nameOrRef.slice(atIdx + 1);
|
|
7142
|
+
const objectPath2 = join14(this.baseDir, "cache", "recipes", "objects", hash2);
|
|
7143
|
+
if (!existsSync12(objectPath2)) return null;
|
|
7144
|
+
return { hash: hash2, content: await readFile11(objectPath2, "utf8") };
|
|
7145
|
+
}
|
|
7146
|
+
if (!existsSync12(indexPath)) return null;
|
|
7147
|
+
let index;
|
|
7148
|
+
try {
|
|
7149
|
+
index = JSON.parse(await readFile11(indexPath, "utf8"));
|
|
7150
|
+
} catch {
|
|
7151
|
+
return null;
|
|
7152
|
+
}
|
|
7153
|
+
const hash = index[nameOrRef];
|
|
7154
|
+
if (!hash) return null;
|
|
7155
|
+
const objectPath = join14(this.baseDir, "cache", "recipes", "objects", hash);
|
|
7156
|
+
if (!existsSync12(objectPath)) return null;
|
|
7157
|
+
return { hash, content: await readFile11(objectPath, "utf8") };
|
|
7158
|
+
}
|
|
7159
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
7160
|
+
// CLI-ARCH-HASH-CHAIN-HISTORY-01 — append-only NDJSON history with hash chain
|
|
7161
|
+
//
|
|
7162
|
+
// Shell history is stored as an append-only NDJSON file
|
|
7163
|
+
// ~/.msapling/history/shell_history.jsonl
|
|
7164
|
+
// Each entry carries a `prev_hash` field so tampering is detectable.
|
|
7165
|
+
// `msapling --verify-history` re-hashes the chain and reports breaks.
|
|
7166
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
7167
|
+
/**
|
|
7168
|
+
* Append a single command to the hash-chain history.
|
|
7169
|
+
* Uses lockfile + in-process mutex as a critical section around:
|
|
7170
|
+
* 1. Load last entry to compute prev_hash
|
|
7171
|
+
* 2. Build + serialize new entry
|
|
7172
|
+
* 3. Append to shell_history.jsonl
|
|
7173
|
+
*
|
|
7174
|
+
* WAL-PATTERN note: append-only log uses appendFile, not rename — the
|
|
7175
|
+
* lockfile is the write-serialisation primitive here.
|
|
7176
|
+
*/
|
|
7177
|
+
async appendHistoryEntry(content) {
|
|
7178
|
+
await this._ready;
|
|
7179
|
+
const path2 = join14(this.baseDir, "history", "shell_history.jsonl");
|
|
7180
|
+
return this.historyMutex.run(async () => {
|
|
7181
|
+
let release2 = null;
|
|
7182
|
+
try {
|
|
7183
|
+
if (!existsSync12(path2)) {
|
|
7184
|
+
await writeFile6(path2, "", "utf8");
|
|
7185
|
+
}
|
|
7186
|
+
release2 = await lockfile.lock(path2, { retries: 5, retryWait: 50 });
|
|
7187
|
+
let prevHash = null;
|
|
7188
|
+
let seq = 1;
|
|
7189
|
+
if (existsSync12(path2)) {
|
|
7190
|
+
const raw = (await readFile11(path2, "utf8")).trimEnd();
|
|
7191
|
+
if (raw.length > 0) {
|
|
7192
|
+
const lines = raw.split("\n");
|
|
7193
|
+
const lastLine = lines[lines.length - 1];
|
|
7194
|
+
try {
|
|
7195
|
+
const last = JSON.parse(lastLine);
|
|
7196
|
+
seq = last.seq + 1;
|
|
7197
|
+
prevHash = hashLine(lastLine);
|
|
7198
|
+
} catch {
|
|
7199
|
+
}
|
|
7200
|
+
}
|
|
7201
|
+
}
|
|
7202
|
+
const entry = {
|
|
7203
|
+
seq,
|
|
7204
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7205
|
+
content,
|
|
7206
|
+
prev_hash: prevHash
|
|
7207
|
+
};
|
|
7208
|
+
const line = JSON.stringify(entry);
|
|
7209
|
+
await appendFile(path2, line + "\n", "utf8");
|
|
7210
|
+
return entry;
|
|
7211
|
+
} finally {
|
|
7212
|
+
if (release2) {
|
|
7213
|
+
try {
|
|
7214
|
+
await lockfile.unlock(path2, { skipStale: true });
|
|
7215
|
+
} catch {
|
|
7216
|
+
}
|
|
7217
|
+
}
|
|
7218
|
+
}
|
|
7219
|
+
});
|
|
7220
|
+
}
|
|
7221
|
+
/**
|
|
7222
|
+
* Load all history entries from shell_history.jsonl.
|
|
7223
|
+
* Skips malformed lines (logs a warning for each).
|
|
7224
|
+
*
|
|
7225
|
+
* // TODO: implement tail-read for large history files to avoid loading entire file into memory
|
|
7226
|
+
*/
|
|
7227
|
+
async loadHistoryEntries() {
|
|
7228
|
+
await this._ready;
|
|
7229
|
+
const path2 = join14(this.baseDir, "history", "shell_history.jsonl");
|
|
7230
|
+
if (!existsSync12(path2)) return [];
|
|
7231
|
+
const raw = await readFile11(path2, "utf8");
|
|
7232
|
+
const entries = [];
|
|
7233
|
+
for (const line of raw.split("\n")) {
|
|
7234
|
+
if (!line.trim()) continue;
|
|
7235
|
+
try {
|
|
7236
|
+
entries.push(JSON.parse(line));
|
|
7237
|
+
} catch {
|
|
7238
|
+
console.warn(`[history] Skipped malformed NDJSON line: ${line.slice(0, 80)}`);
|
|
7239
|
+
}
|
|
7240
|
+
}
|
|
7241
|
+
return entries;
|
|
7242
|
+
}
|
|
7243
|
+
/**
|
|
7244
|
+
* Verify the hash chain of shell_history.jsonl.
|
|
7245
|
+
* Returns { ok: true } if intact, or { ok: false, breaks: [...] } listing
|
|
7246
|
+
* each break as { seq, expected, actual }.
|
|
7247
|
+
*
|
|
7248
|
+
* Used by `msapling --verify-history`.
|
|
7249
|
+
*/
|
|
7250
|
+
async verifyHistory() {
|
|
7251
|
+
const entries = await this.loadHistoryEntries();
|
|
7252
|
+
const breaks = [];
|
|
7253
|
+
let prevLine = null;
|
|
7254
|
+
for (const entry of entries) {
|
|
7255
|
+
if (prevLine !== null) {
|
|
7256
|
+
const expected = hashLine(prevLine);
|
|
7257
|
+
const actual = entry.prev_hash ?? "";
|
|
7258
|
+
if (expected !== actual) {
|
|
7259
|
+
breaks.push({ seq: entry.seq, expected, actual });
|
|
7260
|
+
}
|
|
7261
|
+
} else if (entry.prev_hash !== null) {
|
|
7262
|
+
breaks.push({ seq: entry.seq, expected: "null", actual: entry.prev_hash });
|
|
7263
|
+
}
|
|
7264
|
+
prevLine = JSON.stringify(entry);
|
|
7265
|
+
}
|
|
7266
|
+
return breaks.length === 0 ? { ok: true } : { ok: false, breaks };
|
|
7267
|
+
}
|
|
6898
7268
|
/**
|
|
6899
7269
|
* SYNC-01: Serialised history write with file-based locking.
|
|
6900
7270
|
* Uses proper-lockfile to coordinate writes across multiple terminal instances
|
|
@@ -6905,10 +7275,12 @@ var init_Storage = __esm({
|
|
|
6905
7275
|
* 2. Partial writes don't corrupt the JSON (atomic rename)
|
|
6906
7276
|
*/
|
|
6907
7277
|
async saveHistory(history) {
|
|
7278
|
+
await this._ready;
|
|
6908
7279
|
const path2 = join14(this.baseDir, "history", "shell_history.json");
|
|
6909
7280
|
let release2;
|
|
6910
7281
|
try {
|
|
6911
|
-
|
|
7282
|
+
if (!existsSync12(path2)) writeFileSync(path2, "[]", "utf8");
|
|
7283
|
+
release2 = await lockfile.lock(path2, { realpath: false, retries: 5, retryWait: 50 });
|
|
6912
7284
|
await this.historyMutex.run(async () => {
|
|
6913
7285
|
const tmpPath = `${path2}.tmp`;
|
|
6914
7286
|
const content = JSON.stringify(history, null, 2);
|
|
@@ -6940,16 +7312,38 @@ var init_Storage = __esm({
|
|
|
6940
7312
|
* SYNC-01: Serialised history read with file-based locking.
|
|
6941
7313
|
* Reads are gated behind the file lock so a read that overlaps with an
|
|
6942
7314
|
* in-progress write from another process always sees a complete, valid JSON.
|
|
7315
|
+
*
|
|
7316
|
+
* // TODO: implement tail-read for large history files to avoid loading entire file into memory
|
|
6943
7317
|
*/
|
|
6944
7318
|
async loadHistory() {
|
|
7319
|
+
await this._ready;
|
|
6945
7320
|
const path2 = join14(this.baseDir, "history", "shell_history.json");
|
|
7321
|
+
if (!existsSync12(path2)) return [];
|
|
6946
7322
|
let release2;
|
|
6947
7323
|
try {
|
|
6948
|
-
release2 = await lockfile.lock(path2, { retries: 5, retryWait: 50 });
|
|
7324
|
+
release2 = await lockfile.lock(path2, { realpath: false, retries: 5, retryWait: 50 });
|
|
6949
7325
|
return this.historyMutex.run(async () => {
|
|
6950
7326
|
if (existsSync12(path2)) {
|
|
6951
7327
|
const text = await readFile11(path2, "utf8");
|
|
6952
|
-
|
|
7328
|
+
try {
|
|
7329
|
+
return JSON.parse(text);
|
|
7330
|
+
} catch (parseErr) {
|
|
7331
|
+
const filename = path2.split("/").pop() || "shell_history.json";
|
|
7332
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7333
|
+
const suffix = randomBytes7(4).toString("hex");
|
|
7334
|
+
const corruptBackupPath = join14(
|
|
7335
|
+
this.baseDir,
|
|
7336
|
+
"history",
|
|
7337
|
+
`${filename}.corrupt.${stamp}-${suffix}.bak`
|
|
7338
|
+
);
|
|
7339
|
+
try {
|
|
7340
|
+
renameSync(path2, corruptBackupPath);
|
|
7341
|
+
console.warn(`History file was corrupt; backed up to ${corruptBackupPath}`);
|
|
7342
|
+
} catch (backupErr) {
|
|
7343
|
+
console.error(`Failed to backup corrupt history file: ${backupErr}`);
|
|
7344
|
+
}
|
|
7345
|
+
return [];
|
|
7346
|
+
}
|
|
6953
7347
|
}
|
|
6954
7348
|
return [];
|
|
6955
7349
|
});
|
|
@@ -6965,12 +7359,14 @@ var init_Storage = __esm({
|
|
|
6965
7359
|
}
|
|
6966
7360
|
/**
|
|
6967
7361
|
* SYNC-01: Serialised permissions write with file-based locking.
|
|
7362
|
+
* R20-CLI-2: Typed with PermissionState from Sandbox.ts.
|
|
6968
7363
|
*/
|
|
6969
7364
|
async savePermissions(permissions) {
|
|
6970
7365
|
const path2 = join14(this.baseDir, "vault", "permissions.json");
|
|
6971
7366
|
let release2;
|
|
6972
7367
|
try {
|
|
6973
|
-
|
|
7368
|
+
if (!existsSync12(path2)) writeFileSync(path2, "{}", "utf8");
|
|
7369
|
+
release2 = await lockfile.lock(path2, { realpath: false, retries: 5, retryWait: 50 });
|
|
6974
7370
|
await this.permissionsMutex.run(async () => {
|
|
6975
7371
|
const tmpPath = `${path2}.tmp`;
|
|
6976
7372
|
const content = JSON.stringify(permissions, null, 2);
|
|
@@ -7000,16 +7396,36 @@ var init_Storage = __esm({
|
|
|
7000
7396
|
}
|
|
7001
7397
|
/**
|
|
7002
7398
|
* SYNC-01: Serialised permissions read with file-based locking.
|
|
7399
|
+
* R20-CLI-2: Typed with PermissionState from Sandbox.ts.
|
|
7003
7400
|
*/
|
|
7004
7401
|
async loadPermissions() {
|
|
7005
7402
|
const path2 = join14(this.baseDir, "vault", "permissions.json");
|
|
7403
|
+
if (!existsSync12(path2)) return { trustedCommands: [], trustedPaths: [] };
|
|
7006
7404
|
let release2;
|
|
7007
7405
|
try {
|
|
7008
|
-
release2 = await lockfile.lock(path2, { retries: 5, retryWait: 50 });
|
|
7406
|
+
release2 = await lockfile.lock(path2, { realpath: false, retries: 5, retryWait: 50 });
|
|
7009
7407
|
return this.permissionsMutex.run(async () => {
|
|
7010
7408
|
if (existsSync12(path2)) {
|
|
7011
7409
|
const text = await readFile11(path2, "utf8");
|
|
7012
|
-
|
|
7410
|
+
try {
|
|
7411
|
+
return JSON.parse(text);
|
|
7412
|
+
} catch (parseErr) {
|
|
7413
|
+
const filename = path2.split("/").pop() || "permissions.json";
|
|
7414
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7415
|
+
const suffix = randomBytes7(4).toString("hex");
|
|
7416
|
+
const corruptBackupPath = join14(
|
|
7417
|
+
this.baseDir,
|
|
7418
|
+
"vault",
|
|
7419
|
+
`${filename}.corrupt.${stamp}-${suffix}.bak`
|
|
7420
|
+
);
|
|
7421
|
+
try {
|
|
7422
|
+
renameSync(path2, corruptBackupPath);
|
|
7423
|
+
console.warn(`Permissions file was corrupt; backed up to ${corruptBackupPath}`);
|
|
7424
|
+
} catch (backupErr) {
|
|
7425
|
+
console.error(`Failed to backup corrupt permissions file: ${backupErr}`);
|
|
7426
|
+
}
|
|
7427
|
+
return { trustedCommands: [], trustedPaths: [] };
|
|
7428
|
+
}
|
|
7013
7429
|
}
|
|
7014
7430
|
return { trustedCommands: [], trustedPaths: [] };
|
|
7015
7431
|
});
|
|
@@ -7051,6 +7467,7 @@ import { join as join15 } from "path";
|
|
|
7051
7467
|
import { existsSync as existsSync13 } from "fs";
|
|
7052
7468
|
import * as fs from "fs";
|
|
7053
7469
|
import { readFile as readFile12 } from "fs/promises";
|
|
7470
|
+
import { randomBytes as randomBytes8 } from "crypto";
|
|
7054
7471
|
function ensureConfigDir(p) {
|
|
7055
7472
|
try {
|
|
7056
7473
|
fs.mkdirSync(p, { recursive: true, mode: 448 });
|
|
@@ -7058,7 +7475,9 @@ function ensureConfigDir(p) {
|
|
|
7058
7475
|
if (e.code === "EEXIST" || e.code === "ENOTDIR") {
|
|
7059
7476
|
const stat5 = fs.statSync(p);
|
|
7060
7477
|
if (!stat5.isDirectory()) {
|
|
7061
|
-
|
|
7478
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
7479
|
+
const suffix = randomBytes8(4).toString("hex");
|
|
7480
|
+
fs.renameSync(p, `${p}.broken-${stamp}-${suffix}`);
|
|
7062
7481
|
fs.mkdirSync(p, { recursive: true, mode: 448 });
|
|
7063
7482
|
}
|
|
7064
7483
|
} else {
|
|
@@ -7508,26 +7927,31 @@ function isEmailAddress(str) {
|
|
|
7508
7927
|
function isTokenLike(str) {
|
|
7509
7928
|
return str.length >= 20 && /^[A-Za-z0-9_\-\.]+$/.test(str);
|
|
7510
7929
|
}
|
|
7930
|
+
function setRawModeGuarded(stdin, mode) {
|
|
7931
|
+
try {
|
|
7932
|
+
if (typeof stdin.setRawMode === "function") {
|
|
7933
|
+
stdin.setRawMode(mode);
|
|
7934
|
+
}
|
|
7935
|
+
} catch (e) {
|
|
7936
|
+
}
|
|
7937
|
+
}
|
|
7511
7938
|
async function promptPassword(prompt) {
|
|
7512
7939
|
return new Promise((resolve18) => {
|
|
7513
7940
|
const stdin = process.stdin;
|
|
7514
7941
|
const stdout = process.stdout;
|
|
7515
7942
|
stdout.write(prompt);
|
|
7516
|
-
const wasRaw = stdin.isRaw;
|
|
7517
|
-
|
|
7518
|
-
stdin.setRawMode(true);
|
|
7519
|
-
} catch (e) {
|
|
7520
|
-
}
|
|
7943
|
+
const wasRaw = stdin.isRaw ?? false;
|
|
7944
|
+
setRawModeGuarded(stdin, true);
|
|
7521
7945
|
let password = "";
|
|
7522
7946
|
const onData = (chunk) => {
|
|
7523
7947
|
const char = chunk.toString();
|
|
7524
7948
|
if (char === "\n" || char === "\r") {
|
|
7525
|
-
stdin
|
|
7949
|
+
setRawModeGuarded(stdin, wasRaw);
|
|
7526
7950
|
stdin.removeListener("data", onData);
|
|
7527
7951
|
stdout.write("\n");
|
|
7528
7952
|
resolve18(password);
|
|
7529
7953
|
} else if (char === "") {
|
|
7530
|
-
stdin
|
|
7954
|
+
setRawModeGuarded(stdin, wasRaw);
|
|
7531
7955
|
stdin.removeListener("data", onData);
|
|
7532
7956
|
stdout.write("\n");
|
|
7533
7957
|
resolve18("");
|
|
@@ -7545,11 +7969,8 @@ async function promptTotp(prompt) {
|
|
|
7545
7969
|
const stdin = process.stdin;
|
|
7546
7970
|
const stdout = process.stdout;
|
|
7547
7971
|
stdout.write(prompt);
|
|
7548
|
-
const wasRaw = stdin.isRaw;
|
|
7549
|
-
|
|
7550
|
-
stdin.setRawMode(true);
|
|
7551
|
-
} catch (e) {
|
|
7552
|
-
}
|
|
7972
|
+
const wasRaw = stdin.isRaw ?? false;
|
|
7973
|
+
setRawModeGuarded(stdin, true);
|
|
7553
7974
|
let code = "";
|
|
7554
7975
|
const onData = (chunk) => {
|
|
7555
7976
|
const char = chunk.toString();
|
|
@@ -7559,7 +7980,7 @@ async function promptTotp(prompt) {
|
|
|
7559
7980
|
stdout.write("\n");
|
|
7560
7981
|
resolve18(code);
|
|
7561
7982
|
} else if (char === "") {
|
|
7562
|
-
stdin
|
|
7983
|
+
setRawModeGuarded(stdin, wasRaw);
|
|
7563
7984
|
stdin.removeListener("data", onData);
|
|
7564
7985
|
stdout.write("\n");
|
|
7565
7986
|
resolve18("");
|
|
@@ -7647,7 +8068,7 @@ async function loginWithGithubDevice(context) {
|
|
|
7647
8068
|
`Open ${verification_uri} in your browser and enter code: ${user_code}
|
|
7648
8069
|
(Waiting for authorization \u2014 expires in ${expires_in}s)`
|
|
7649
8070
|
);
|
|
7650
|
-
|
|
8071
|
+
let pollMs = (interval + 1) * 1e3;
|
|
7651
8072
|
const deadline = Date.now() + expires_in * 1e3;
|
|
7652
8073
|
let githubToken = null;
|
|
7653
8074
|
while (Date.now() < deadline) {
|
|
@@ -7674,6 +8095,7 @@ async function loginWithGithubDevice(context) {
|
|
|
7674
8095
|
}
|
|
7675
8096
|
if (tokenData.error === "authorization_pending") continue;
|
|
7676
8097
|
if (tokenData.error === "slow_down") {
|
|
8098
|
+
pollMs += 5e3;
|
|
7677
8099
|
await new Promise((resolve18) => setTimeout(resolve18, 5e3));
|
|
7678
8100
|
continue;
|
|
7679
8101
|
}
|
|
@@ -7714,7 +8136,7 @@ var init_login = __esm({
|
|
|
7714
8136
|
init_esm_shims();
|
|
7715
8137
|
GITHUB_CLIENT_ID = "Ov23liOA9yKFLUEEVY3G";
|
|
7716
8138
|
GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
|
|
7717
|
-
GITHUB_TOKEN_URL = "https://github.com/oauth/access_token";
|
|
8139
|
+
GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
|
|
7718
8140
|
loginCommand = {
|
|
7719
8141
|
name: "login",
|
|
7720
8142
|
args: "[email|github|token]",
|
|
@@ -8547,6 +8969,7 @@ var init_recipe = __esm({
|
|
|
8547
8969
|
"src/commands/recipe.ts"() {
|
|
8548
8970
|
"use strict";
|
|
8549
8971
|
init_esm_shims();
|
|
8972
|
+
init_src3();
|
|
8550
8973
|
RECIPE_DIRS = [".msapling/recipes", ".claude/recipes", ".gemini/recipes"];
|
|
8551
8974
|
NAME_SUFFIXES = ["", "-workflow"];
|
|
8552
8975
|
FILE_EXTS = [".yaml", ".yml"];
|
|
@@ -8580,14 +9003,21 @@ var init_recipe = __esm({
|
|
|
8580
9003
|
context.addMessage("system", `Usage: /recipe ${name} <prompt> (the prompt fills $SELECTION / $PROMPT in step templates)`);
|
|
8581
9004
|
return;
|
|
8582
9005
|
}
|
|
9006
|
+
let text;
|
|
8583
9007
|
let recipe;
|
|
8584
9008
|
try {
|
|
8585
|
-
|
|
9009
|
+
text = await readFile16(path2, "utf8");
|
|
8586
9010
|
recipe = parseYaml(text);
|
|
8587
9011
|
} catch (e) {
|
|
8588
9012
|
context.addMessage("error", `Failed to load ${path2}: ${e.message}`);
|
|
8589
9013
|
return;
|
|
8590
9014
|
}
|
|
9015
|
+
try {
|
|
9016
|
+
const storage = new StorageManager();
|
|
9017
|
+
const hash = await storage.registerRecipe(name, text);
|
|
9018
|
+
context.addMessage("system", `[recipe] Resolved ${name} \u2192 recipe@${hash.slice(0, 12)}\u2026`);
|
|
9019
|
+
} catch {
|
|
9020
|
+
}
|
|
8591
9021
|
const steps = recipe.steps ?? [];
|
|
8592
9022
|
if (steps.length === 0) {
|
|
8593
9023
|
context.addMessage("system", `Recipe '${name}' has no steps.`);
|
|
@@ -9721,6 +10151,207 @@ var init_todo = __esm({
|
|
|
9721
10151
|
}
|
|
9722
10152
|
});
|
|
9723
10153
|
|
|
10154
|
+
// src/commands/outputStyle.ts
|
|
10155
|
+
import { homedir as homedir15 } from "os";
|
|
10156
|
+
import { join as join27, basename, extname as extname3 } from "path";
|
|
10157
|
+
import { existsSync as existsSync24, mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync, writeFileSync as writeFileSync2 } from "fs";
|
|
10158
|
+
function stylesDir() {
|
|
10159
|
+
return join27(homedir15(), ".msapling", "output-styles");
|
|
10160
|
+
}
|
|
10161
|
+
function activeFile() {
|
|
10162
|
+
return join27(stylesDir(), ".active");
|
|
10163
|
+
}
|
|
10164
|
+
function parseStyleFile(text) {
|
|
10165
|
+
const fm = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
|
|
10166
|
+
if (!fm) {
|
|
10167
|
+
const lines = text.split(/\r?\n/);
|
|
10168
|
+
for (const line of lines) {
|
|
10169
|
+
if (!line.trim()) continue;
|
|
10170
|
+
const desc = line.replace(/^#\s+/, "").trim();
|
|
10171
|
+
return { description: desc || "(user style)", body: text.trim() };
|
|
10172
|
+
}
|
|
10173
|
+
return { description: "(user style)", body: text.trim() };
|
|
10174
|
+
}
|
|
10175
|
+
const meta = fm[1];
|
|
10176
|
+
const body = text.slice(fm[0].length).trim();
|
|
10177
|
+
const descMatch = meta.match(/^description:\s*(.+)$/m);
|
|
10178
|
+
const description = descMatch ? descMatch[1].trim().replace(/^['"]|['"]$/g, "") : "(user style)";
|
|
10179
|
+
return { description, body };
|
|
10180
|
+
}
|
|
10181
|
+
function listUserStyles() {
|
|
10182
|
+
const dir = stylesDir();
|
|
10183
|
+
if (!existsSync24(dir)) return [];
|
|
10184
|
+
const out = [];
|
|
10185
|
+
for (const entry of readdirSync3(dir)) {
|
|
10186
|
+
if (extname3(entry).toLowerCase() !== ".md") continue;
|
|
10187
|
+
const full = join27(dir, entry);
|
|
10188
|
+
try {
|
|
10189
|
+
const text = readFileSync(full, "utf8");
|
|
10190
|
+
const { description, body } = parseStyleFile(text);
|
|
10191
|
+
out.push({
|
|
10192
|
+
name: basename(entry, ".md"),
|
|
10193
|
+
description,
|
|
10194
|
+
body,
|
|
10195
|
+
source: "user",
|
|
10196
|
+
path: full
|
|
10197
|
+
});
|
|
10198
|
+
} catch {
|
|
10199
|
+
}
|
|
10200
|
+
}
|
|
10201
|
+
return out;
|
|
10202
|
+
}
|
|
10203
|
+
function listStyles() {
|
|
10204
|
+
const user = listUserStyles();
|
|
10205
|
+
const userNames = new Set(user.map((s) => s.name));
|
|
10206
|
+
const builtins = BUILTIN_STYLES.filter((s) => !userNames.has(s.name));
|
|
10207
|
+
return [...builtins, ...user];
|
|
10208
|
+
}
|
|
10209
|
+
function findStyle(name) {
|
|
10210
|
+
return listStyles().find((s) => s.name === name) ?? null;
|
|
10211
|
+
}
|
|
10212
|
+
function getActiveStyleName() {
|
|
10213
|
+
try {
|
|
10214
|
+
const f = activeFile();
|
|
10215
|
+
if (!existsSync24(f)) return "default";
|
|
10216
|
+
return readFileSync(f, "utf8").trim() || "default";
|
|
10217
|
+
} catch {
|
|
10218
|
+
return "default";
|
|
10219
|
+
}
|
|
10220
|
+
}
|
|
10221
|
+
function setActiveStyleName(name) {
|
|
10222
|
+
const dir = stylesDir();
|
|
10223
|
+
if (!existsSync24(dir)) mkdirSync5(dir, { recursive: true });
|
|
10224
|
+
writeFileSync2(activeFile(), `${name}
|
|
10225
|
+
`, "utf8");
|
|
10226
|
+
}
|
|
10227
|
+
function getActiveStyle() {
|
|
10228
|
+
const name = getActiveStyleName();
|
|
10229
|
+
return findStyle(name) ?? BUILTIN_STYLES[0];
|
|
10230
|
+
}
|
|
10231
|
+
function createUserStyle(name, description, body) {
|
|
10232
|
+
if (!/^[a-z0-9][a-z0-9_-]*$/i.test(name)) {
|
|
10233
|
+
throw new Error(`Invalid style name "${name}" \u2014 use letters, digits, _ and - only.`);
|
|
10234
|
+
}
|
|
10235
|
+
const dir = stylesDir();
|
|
10236
|
+
if (!existsSync24(dir)) mkdirSync5(dir, { recursive: true });
|
|
10237
|
+
const target = join27(dir, `${name}.md`);
|
|
10238
|
+
const frontmatter = `---
|
|
10239
|
+
description: ${description.replace(/\n/g, " ")}
|
|
10240
|
+
---
|
|
10241
|
+
|
|
10242
|
+
`;
|
|
10243
|
+
writeFileSync2(target, frontmatter + body.trim() + "\n", "utf8");
|
|
10244
|
+
return target;
|
|
10245
|
+
}
|
|
10246
|
+
var BUILTIN_STYLES, outputStyleCommand;
|
|
10247
|
+
var init_outputStyle = __esm({
|
|
10248
|
+
"src/commands/outputStyle.ts"() {
|
|
10249
|
+
"use strict";
|
|
10250
|
+
init_esm_shims();
|
|
10251
|
+
BUILTIN_STYLES = [
|
|
10252
|
+
{
|
|
10253
|
+
name: "default",
|
|
10254
|
+
description: "Default MSapling behavior \u2014 no extra system prefix.",
|
|
10255
|
+
body: "",
|
|
10256
|
+
source: "builtin",
|
|
10257
|
+
path: null
|
|
10258
|
+
},
|
|
10259
|
+
{
|
|
10260
|
+
name: "concise",
|
|
10261
|
+
description: "Terse, code-first answers; minimal prose.",
|
|
10262
|
+
body: "You are MSapling in concise mode. Keep every response as short as possible.\nPrefer code blocks, bullet lists, and direct answers. Skip preamble, recap,\nand offers to elaborate unless explicitly asked.",
|
|
10263
|
+
source: "builtin",
|
|
10264
|
+
path: null
|
|
10265
|
+
},
|
|
10266
|
+
{
|
|
10267
|
+
name: "explanatory",
|
|
10268
|
+
description: "Walk through reasoning and trade-offs before the answer.",
|
|
10269
|
+
body: "You are MSapling in explanatory mode. Briefly walk through your reasoning,\npoint out trade-offs, and cite the relevant file or doc before giving the\nfinal answer. Aim for 2\u20133 short paragraphs, then a clear conclusion.",
|
|
10270
|
+
source: "builtin",
|
|
10271
|
+
path: null
|
|
10272
|
+
},
|
|
10273
|
+
{
|
|
10274
|
+
name: "learning",
|
|
10275
|
+
description: "Teach-by-doing: explain concepts, ask checkpoint questions.",
|
|
10276
|
+
body: "You are MSapling in learning mode. Assume the user is new to the topic.\nExplain key concepts in plain language, surface common pitfalls, and end\nwith a one-sentence comprehension check the user can answer or skip.",
|
|
10277
|
+
source: "builtin",
|
|
10278
|
+
path: null
|
|
10279
|
+
}
|
|
10280
|
+
];
|
|
10281
|
+
outputStyleCommand = {
|
|
10282
|
+
name: "output-style",
|
|
10283
|
+
aliases: ["style"],
|
|
10284
|
+
args: "[list|use <name>|new <name> <description>]",
|
|
10285
|
+
description: "List, switch, or create assistant output styles",
|
|
10286
|
+
category: "model",
|
|
10287
|
+
handler: (args2, context) => {
|
|
10288
|
+
const sub = (args2[0] ?? "list").toLowerCase();
|
|
10289
|
+
if (sub === "list" || sub === "ls") {
|
|
10290
|
+
const styles = listStyles();
|
|
10291
|
+
const active = getActiveStyleName();
|
|
10292
|
+
context.addMessage("system", "Output Styles:");
|
|
10293
|
+
for (const s of styles) {
|
|
10294
|
+
const marker = s.name === active ? "*" : " ";
|
|
10295
|
+
const tag = s.source === "builtin" ? "[builtin]" : "[user]";
|
|
10296
|
+
context.addMessage("system", ` ${marker} ${s.name.padEnd(14)} ${tag} ${s.description}`);
|
|
10297
|
+
}
|
|
10298
|
+
context.addMessage("system", `Active: ${active}`);
|
|
10299
|
+
return;
|
|
10300
|
+
}
|
|
10301
|
+
if (sub === "use") {
|
|
10302
|
+
const name = args2[1];
|
|
10303
|
+
if (!name) {
|
|
10304
|
+
context.addMessage("error", "Usage: /output-style use <name>");
|
|
10305
|
+
return;
|
|
10306
|
+
}
|
|
10307
|
+
const style = findStyle(name);
|
|
10308
|
+
if (!style) {
|
|
10309
|
+
context.addMessage("error", `Unknown style: ${name}. Try /output-style list.`);
|
|
10310
|
+
return;
|
|
10311
|
+
}
|
|
10312
|
+
setActiveStyleName(name);
|
|
10313
|
+
context.addMessage("system", `Active output style: ${name} \u2014 ${style.description}`);
|
|
10314
|
+
return;
|
|
10315
|
+
}
|
|
10316
|
+
if (sub === "new" || sub === "create") {
|
|
10317
|
+
const name = args2[1];
|
|
10318
|
+
const description = args2.slice(2).join(" ");
|
|
10319
|
+
if (!name) {
|
|
10320
|
+
context.addMessage("error", "Usage: /output-style new <name> <description>");
|
|
10321
|
+
return;
|
|
10322
|
+
}
|
|
10323
|
+
try {
|
|
10324
|
+
const path2 = createUserStyle(name, description || `User style: ${name}`, "");
|
|
10325
|
+
context.addMessage(
|
|
10326
|
+
"system",
|
|
10327
|
+
`Created ${path2} \u2014 edit the file to define the system prompt body, then /output-style use ${name}`
|
|
10328
|
+
);
|
|
10329
|
+
} catch (e) {
|
|
10330
|
+
context.addMessage("error", e.message ?? String(e));
|
|
10331
|
+
}
|
|
10332
|
+
return;
|
|
10333
|
+
}
|
|
10334
|
+
if (sub === "show") {
|
|
10335
|
+
const style = getActiveStyle();
|
|
10336
|
+
context.addMessage("system", `Active style: ${style.name} (${style.source})`);
|
|
10337
|
+
context.addMessage("system", `Description: ${style.description}`);
|
|
10338
|
+
if (style.body) {
|
|
10339
|
+
context.addMessage("system", "\u2500\u2500\u2500 body \u2500\u2500\u2500");
|
|
10340
|
+
context.addMessage("system", style.body);
|
|
10341
|
+
} else {
|
|
10342
|
+
context.addMessage("system", "(no system-prompt body \u2014 default behavior)");
|
|
10343
|
+
}
|
|
10344
|
+
return;
|
|
10345
|
+
}
|
|
10346
|
+
context.addMessage(
|
|
10347
|
+
"error",
|
|
10348
|
+
`Unknown subcommand: ${sub}. Try /output-style list|use|new|show`
|
|
10349
|
+
);
|
|
10350
|
+
}
|
|
10351
|
+
};
|
|
10352
|
+
}
|
|
10353
|
+
});
|
|
10354
|
+
|
|
9724
10355
|
// src/commands/index.ts
|
|
9725
10356
|
var commands_exports = {};
|
|
9726
10357
|
__export(commands_exports, {
|
|
@@ -9767,6 +10398,7 @@ var init_commands = __esm({
|
|
|
9767
10398
|
init_plan();
|
|
9768
10399
|
init_note();
|
|
9769
10400
|
init_todo();
|
|
10401
|
+
init_outputStyle();
|
|
9770
10402
|
commands = [
|
|
9771
10403
|
loginCommand,
|
|
9772
10404
|
unlockCommand,
|
|
@@ -9798,7 +10430,52 @@ var init_commands = __esm({
|
|
|
9798
10430
|
shortcutsCommand,
|
|
9799
10431
|
planCommand,
|
|
9800
10432
|
noteCommand,
|
|
9801
|
-
todoCommand
|
|
10433
|
+
todoCommand,
|
|
10434
|
+
outputStyleCommand
|
|
10435
|
+
];
|
|
10436
|
+
}
|
|
10437
|
+
});
|
|
10438
|
+
|
|
10439
|
+
// src/runtime/doctorRedact.ts
|
|
10440
|
+
function redactSecrets(text) {
|
|
10441
|
+
let out = text;
|
|
10442
|
+
for (const [re, replacement] of PATTERNS) {
|
|
10443
|
+
out = out.replace(re, replacement);
|
|
10444
|
+
}
|
|
10445
|
+
return out;
|
|
10446
|
+
}
|
|
10447
|
+
var PATTERNS;
|
|
10448
|
+
var init_doctorRedact = __esm({
|
|
10449
|
+
"src/runtime/doctorRedact.ts"() {
|
|
10450
|
+
"use strict";
|
|
10451
|
+
init_esm_shims();
|
|
10452
|
+
PATTERNS = [
|
|
10453
|
+
// Legacy key=value pairs (from original doctor.ts redactSecrets).
|
|
10454
|
+
[/token[=:]\s*['"]?[a-zA-Z0-9_.-]+['"]?/gi, "token=***"],
|
|
10455
|
+
[/password[=:]\s*['"]?[a-zA-Z0-9_.-]+['"]?/gi, "password=***"],
|
|
10456
|
+
[/api[_-]?key[=:]\s*['"]?[a-zA-Z0-9_.-]+['"]?/gi, "api_key=***"],
|
|
10457
|
+
[/secret[=:]\s*['"]?[a-zA-Z0-9_.-]+['"]?/gi, "secret=***"],
|
|
10458
|
+
[/MSAPLING_TOKEN=.*/gi, "MSAPLING_TOKEN=***"],
|
|
10459
|
+
[/MSAPLING_API_KEY=.*/gi, "MSAPLING_API_KEY=***"],
|
|
10460
|
+
// Bearer tokens (case-insensitive).
|
|
10461
|
+
[/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer ***"],
|
|
10462
|
+
// GitHub PATs — keep the prefix so the credential class is identifiable.
|
|
10463
|
+
[/\bghp_[A-Za-z0-9]{20,}/g, "ghp_***"],
|
|
10464
|
+
[/\bghs_[A-Za-z0-9]{20,}/g, "ghs_***"],
|
|
10465
|
+
[/\bgho_[A-Za-z0-9]{20,}/g, "gho_***"],
|
|
10466
|
+
[/\bghu_[A-Za-z0-9]{20,}/g, "ghu_***"],
|
|
10467
|
+
[/\bghr_[A-Za-z0-9]{20,}/g, "ghr_***"],
|
|
10468
|
+
[/\bgithub_pat_[A-Za-z0-9_]{20,}/g, "github_pat_***"],
|
|
10469
|
+
// AWS access key IDs.
|
|
10470
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, "AKIA***"],
|
|
10471
|
+
[/\bASIA[0-9A-Z]{16}\b/g, "ASIA***"],
|
|
10472
|
+
// Slack tokens (xoxa/xoxb/xoxp/xoxr/xoxs/xoxe-...).
|
|
10473
|
+
[/\bxox[abpres]-[A-Za-z0-9-]{10,}/gi, "xox-***"],
|
|
10474
|
+
// Anthropic + OpenAI style. Anthropic first to preserve "sk-ant-" prefix.
|
|
10475
|
+
[/\bsk-ant-[A-Za-z0-9_-]{20,}/g, "sk-ant-***"],
|
|
10476
|
+
[/\bsk-[A-Za-z0-9_-]{20,}/g, "sk-***"],
|
|
10477
|
+
// JWT-ish payload: three base64url segments separated by dots, header starts eyJ.
|
|
10478
|
+
[/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/g, "***jwt***"]
|
|
9802
10479
|
];
|
|
9803
10480
|
}
|
|
9804
10481
|
});
|
|
@@ -9808,15 +10485,12 @@ var doctor_exports = {};
|
|
|
9808
10485
|
__export(doctor_exports, {
|
|
9809
10486
|
runDoctor: () => runDoctor
|
|
9810
10487
|
});
|
|
9811
|
-
import { homedir as
|
|
9812
|
-
import { join as
|
|
9813
|
-
import { existsSync as
|
|
10488
|
+
import { homedir as homedir16, platform as platform3 } from "os";
|
|
10489
|
+
import { join as join28 } from "path";
|
|
10490
|
+
import { existsSync as existsSync26, statSync as statSync6, accessSync } from "fs";
|
|
9814
10491
|
import { readdir as readdir3 } from "fs/promises";
|
|
9815
10492
|
import { exec } from "child_process";
|
|
9816
10493
|
import { promisify } from "util";
|
|
9817
|
-
function redactSecrets(text) {
|
|
9818
|
-
return text.replace(/token[=:]\s*['"]?[a-zA-Z0-9_-]+['"]?/gi, "token=***").replace(/password[=:]\s*['"]?[a-zA-Z0-9_-]+['"]?/gi, "password=***").replace(/api[_-]?key[=:]\s*['"]?[a-zA-Z0-9_-]+['"]?/gi, "api_key=***").replace(/secret[=:]\s*['"]?[a-zA-Z0-9_-]+['"]?/gi, "secret=***").replace(/MSAPLING_TOKEN=.*/gi, "MSAPLING_TOKEN=***").replace(/MSAPLING_API_KEY=.*/gi, "MSAPLING_API_KEY=***");
|
|
9819
|
-
}
|
|
9820
10494
|
async function checkNodeVersion() {
|
|
9821
10495
|
const version = process.version;
|
|
9822
10496
|
const match = version.match(/v(\d+)/);
|
|
@@ -9836,8 +10510,8 @@ async function checkNodeVersion() {
|
|
|
9836
10510
|
};
|
|
9837
10511
|
}
|
|
9838
10512
|
async function checkConfigDir() {
|
|
9839
|
-
const configDir =
|
|
9840
|
-
if (!
|
|
10513
|
+
const configDir = join28(homedir16(), ".msapling");
|
|
10514
|
+
if (!existsSync26(configDir)) {
|
|
9841
10515
|
return {
|
|
9842
10516
|
name: "Config directory",
|
|
9843
10517
|
status: "WARN",
|
|
@@ -9854,7 +10528,7 @@ async function checkConfigDir() {
|
|
|
9854
10528
|
remediation: `rm "${configDir}" && mkdir -p "${configDir}"`
|
|
9855
10529
|
};
|
|
9856
10530
|
}
|
|
9857
|
-
if (
|
|
10531
|
+
if (platform3() !== "win32") {
|
|
9858
10532
|
const mode = stats.mode & 511;
|
|
9859
10533
|
const safe = (mode & 63) === 0;
|
|
9860
10534
|
if (!safe) {
|
|
@@ -9900,15 +10574,15 @@ async function checkKeytar() {
|
|
|
9900
10574
|
}
|
|
9901
10575
|
async function checkPathConflicts() {
|
|
9902
10576
|
const pathEnv = process.env.PATH || "";
|
|
9903
|
-
const paths = pathEnv.split(
|
|
10577
|
+
const paths = pathEnv.split(platform3() === "win32" ? ";" : ":");
|
|
9904
10578
|
const conflicts = [];
|
|
9905
10579
|
for (const dir of paths) {
|
|
9906
|
-
if (!dir || !
|
|
10580
|
+
if (!dir || !existsSync26(dir)) continue;
|
|
9907
10581
|
try {
|
|
9908
10582
|
const files = await readdir3(dir);
|
|
9909
10583
|
for (const file of files) {
|
|
9910
10584
|
if (file === "msapling" || file === "msapling.exe" || file === "msapling.py") {
|
|
9911
|
-
const fullPath =
|
|
10585
|
+
const fullPath = join28(dir, file);
|
|
9912
10586
|
conflicts.push(fullPath);
|
|
9913
10587
|
}
|
|
9914
10588
|
}
|
|
@@ -10008,11 +10682,11 @@ async function checkTokenValidity() {
|
|
|
10008
10682
|
}
|
|
10009
10683
|
}
|
|
10010
10684
|
async function checkOsSpecific() {
|
|
10011
|
-
if (
|
|
10685
|
+
if (platform3() === "win32") {
|
|
10012
10686
|
try {
|
|
10013
|
-
const configDir =
|
|
10687
|
+
const configDir = join28(homedir16(), ".msapling");
|
|
10014
10688
|
const longPath = "A".repeat(260);
|
|
10015
|
-
const testPath =
|
|
10689
|
+
const testPath = join28(configDir, longPath);
|
|
10016
10690
|
try {
|
|
10017
10691
|
accessSync(configDir);
|
|
10018
10692
|
} catch {
|
|
@@ -10037,14 +10711,14 @@ async function checkOsSpecific() {
|
|
|
10037
10711
|
};
|
|
10038
10712
|
}
|
|
10039
10713
|
}
|
|
10040
|
-
if (
|
|
10714
|
+
if (platform3() === "darwin") {
|
|
10041
10715
|
return {
|
|
10042
10716
|
name: "OS-specific (macOS)",
|
|
10043
10717
|
status: "PASS",
|
|
10044
10718
|
message: "macOS detected"
|
|
10045
10719
|
};
|
|
10046
10720
|
}
|
|
10047
|
-
if (
|
|
10721
|
+
if (platform3() === "linux") {
|
|
10048
10722
|
try {
|
|
10049
10723
|
await import("keytar");
|
|
10050
10724
|
return {
|
|
@@ -10064,7 +10738,7 @@ async function checkOsSpecific() {
|
|
|
10064
10738
|
return {
|
|
10065
10739
|
name: "OS-specific",
|
|
10066
10740
|
status: "PASS",
|
|
10067
|
-
message: `${
|
|
10741
|
+
message: `${platform3()} detected`
|
|
10068
10742
|
};
|
|
10069
10743
|
}
|
|
10070
10744
|
function formatCheckResult(result, maxLabelWidth) {
|
|
@@ -10128,14 +10802,22 @@ async function runDoctor(debug = false) {
|
|
|
10128
10802
|
output.push(...dumpEnv(true));
|
|
10129
10803
|
output.push("");
|
|
10130
10804
|
}
|
|
10131
|
-
|
|
10132
|
-
|
|
10805
|
+
const rendered = output.join("\n");
|
|
10806
|
+
console.log(rendered);
|
|
10807
|
+
return {
|
|
10808
|
+
exitCode: failCount > 0 ? 1 : 0,
|
|
10809
|
+
checks,
|
|
10810
|
+
failCount,
|
|
10811
|
+
warnCount,
|
|
10812
|
+
rendered
|
|
10813
|
+
};
|
|
10133
10814
|
}
|
|
10134
10815
|
var execAsync;
|
|
10135
10816
|
var init_doctor2 = __esm({
|
|
10136
10817
|
"src/runtime/doctor.ts"() {
|
|
10137
10818
|
"use strict";
|
|
10138
10819
|
init_esm_shims();
|
|
10820
|
+
init_doctorRedact();
|
|
10139
10821
|
execAsync = promisify(exec);
|
|
10140
10822
|
}
|
|
10141
10823
|
});
|
|
@@ -11286,7 +11968,7 @@ async function mergeToolRegistries(localTools, client) {
|
|
|
11286
11968
|
return Array.from(merged.values());
|
|
11287
11969
|
}
|
|
11288
11970
|
for (const backendTool of registry.tools) {
|
|
11289
|
-
const tier = backendTool.tier_required === "free" || backendTool.tier_required === "pro" ? backendTool.tier_required : "
|
|
11971
|
+
const tier = backendTool.tier_required === "free" || backendTool.tier_required === "pro" || backendTool.tier_required === "lifetime" || backendTool.tier_required === "enterprise" ? backendTool.tier_required : "enterprise";
|
|
11290
11972
|
if (!merged.has(backendTool.name)) {
|
|
11291
11973
|
merged.set(backendTool.name, {
|
|
11292
11974
|
name: backendTool.name,
|
|
@@ -11665,8 +12347,8 @@ __export(server_exports, {
|
|
|
11665
12347
|
runStdio: () => runStdio,
|
|
11666
12348
|
runStdioWithRegistry: () => runStdioWithRegistry
|
|
11667
12349
|
});
|
|
11668
|
-
import { readdirSync as
|
|
11669
|
-
import { join as
|
|
12350
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync2, statSync as statSync7 } from "fs";
|
|
12351
|
+
import { join as join29, relative as relative14, resolve as resolve17 } from "path";
|
|
11670
12352
|
function asResult2(text, isError = false) {
|
|
11671
12353
|
return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
|
|
11672
12354
|
}
|
|
@@ -11679,14 +12361,14 @@ function buildFileTree(root, maxFiles) {
|
|
|
11679
12361
|
const dir = queue.shift();
|
|
11680
12362
|
let entries;
|
|
11681
12363
|
try {
|
|
11682
|
-
entries =
|
|
12364
|
+
entries = readdirSync4(dir);
|
|
11683
12365
|
} catch {
|
|
11684
12366
|
continue;
|
|
11685
12367
|
}
|
|
11686
12368
|
for (const name of entries) {
|
|
11687
12369
|
if (out.length >= maxFiles) break;
|
|
11688
12370
|
if (SKIP_DIRS2.has(name)) continue;
|
|
11689
|
-
const full =
|
|
12371
|
+
const full = join29(dir, name);
|
|
11690
12372
|
let s;
|
|
11691
12373
|
try {
|
|
11692
12374
|
s = statSync7(full);
|
|
@@ -11710,7 +12392,7 @@ function readFilesAsContext(root, files, maxKB) {
|
|
|
11710
12392
|
for (const f of files) {
|
|
11711
12393
|
let body;
|
|
11712
12394
|
try {
|
|
11713
|
-
body =
|
|
12395
|
+
body = readFileSync2(f, "utf8");
|
|
11714
12396
|
} catch {
|
|
11715
12397
|
continue;
|
|
11716
12398
|
}
|
|
@@ -11724,11 +12406,37 @@ ${body}
|
|
|
11724
12406
|
}
|
|
11725
12407
|
return parts.join("\n\n");
|
|
11726
12408
|
}
|
|
12409
|
+
function installDrainHandlers(server) {
|
|
12410
|
+
const abort = new AbortController();
|
|
12411
|
+
let drainStarted = false;
|
|
12412
|
+
const onSignal = (sig) => {
|
|
12413
|
+
if (drainStarted) return;
|
|
12414
|
+
drainStarted = true;
|
|
12415
|
+
process.stderr.write(`[mcp-server] received ${sig}, starting graceful drain
|
|
12416
|
+
`);
|
|
12417
|
+
server.drain().then((forcedResponses) => {
|
|
12418
|
+
for (const resp of forcedResponses) {
|
|
12419
|
+
try {
|
|
12420
|
+
process.stdout.write(JSON.stringify(resp) + "\n");
|
|
12421
|
+
} catch {
|
|
12422
|
+
}
|
|
12423
|
+
}
|
|
12424
|
+
process.stderr.write("[mcp-server] drain complete, exiting\n");
|
|
12425
|
+
abort.abort();
|
|
12426
|
+
process.exit(0);
|
|
12427
|
+
});
|
|
12428
|
+
};
|
|
12429
|
+
process.on("SIGTERM", () => onSignal("SIGTERM"));
|
|
12430
|
+
process.on("SIGINT", () => onSignal("SIGINT"));
|
|
12431
|
+
return abort;
|
|
12432
|
+
}
|
|
11727
12433
|
async function runStdio(client) {
|
|
11728
12434
|
const server = new MCPServer(client);
|
|
12435
|
+
const abort = installDrainHandlers(server);
|
|
11729
12436
|
let buffer = "";
|
|
11730
12437
|
const decoder = new TextDecoder();
|
|
11731
12438
|
for await (const chunk of process.stdin) {
|
|
12439
|
+
if (abort.signal.aborted) break;
|
|
11732
12440
|
buffer += decoder.decode(chunk, { stream: true });
|
|
11733
12441
|
let idx;
|
|
11734
12442
|
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
@@ -11748,9 +12456,11 @@ async function runStdio(client) {
|
|
|
11748
12456
|
}
|
|
11749
12457
|
async function runStdioWithRegistry(client, registryClient) {
|
|
11750
12458
|
const server = new MCPServer(client, registryClient || client);
|
|
12459
|
+
const abort = installDrainHandlers(server);
|
|
11751
12460
|
let buffer = "";
|
|
11752
12461
|
const decoder = new TextDecoder();
|
|
11753
12462
|
for await (const chunk of process.stdin) {
|
|
12463
|
+
if (abort.signal.aborted) break;
|
|
11754
12464
|
buffer += decoder.decode(chunk, { stream: true });
|
|
11755
12465
|
let idx;
|
|
11756
12466
|
while ((idx = buffer.indexOf("\n")) !== -1) {
|
|
@@ -11768,7 +12478,7 @@ async function runStdioWithRegistry(client, registryClient) {
|
|
|
11768
12478
|
}
|
|
11769
12479
|
}
|
|
11770
12480
|
}
|
|
11771
|
-
var TOOLS, PROTOCOL_VERSION2, PRO_TIERS2, TIER_CACHE_TTL_MS, SAFE_PATH_RE, MCPServer;
|
|
12481
|
+
var AP4_MAX_BLOB_BYTES, TOOLS, PROTOCOL_VERSION2, PRO_TIERS2, TIER_CACHE_TTL_MS, DRAIN_TIMEOUT_MS, SAFE_PATH_RE, MCPServer;
|
|
11772
12482
|
var init_server = __esm({
|
|
11773
12483
|
"../core/src/mcp/server.ts"() {
|
|
11774
12484
|
"use strict";
|
|
@@ -11777,6 +12487,7 @@ var init_server = __esm({
|
|
|
11777
12487
|
init_libesm();
|
|
11778
12488
|
init_registry_merger();
|
|
11779
12489
|
init_local_tools();
|
|
12490
|
+
AP4_MAX_BLOB_BYTES = 4 * 1024;
|
|
11780
12491
|
TOOLS = [
|
|
11781
12492
|
{
|
|
11782
12493
|
name: "msapling_chat",
|
|
@@ -11895,11 +12606,57 @@ var init_server = __esm({
|
|
|
11895
12606
|
tier: "free",
|
|
11896
12607
|
inputSchema: { type: "object", properties: {} }
|
|
11897
12608
|
},
|
|
12609
|
+
{
|
|
12610
|
+
name: "msapling_list_wakeups",
|
|
12611
|
+
description: "[Pro] List pending AI wake-ups/scheduled tasks for your account.",
|
|
12612
|
+
tier: "pro",
|
|
12613
|
+
inputSchema: {
|
|
12614
|
+
type: "object",
|
|
12615
|
+
properties: {
|
|
12616
|
+
status: { type: "string", enum: ["pending", "fired", "cancelled", "expired"], description: "Filter by status" },
|
|
12617
|
+
session_id: { type: "string", description: "Filter by chat session ID" }
|
|
12618
|
+
}
|
|
12619
|
+
}
|
|
12620
|
+
},
|
|
12621
|
+
{
|
|
12622
|
+
name: "msapling_schedule_wakeup",
|
|
12623
|
+
description: "[Pro] Schedule a future AI task or follow-up wakeup.",
|
|
12624
|
+
tier: "pro",
|
|
12625
|
+
inputSchema: {
|
|
12626
|
+
type: "object",
|
|
12627
|
+
required: ["delay_seconds", "reason"],
|
|
12628
|
+
properties: {
|
|
12629
|
+
delay_seconds: { type: "number", description: "Delay in seconds from now" },
|
|
12630
|
+
reason: { type: "string", description: "Short description of what the AI should do" },
|
|
12631
|
+
prompt: { type: "string", description: "Specific prompt to process when the wakeup fires" },
|
|
12632
|
+
session_id: { type: "string", description: "Associate with an existing chat session" }
|
|
12633
|
+
}
|
|
12634
|
+
}
|
|
12635
|
+
},
|
|
12636
|
+
{
|
|
12637
|
+
name: "msapling_cancel_wakeup",
|
|
12638
|
+
description: "[Pro] Cancel a pending AI wakeup.",
|
|
12639
|
+
tier: "pro",
|
|
12640
|
+
inputSchema: {
|
|
12641
|
+
type: "object",
|
|
12642
|
+
required: ["id"],
|
|
12643
|
+
properties: {
|
|
12644
|
+
id: { type: "string", description: "The wakeup UUID to cancel" }
|
|
12645
|
+
}
|
|
12646
|
+
}
|
|
12647
|
+
},
|
|
12648
|
+
{
|
|
12649
|
+
name: "msapling_fleet_status",
|
|
12650
|
+
description: "[Pro] View status of your remote agent fleet and active tasks.",
|
|
12651
|
+
tier: "pro",
|
|
12652
|
+
inputSchema: { type: "object", properties: {} }
|
|
12653
|
+
},
|
|
11898
12654
|
...LOCAL_TOOLS
|
|
11899
12655
|
];
|
|
11900
12656
|
PROTOCOL_VERSION2 = "2024-11-05";
|
|
11901
12657
|
PRO_TIERS2 = /* @__PURE__ */ new Set(["pro", "monthly", "lifetime", "enterprise", "admin", "superadmin"]);
|
|
11902
12658
|
TIER_CACHE_TTL_MS = 6e4;
|
|
12659
|
+
DRAIN_TIMEOUT_MS = 5e3;
|
|
11903
12660
|
SAFE_PATH_RE = /^[A-Za-z0-9_./\-\\: ]+$/;
|
|
11904
12661
|
MCPServer = class {
|
|
11905
12662
|
constructor(client, backendClient) {
|
|
@@ -11910,6 +12667,83 @@ var init_server = __esm({
|
|
|
11910
12667
|
backendClient;
|
|
11911
12668
|
tierCache = null;
|
|
11912
12669
|
registryCache = null;
|
|
12670
|
+
/**
|
|
12671
|
+
* CLIENT-CLI-16: When true, the server is shutting down. New tool calls
|
|
12672
|
+
* are rejected with a JSON-RPC error; only in-flight calls are allowed
|
|
12673
|
+
* to finish.
|
|
12674
|
+
*/
|
|
12675
|
+
_isDraining = false;
|
|
12676
|
+
/**
|
|
12677
|
+
* CLIENT-CLI-16: Set of in-flight tool call IDs. Used during draining to
|
|
12678
|
+
* wait for completion before closing the transport. Capped at
|
|
12679
|
+
* MAX_INFLIGHT_TOOLS.
|
|
12680
|
+
*/
|
|
12681
|
+
_inflightCalls = /* @__PURE__ */ new Map();
|
|
12682
|
+
/**
|
|
12683
|
+
* CLIENT-CLI-16: Resolved when all in-flight calls complete during drain,
|
|
12684
|
+
* or when the drain timeout fires.
|
|
12685
|
+
*/
|
|
12686
|
+
_drainResolve = null;
|
|
12687
|
+
/** CLIENT-CLI-16: Whether the server is currently draining. */
|
|
12688
|
+
get isDraining() {
|
|
12689
|
+
return this._isDraining;
|
|
12690
|
+
}
|
|
12691
|
+
/**
|
|
12692
|
+
* CLIENT-CLI-16: Enter draining mode. Returns a promise that resolves when
|
|
12693
|
+
* all in-flight calls complete or DRAIN_TIMEOUT_MS elapses, whichever
|
|
12694
|
+
* comes first.
|
|
12695
|
+
*
|
|
12696
|
+
* After the promise resolves, any remaining in-flight calls have been
|
|
12697
|
+
* forcefully closed with error responses (written to stdout by the caller).
|
|
12698
|
+
*/
|
|
12699
|
+
async drain() {
|
|
12700
|
+
this._isDraining = true;
|
|
12701
|
+
const inflight = this._inflightCalls.size;
|
|
12702
|
+
process.stderr.write(
|
|
12703
|
+
`[mcp-server] draining: ${inflight} in-flight tool call(s)
|
|
12704
|
+
`
|
|
12705
|
+
);
|
|
12706
|
+
if (inflight === 0) {
|
|
12707
|
+
return [];
|
|
12708
|
+
}
|
|
12709
|
+
const forcedResponses = await new Promise((resolve18) => {
|
|
12710
|
+
this._drainResolve = () => resolve18([]);
|
|
12711
|
+
setTimeout(() => {
|
|
12712
|
+
this._drainResolve = null;
|
|
12713
|
+
const remaining = Array.from(this._inflightCalls.values());
|
|
12714
|
+
if (remaining.length === 0) {
|
|
12715
|
+
resolve18([]);
|
|
12716
|
+
return;
|
|
12717
|
+
}
|
|
12718
|
+
process.stderr.write(
|
|
12719
|
+
`[mcp-server] drain timeout: force-closing ${remaining.length} call(s)
|
|
12720
|
+
`
|
|
12721
|
+
);
|
|
12722
|
+
const errorResponses = remaining.map((call) => ({
|
|
12723
|
+
jsonrpc: "2.0",
|
|
12724
|
+
id: call.id,
|
|
12725
|
+
error: {
|
|
12726
|
+
code: -32e3,
|
|
12727
|
+
message: `Server shutdown: tool call "${call.name}" timed out after ${DRAIN_TIMEOUT_MS}ms`
|
|
12728
|
+
}
|
|
12729
|
+
}));
|
|
12730
|
+
this._inflightCalls.clear();
|
|
12731
|
+
resolve18(errorResponses);
|
|
12732
|
+
}, DRAIN_TIMEOUT_MS);
|
|
12733
|
+
});
|
|
12734
|
+
return forcedResponses;
|
|
12735
|
+
}
|
|
12736
|
+
/**
|
|
12737
|
+
* CLIENT-CLI-16: Called when an in-flight tool call completes during drain.
|
|
12738
|
+
* If all calls are done, resolves the drain promise.
|
|
12739
|
+
*/
|
|
12740
|
+
_completeInflight(id) {
|
|
12741
|
+
this._inflightCalls.delete(id);
|
|
12742
|
+
if (this._isDraining && this._inflightCalls.size === 0 && this._drainResolve) {
|
|
12743
|
+
this._drainResolve();
|
|
12744
|
+
this._drainResolve = null;
|
|
12745
|
+
}
|
|
12746
|
+
}
|
|
11913
12747
|
/**
|
|
11914
12748
|
* Fetch the caller's tier (Pro vs free) and cache it briefly. We avoid a
|
|
11915
12749
|
* `me()` call per `tools/list` because Claude Code calls tools/list often
|
|
@@ -11941,6 +12775,25 @@ var init_server = __esm({
|
|
|
11941
12775
|
invalidateTierCache() {
|
|
11942
12776
|
this.tierCache = null;
|
|
11943
12777
|
}
|
|
12778
|
+
/**
|
|
12779
|
+
* CLI-R12-20260507-TIER-02: Map tier name to numeric level for comparison.
|
|
12780
|
+
* Higher level = more restrictive/expensive.
|
|
12781
|
+
* free (1) < pro (5) < lifetime (10) < enterprise (100)
|
|
12782
|
+
*/
|
|
12783
|
+
tierLevel(tier) {
|
|
12784
|
+
switch (String(tier).toLowerCase()) {
|
|
12785
|
+
case "free":
|
|
12786
|
+
return 1;
|
|
12787
|
+
case "pro":
|
|
12788
|
+
return 5;
|
|
12789
|
+
case "lifetime":
|
|
12790
|
+
return 10;
|
|
12791
|
+
case "enterprise":
|
|
12792
|
+
return 100;
|
|
12793
|
+
default:
|
|
12794
|
+
return 100;
|
|
12795
|
+
}
|
|
12796
|
+
}
|
|
11944
12797
|
/**
|
|
11945
12798
|
* Fetch and cache the merged tool registry (local + backend).
|
|
11946
12799
|
* Called on first tools/list request. Cached for session lifetime.
|
|
@@ -11978,30 +12831,62 @@ var init_server = __esm({
|
|
|
11978
12831
|
return { jsonrpc: "2.0", id, result: { tools: wireFormat } };
|
|
11979
12832
|
}
|
|
11980
12833
|
case "tools/call": {
|
|
12834
|
+
if (this._isDraining) {
|
|
12835
|
+
return {
|
|
12836
|
+
jsonrpc: "2.0",
|
|
12837
|
+
id,
|
|
12838
|
+
error: {
|
|
12839
|
+
code: -32e3,
|
|
12840
|
+
message: "Server shutting down: not accepting new tool calls"
|
|
12841
|
+
}
|
|
12842
|
+
};
|
|
12843
|
+
}
|
|
11981
12844
|
const name = req.params?.name;
|
|
11982
12845
|
const args2 = req.params?.arguments ?? {};
|
|
11983
|
-
|
|
11984
|
-
|
|
11985
|
-
|
|
11986
|
-
|
|
11987
|
-
|
|
11988
|
-
} catch (e) {
|
|
11989
|
-
const msg = e instanceof MSaplingError ? `[backend ${e.status ?? "?"} ${e.code ?? ""}] ${e.message}` : e?.message ?? "tool call failed";
|
|
11990
|
-
return { jsonrpc: "2.0", id, result: asResult2(msg, true) };
|
|
12846
|
+
this._inflightCalls.set(id, {
|
|
12847
|
+
id,
|
|
12848
|
+
name: name ?? "unknown",
|
|
12849
|
+
startedAt: Date.now(),
|
|
12850
|
+
resolve: () => {
|
|
11991
12851
|
}
|
|
11992
|
-
}
|
|
11993
|
-
|
|
11994
|
-
|
|
11995
|
-
|
|
11996
|
-
|
|
11997
|
-
|
|
11998
|
-
|
|
11999
|
-
|
|
12000
|
-
|
|
12001
|
-
|
|
12852
|
+
});
|
|
12853
|
+
try {
|
|
12854
|
+
const localTool = TOOLS.find((t) => t.name === name);
|
|
12855
|
+
if (localTool) {
|
|
12856
|
+
try {
|
|
12857
|
+
const result = await this.callTool(name, args2);
|
|
12858
|
+
const resp = { jsonrpc: "2.0", id, result };
|
|
12859
|
+
return resp;
|
|
12860
|
+
} catch (e) {
|
|
12861
|
+
const msg = e instanceof MSaplingError ? `[backend ${e.status ?? "?"} ${e.code ?? ""}] ${e.message}` : e?.message ?? "tool call failed";
|
|
12862
|
+
return { jsonrpc: "2.0", id, result: asResult2(msg, true) };
|
|
12863
|
+
}
|
|
12864
|
+
}
|
|
12865
|
+
const merged = await this.getMergedRegistry();
|
|
12866
|
+
const backendTool = merged.find((t) => t.name === name && t.runs_on === "backend");
|
|
12867
|
+
if (backendTool) {
|
|
12868
|
+
const isPro = await this.getIsProCached();
|
|
12869
|
+
const toolTierLevel = this.tierLevel(backendTool.tier);
|
|
12870
|
+
const userTierLevel = isPro ? 5 : 1;
|
|
12871
|
+
if (toolTierLevel > userTierLevel) {
|
|
12872
|
+
return {
|
|
12873
|
+
jsonrpc: "2.0",
|
|
12874
|
+
id,
|
|
12875
|
+
result: asResult2(`Access denied: tool "${name}" requires ${backendTool.tier} tier`, true)
|
|
12876
|
+
};
|
|
12877
|
+
}
|
|
12878
|
+
try {
|
|
12879
|
+
const result = await this.invokeBackendTool(name, args2);
|
|
12880
|
+
return { jsonrpc: "2.0", id, result };
|
|
12881
|
+
} catch (e) {
|
|
12882
|
+
const msg = e instanceof MSaplingError ? `[backend ${e.status ?? "?"} ${e.code ?? ""}] ${e.message}` : e?.message ?? "tool call failed";
|
|
12883
|
+
return { jsonrpc: "2.0", id, result: asResult2(msg, true) };
|
|
12884
|
+
}
|
|
12002
12885
|
}
|
|
12886
|
+
return { jsonrpc: "2.0", id, error: { code: -32601, message: `Unknown tool: ${name}` } };
|
|
12887
|
+
} finally {
|
|
12888
|
+
this._completeInflight(id);
|
|
12003
12889
|
}
|
|
12004
|
-
return { jsonrpc: "2.0", id, error: { code: -32601, message: `Unknown tool: ${name}` } };
|
|
12005
12890
|
}
|
|
12006
12891
|
case "shutdown":
|
|
12007
12892
|
return { jsonrpc: "2.0", id, result: {} };
|
|
@@ -12052,12 +12937,30 @@ ${r.response ?? ""}`;
|
|
|
12052
12937
|
return asResult2(text);
|
|
12053
12938
|
}
|
|
12054
12939
|
case "msapling_diff": {
|
|
12940
|
+
const oldContent = String(args2.old_content ?? "");
|
|
12941
|
+
const newContent = String(args2.new_content ?? "");
|
|
12942
|
+
const oldBytes = Buffer.byteLength(oldContent, "utf8");
|
|
12943
|
+
const newBytes = Buffer.byteLength(newContent, "utf8");
|
|
12944
|
+
if (oldBytes > AP4_MAX_BLOB_BYTES || newBytes > AP4_MAX_BLOB_BYTES) {
|
|
12945
|
+
return asResult2(
|
|
12946
|
+
`AP-4: Blobs exceed 4 KB (old=${oldBytes}B, new=${newBytes}B). Use the backend endpoint: POST /api/projects/:id/diff`,
|
|
12947
|
+
true
|
|
12948
|
+
);
|
|
12949
|
+
}
|
|
12055
12950
|
const filename = String(args2.filename ?? "file");
|
|
12056
|
-
const patch = createPatch(filename,
|
|
12951
|
+
const patch = createPatch(filename, oldContent, newContent, "", "");
|
|
12057
12952
|
return asResult2(patch);
|
|
12058
12953
|
}
|
|
12059
12954
|
case "msapling_apply_diff": {
|
|
12060
|
-
const
|
|
12955
|
+
const originalContent = String(args2.original_content ?? "");
|
|
12956
|
+
const originalBytes = Buffer.byteLength(originalContent, "utf8");
|
|
12957
|
+
if (originalBytes > AP4_MAX_BLOB_BYTES) {
|
|
12958
|
+
return asResult2(
|
|
12959
|
+
`AP-4: Blob exceeds 4 KB (${originalBytes}B). Use the backend endpoint: POST /api/projects/:id/apply`,
|
|
12960
|
+
true
|
|
12961
|
+
);
|
|
12962
|
+
}
|
|
12963
|
+
const applied = applyPatch(originalContent, String(args2.diff_text ?? ""));
|
|
12061
12964
|
if (applied === false) {
|
|
12062
12965
|
return asResult2("Diff did not apply cleanly (hunks rejected).", true);
|
|
12063
12966
|
}
|
|
@@ -12154,6 +13057,36 @@ ${text}`);
|
|
|
12154
13057
|
return asResult2(`Available models (${models.length} total, showing first 30):
|
|
12155
13058
|
${lines.join("\n")}`);
|
|
12156
13059
|
}
|
|
13060
|
+
case "msapling_list_wakeups": {
|
|
13061
|
+
const isPro = await this.getIsProCached();
|
|
13062
|
+
if (!isPro) return asResult2("msapling_list_wakeups requires a Pro subscription.", true);
|
|
13063
|
+
const data = await this.client.listWakeups(args2);
|
|
13064
|
+
return asResult2(JSON.stringify(data, null, 2));
|
|
13065
|
+
}
|
|
13066
|
+
case "msapling_schedule_wakeup": {
|
|
13067
|
+
const isPro = await this.getIsProCached();
|
|
13068
|
+
if (!isPro) return asResult2("msapling_schedule_wakeup requires a Pro subscription.", true);
|
|
13069
|
+
const data = await this.client.createWakeup({
|
|
13070
|
+
delay_seconds: Number(args2.delay_seconds),
|
|
13071
|
+
reason: String(args2.reason ?? ""),
|
|
13072
|
+
prompt: args2.prompt ? String(args2.prompt) : void 0,
|
|
13073
|
+
session_id: args2.session_id ? String(args2.session_id) : void 0
|
|
13074
|
+
});
|
|
13075
|
+
return asResult2(JSON.stringify(data, null, 2));
|
|
13076
|
+
}
|
|
13077
|
+
case "msapling_cancel_wakeup": {
|
|
13078
|
+
const isPro = await this.getIsProCached();
|
|
13079
|
+
if (!isPro) return asResult2("msapling_cancel_wakeup requires a Pro subscription.", true);
|
|
13080
|
+
await this.client.cancelWakeup(String(args2.id));
|
|
13081
|
+
return asResult2("Wakeup cancelled");
|
|
13082
|
+
}
|
|
13083
|
+
case "msapling_fleet_status": {
|
|
13084
|
+
const isPro = await this.getIsProCached();
|
|
13085
|
+
if (!isPro) return asResult2("msapling_fleet_status requires a Pro subscription.", true);
|
|
13086
|
+
const status = await this.client.getFleetStatus();
|
|
13087
|
+
const tasks = await this.client.getActiveTasks();
|
|
13088
|
+
return asResult2(JSON.stringify({ status, tasks }, null, 2));
|
|
13089
|
+
}
|
|
12157
13090
|
// Local MCP tools (filesystem, shell, git)
|
|
12158
13091
|
case "local_run_command":
|
|
12159
13092
|
case "local_list_directory":
|
|
@@ -12201,7 +13134,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
12201
13134
|
var Header = () => /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [
|
|
12202
13135
|
/* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
|
|
12203
13136
|
"\u25CF MSapling CLI v",
|
|
12204
|
-
"2.3.6-beta.
|
|
13137
|
+
"2.3.6-beta.7"
|
|
12205
13138
|
] }),
|
|
12206
13139
|
/* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Platinum Tier Architecture" }) })
|
|
12207
13140
|
] });
|
|
@@ -12717,9 +13650,9 @@ ${prompt}` : prompt;
|
|
|
12717
13650
|
for (const mention of fileMentions) {
|
|
12718
13651
|
const filePath = mention.slice(1);
|
|
12719
13652
|
try {
|
|
12720
|
-
const { existsSync:
|
|
13653
|
+
const { existsSync: existsSync27 } = await import("fs");
|
|
12721
13654
|
const { readFile: readFile23 } = await import("fs/promises");
|
|
12722
|
-
if (
|
|
13655
|
+
if (existsSync27(filePath)) {
|
|
12723
13656
|
const content = await readFile23(filePath, "utf8");
|
|
12724
13657
|
const MAX_LEN = 32768;
|
|
12725
13658
|
const truncated = content.length > MAX_LEN ? content.slice(0, MAX_LEN) + "\n...[TRUNCATED]" : content;
|
|
@@ -12772,7 +13705,66 @@ ${finalCmd}`;
|
|
|
12772
13705
|
init_esm_shims();
|
|
12773
13706
|
init_src3();
|
|
12774
13707
|
import { readFile as readFile22 } from "fs/promises";
|
|
12775
|
-
import { existsSync as
|
|
13708
|
+
import { existsSync as existsSync25 } from "fs";
|
|
13709
|
+
|
|
13710
|
+
// src/state/parseApprovalMode.ts
|
|
13711
|
+
init_esm_shims();
|
|
13712
|
+
var VALID_MODES = [
|
|
13713
|
+
"default",
|
|
13714
|
+
"plan",
|
|
13715
|
+
"acceptEdits",
|
|
13716
|
+
"bypassPermissions"
|
|
13717
|
+
];
|
|
13718
|
+
function parseApprovalMode(raw, now) {
|
|
13719
|
+
if (!raw || typeof raw !== "object") {
|
|
13720
|
+
return { kind: "invalid", error: "settings file is not a JSON object" };
|
|
13721
|
+
}
|
|
13722
|
+
const block = raw.approvalMode;
|
|
13723
|
+
if (block === void 0) return { kind: "absent" };
|
|
13724
|
+
if (typeof block === "string") {
|
|
13725
|
+
if (!VALID_MODES.includes(block)) {
|
|
13726
|
+
return { kind: "invalid", error: `unknown approvalMode "${block}"` };
|
|
13727
|
+
}
|
|
13728
|
+
return { kind: "ok", mode: block };
|
|
13729
|
+
}
|
|
13730
|
+
if (typeof block !== "object" || block === null) {
|
|
13731
|
+
return {
|
|
13732
|
+
kind: "invalid",
|
|
13733
|
+
error: `approvalMode must be a string or object, got ${typeof block}`
|
|
13734
|
+
};
|
|
13735
|
+
}
|
|
13736
|
+
const obj = block;
|
|
13737
|
+
const mode = obj.mode;
|
|
13738
|
+
if (typeof mode !== "string" || !VALID_MODES.includes(mode)) {
|
|
13739
|
+
return { kind: "invalid", error: `approvalMode.mode invalid: ${JSON.stringify(mode)}` };
|
|
13740
|
+
}
|
|
13741
|
+
if (obj.ttlMs === void 0 && obj.timestamp === void 0) {
|
|
13742
|
+
return { kind: "ok", mode };
|
|
13743
|
+
}
|
|
13744
|
+
if (typeof obj.ttlMs !== "number" || !Number.isFinite(obj.ttlMs) || obj.ttlMs <= 0) {
|
|
13745
|
+
return {
|
|
13746
|
+
kind: "invalid",
|
|
13747
|
+
error: `approvalMode.ttlMs must be a positive number, got ${JSON.stringify(obj.ttlMs)}`
|
|
13748
|
+
};
|
|
13749
|
+
}
|
|
13750
|
+
if (typeof obj.timestamp !== "number" || !Number.isFinite(obj.timestamp) || obj.timestamp <= 0) {
|
|
13751
|
+
return {
|
|
13752
|
+
kind: "invalid",
|
|
13753
|
+
error: `approvalMode.timestamp must be a positive number, got ${JSON.stringify(obj.timestamp)}`
|
|
13754
|
+
};
|
|
13755
|
+
}
|
|
13756
|
+
if (mode !== "bypassPermissions") {
|
|
13757
|
+
return { kind: "ok", mode };
|
|
13758
|
+
}
|
|
13759
|
+
const age = now - obj.timestamp;
|
|
13760
|
+
if (age > obj.ttlMs) {
|
|
13761
|
+
return { kind: "expired", mode: "default", agedMs: age };
|
|
13762
|
+
}
|
|
13763
|
+
const remainingMs = obj.ttlMs - age;
|
|
13764
|
+
return { kind: "ok", mode: "bypassPermissions", ttl: { remainingMs } };
|
|
13765
|
+
}
|
|
13766
|
+
|
|
13767
|
+
// src/state/initSession.ts
|
|
12776
13768
|
async function initSession(ctx) {
|
|
12777
13769
|
try {
|
|
12778
13770
|
const { settings } = await loadSettings(
|
|
@@ -12786,38 +13778,40 @@ async function initSession(ctx) {
|
|
|
12786
13778
|
ctx.setShellEscapeEnabled(settings.shellEscapeEnabled !== false);
|
|
12787
13779
|
}
|
|
12788
13780
|
try {
|
|
12789
|
-
const { homedir:
|
|
12790
|
-
const { join:
|
|
12791
|
-
const userSettingsPath =
|
|
12792
|
-
if (
|
|
13781
|
+
const { homedir: homedir17 } = await import("os");
|
|
13782
|
+
const { join: join31 } = await import("path");
|
|
13783
|
+
const userSettingsPath = join31(homedir17(), ".msapling", "settings.json");
|
|
13784
|
+
if (existsSync25(userSettingsPath)) {
|
|
12793
13785
|
const userText = await readFile22(userSettingsPath, "utf8");
|
|
12794
|
-
|
|
12795
|
-
|
|
13786
|
+
let parsed;
|
|
13787
|
+
try {
|
|
13788
|
+
parsed = JSON.parse(userText);
|
|
13789
|
+
} catch (e) {
|
|
13790
|
+
ctx.addMessage("system", `\u26A0 ~/.msapling/settings.json is not valid JSON: ${e.message}. Using default mode.`);
|
|
13791
|
+
parsed = {};
|
|
13792
|
+
}
|
|
13793
|
+
const result = parseApprovalMode(parsed, Date.now());
|
|
12796
13794
|
let modeToApply = "default";
|
|
12797
|
-
|
|
12798
|
-
|
|
12799
|
-
|
|
12800
|
-
|
|
12801
|
-
|
|
12802
|
-
|
|
12803
|
-
|
|
12804
|
-
|
|
12805
|
-
|
|
12806
|
-
|
|
13795
|
+
switch (result.kind) {
|
|
13796
|
+
case "absent":
|
|
13797
|
+
break;
|
|
13798
|
+
case "invalid":
|
|
13799
|
+
ctx.addMessage("system", `\u26A0 Persisted approvalMode invalid (${result.error}). Reverting to default mode.`);
|
|
13800
|
+
break;
|
|
13801
|
+
case "expired":
|
|
13802
|
+
ctx.addMessage("system", "\u26A0 bypassPermissions TTL expired. Reverting to default mode.");
|
|
13803
|
+
break;
|
|
13804
|
+
case "ok":
|
|
13805
|
+
modeToApply = result.mode;
|
|
13806
|
+
if (result.mode === "bypassPermissions") {
|
|
13807
|
+
if (result.ttl) {
|
|
13808
|
+
const mins = (result.ttl.remainingMs / 1e3 / 60).toFixed(1);
|
|
13809
|
+
ctx.addMessage("system", `\u26A0 bypassPermissions active (expires in ~${mins} min). Use /mode default to re-enable controls.`);
|
|
12807
13810
|
} else {
|
|
12808
|
-
|
|
12809
|
-
const remaining = ttlMs - age;
|
|
12810
|
-
ctx.addMessage("system", `\u26A0 bypassPermissions active (expires in ~${(remaining / 1e3 / 60).toFixed(1)} min). Use /mode default to re-enable controls.`);
|
|
13811
|
+
ctx.addMessage("system", "\u26A0 bypassPermissions active (no TTL). Use /mode default to re-enable controls.");
|
|
12811
13812
|
}
|
|
12812
|
-
} else {
|
|
12813
|
-
modeToApply = mode;
|
|
12814
13813
|
}
|
|
12815
|
-
|
|
12816
|
-
} else if (typeof approvalMode === "string" && validModes.includes(approvalMode)) {
|
|
12817
|
-
modeToApply = approvalMode;
|
|
12818
|
-
if (modeToApply === "bypassPermissions") {
|
|
12819
|
-
ctx.addMessage("system", "\u26A0 bypassPermissions active (no TTL). Use /mode default to re-enable controls.");
|
|
12820
|
-
}
|
|
13814
|
+
break;
|
|
12821
13815
|
}
|
|
12822
13816
|
ctx.setMode(modeToApply);
|
|
12823
13817
|
}
|
|
@@ -13083,9 +14077,26 @@ var App = ({ compact: compact2 = false }) => {
|
|
|
13083
14077
|
|
|
13084
14078
|
// src/runtime/bootstrap.ts
|
|
13085
14079
|
init_esm_shims();
|
|
14080
|
+
import { readFileSync as readFileSync3 } from "fs";
|
|
14081
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
14082
|
+
import { dirname as dirname3, join as join30 } from "path";
|
|
14083
|
+
function readCliVersion2() {
|
|
14084
|
+
const here = dirname3(fileURLToPath2(import.meta.url));
|
|
14085
|
+
for (const rel of ["../package.json", "../../package.json"]) {
|
|
14086
|
+
try {
|
|
14087
|
+
const pkg = JSON.parse(readFileSync3(join30(here, rel), "utf8"));
|
|
14088
|
+
if (pkg.name && pkg.version) {
|
|
14089
|
+
return { name: pkg.name, version: pkg.version };
|
|
14090
|
+
}
|
|
14091
|
+
} catch {
|
|
14092
|
+
}
|
|
14093
|
+
}
|
|
14094
|
+
return { name: "@mtreeai/msapling-cli", version: "unknown" };
|
|
14095
|
+
}
|
|
14096
|
+
var CLI_PKG = readCliVersion2();
|
|
13086
14097
|
function handleCliArgs(args2) {
|
|
13087
14098
|
if (args2.includes("--version") || args2.includes("-v")) {
|
|
13088
|
-
console.log(
|
|
14099
|
+
console.log(`${CLI_PKG.name} ${CLI_PKG.version}`);
|
|
13089
14100
|
process.exit(0);
|
|
13090
14101
|
}
|
|
13091
14102
|
if (args2.includes("--help") || args2.includes("-h")) {
|
|
@@ -13104,7 +14115,8 @@ function handleCliArgs(args2) {
|
|
|
13104
14115
|
(async () => {
|
|
13105
14116
|
const { runDoctor: runDoctor2 } = await Promise.resolve().then(() => (init_doctor2(), doctor_exports));
|
|
13106
14117
|
const debug = args2.includes("--debug");
|
|
13107
|
-
await runDoctor2(debug);
|
|
14118
|
+
const result = await runDoctor2(debug);
|
|
14119
|
+
process.exit(result.exitCode);
|
|
13108
14120
|
})().catch((e) => {
|
|
13109
14121
|
process.stderr.write(`[msapling-doctor] fatal: ${e?.message ?? e}
|
|
13110
14122
|
`);
|