@mtreeai/msapling-cli 2.3.6-beta.1 → 2.3.6-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.js +2069 -493
  2. package/package.json +55 -56
  3. package/LICENSE +0 -24
package/dist/index.js CHANGED
@@ -79,6 +79,11 @@ var init_src = __esm({
79
79
  MSaplingClient = class {
80
80
  apiUrl;
81
81
  token;
82
+ // CLI-AUTH-CSRF-01: simple cookie jar so we can implement the backend's
83
+ // double-submit CSRF pattern (msapling_csrftoken cookie ↔ X-CSRF-Token header).
84
+ // Without this, every authenticated POST/PATCH/DELETE returns 403
85
+ // CSRF_COOKIE_MISSING even with a valid Bearer token.
86
+ cookies = /* @__PURE__ */ new Map();
82
87
  constructor(options = {}) {
83
88
  this.apiUrl = (options.apiUrl || "https://api.msapling.com").replace(/\/$/, "");
84
89
  this.token = options.token || null;
@@ -89,12 +94,38 @@ var init_src = __esm({
89
94
  getApiUrl() {
90
95
  return this.apiUrl;
91
96
  }
97
+ serializeCookies() {
98
+ return Array.from(this.cookies.entries()).map(([k, v]) => `${k}=${v}`).join("; ");
99
+ }
100
+ updateCookiesFromResponse(response) {
101
+ const setCookieArr = response.headers.getSetCookie?.() ?? [];
102
+ for (const line of setCookieArr) {
103
+ const firstSemi = line.indexOf(";");
104
+ const pair = firstSemi >= 0 ? line.slice(0, firstSemi) : line;
105
+ const eq = pair.indexOf("=");
106
+ if (eq <= 0) continue;
107
+ const name = pair.slice(0, eq).trim();
108
+ const value = pair.slice(eq + 1).trim();
109
+ if (!name || !value) continue;
110
+ this.cookies.set(name, value);
111
+ }
112
+ }
92
113
  async request(path2, options = {}) {
93
114
  const headers = new Headers(options.headers);
94
115
  if (this.token) {
95
116
  headers.set("Authorization", `Bearer ${this.token}`);
96
117
  }
97
- headers.set("Content-Type", "application/json");
118
+ if (!headers.has("Content-Type")) {
119
+ headers.set("Content-Type", "application/json");
120
+ }
121
+ if (this.cookies.size > 0) {
122
+ headers.set("Cookie", this.serializeCookies());
123
+ }
124
+ const method = (options.method || "GET").toUpperCase();
125
+ if (method !== "GET" && method !== "HEAD") {
126
+ const csrf = this.cookies.get("msapling_csrftoken");
127
+ if (csrf) headers.set("X-CSRF-Token", csrf);
128
+ }
98
129
  const controller = new AbortController();
99
130
  const timeout = setTimeout(() => controller.abort(), 3e4);
100
131
  try {
@@ -106,6 +137,7 @@ var init_src = __esm({
106
137
  keepalive: true
107
138
  });
108
139
  clearTimeout(timeout);
140
+ this.updateCookiesFromResponse(response);
109
141
  if (!response.ok) {
110
142
  let detail = "Unknown error";
111
143
  let code = "unknown";
@@ -272,7 +304,11 @@ var init_src = __esm({
272
304
  try {
273
305
  const parts = token.split(".");
274
306
  if (parts.length !== 3) return null;
275
- const payload = JSON.parse(atob(parts[1]));
307
+ let b64 = parts[1].replace(/-/g, "+").replace(/_/g, "/");
308
+ const padLen = (4 - b64.length % 4) % 4;
309
+ b64 += "=".repeat(padLen);
310
+ const decoded = typeof atob === "function" ? atob(b64) : Buffer.from(b64, "base64").toString("binary");
311
+ const payload = JSON.parse(decoded);
276
312
  return payload;
277
313
  } catch {
278
314
  return null;
@@ -280,8 +316,19 @@ var init_src = __esm({
280
316
  }
281
317
  /**
282
318
  * Login with email and password.
283
- * Returns access_token on success, or indicates TOTP is required.
284
- * On 401: throws MSaplingError with code 'invalid_credentials'
319
+ *
320
+ * The JWT `mfa` claim is the *session* MFA state ("has this session completed
321
+ * TOTP yet"), not "TOTP is required". Treating `mfa:false` as TOTP-required
322
+ * was wrong: it blocked every user without 2FA enabled, including all
323
+ * guest/free accounts and the live backend which doesn't enforce TOTP yet.
324
+ *
325
+ * New contract: accept the token as fully usable. If the backend enforces
326
+ * MFA on a subsequent protected endpoint, that endpoint will reply with
327
+ * `mfa_required` and the caller can step up via verifyLoginTotp(). Until
328
+ * the backend exposes an explicit `mfa_required` field on the login
329
+ * response, we don't pre-emptively branch.
330
+ *
331
+ * On 401: throws MSaplingError with code 'invalid_credentials'.
285
332
  */
286
333
  async loginEmailPassword(email, password) {
287
334
  const params = new URLSearchParams();
@@ -299,12 +346,9 @@ var init_src = __esm({
299
346
  signal: controller.signal
300
347
  });
301
348
  clearTimeout(timeout);
349
+ this.updateCookiesFromResponse(response);
302
350
  if (response.ok) {
303
351
  const data = await response.json();
304
- const payload = this.decodeJwtPayload(data.access_token);
305
- if (payload && payload.mfa === false) {
306
- return { kind: "totp_required", partialToken: data.access_token };
307
- }
308
352
  return { kind: "success", token: data.access_token };
309
353
  }
310
354
  if (response.status === 401) {
@@ -326,6 +370,42 @@ var init_src = __esm({
326
370
  clearTimeout(timeout);
327
371
  }
328
372
  }
373
+ /**
374
+ * Sign in as a guest. Backend issues a guest-scoped JWT with limited tier
375
+ * capabilities. No prior credentials required.
376
+ */
377
+ async loginGuest() {
378
+ const controller = new AbortController();
379
+ const timeout = setTimeout(() => controller.abort(), 3e4);
380
+ try {
381
+ const response = await fetch(`${this.apiUrl}/auth/guest`, {
382
+ method: "POST",
383
+ headers: { "Content-Type": "application/json" },
384
+ body: "{}",
385
+ signal: controller.signal
386
+ });
387
+ clearTimeout(timeout);
388
+ this.updateCookiesFromResponse(response);
389
+ if (response.ok) {
390
+ const data = await response.json();
391
+ return { kind: "success", token: data.access_token };
392
+ }
393
+ let detail = "Guest login failed";
394
+ try {
395
+ const err = await response.json();
396
+ detail = err.detail || detail;
397
+ } catch (e) {
398
+ }
399
+ throw new MSaplingError(detail, response.status, "guest_login_failed");
400
+ } catch (e) {
401
+ if (e.name === "AbortError") {
402
+ throw new MSaplingError("Request timed out after 30s.", 408, "timeout");
403
+ }
404
+ throw e;
405
+ } finally {
406
+ clearTimeout(timeout);
407
+ }
408
+ }
329
409
  /**
330
410
  * Verify TOTP code during login.
331
411
  * Requires a partial token from a prior /auth/login call when MFA is enabled.
@@ -335,16 +415,21 @@ var init_src = __esm({
335
415
  const controller = new AbortController();
336
416
  const timeout = setTimeout(() => controller.abort(), 3e4);
337
417
  try {
338
- const response = await fetch(`${this.apiUrl}/auth/login-verify`, {
418
+ const csrf = this.cookies.get("msapling_csrftoken");
419
+ const verifyHeaders = {
420
+ "Authorization": `Bearer ${this.token}`,
421
+ "Content-Type": "application/json"
422
+ };
423
+ if (this.cookies.size > 0) verifyHeaders["Cookie"] = this.serializeCookies();
424
+ if (csrf) verifyHeaders["X-CSRF-Token"] = csrf;
425
+ const response = await fetch(`${this.apiUrl}/api/mfa/login-verify`, {
339
426
  method: "POST",
340
- headers: {
341
- "Authorization": `Bearer ${this.token}`,
342
- "Content-Type": "application/json"
343
- },
427
+ headers: verifyHeaders,
344
428
  body: JSON.stringify({ code }),
345
429
  signal: controller.signal
346
430
  });
347
431
  clearTimeout(timeout);
432
+ this.updateCookiesFromResponse(response);
348
433
  if (response.ok) {
349
434
  const data = await response.json();
350
435
  return { kind: "success", token: data.access_token };
@@ -374,8 +459,9 @@ var init_src = __esm({
374
459
  */
375
460
  async chatOnce(prompt, model, chatId) {
376
461
  let acc = "";
377
- for await (const chunk of this.streamChat({ prompt, model, chat_id: chatId })) {
378
- if (chunk.content) acc += chunk.content;
462
+ for await (const chunk of this.streamChat({ content: prompt, model, chat_id: chatId ?? "" })) {
463
+ if (chunk.delta) acc += chunk.delta;
464
+ else if (chunk.content) acc += chunk.content;
379
465
  }
380
466
  return acc;
381
467
  }
@@ -455,6 +541,33 @@ var init_src = __esm({
455
541
  });
456
542
  return data.hash;
457
543
  }
544
+ async getActiveTasks() {
545
+ return this.request("/api/task-monitor/active");
546
+ }
547
+ /**
548
+ * Wakeup Management (LAB-CHAT-PARITY-04)
549
+ */
550
+ async listWakeups(params = {}) {
551
+ const queryParams = new URLSearchParams(params);
552
+ return this.request(`/api/wakeups/?${queryParams.toString()}`);
553
+ }
554
+ async createWakeup(data) {
555
+ return this.request("/api/wakeups/", {
556
+ method: "POST",
557
+ body: JSON.stringify(data)
558
+ });
559
+ }
560
+ async cancelWakeup(id) {
561
+ await this.request(`/api/wakeups/${id}`, {
562
+ method: "DELETE"
563
+ });
564
+ }
565
+ /**
566
+ * Orchestration & Fleet Status
567
+ */
568
+ async getFleetStatus() {
569
+ return this.request("/api/orchestration/status");
570
+ }
458
571
  async proposeEdit(params) {
459
572
  return await this.request("/api/mdrive/ram/blocks/propose", {
460
573
  method: "POST",
@@ -462,6 +575,9 @@ var init_src = __esm({
462
575
  });
463
576
  }
464
577
  async *streamChat(params) {
578
+ if (!params.frame_id) {
579
+ params = { ...params, frame_id: "earth_surface" };
580
+ }
465
581
  const controller = new AbortController();
466
582
  const timeout = setTimeout(() => controller.abort(), 3e4);
467
583
  try {
@@ -487,22 +603,34 @@ var init_src = __esm({
487
603
  if (!response.body) throw new Error("No response body");
488
604
  const reader = response.body.getReader();
489
605
  const decoder = new TextDecoder();
606
+ let pending = "";
607
+ const yieldLine = function* (raw) {
608
+ const line = raw.replace(/\r$/, "").trim();
609
+ if (!line) return;
610
+ try {
611
+ yield JSON.parse(line);
612
+ } catch {
613
+ if (line.startsWith("{")) {
614
+ console.warn(`[Stream] Dropped malformed JSON: ${line}`);
615
+ }
616
+ }
617
+ };
490
618
  while (true) {
491
619
  const { done, value } = await reader.read();
492
620
  if (done) break;
493
- const chunk = decoder.decode(value, { stream: true });
494
- const lines = chunk.split("\n").filter((l) => l.trim());
495
- for (const line of lines) {
496
- try {
497
- const data = JSON.parse(line);
498
- yield data;
499
- } catch (e) {
500
- if (line.startsWith("{")) {
501
- console.warn(`[Stream] Dropped partial JSON: ${line}`);
502
- }
503
- }
621
+ pending += decoder.decode(value, { stream: true });
622
+ let nlIdx;
623
+ while ((nlIdx = pending.indexOf("\n")) !== -1) {
624
+ const raw = pending.slice(0, nlIdx);
625
+ pending = pending.slice(nlIdx + 1);
626
+ yield* yieldLine(raw);
504
627
  }
505
628
  }
629
+ pending += decoder.decode();
630
+ if (pending.length > 0) {
631
+ yield* yieldLine(pending);
632
+ pending = "";
633
+ }
506
634
  } catch (e) {
507
635
  if (e.name === "AbortError") {
508
636
  throw new MSaplingError("Request timed out after 30s.", 408, "timeout");
@@ -657,6 +785,7 @@ import { resolve as resolve2, normalize as normalize2, relative as relative2, is
657
785
  import { writeFile, readFile as readFile2, mkdir } from "fs/promises";
658
786
  import { existsSync as existsSync2 } from "fs";
659
787
  import { homedir } from "os";
788
+ import { randomBytes } from "crypto";
660
789
  var MAX_CONTENT_BYTES, WriteFileTool;
661
790
  var init_WriteFileTool = __esm({
662
791
  "../core/src/tools/WriteFileTool.ts"() {
@@ -732,7 +861,9 @@ var init_WriteFileTool = __esm({
732
861
  if (existsSync2(resolvedTarget)) {
733
862
  const existingContent = await readFile2(resolvedTarget, "utf8");
734
863
  const filename = resolvedTarget.split(/[\\/]/).pop() ?? "file";
735
- const backupPath = join2(homedir(), ".msapling", "backups", `${filename}.${Date.now()}.bak`);
864
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
865
+ const suffix = randomBytes(4).toString("hex");
866
+ const backupPath = join2(homedir(), ".msapling", "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
736
867
  await mkdir(join2(homedir(), ".msapling", "backups"), { recursive: true });
737
868
  await writeFile(backupPath, existingContent, "utf8");
738
869
  backedUpTo = backupPath;
@@ -1028,7 +1159,7 @@ var init_RunCommandTool = __esm({
1028
1159
  this.activeCommands++;
1029
1160
  return;
1030
1161
  }
1031
- return new Promise((resolve17) => this.queue.push(resolve17));
1162
+ return new Promise((resolve18) => this.queue.push(resolve18));
1032
1163
  }
1033
1164
  static releaseLock() {
1034
1165
  if (this.queue.length > 0) {
@@ -1107,9 +1238,9 @@ var init_RunCommandTool = __esm({
1107
1238
  const chunks = { stdout: [], stderr: [] };
1108
1239
  proc.stdout?.on("data", (chunk) => chunks.stdout.push(chunk));
1109
1240
  proc.stderr?.on("data", (chunk) => chunks.stderr.push(chunk));
1110
- const exitCode = await new Promise((resolve17) => {
1111
- proc.on("exit", (code) => resolve17(code ?? 1));
1112
- proc.on("error", () => resolve17(1));
1241
+ const exitCode = await new Promise((resolve18) => {
1242
+ proc.on("exit", (code) => resolve18(code ?? 1));
1243
+ proc.on("error", () => resolve18(1));
1113
1244
  });
1114
1245
  const stdout = Buffer.concat(chunks.stdout).toString("utf-8");
1115
1246
  const stderr = Buffer.concat(chunks.stderr).toString("utf-8");
@@ -1220,9 +1351,9 @@ var init_src2 = __esm({
1220
1351
  const messages = this.parser.parse(value);
1221
1352
  for (const msg of messages) {
1222
1353
  if (msg.id !== void 0 && this.pendingRequests.has(Number(msg.id))) {
1223
- const resolve17 = this.pendingRequests.get(Number(msg.id));
1224
- if (resolve17) {
1225
- resolve17(msg.result || msg.error);
1354
+ const resolve18 = this.pendingRequests.get(Number(msg.id));
1355
+ if (resolve18) {
1356
+ resolve18(msg.result || msg.error);
1226
1357
  this.pendingRequests.delete(Number(msg.id));
1227
1358
  }
1228
1359
  }
@@ -1237,8 +1368,8 @@ var init_src2 = __esm({
1237
1368
  const message = `Content-Length: ${Buffer.byteLength(content, "utf8")}\r
1238
1369
  \r
1239
1370
  ${content}`;
1240
- return new Promise((resolve17) => {
1241
- this.pendingRequests.set(id, resolve17);
1371
+ return new Promise((resolve18) => {
1372
+ this.pendingRequests.set(id, resolve18);
1242
1373
  this.process.stdin.write(message);
1243
1374
  this.process.stdin.flush();
1244
1375
  });
@@ -1370,9 +1501,9 @@ var init_SubShellTool = __esm({
1370
1501
  }
1371
1502
  throw e;
1372
1503
  }
1373
- await new Promise((resolve17) => {
1374
- proc.on("exit", () => resolve17());
1375
- proc.on("error", () => resolve17());
1504
+ await new Promise((resolve18) => {
1505
+ proc.on("exit", () => resolve18());
1506
+ proc.on("error", () => resolve18());
1376
1507
  });
1377
1508
  return { content: `Successfully launched separate window for ${args2.worker_id}` };
1378
1509
  }
@@ -1455,13 +1586,13 @@ async function findRg() {
1455
1586
  const candidates = ["rg", "C:\\Program Files\\ripgrep\\rg.exe"];
1456
1587
  for (const bin of candidates) {
1457
1588
  try {
1458
- const exited = await new Promise((resolve17) => {
1589
+ const exited = await new Promise((resolve18) => {
1459
1590
  try {
1460
1591
  const p = spawn4(bin, ["--version"], { stdio: ["ignore", "pipe", "pipe"] });
1461
- p.on("error", () => resolve17(null));
1462
- p.on("exit", (code) => resolve17(code));
1592
+ p.on("error", () => resolve18(null));
1593
+ p.on("exit", (code) => resolve18(code));
1463
1594
  } catch {
1464
- resolve17(null);
1595
+ resolve18(null);
1465
1596
  }
1466
1597
  });
1467
1598
  if (exited === 0) return bin;
@@ -1471,7 +1602,7 @@ async function findRg() {
1471
1602
  return null;
1472
1603
  }
1473
1604
  function runRg(bin, args2) {
1474
- return new Promise((resolve17) => {
1605
+ return new Promise((resolve18) => {
1475
1606
  const p = spawn4(bin, args2, { stdio: ["ignore", "pipe", "pipe"] });
1476
1607
  let stdout = "";
1477
1608
  let stderr = "";
@@ -1482,10 +1613,10 @@ function runRg(bin, args2) {
1482
1613
  stderr += d.toString("utf8");
1483
1614
  });
1484
1615
  p.on("error", (e) => {
1485
- resolve17({ stdout, stderr: stderr + (e?.message ?? ""), exitCode: -1 });
1616
+ resolve18({ stdout, stderr: stderr + (e?.message ?? ""), exitCode: -1 });
1486
1617
  });
1487
1618
  p.on("exit", (code) => {
1488
- resolve17({ stdout, stderr, exitCode: code });
1619
+ resolve18({ stdout, stderr, exitCode: code });
1489
1620
  });
1490
1621
  });
1491
1622
  }
@@ -2037,6 +2168,7 @@ import { resolve as resolve6, normalize as normalize5, relative as relative6, is
2037
2168
  import { readFile as readFile4, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
2038
2169
  import { existsSync as existsSync5 } from "fs";
2039
2170
  import { homedir as homedir2 } from "os";
2171
+ import { randomBytes as randomBytes2 } from "crypto";
2040
2172
  var PatchFileTool;
2041
2173
  var init_PatchFileTool = __esm({
2042
2174
  "../core/src/tools/PatchFileTool.ts"() {
@@ -2143,7 +2275,9 @@ File preview (first 200 chars): ${JSON.stringify(preview)}`,
2143
2275
  let backedUpTo = null;
2144
2276
  try {
2145
2277
  const filename = resolvedTarget.split(/[\\/]/).pop() ?? "file";
2146
- const backupPath = join6(homedir2(), ".msapling", "backups", `${filename}.${Date.now()}.bak`);
2278
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
2279
+ const suffix = randomBytes2(4).toString("hex");
2280
+ const backupPath = join6(homedir2(), ".msapling", "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
2147
2281
  await mkdir2(join6(homedir2(), ".msapling", "backups"), { recursive: true });
2148
2282
  await writeFile2(backupPath, originalContent, "utf8");
2149
2283
  backedUpTo = backupPath;
@@ -2734,12 +2868,12 @@ Command: ${command}`,
2734
2868
  proc.stdout?.on("data", (chunk) => chunks.stdout.push(chunk));
2735
2869
  proc.stderr?.on("data", (chunk) => chunks.stderr.push(chunk));
2736
2870
  const timeoutPromise = new Promise(
2737
- (resolve17) => setTimeout(() => resolve17("timeout"), timeoutMs)
2871
+ (resolve18) => setTimeout(() => resolve18("timeout"), timeoutMs)
2738
2872
  );
2739
2873
  const processPromise = (async () => {
2740
- const exitCode2 = await new Promise((resolve17) => {
2741
- proc.on("exit", (code) => resolve17(code ?? 1));
2742
- proc.on("error", () => resolve17(1));
2874
+ const exitCode2 = await new Promise((resolve18) => {
2875
+ proc.on("exit", (code) => resolve18(code ?? 1));
2876
+ proc.on("error", () => resolve18(1));
2743
2877
  });
2744
2878
  const stdout2 = Buffer.concat(chunks.stdout).toString("utf-8");
2745
2879
  const stderr2 = Buffer.concat(chunks.stderr).toString("utf-8");
@@ -3003,6 +3137,7 @@ import { resolve as resolve9, normalize as normalize8, relative as relative9, is
3003
3137
  import { readFile as readFile6, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
3004
3138
  import { existsSync as existsSync7 } from "fs";
3005
3139
  import { homedir as homedir3 } from "os";
3140
+ import { randomBytes as randomBytes3 } from "crypto";
3006
3141
  function normaliseSource(source) {
3007
3142
  if (source === "") return [];
3008
3143
  const lines = source.split("\n");
@@ -3179,11 +3314,13 @@ var init_NotebookEditTool = __esm({
3179
3314
  let backedUpTo = null;
3180
3315
  try {
3181
3316
  const filename = absPath.split(/[\\/]/).pop() ?? "notebook.ipynb";
3317
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3318
+ const suffix = randomBytes3(4).toString("hex");
3182
3319
  const backupPath = join8(
3183
3320
  homedir3(),
3184
3321
  ".msapling",
3185
3322
  "backups",
3186
- `${filename}.${Date.now()}.bak`
3323
+ `${filename}.backup-${stamp}-${suffix}.bak`
3187
3324
  );
3188
3325
  await mkdir3(join8(homedir3(), ".msapling", "backups"), { recursive: true });
3189
3326
  await writeFile3(backupPath, rawJson, "utf8");
@@ -3246,7 +3383,7 @@ var init_NotebookEditTool = __esm({
3246
3383
  File: ${absPath}`;
3247
3384
  if (backedUpTo) {
3248
3385
  summary += `
3249
- Pre-edit content backed up to: ${backedUpTo}`;
3386
+ Backup: ${backedUpTo}`;
3250
3387
  }
3251
3388
  return { content: summary };
3252
3389
  }
@@ -3259,6 +3396,7 @@ import { resolve as resolve10, normalize as normalize9, relative as relative10,
3259
3396
  import { readFile as readFile7, writeFile as writeFile4, mkdir as mkdir4 } from "fs/promises";
3260
3397
  import { existsSync as existsSync8 } from "fs";
3261
3398
  import { homedir as homedir4 } from "os";
3399
+ import { randomBytes as randomBytes4 } from "crypto";
3262
3400
  var MAX_EDITS, MultiEditFileTool;
3263
3401
  var init_MultiEditFileTool = __esm({
3264
3402
  "../core/src/tools/MultiEditFileTool.ts"() {
@@ -3419,11 +3557,13 @@ No changes were written (atomic: all-or-nothing).`,
3419
3557
  let backedUpTo = null;
3420
3558
  try {
3421
3559
  const filename = resolvedTarget.split(/[\\/]/).pop() ?? "file";
3560
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3561
+ const suffix = randomBytes4(4).toString("hex");
3422
3562
  const backupPath = join9(
3423
3563
  homedir4(),
3424
3564
  ".msapling",
3425
3565
  "backups",
3426
- `${filename}.${Date.now()}.bak`
3566
+ `${filename}.backup-${stamp}-${suffix}.bak`
3427
3567
  );
3428
3568
  await mkdir4(join9(homedir4(), ".msapling", "backups"), { recursive: true });
3429
3569
  await writeFile4(backupPath, originalContent, "utf8");
@@ -3456,6 +3596,7 @@ Pre-edit content backed up to: ${backedUpTo}`;
3456
3596
  import { resolve as resolve11, normalize as normalize10, relative as relative11, isAbsolute as isAbsolute11, dirname } from "path";
3457
3597
  import { rename, mkdir as mkdir5, copyFile, rm, stat as stat2, readdir as readdir2 } from "fs/promises";
3458
3598
  import { existsSync as existsSync9, statSync as statSync3 } from "fs";
3599
+ import { randomBytes as randomBytes5 } from "crypto";
3459
3600
  function containedPath(p, root) {
3460
3601
  const abs = isAbsolute11(p) ? normalize10(p) : resolve11(root, p.trim());
3461
3602
  const rel = relative11(root, abs);
@@ -3479,17 +3620,19 @@ async function copyDir(src, dst) {
3479
3620
  }
3480
3621
  async function backupFile(absPath) {
3481
3622
  try {
3482
- const { readFile: readFile23, writeFile: writeFile12, mkdir: mkdir10 } = await import("fs/promises");
3483
- const { homedir: homedir15 } = await import("os");
3484
- const { join: join27 } = await import("path");
3623
+ const { readFile: readFile23, writeFile: writeFile12, mkdir: mkdir9 } = await import("fs/promises");
3624
+ const { homedir: homedir17 } = await import("os");
3625
+ const { join: join31 } = await import("path");
3485
3626
  const filename = absPath.split(/[\\/]/).pop() ?? "file";
3486
- const backupPath = join27(
3487
- homedir15(),
3627
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3628
+ const suffix = randomBytes5(4).toString("hex");
3629
+ const backupPath = join31(
3630
+ homedir17(),
3488
3631
  ".msapling",
3489
3632
  "backups",
3490
- `${filename}.${Date.now()}.bak`
3633
+ `${filename}.backup-${stamp}-${suffix}.bak`
3491
3634
  );
3492
- await mkdir10(join27(homedir15(), ".msapling", "backups"), { recursive: true });
3635
+ await mkdir9(join31(homedir17(), ".msapling", "backups"), { recursive: true });
3493
3636
  const content = await readFile23(absPath, "utf8");
3494
3637
  await writeFile12(backupPath, content, "utf8");
3495
3638
  return backupPath;
@@ -3641,8 +3784,9 @@ Overwritten destination backed up to: ${backedUpTo}`;
3641
3784
  });
3642
3785
 
3643
3786
  // ../core/src/tools/DeleteFileTool.ts
3644
- import { resolve as resolve12, normalize as normalize11, relative as relative12, isAbsolute as isAbsolute12, join as join10 } from "path";
3787
+ import { resolve as resolve12, normalize as normalize11, relative as relative12, isAbsolute as isAbsolute12, join as join11 } from "path";
3645
3788
  import { rm as rm2, stat as stat3 } from "fs/promises";
3789
+ import { randomBytes as randomBytes6 } from "crypto";
3646
3790
  var DeleteFileTool;
3647
3791
  var init_DeleteFileTool = __esm({
3648
3792
  "../core/src/tools/DeleteFileTool.ts"() {
@@ -3715,12 +3859,14 @@ var init_DeleteFileTool = __esm({
3715
3859
  let backedUpTo = null;
3716
3860
  if (isFile) {
3717
3861
  try {
3718
- const { readFile: readFile23, writeFile: writeFile12, mkdir: mkdir10 } = await import("fs/promises");
3719
- const { homedir: homedir15 } = await import("os");
3862
+ const { readFile: readFile23, writeFile: writeFile12, mkdir: mkdir9 } = await import("fs/promises");
3863
+ const { homedir: homedir17 } = await import("os");
3720
3864
  const existingContent = await readFile23(abs, "utf8");
3721
3865
  const filename = abs.split(/[\\/]/).pop() ?? "file";
3722
- const backupPath = join10(homedir15(), ".msapling", "backups", `${filename}.${Date.now()}.bak`);
3723
- await mkdir10(join10(homedir15(), ".msapling", "backups"), { recursive: true });
3866
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
3867
+ const suffix = randomBytes6(4).toString("hex");
3868
+ const backupPath = join11(homedir17(), ".msapling", "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
3869
+ await mkdir9(join11(homedir17(), ".msapling", "backups"), { recursive: true });
3724
3870
  await writeFile12(backupPath, existingContent, "utf8");
3725
3871
  backedUpTo = backupPath;
3726
3872
  } catch {
@@ -3744,7 +3890,7 @@ var init_DeleteFileTool = __esm({
3744
3890
  let summary = `Deleted ${kind}: ${abs}`;
3745
3891
  if (backedUpTo) {
3746
3892
  summary += `
3747
- Content backed up to: ${backedUpTo}`;
3893
+ Backup: ${backedUpTo}`;
3748
3894
  }
3749
3895
  return { content: summary };
3750
3896
  }
@@ -3942,12 +4088,81 @@ var init_Sandbox = __esm({
3942
4088
  }
3943
4089
  return { status: "safe", hash };
3944
4090
  }
3945
- getRestrictedEnv() {
3946
- const safeKeys = ["PATH", "LANG", "LC_ALL", "NODE_ENV", "BUN_ENV"];
4091
+ /**
4092
+ * CLI-AUDIT-AUTO-44: curate PATH for sandboxed subprocesses.
4093
+ *
4094
+ * Previously `getRestrictedEnv` forwarded `process.env.PATH` verbatim. That
4095
+ * means any directory injected onto the user's PATH (npm-global, ~/.local/bin,
4096
+ * a malicious package's postinstall step adding /tmp/evil) inherits straight
4097
+ * into every sandboxed tool spawn. Containment is then trivially defeated:
4098
+ * "git" might resolve to /tmp/evil/git.
4099
+ *
4100
+ * Strategy:
4101
+ * - Default to a small allowlist of canonical system dirs (matches POSIX
4102
+ * `/etc/login.defs` style + common Windows system roots).
4103
+ * - Filter the caller's PATH against the allowlist, preserving order so
4104
+ * that legitimate /usr/local/bin/git still wins over /usr/bin/git.
4105
+ * - Drop world-writable / home-relative entries (`/tmp`, `~`, `.`, ``).
4106
+ * - Caller can override with an explicit `pathOverride` argument (e.g.
4107
+ * boot-time validation, tests).
4108
+ */
4109
+ static SAFE_PATH_ENTRIES = /* @__PURE__ */ new Set([
4110
+ // POSIX
4111
+ "/usr/local/sbin",
4112
+ "/usr/local/bin",
4113
+ "/usr/sbin",
4114
+ "/usr/bin",
4115
+ "/sbin",
4116
+ "/bin",
4117
+ // macOS extras
4118
+ "/opt/homebrew/bin",
4119
+ "/opt/homebrew/sbin",
4120
+ "/opt/local/bin",
4121
+ // Windows
4122
+ "C:\\Windows\\System32",
4123
+ "C:\\Windows",
4124
+ "C:\\Windows\\System32\\Wbem",
4125
+ "C:\\Windows\\System32\\WindowsPowerShell\\v1.0"
4126
+ ]);
4127
+ /**
4128
+ * Decide whether a single PATH entry is acceptable inside the sandbox.
4129
+ * Exposed (static) for unit testing.
4130
+ */
4131
+ static isSafePathEntry(entry) {
4132
+ if (!entry) return false;
4133
+ const e = entry.trim();
4134
+ if (!e) return false;
4135
+ if (e === "." || e === "..") return false;
4136
+ if (e.startsWith("~")) return false;
4137
+ if (e.startsWith("/tmp") || e.startsWith("/var/tmp")) return false;
4138
+ if (process.platform === "win32") {
4139
+ const lower = e.toLowerCase();
4140
+ for (const safe of _Sandbox.SAFE_PATH_ENTRIES) {
4141
+ if (safe.toLowerCase() === lower) return true;
4142
+ }
4143
+ return false;
4144
+ }
4145
+ return _Sandbox.SAFE_PATH_ENTRIES.has(e);
4146
+ }
4147
+ /**
4148
+ * Build a curated PATH from the host environment, retaining only entries that
4149
+ * pass `isSafePathEntry`. Order is preserved. If nothing matches, fall back
4150
+ * to a minimal platform default so subprocess spawn never sees an empty PATH.
4151
+ */
4152
+ static curatePath(rawPath) {
4153
+ const sep3 = process.platform === "win32" ? ";" : ":";
4154
+ const entries = (rawPath ?? "").split(sep3);
4155
+ const kept = entries.filter((e) => _Sandbox.isSafePathEntry(e));
4156
+ if (kept.length > 0) return kept.join(sep3);
4157
+ return process.platform === "win32" ? "C:\\Windows\\System32;C:\\Windows" : "/usr/local/bin:/usr/bin:/bin";
4158
+ }
4159
+ getRestrictedEnv(pathOverride) {
4160
+ const safeKeys = ["LANG", "LC_ALL", "NODE_ENV", "BUN_ENV"];
3947
4161
  const filteredEnv = {};
3948
4162
  for (const key of safeKeys) {
3949
4163
  if (process.env[key]) filteredEnv[key] = process.env[key];
3950
4164
  }
4165
+ filteredEnv["PATH"] = pathOverride ?? _Sandbox.curatePath(process.env.PATH);
3951
4166
  filteredEnv["MSAPLING_SANDBOX"] = "true";
3952
4167
  return filteredEnv;
3953
4168
  }
@@ -3991,10 +4206,10 @@ var init_Voice = __esm({
3991
4206
  `;
3992
4207
  try {
3993
4208
  if (process.platform === "win32") {
3994
- await new Promise((resolve17, reject) => {
4209
+ await new Promise((resolve18, reject) => {
3995
4210
  try {
3996
4211
  const proc = spawn6("powershell", ["-Command", psCommand]);
3997
- proc.on("exit", () => resolve17());
4212
+ proc.on("exit", () => resolve18());
3998
4213
  proc.on("error", reject);
3999
4214
  } catch (e) {
4000
4215
  reject(e);
@@ -4108,7 +4323,7 @@ function matches(entry, ctx) {
4108
4323
  async function runOne(entry, ctx) {
4109
4324
  const timeoutMs = entry.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
4110
4325
  const command = entry.command;
4111
- return new Promise((resolve17) => {
4326
+ return new Promise((resolve18) => {
4112
4327
  const isWindows = process.platform === "win32";
4113
4328
  const child = spawn7(isWindows ? "cmd.exe" : "sh", isWindows ? ["/c", command] : ["-c", command], {
4114
4329
  cwd: ctx.cwd ?? process.cwd(),
@@ -4123,7 +4338,7 @@ async function runOne(entry, ctx) {
4123
4338
  settled = true;
4124
4339
  clearTimeout(killer);
4125
4340
  const blocked = !!entry.blocking && (exitCode === null || exitCode !== 0);
4126
- resolve17({ command, exitCode, stdout, stderr, timedOut, blocked });
4341
+ resolve18({ command, exitCode, stdout, stderr, timedOut, blocked });
4127
4342
  };
4128
4343
  const killer = setTimeout(() => {
4129
4344
  timedOut = true;
@@ -4371,6 +4586,35 @@ ${blocker.stderr || "(empty)"}`,
4371
4586
  };
4372
4587
  }
4373
4588
  }
4589
+ if (args2.path) {
4590
+ const check = this.sandbox.isPathSafe(args2.path);
4591
+ if (!check.safe) {
4592
+ await this.voice.speak(`Security block detected`, "urgent");
4593
+ const staleKey = `${toolName}:${(args2.path || "").trim()}`;
4594
+ if (this.trustStore?.has(staleKey)) {
4595
+ this.trustStore.delete(staleKey).catch(() => {
4596
+ });
4597
+ }
4598
+ return { content: `Security Block: ${check.reason}`, isError: true };
4599
+ }
4600
+ }
4601
+ if (toolName === "run_command" || toolName === "bash_command") {
4602
+ if (!args2.command) {
4603
+ return { content: `Error: ${toolName} requires a command argument`, isError: true };
4604
+ }
4605
+ const analysis = this.sandbox.analyzeCommand(args2.command);
4606
+ if (analysis.status === "blocked") {
4607
+ const staleCmdKey = toolName === "bash_command" ? `bash_command:${(args2.command || "").trim().replace(/\s+/g, " ")}${args2.cwd ? `:cwd=${args2.cwd}` : ""}` : `run_command:${(args2.command || "").trim()}`;
4608
+ if (this.trustStore?.has(staleCmdKey)) {
4609
+ this.trustStore.delete(staleCmdKey).catch(() => {
4610
+ });
4611
+ }
4612
+ return { content: `Security Block: ${analysis.reason}`, isError: true };
4613
+ }
4614
+ if (analysis.status === "dangerous" && this.mode !== "bypassPermissions") {
4615
+ return { content: `Dangerous Command Blocked: ${analysis.reason}. Use manual approval or trust hash ${analysis.hash}.`, isError: true };
4616
+ }
4617
+ }
4374
4618
  if (this.needsApproval(toolName) && this.approvalCallback) {
4375
4619
  let cmdKey = "";
4376
4620
  if (toolName === "run_command") {
@@ -4382,9 +4626,6 @@ ${blocker.stderr || "(empty)"}`,
4382
4626
  } else {
4383
4627
  cmdKey = `${toolName}:${(args2.path || args2.instruction || "").trim()}`;
4384
4628
  }
4385
- if ((toolName === "run_command" || toolName === "bash_command") && !args2.command) {
4386
- return { content: `Error: ${toolName} requires a command argument`, isError: true };
4387
- }
4388
4629
  const alreadyTrusted = this.trustStore ? this.trustStore.has(cmdKey) : this.sessionTrust.has(cmdKey);
4389
4630
  if (!alreadyTrusted) {
4390
4631
  const decision = await this.approvalCallback({
@@ -4431,22 +4672,6 @@ ${blocker.stderr || "(empty)"}`,
4431
4672
  Please approve the diff in the UI to sync this change locally.`
4432
4673
  };
4433
4674
  }
4434
- if (args2.path) {
4435
- const check = this.sandbox.isPathSafe(args2.path);
4436
- if (!check.safe) {
4437
- await this.voice.speak(`Security block detected`, "urgent");
4438
- return { content: `Security Block: ${check.reason}`, isError: true };
4439
- }
4440
- }
4441
- if (toolName === "run_command" || toolName === "bash_command") {
4442
- const analysis = this.sandbox.analyzeCommand(args2.command);
4443
- if (analysis.status === "blocked") {
4444
- return { content: `Security Block: ${analysis.reason}`, isError: true };
4445
- }
4446
- if (analysis.status === "dangerous" && this.mode !== "bypassPermissions") {
4447
- return { content: `Dangerous Command Blocked: ${analysis.reason}. Use manual approval or trust hash ${analysis.hash}.`, isError: true };
4448
- }
4449
- }
4450
4675
  const result = await tool.execute(args2, projectRoot);
4451
4676
  if (this.hooks) {
4452
4677
  this.hooks.fire({
@@ -4519,7 +4744,7 @@ var init_Safety = __esm({
4519
4744
 
4520
4745
  // ../core/src/ProjectConfig.ts
4521
4746
  import { homedir as homedir5 } from "os";
4522
- import { join as join11, dirname as dirname2, parse as parsePath } from "path";
4747
+ import { join as join12, dirname as dirname2, parse as parsePath } from "path";
4523
4748
  import { existsSync as existsSync10 } from "fs";
4524
4749
  import { readFile as readFile9 } from "fs/promises";
4525
4750
  async function readIfExists(path2) {
@@ -4533,7 +4758,7 @@ async function readIfExists(path2) {
4533
4758
  }
4534
4759
  async function findInDir(dir) {
4535
4760
  for (const filename of FILENAMES) {
4536
- const path2 = join11(dir, filename);
4761
+ const path2 = join12(dir, filename);
4537
4762
  const content = await readIfExists(path2);
4538
4763
  if (content !== null) {
4539
4764
  return { path: path2, filename, content };
@@ -4558,7 +4783,7 @@ async function findProjectConfig(start) {
4558
4783
  async function findUserConfig() {
4559
4784
  const home = homedir5();
4560
4785
  if (!home) return null;
4561
- const userDir = join11(home, ".msapling");
4786
+ const userDir = join12(home, ".msapling");
4562
4787
  return findInDir(userDir);
4563
4788
  }
4564
4789
  function buildCombined(user, project) {
@@ -4968,8 +5193,8 @@ var init_Mutex = __esm({
4968
5193
  */
4969
5194
  acquire() {
4970
5195
  let release2;
4971
- const next = new Promise((resolve17) => {
4972
- release2 = resolve17;
5196
+ const next = new Promise((resolve18) => {
5197
+ release2 = resolve18;
4973
5198
  });
4974
5199
  const entry = this._queue.then(() => release2);
4975
5200
  this._queue = this._queue.then(() => next);
@@ -4992,17 +5217,17 @@ var init_Mutex = __esm({
4992
5217
  });
4993
5218
 
4994
5219
  // ../core/src/TrustStore.ts
4995
- import { join as join12 } from "path";
4996
- import { homedir as homedir6 } from "os";
5220
+ import { join as join13 } from "path";
5221
+ import { homedir as homedir6, platform as platform2 } from "os";
4997
5222
  import { existsSync as existsSync11, mkdirSync } from "fs";
4998
- import { readFile as readFile10, writeFile as writeFile5 } from "fs/promises";
5223
+ import { readFile as readFile10, writeFile as writeFile5, chmod } from "fs/promises";
4999
5224
  var USER_SETTINGS_PATH, TrustStore;
5000
5225
  var init_TrustStore = __esm({
5001
5226
  "../core/src/TrustStore.ts"() {
5002
5227
  "use strict";
5003
5228
  init_esm_shims();
5004
5229
  init_Mutex();
5005
- USER_SETTINGS_PATH = join12(homedir6(), ".msapling", "settings.json");
5230
+ USER_SETTINGS_PATH = join13(homedir6(), ".msapling", "settings.json");
5006
5231
  TrustStore = class {
5007
5232
  /** Current in-memory set of trusted `tool:command` keys. */
5008
5233
  trusted = /* @__PURE__ */ new Set();
@@ -5032,9 +5257,15 @@ var init_TrustStore = __esm({
5032
5257
  * updating `trustedCommands`.
5033
5258
  */
5034
5259
  async writeSettings(settings) {
5035
- const dir = join12(homedir6(), ".msapling");
5260
+ const dir = join13(homedir6(), ".msapling");
5036
5261
  if (!existsSync11(dir)) mkdirSync(dir, { recursive: true });
5037
5262
  await writeFile5(this.settingsPath, JSON.stringify(settings, null, 2), "utf8");
5263
+ if (platform2() !== "win32") {
5264
+ try {
5265
+ await chmod(this.settingsPath, 384);
5266
+ } catch {
5267
+ }
5268
+ }
5038
5269
  }
5039
5270
  // ── Public API ─────────────────────────────────────────────────────────────
5040
5271
  /**
@@ -5112,7 +5343,7 @@ var require_polyfills = __commonJS({
5112
5343
  var constants = __require("constants");
5113
5344
  var origCwd = process.cwd;
5114
5345
  var cwd = null;
5115
- var platform2 = process.env.GRACEFUL_FS_PLATFORM || process.platform;
5346
+ var platform4 = process.env.GRACEFUL_FS_PLATFORM || process.platform;
5116
5347
  process.cwd = function() {
5117
5348
  if (!cwd)
5118
5349
  cwd = origCwd.call(process);
@@ -5132,54 +5363,54 @@ var require_polyfills = __commonJS({
5132
5363
  }
5133
5364
  var chdir;
5134
5365
  module.exports = patch;
5135
- function patch(fs) {
5366
+ function patch(fs3) {
5136
5367
  if (constants.hasOwnProperty("O_SYMLINK") && process.version.match(/^v0\.6\.[0-2]|^v0\.5\./)) {
5137
- patchLchmod(fs);
5138
- }
5139
- if (!fs.lutimes) {
5140
- patchLutimes(fs);
5141
- }
5142
- fs.chown = chownFix(fs.chown);
5143
- fs.fchown = chownFix(fs.fchown);
5144
- fs.lchown = chownFix(fs.lchown);
5145
- fs.chmod = chmodFix(fs.chmod);
5146
- fs.fchmod = chmodFix(fs.fchmod);
5147
- fs.lchmod = chmodFix(fs.lchmod);
5148
- fs.chownSync = chownFixSync(fs.chownSync);
5149
- fs.fchownSync = chownFixSync(fs.fchownSync);
5150
- fs.lchownSync = chownFixSync(fs.lchownSync);
5151
- fs.chmodSync = chmodFixSync(fs.chmodSync);
5152
- fs.fchmodSync = chmodFixSync(fs.fchmodSync);
5153
- fs.lchmodSync = chmodFixSync(fs.lchmodSync);
5154
- fs.stat = statFix(fs.stat);
5155
- fs.fstat = statFix(fs.fstat);
5156
- fs.lstat = statFix(fs.lstat);
5157
- fs.statSync = statFixSync(fs.statSync);
5158
- fs.fstatSync = statFixSync(fs.fstatSync);
5159
- fs.lstatSync = statFixSync(fs.lstatSync);
5160
- if (fs.chmod && !fs.lchmod) {
5161
- fs.lchmod = function(path2, mode, cb) {
5368
+ patchLchmod(fs3);
5369
+ }
5370
+ if (!fs3.lutimes) {
5371
+ patchLutimes(fs3);
5372
+ }
5373
+ fs3.chown = chownFix(fs3.chown);
5374
+ fs3.fchown = chownFix(fs3.fchown);
5375
+ fs3.lchown = chownFix(fs3.lchown);
5376
+ fs3.chmod = chmodFix(fs3.chmod);
5377
+ fs3.fchmod = chmodFix(fs3.fchmod);
5378
+ fs3.lchmod = chmodFix(fs3.lchmod);
5379
+ fs3.chownSync = chownFixSync(fs3.chownSync);
5380
+ fs3.fchownSync = chownFixSync(fs3.fchownSync);
5381
+ fs3.lchownSync = chownFixSync(fs3.lchownSync);
5382
+ fs3.chmodSync = chmodFixSync(fs3.chmodSync);
5383
+ fs3.fchmodSync = chmodFixSync(fs3.fchmodSync);
5384
+ fs3.lchmodSync = chmodFixSync(fs3.lchmodSync);
5385
+ fs3.stat = statFix(fs3.stat);
5386
+ fs3.fstat = statFix(fs3.fstat);
5387
+ fs3.lstat = statFix(fs3.lstat);
5388
+ fs3.statSync = statFixSync(fs3.statSync);
5389
+ fs3.fstatSync = statFixSync(fs3.fstatSync);
5390
+ fs3.lstatSync = statFixSync(fs3.lstatSync);
5391
+ if (fs3.chmod && !fs3.lchmod) {
5392
+ fs3.lchmod = function(path2, mode, cb) {
5162
5393
  if (cb) process.nextTick(cb);
5163
5394
  };
5164
- fs.lchmodSync = function() {
5395
+ fs3.lchmodSync = function() {
5165
5396
  };
5166
5397
  }
5167
- if (fs.chown && !fs.lchown) {
5168
- fs.lchown = function(path2, uid, gid, cb) {
5398
+ if (fs3.chown && !fs3.lchown) {
5399
+ fs3.lchown = function(path2, uid, gid, cb) {
5169
5400
  if (cb) process.nextTick(cb);
5170
5401
  };
5171
- fs.lchownSync = function() {
5402
+ fs3.lchownSync = function() {
5172
5403
  };
5173
5404
  }
5174
- if (platform2 === "win32") {
5175
- fs.rename = typeof fs.rename !== "function" ? fs.rename : (function(fs$rename) {
5405
+ if (platform4 === "win32") {
5406
+ fs3.rename = typeof fs3.rename !== "function" ? fs3.rename : (function(fs$rename) {
5176
5407
  function rename2(from, to, cb) {
5177
5408
  var start = Date.now();
5178
5409
  var backoff = 0;
5179
5410
  fs$rename(from, to, function CB(er) {
5180
5411
  if (er && (er.code === "EACCES" || er.code === "EPERM" || er.code === "EBUSY") && Date.now() - start < 6e4) {
5181
5412
  setTimeout(function() {
5182
- fs.stat(to, function(stater, st) {
5413
+ fs3.stat(to, function(stater, st) {
5183
5414
  if (stater && stater.code === "ENOENT")
5184
5415
  fs$rename(from, to, CB);
5185
5416
  else
@@ -5195,9 +5426,9 @@ var require_polyfills = __commonJS({
5195
5426
  }
5196
5427
  if (Object.setPrototypeOf) Object.setPrototypeOf(rename2, fs$rename);
5197
5428
  return rename2;
5198
- })(fs.rename);
5429
+ })(fs3.rename);
5199
5430
  }
5200
- fs.read = typeof fs.read !== "function" ? fs.read : (function(fs$read) {
5431
+ fs3.read = typeof fs3.read !== "function" ? fs3.read : (function(fs$read) {
5201
5432
  function read(fd, buffer, offset, length, position, callback_) {
5202
5433
  var callback;
5203
5434
  if (callback_ && typeof callback_ === "function") {
@@ -5205,22 +5436,22 @@ var require_polyfills = __commonJS({
5205
5436
  callback = function(er, _, __) {
5206
5437
  if (er && er.code === "EAGAIN" && eagCounter < 10) {
5207
5438
  eagCounter++;
5208
- return fs$read.call(fs, fd, buffer, offset, length, position, callback);
5439
+ return fs$read.call(fs3, fd, buffer, offset, length, position, callback);
5209
5440
  }
5210
5441
  callback_.apply(this, arguments);
5211
5442
  };
5212
5443
  }
5213
- return fs$read.call(fs, fd, buffer, offset, length, position, callback);
5444
+ return fs$read.call(fs3, fd, buffer, offset, length, position, callback);
5214
5445
  }
5215
5446
  if (Object.setPrototypeOf) Object.setPrototypeOf(read, fs$read);
5216
5447
  return read;
5217
- })(fs.read);
5218
- fs.readSync = typeof fs.readSync !== "function" ? fs.readSync : /* @__PURE__ */ (function(fs$readSync) {
5448
+ })(fs3.read);
5449
+ fs3.readSync = typeof fs3.readSync !== "function" ? fs3.readSync : /* @__PURE__ */ (function(fs$readSync) {
5219
5450
  return function(fd, buffer, offset, length, position) {
5220
5451
  var eagCounter = 0;
5221
5452
  while (true) {
5222
5453
  try {
5223
- return fs$readSync.call(fs, fd, buffer, offset, length, position);
5454
+ return fs$readSync.call(fs3, fd, buffer, offset, length, position);
5224
5455
  } catch (er) {
5225
5456
  if (er.code === "EAGAIN" && eagCounter < 10) {
5226
5457
  eagCounter++;
@@ -5230,10 +5461,10 @@ var require_polyfills = __commonJS({
5230
5461
  }
5231
5462
  }
5232
5463
  };
5233
- })(fs.readSync);
5234
- function patchLchmod(fs2) {
5235
- fs2.lchmod = function(path2, mode, callback) {
5236
- fs2.open(
5464
+ })(fs3.readSync);
5465
+ function patchLchmod(fs4) {
5466
+ fs4.lchmod = function(path2, mode, callback) {
5467
+ fs4.open(
5237
5468
  path2,
5238
5469
  constants.O_WRONLY | constants.O_SYMLINK,
5239
5470
  mode,
@@ -5242,80 +5473,80 @@ var require_polyfills = __commonJS({
5242
5473
  if (callback) callback(err);
5243
5474
  return;
5244
5475
  }
5245
- fs2.fchmod(fd, mode, function(err2) {
5246
- fs2.close(fd, function(err22) {
5476
+ fs4.fchmod(fd, mode, function(err2) {
5477
+ fs4.close(fd, function(err22) {
5247
5478
  if (callback) callback(err2 || err22);
5248
5479
  });
5249
5480
  });
5250
5481
  }
5251
5482
  );
5252
5483
  };
5253
- fs2.lchmodSync = function(path2, mode) {
5254
- var fd = fs2.openSync(path2, constants.O_WRONLY | constants.O_SYMLINK, mode);
5484
+ fs4.lchmodSync = function(path2, mode) {
5485
+ var fd = fs4.openSync(path2, constants.O_WRONLY | constants.O_SYMLINK, mode);
5255
5486
  var threw = true;
5256
5487
  var ret;
5257
5488
  try {
5258
- ret = fs2.fchmodSync(fd, mode);
5489
+ ret = fs4.fchmodSync(fd, mode);
5259
5490
  threw = false;
5260
5491
  } finally {
5261
5492
  if (threw) {
5262
5493
  try {
5263
- fs2.closeSync(fd);
5494
+ fs4.closeSync(fd);
5264
5495
  } catch (er) {
5265
5496
  }
5266
5497
  } else {
5267
- fs2.closeSync(fd);
5498
+ fs4.closeSync(fd);
5268
5499
  }
5269
5500
  }
5270
5501
  return ret;
5271
5502
  };
5272
5503
  }
5273
- function patchLutimes(fs2) {
5274
- if (constants.hasOwnProperty("O_SYMLINK") && fs2.futimes) {
5275
- fs2.lutimes = function(path2, at, mt, cb) {
5276
- fs2.open(path2, constants.O_SYMLINK, function(er, fd) {
5504
+ function patchLutimes(fs4) {
5505
+ if (constants.hasOwnProperty("O_SYMLINK") && fs4.futimes) {
5506
+ fs4.lutimes = function(path2, at, mt, cb) {
5507
+ fs4.open(path2, constants.O_SYMLINK, function(er, fd) {
5277
5508
  if (er) {
5278
5509
  if (cb) cb(er);
5279
5510
  return;
5280
5511
  }
5281
- fs2.futimes(fd, at, mt, function(er2) {
5282
- fs2.close(fd, function(er22) {
5512
+ fs4.futimes(fd, at, mt, function(er2) {
5513
+ fs4.close(fd, function(er22) {
5283
5514
  if (cb) cb(er2 || er22);
5284
5515
  });
5285
5516
  });
5286
5517
  });
5287
5518
  };
5288
- fs2.lutimesSync = function(path2, at, mt) {
5289
- var fd = fs2.openSync(path2, constants.O_SYMLINK);
5519
+ fs4.lutimesSync = function(path2, at, mt) {
5520
+ var fd = fs4.openSync(path2, constants.O_SYMLINK);
5290
5521
  var ret;
5291
5522
  var threw = true;
5292
5523
  try {
5293
- ret = fs2.futimesSync(fd, at, mt);
5524
+ ret = fs4.futimesSync(fd, at, mt);
5294
5525
  threw = false;
5295
5526
  } finally {
5296
5527
  if (threw) {
5297
5528
  try {
5298
- fs2.closeSync(fd);
5529
+ fs4.closeSync(fd);
5299
5530
  } catch (er) {
5300
5531
  }
5301
5532
  } else {
5302
- fs2.closeSync(fd);
5533
+ fs4.closeSync(fd);
5303
5534
  }
5304
5535
  }
5305
5536
  return ret;
5306
5537
  };
5307
- } else if (fs2.futimes) {
5308
- fs2.lutimes = function(_a, _b, _c, cb) {
5538
+ } else if (fs4.futimes) {
5539
+ fs4.lutimes = function(_a, _b, _c, cb) {
5309
5540
  if (cb) process.nextTick(cb);
5310
5541
  };
5311
- fs2.lutimesSync = function() {
5542
+ fs4.lutimesSync = function() {
5312
5543
  };
5313
5544
  }
5314
5545
  }
5315
5546
  function chmodFix(orig) {
5316
5547
  if (!orig) return orig;
5317
5548
  return function(target, mode, cb) {
5318
- return orig.call(fs, target, mode, function(er) {
5549
+ return orig.call(fs3, target, mode, function(er) {
5319
5550
  if (chownErOk(er)) er = null;
5320
5551
  if (cb) cb.apply(this, arguments);
5321
5552
  });
@@ -5325,7 +5556,7 @@ var require_polyfills = __commonJS({
5325
5556
  if (!orig) return orig;
5326
5557
  return function(target, mode) {
5327
5558
  try {
5328
- return orig.call(fs, target, mode);
5559
+ return orig.call(fs3, target, mode);
5329
5560
  } catch (er) {
5330
5561
  if (!chownErOk(er)) throw er;
5331
5562
  }
@@ -5334,7 +5565,7 @@ var require_polyfills = __commonJS({
5334
5565
  function chownFix(orig) {
5335
5566
  if (!orig) return orig;
5336
5567
  return function(target, uid, gid, cb) {
5337
- return orig.call(fs, target, uid, gid, function(er) {
5568
+ return orig.call(fs3, target, uid, gid, function(er) {
5338
5569
  if (chownErOk(er)) er = null;
5339
5570
  if (cb) cb.apply(this, arguments);
5340
5571
  });
@@ -5344,7 +5575,7 @@ var require_polyfills = __commonJS({
5344
5575
  if (!orig) return orig;
5345
5576
  return function(target, uid, gid) {
5346
5577
  try {
5347
- return orig.call(fs, target, uid, gid);
5578
+ return orig.call(fs3, target, uid, gid);
5348
5579
  } catch (er) {
5349
5580
  if (!chownErOk(er)) throw er;
5350
5581
  }
@@ -5364,13 +5595,13 @@ var require_polyfills = __commonJS({
5364
5595
  }
5365
5596
  if (cb) cb.apply(this, arguments);
5366
5597
  }
5367
- return options ? orig.call(fs, target, options, callback) : orig.call(fs, target, callback);
5598
+ return options ? orig.call(fs3, target, options, callback) : orig.call(fs3, target, callback);
5368
5599
  };
5369
5600
  }
5370
5601
  function statFixSync(orig) {
5371
5602
  if (!orig) return orig;
5372
5603
  return function(target, options) {
5373
- var stats = options ? orig.call(fs, target, options) : orig.call(fs, target);
5604
+ var stats = options ? orig.call(fs3, target, options) : orig.call(fs3, target);
5374
5605
  if (stats) {
5375
5606
  if (stats.uid < 0) stats.uid += 4294967296;
5376
5607
  if (stats.gid < 0) stats.gid += 4294967296;
@@ -5401,7 +5632,7 @@ var require_legacy_streams = __commonJS({
5401
5632
  init_esm_shims();
5402
5633
  var Stream = __require("stream").Stream;
5403
5634
  module.exports = legacy;
5404
- function legacy(fs) {
5635
+ function legacy(fs3) {
5405
5636
  return {
5406
5637
  ReadStream,
5407
5638
  WriteStream
@@ -5444,7 +5675,7 @@ var require_legacy_streams = __commonJS({
5444
5675
  });
5445
5676
  return;
5446
5677
  }
5447
- fs.open(this.path, this.flags, this.mode, function(err, fd) {
5678
+ fs3.open(this.path, this.flags, this.mode, function(err, fd) {
5448
5679
  if (err) {
5449
5680
  self.emit("error", err);
5450
5681
  self.readable = false;
@@ -5483,7 +5714,7 @@ var require_legacy_streams = __commonJS({
5483
5714
  this.busy = false;
5484
5715
  this._queue = [];
5485
5716
  if (this.fd === null) {
5486
- this._open = fs.open;
5717
+ this._open = fs3.open;
5487
5718
  this._queue.push([this._open, this.path, this.flags, this.mode, void 0]);
5488
5719
  this.flush();
5489
5720
  }
@@ -5521,7 +5752,7 @@ var require_graceful_fs = __commonJS({
5521
5752
  "../../node_modules/.bun/graceful-fs@4.2.11/node_modules/graceful-fs/graceful-fs.js"(exports, module) {
5522
5753
  "use strict";
5523
5754
  init_esm_shims();
5524
- var fs = __require("fs");
5755
+ var fs3 = __require("fs");
5525
5756
  var polyfills = require_polyfills();
5526
5757
  var legacy = require_legacy_streams();
5527
5758
  var clone = require_clone();
@@ -5553,12 +5784,12 @@ var require_graceful_fs = __commonJS({
5553
5784
  m = "GFS4: " + m.split(/\n/).join("\nGFS4: ");
5554
5785
  console.error(m);
5555
5786
  };
5556
- if (!fs[gracefulQueue]) {
5787
+ if (!fs3[gracefulQueue]) {
5557
5788
  queue = global[gracefulQueue] || [];
5558
- publishQueue(fs, queue);
5559
- fs.close = (function(fs$close) {
5789
+ publishQueue(fs3, queue);
5790
+ fs3.close = (function(fs$close) {
5560
5791
  function close(fd, cb) {
5561
- return fs$close.call(fs, fd, function(err) {
5792
+ return fs$close.call(fs3, fd, function(err) {
5562
5793
  if (!err) {
5563
5794
  resetQueue();
5564
5795
  }
@@ -5570,40 +5801,40 @@ var require_graceful_fs = __commonJS({
5570
5801
  value: fs$close
5571
5802
  });
5572
5803
  return close;
5573
- })(fs.close);
5574
- fs.closeSync = (function(fs$closeSync) {
5804
+ })(fs3.close);
5805
+ fs3.closeSync = (function(fs$closeSync) {
5575
5806
  function closeSync(fd) {
5576
- fs$closeSync.apply(fs, arguments);
5807
+ fs$closeSync.apply(fs3, arguments);
5577
5808
  resetQueue();
5578
5809
  }
5579
5810
  Object.defineProperty(closeSync, previousSymbol, {
5580
5811
  value: fs$closeSync
5581
5812
  });
5582
5813
  return closeSync;
5583
- })(fs.closeSync);
5814
+ })(fs3.closeSync);
5584
5815
  if (/\bgfs4\b/i.test(process.env.NODE_DEBUG || "")) {
5585
5816
  process.on("exit", function() {
5586
- debug(fs[gracefulQueue]);
5587
- __require("assert").equal(fs[gracefulQueue].length, 0);
5817
+ debug(fs3[gracefulQueue]);
5818
+ __require("assert").equal(fs3[gracefulQueue].length, 0);
5588
5819
  });
5589
5820
  }
5590
5821
  }
5591
5822
  var queue;
5592
5823
  if (!global[gracefulQueue]) {
5593
- publishQueue(global, fs[gracefulQueue]);
5594
- }
5595
- module.exports = patch(clone(fs));
5596
- if (process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH && !fs.__patched) {
5597
- module.exports = patch(fs);
5598
- fs.__patched = true;
5599
- }
5600
- function patch(fs2) {
5601
- polyfills(fs2);
5602
- fs2.gracefulify = patch;
5603
- fs2.createReadStream = createReadStream;
5604
- fs2.createWriteStream = createWriteStream;
5605
- var fs$readFile = fs2.readFile;
5606
- fs2.readFile = readFile23;
5824
+ publishQueue(global, fs3[gracefulQueue]);
5825
+ }
5826
+ module.exports = patch(clone(fs3));
5827
+ if (process.env.TEST_GRACEFUL_FS_GLOBAL_PATCH && !fs3.__patched) {
5828
+ module.exports = patch(fs3);
5829
+ fs3.__patched = true;
5830
+ }
5831
+ function patch(fs4) {
5832
+ polyfills(fs4);
5833
+ fs4.gracefulify = patch;
5834
+ fs4.createReadStream = createReadStream;
5835
+ fs4.createWriteStream = createWriteStream;
5836
+ var fs$readFile = fs4.readFile;
5837
+ fs4.readFile = readFile23;
5607
5838
  function readFile23(path2, options, cb) {
5608
5839
  if (typeof options === "function")
5609
5840
  cb = options, options = null;
@@ -5619,8 +5850,8 @@ var require_graceful_fs = __commonJS({
5619
5850
  });
5620
5851
  }
5621
5852
  }
5622
- var fs$writeFile = fs2.writeFile;
5623
- fs2.writeFile = writeFile12;
5853
+ var fs$writeFile = fs4.writeFile;
5854
+ fs4.writeFile = writeFile12;
5624
5855
  function writeFile12(path2, data, options, cb) {
5625
5856
  if (typeof options === "function")
5626
5857
  cb = options, options = null;
@@ -5636,10 +5867,10 @@ var require_graceful_fs = __commonJS({
5636
5867
  });
5637
5868
  }
5638
5869
  }
5639
- var fs$appendFile = fs2.appendFile;
5870
+ var fs$appendFile = fs4.appendFile;
5640
5871
  if (fs$appendFile)
5641
- fs2.appendFile = appendFile;
5642
- function appendFile(path2, data, options, cb) {
5872
+ fs4.appendFile = appendFile2;
5873
+ function appendFile2(path2, data, options, cb) {
5643
5874
  if (typeof options === "function")
5644
5875
  cb = options, options = null;
5645
5876
  return go$appendFile(path2, data, options, cb);
@@ -5654,9 +5885,9 @@ var require_graceful_fs = __commonJS({
5654
5885
  });
5655
5886
  }
5656
5887
  }
5657
- var fs$copyFile = fs2.copyFile;
5888
+ var fs$copyFile = fs4.copyFile;
5658
5889
  if (fs$copyFile)
5659
- fs2.copyFile = copyFile2;
5890
+ fs4.copyFile = copyFile2;
5660
5891
  function copyFile2(src, dest, flags, cb) {
5661
5892
  if (typeof flags === "function") {
5662
5893
  cb = flags;
@@ -5674,10 +5905,10 @@ var require_graceful_fs = __commonJS({
5674
5905
  });
5675
5906
  }
5676
5907
  }
5677
- var fs$readdir = fs2.readdir;
5678
- fs2.readdir = readdir4;
5908
+ var fs$readdir = fs4.readdir;
5909
+ fs4.readdir = readdir5;
5679
5910
  var noReaddirOptionVersions = /^v[0-5]\./;
5680
- function readdir4(path2, options, cb) {
5911
+ function readdir5(path2, options, cb) {
5681
5912
  if (typeof options === "function")
5682
5913
  cb = options, options = null;
5683
5914
  var go$readdir = noReaddirOptionVersions.test(process.version) ? function go$readdir2(path3, options2, cb2, startTime) {
@@ -5716,21 +5947,21 @@ var require_graceful_fs = __commonJS({
5716
5947
  }
5717
5948
  }
5718
5949
  if (process.version.substr(0, 4) === "v0.8") {
5719
- var legStreams = legacy(fs2);
5950
+ var legStreams = legacy(fs4);
5720
5951
  ReadStream = legStreams.ReadStream;
5721
5952
  WriteStream = legStreams.WriteStream;
5722
5953
  }
5723
- var fs$ReadStream = fs2.ReadStream;
5954
+ var fs$ReadStream = fs4.ReadStream;
5724
5955
  if (fs$ReadStream) {
5725
5956
  ReadStream.prototype = Object.create(fs$ReadStream.prototype);
5726
5957
  ReadStream.prototype.open = ReadStream$open;
5727
5958
  }
5728
- var fs$WriteStream = fs2.WriteStream;
5959
+ var fs$WriteStream = fs4.WriteStream;
5729
5960
  if (fs$WriteStream) {
5730
5961
  WriteStream.prototype = Object.create(fs$WriteStream.prototype);
5731
5962
  WriteStream.prototype.open = WriteStream$open;
5732
5963
  }
5733
- Object.defineProperty(fs2, "ReadStream", {
5964
+ Object.defineProperty(fs4, "ReadStream", {
5734
5965
  get: function() {
5735
5966
  return ReadStream;
5736
5967
  },
@@ -5740,7 +5971,7 @@ var require_graceful_fs = __commonJS({
5740
5971
  enumerable: true,
5741
5972
  configurable: true
5742
5973
  });
5743
- Object.defineProperty(fs2, "WriteStream", {
5974
+ Object.defineProperty(fs4, "WriteStream", {
5744
5975
  get: function() {
5745
5976
  return WriteStream;
5746
5977
  },
@@ -5751,7 +5982,7 @@ var require_graceful_fs = __commonJS({
5751
5982
  configurable: true
5752
5983
  });
5753
5984
  var FileReadStream = ReadStream;
5754
- Object.defineProperty(fs2, "FileReadStream", {
5985
+ Object.defineProperty(fs4, "FileReadStream", {
5755
5986
  get: function() {
5756
5987
  return FileReadStream;
5757
5988
  },
@@ -5762,7 +5993,7 @@ var require_graceful_fs = __commonJS({
5762
5993
  configurable: true
5763
5994
  });
5764
5995
  var FileWriteStream = WriteStream;
5765
- Object.defineProperty(fs2, "FileWriteStream", {
5996
+ Object.defineProperty(fs4, "FileWriteStream", {
5766
5997
  get: function() {
5767
5998
  return FileWriteStream;
5768
5999
  },
@@ -5811,13 +6042,13 @@ var require_graceful_fs = __commonJS({
5811
6042
  });
5812
6043
  }
5813
6044
  function createReadStream(path2, options) {
5814
- return new fs2.ReadStream(path2, options);
6045
+ return new fs4.ReadStream(path2, options);
5815
6046
  }
5816
6047
  function createWriteStream(path2, options) {
5817
- return new fs2.WriteStream(path2, options);
6048
+ return new fs4.WriteStream(path2, options);
5818
6049
  }
5819
- var fs$open = fs2.open;
5820
- fs2.open = open;
6050
+ var fs$open = fs4.open;
6051
+ fs4.open = open;
5821
6052
  function open(path2, flags, mode, cb) {
5822
6053
  if (typeof mode === "function")
5823
6054
  cb = mode, mode = null;
@@ -5833,20 +6064,20 @@ var require_graceful_fs = __commonJS({
5833
6064
  });
5834
6065
  }
5835
6066
  }
5836
- return fs2;
6067
+ return fs4;
5837
6068
  }
5838
6069
  function enqueue(elem) {
5839
6070
  debug("ENQUEUE", elem[0].name, elem[1]);
5840
- fs[gracefulQueue].push(elem);
6071
+ fs3[gracefulQueue].push(elem);
5841
6072
  retry();
5842
6073
  }
5843
6074
  var retryTimer;
5844
6075
  function resetQueue() {
5845
6076
  var now = Date.now();
5846
- for (var i = 0; i < fs[gracefulQueue].length; ++i) {
5847
- if (fs[gracefulQueue][i].length > 2) {
5848
- fs[gracefulQueue][i][3] = now;
5849
- fs[gracefulQueue][i][4] = now;
6077
+ for (var i = 0; i < fs3[gracefulQueue].length; ++i) {
6078
+ if (fs3[gracefulQueue][i].length > 2) {
6079
+ fs3[gracefulQueue][i][3] = now;
6080
+ fs3[gracefulQueue][i][4] = now;
5850
6081
  }
5851
6082
  }
5852
6083
  retry();
@@ -5854,9 +6085,9 @@ var require_graceful_fs = __commonJS({
5854
6085
  function retry() {
5855
6086
  clearTimeout(retryTimer);
5856
6087
  retryTimer = void 0;
5857
- if (fs[gracefulQueue].length === 0)
6088
+ if (fs3[gracefulQueue].length === 0)
5858
6089
  return;
5859
- var elem = fs[gracefulQueue].shift();
6090
+ var elem = fs3[gracefulQueue].shift();
5860
6091
  var fn = elem[0];
5861
6092
  var args2 = elem[1];
5862
6093
  var err = elem[2];
@@ -5878,7 +6109,7 @@ var require_graceful_fs = __commonJS({
5878
6109
  debug("RETRY", fn.name, args2);
5879
6110
  fn.apply(null, args2.concat([startTime]));
5880
6111
  } else {
5881
- fs[gracefulQueue].push(elem);
6112
+ fs3[gracefulQueue].push(elem);
5882
6113
  }
5883
6114
  }
5884
6115
  if (retryTimer === void 0) {
@@ -6324,10 +6555,10 @@ var require_mtime_precision = __commonJS({
6324
6555
  "use strict";
6325
6556
  init_esm_shims();
6326
6557
  var cacheSymbol = /* @__PURE__ */ Symbol();
6327
- function probe(file, fs, callback) {
6328
- const cachedPrecision = fs[cacheSymbol];
6558
+ function probe(file, fs3, callback) {
6559
+ const cachedPrecision = fs3[cacheSymbol];
6329
6560
  if (cachedPrecision) {
6330
- return fs.stat(file, (err, stat5) => {
6561
+ return fs3.stat(file, (err, stat5) => {
6331
6562
  if (err) {
6332
6563
  return callback(err);
6333
6564
  }
@@ -6335,16 +6566,16 @@ var require_mtime_precision = __commonJS({
6335
6566
  });
6336
6567
  }
6337
6568
  const mtime = new Date(Math.ceil(Date.now() / 1e3) * 1e3 + 5);
6338
- fs.utimes(file, mtime, mtime, (err) => {
6569
+ fs3.utimes(file, mtime, mtime, (err) => {
6339
6570
  if (err) {
6340
6571
  return callback(err);
6341
6572
  }
6342
- fs.stat(file, (err2, stat5) => {
6573
+ fs3.stat(file, (err2, stat5) => {
6343
6574
  if (err2) {
6344
6575
  return callback(err2);
6345
6576
  }
6346
6577
  const precision = stat5.mtime.getTime() % 1e3 === 0 ? "s" : "ms";
6347
- Object.defineProperty(fs, cacheSymbol, { value: precision });
6578
+ Object.defineProperty(fs3, cacheSymbol, { value: precision });
6348
6579
  callback(null, stat5.mtime, precision);
6349
6580
  });
6350
6581
  });
@@ -6367,7 +6598,7 @@ var require_lockfile = __commonJS({
6367
6598
  "use strict";
6368
6599
  init_esm_shims();
6369
6600
  var path2 = __require("path");
6370
- var fs = require_graceful_fs();
6601
+ var fs3 = require_graceful_fs();
6371
6602
  var retry = require_retry2();
6372
6603
  var onExit = require_signal_exit();
6373
6604
  var mtimePrecision = require_mtime_precision();
@@ -6498,7 +6729,7 @@ var require_lockfile = __commonJS({
6498
6729
  update: null,
6499
6730
  realpath: true,
6500
6731
  retries: 0,
6501
- fs,
6732
+ fs: fs3,
6502
6733
  onCompromised: (err) => {
6503
6734
  throw err;
6504
6735
  },
@@ -6542,7 +6773,7 @@ var require_lockfile = __commonJS({
6542
6773
  }
6543
6774
  function unlock2(file, options, callback) {
6544
6775
  options = {
6545
- fs,
6776
+ fs: fs3,
6546
6777
  realpath: true,
6547
6778
  ...options
6548
6779
  };
@@ -6564,7 +6795,7 @@ var require_lockfile = __commonJS({
6564
6795
  options = {
6565
6796
  stale: 1e4,
6566
6797
  realpath: true,
6567
- fs,
6798
+ fs: fs3,
6568
6799
  ...options
6569
6800
  };
6570
6801
  options.stale = Math.max(options.stale || 0, 2e3);
@@ -6604,16 +6835,16 @@ var require_adapter = __commonJS({
6604
6835
  "../../node_modules/.bun/proper-lockfile@4.1.2/node_modules/proper-lockfile/lib/adapter.js"(exports, module) {
6605
6836
  "use strict";
6606
6837
  init_esm_shims();
6607
- var fs = require_graceful_fs();
6608
- function createSyncFs(fs2) {
6838
+ var fs3 = require_graceful_fs();
6839
+ function createSyncFs(fs4) {
6609
6840
  const methods = ["mkdir", "realpath", "stat", "rmdir", "utimes"];
6610
- const newFs = { ...fs2 };
6841
+ const newFs = { ...fs4 };
6611
6842
  methods.forEach((method) => {
6612
6843
  newFs[method] = (...args2) => {
6613
6844
  const callback = args2.pop();
6614
6845
  let ret;
6615
6846
  try {
6616
- ret = fs2[`${method}Sync`](...args2);
6847
+ ret = fs4[`${method}Sync`](...args2);
6617
6848
  } catch (err) {
6618
6849
  return callback(err);
6619
6850
  }
@@ -6623,12 +6854,12 @@ var require_adapter = __commonJS({
6623
6854
  return newFs;
6624
6855
  }
6625
6856
  function toPromise(method) {
6626
- return (...args2) => new Promise((resolve17, reject) => {
6857
+ return (...args2) => new Promise((resolve18, reject) => {
6627
6858
  args2.push((err, result) => {
6628
6859
  if (err) {
6629
6860
  reject(err);
6630
6861
  } else {
6631
- resolve17(result);
6862
+ resolve18(result);
6632
6863
  }
6633
6864
  });
6634
6865
  method(...args2);
@@ -6651,7 +6882,7 @@ var require_adapter = __commonJS({
6651
6882
  }
6652
6883
  function toSyncOptions(options) {
6653
6884
  options = { ...options };
6654
- options.fs = createSyncFs(options.fs || fs);
6885
+ options.fs = createSyncFs(options.fs || fs3);
6655
6886
  if (typeof options.retries === "number" && options.retries > 0 || options.retries && typeof options.retries.retries === "number" && options.retries.retries > 0) {
6656
6887
  throw Object.assign(new Error("Cannot use retries with the sync api"), { code: "ESYNC" });
6657
6888
  }
@@ -6712,7 +6943,7 @@ var init_keytar = __esm({
6712
6943
 
6713
6944
  // node-file:<repo>\node_modules\.bun\keytar@7.9.0\node_modules\keytar\build\Release\keytar.node
6714
6945
  var require_keytar = __commonJS({
6715
- "node-file:D:\\Projects\\MSapling_CLI\\node_modules\\.bun\\keytar@7.9.0\\node_modules\\keytar\\build\\Release\\keytar.node"(exports, module) {
6946
+ "node-file:F:\\MForest\\projects\\MSapling_CLI\\node_modules\\.bun\\keytar@7.9.0\\node_modules\\keytar\\build\\Release\\keytar.node"(exports, module) {
6716
6947
  "use strict";
6717
6948
  init_esm_shims();
6718
6949
  init_keytar();
@@ -6764,10 +6995,14 @@ var require_keytar2 = __commonJS({
6764
6995
  });
6765
6996
 
6766
6997
  // ../core/src/Storage.ts
6767
- import { join as join13 } from "path";
6998
+ import { join as join14 } from "path";
6768
6999
  import { homedir as homedir7 } from "os";
6769
- import { chmodSync, existsSync as existsSync12, renameSync, unlinkSync } from "fs";
6770
- import { mkdir as mkdir6, writeFile as writeFile6, readFile as readFile11 } from "fs/promises";
7000
+ import { chmodSync, existsSync as existsSync12, renameSync, unlinkSync, writeFileSync } from "fs";
7001
+ import { mkdir as mkdir6, writeFile as writeFile6, readFile as readFile11, appendFile } from "fs/promises";
7002
+ import { randomBytes as randomBytes7, createHash as createHash3 } from "crypto";
7003
+ function hashLine(line) {
7004
+ return createHash3("sha256").update(line, "utf8").digest("hex");
7005
+ }
6771
7006
  var lockfile, keytar, StorageManager;
6772
7007
  var init_Storage = __esm({
6773
7008
  "../core/src/Storage.ts"() {
@@ -6794,15 +7029,26 @@ var init_Storage = __esm({
6794
7029
  */
6795
7030
  _ready;
6796
7031
  constructor() {
6797
- this.baseDir = join13(homedir7(), ".msapling");
7032
+ this.baseDir = join14(homedir7(), ".msapling");
6798
7033
  this._ready = this.ensureDirs();
6799
7034
  }
6800
7035
  async ensureDirs() {
6801
7036
  try {
6802
7037
  await mkdir6(this.baseDir, { recursive: true });
6803
- const subdirs = ["history", "backups", "cache", "vault"];
7038
+ const subdirs = [
7039
+ "history",
7040
+ "backups",
7041
+ "cache",
7042
+ // CLI-ARCH-CONTENT-HASH-VAULT-01: content-addressed vault layout
7043
+ "vault",
7044
+ "vault/objects",
7045
+ "vault/refs",
7046
+ // CLI-ARCH-RECIPE-AT-HASH-URI-01: recipe hash cache
7047
+ "cache/recipes",
7048
+ "cache/recipes/objects"
7049
+ ];
6804
7050
  for (const sub of subdirs) {
6805
- const path2 = join13(this.baseDir, sub);
7051
+ const path2 = join14(this.baseDir, sub);
6806
7052
  await mkdir6(path2, { recursive: true });
6807
7053
  }
6808
7054
  if (process.platform !== "win32") {
@@ -6820,7 +7066,7 @@ var init_Storage = __esm({
6820
7066
  await this._ready;
6821
7067
  const KEYCHAIN_SERVICE = "msapling-cli";
6822
7068
  const KEYCHAIN_ACCOUNT = "auth_token";
6823
- const filePath = join13(this.baseDir, "vault", "token");
7069
+ const filePath = join14(this.baseDir, "vault", "token");
6824
7070
  try {
6825
7071
  await keytar.setPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT, token);
6826
7072
  try {
@@ -6840,7 +7086,7 @@ var init_Storage = __esm({
6840
7086
  async loadToken() {
6841
7087
  const KEYCHAIN_SERVICE = "msapling-cli";
6842
7088
  const KEYCHAIN_ACCOUNT = "auth_token";
6843
- const filePath = join13(this.baseDir, "vault", "token");
7089
+ const filePath = join14(this.baseDir, "vault", "token");
6844
7090
  try {
6845
7091
  const keychainToken = await keytar.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
6846
7092
  if (keychainToken) {
@@ -6861,7 +7107,7 @@ var init_Storage = __esm({
6861
7107
  async clearToken() {
6862
7108
  const KEYCHAIN_SERVICE = "msapling-cli";
6863
7109
  const KEYCHAIN_ACCOUNT = "auth_token";
6864
- const filePath = join13(this.baseDir, "vault", "token");
7110
+ const filePath = join14(this.baseDir, "vault", "token");
6865
7111
  try {
6866
7112
  await keytar.deletePassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
6867
7113
  } catch (e) {
@@ -6869,13 +7115,237 @@ var init_Storage = __esm({
6869
7115
  }
6870
7116
  try {
6871
7117
  if (existsSync12(filePath)) {
6872
- const fs = await import("fs/promises");
6873
- await fs.unlink(filePath);
7118
+ const fs3 = await import("fs/promises");
7119
+ await fs3.unlink(filePath);
6874
7120
  }
6875
7121
  } catch (e) {
6876
7122
  console.warn(`Failed to delete token file: ${e}`);
6877
7123
  }
6878
7124
  }
7125
+ // ─────────────────────────────────────────────────────────────────────────
7126
+ // CLI-ARCH-CONTENT-HASH-VAULT-01 — content-addressed vault helpers
7127
+ //
7128
+ // Every vault value is a content-addressed blob stored under
7129
+ // ~/.msapling/vault/objects/<sha256-hex>
7130
+ // A human-readable ref file under
7131
+ // ~/.msapling/vault/refs/<label>
7132
+ // holds the sha256-hex of the current value.
7133
+ //
7134
+ // Updating a value: write new object → atomically rewrite ref.
7135
+ // Old objects are GC'd separately (retention window TBD — see roadmap).
7136
+ // The keytar account name stores the sha256-hex so the plaintext label
7137
+ // remains opaque on disk (label → hash is in keytar; hash → blob is on disk).
7138
+ // ─────────────────────────────────────────────────────────────────────────
7139
+ /**
7140
+ * Write a value into the content-addressed object store and atomically
7141
+ * update the named ref. Returns the sha256 hex of the stored object.
7142
+ *
7143
+ * WAL-PATTERN: write to objects/<hash> (idempotent — same content = same
7144
+ * path), then rename refs/<label>.tmp → refs/<label>.
7145
+ */
7146
+ async writeVaultRef(label, value) {
7147
+ await this._ready;
7148
+ const hash = createHash3("sha256").update(value, "utf8").digest("hex");
7149
+ const objectPath = join14(this.baseDir, "vault", "objects", hash);
7150
+ const refPath = join14(this.baseDir, "vault", "refs", label);
7151
+ const refTmp = `${refPath}.tmp`;
7152
+ await writeFile6(objectPath, value, "utf8");
7153
+ if (process.platform !== "win32") {
7154
+ chmodSync(objectPath, 384);
7155
+ }
7156
+ await writeFile6(refTmp, hash, "utf8");
7157
+ renameSync(refTmp, refPath);
7158
+ return hash;
7159
+ }
7160
+ /**
7161
+ * Read a vault value by label. Returns null if the ref or its object is
7162
+ * missing (bootstrap / first-run case).
7163
+ */
7164
+ async readVaultRef(label) {
7165
+ await this._ready;
7166
+ const refPath = join14(this.baseDir, "vault", "refs", label);
7167
+ if (!existsSync12(refPath)) return null;
7168
+ const hash = (await readFile11(refPath, "utf8")).trim();
7169
+ const objectPath = join14(this.baseDir, "vault", "objects", hash);
7170
+ if (!existsSync12(objectPath)) return null;
7171
+ return readFile11(objectPath, "utf8");
7172
+ }
7173
+ // ─────────────────────────────────────────────────────────────────────────
7174
+ // CLI-ARCH-RECIPE-AT-HASH-URI-01 — recipe hash registry helpers
7175
+ //
7176
+ // On first load of a recipe file, its content is sha256-hashed and the
7177
+ // mapping { name → hash } is stored in
7178
+ // ~/.msapling/cache/recipes/index.json
7179
+ // The immutable blob is written to
7180
+ // ~/.msapling/cache/recipes/objects/<sha256>
7181
+ //
7182
+ // Invocations reference recipes as `recipe@<sha256>` URIs. The /recipe
7183
+ // command resolves a bare name through the index and logs the hash for
7184
+ // traceability.
7185
+ // ─────────────────────────────────────────────────────────────────────────
7186
+ /**
7187
+ * Register a recipe file in the local hash index. Idempotent — if the
7188
+ * content hash already exists the call is a no-op (returns existing hash).
7189
+ * Returns the sha256 hex for use in log messages / URI construction.
7190
+ */
7191
+ async registerRecipe(name, content) {
7192
+ await this._ready;
7193
+ const hash = createHash3("sha256").update(content, "utf8").digest("hex");
7194
+ const objectPath = join14(this.baseDir, "cache", "recipes", "objects", hash);
7195
+ const indexPath = join14(this.baseDir, "cache", "recipes", "index.json");
7196
+ const indexTmp = `${indexPath}.tmp`;
7197
+ if (!existsSync12(objectPath)) {
7198
+ await writeFile6(objectPath, content, "utf8");
7199
+ }
7200
+ let index = {};
7201
+ if (existsSync12(indexPath)) {
7202
+ try {
7203
+ index = JSON.parse(await readFile11(indexPath, "utf8"));
7204
+ } catch {
7205
+ index = {};
7206
+ }
7207
+ }
7208
+ index[name] = hash;
7209
+ await writeFile6(indexTmp, JSON.stringify(index, null, 2), "utf8");
7210
+ renameSync(indexTmp, indexPath);
7211
+ return hash;
7212
+ }
7213
+ /**
7214
+ * Resolve a recipe name or `recipe@<hash>` URI to its cached content.
7215
+ * Returns { hash, content } or null if not found in the local cache.
7216
+ */
7217
+ async resolveRecipe(nameOrRef) {
7218
+ await this._ready;
7219
+ const indexPath = join14(this.baseDir, "cache", "recipes", "index.json");
7220
+ const atIdx = nameOrRef.indexOf("@");
7221
+ if (atIdx !== -1) {
7222
+ const hash2 = nameOrRef.slice(atIdx + 1);
7223
+ const objectPath2 = join14(this.baseDir, "cache", "recipes", "objects", hash2);
7224
+ if (!existsSync12(objectPath2)) return null;
7225
+ return { hash: hash2, content: await readFile11(objectPath2, "utf8") };
7226
+ }
7227
+ if (!existsSync12(indexPath)) return null;
7228
+ let index;
7229
+ try {
7230
+ index = JSON.parse(await readFile11(indexPath, "utf8"));
7231
+ } catch {
7232
+ return null;
7233
+ }
7234
+ const hash = index[nameOrRef];
7235
+ if (!hash) return null;
7236
+ const objectPath = join14(this.baseDir, "cache", "recipes", "objects", hash);
7237
+ if (!existsSync12(objectPath)) return null;
7238
+ return { hash, content: await readFile11(objectPath, "utf8") };
7239
+ }
7240
+ // ─────────────────────────────────────────────────────────────────────────
7241
+ // CLI-ARCH-HASH-CHAIN-HISTORY-01 — append-only NDJSON history with hash chain
7242
+ //
7243
+ // Shell history is stored as an append-only NDJSON file
7244
+ // ~/.msapling/history/shell_history.jsonl
7245
+ // Each entry carries a `prev_hash` field so tampering is detectable.
7246
+ // `msapling --verify-history` re-hashes the chain and reports breaks.
7247
+ // ─────────────────────────────────────────────────────────────────────────
7248
+ /**
7249
+ * Append a single command to the hash-chain history.
7250
+ * Uses lockfile + in-process mutex as a critical section around:
7251
+ * 1. Load last entry to compute prev_hash
7252
+ * 2. Build + serialize new entry
7253
+ * 3. Append to shell_history.jsonl
7254
+ *
7255
+ * WAL-PATTERN note: append-only log uses appendFile, not rename — the
7256
+ * lockfile is the write-serialisation primitive here.
7257
+ */
7258
+ async appendHistoryEntry(content) {
7259
+ await this._ready;
7260
+ const path2 = join14(this.baseDir, "history", "shell_history.jsonl");
7261
+ return this.historyMutex.run(async () => {
7262
+ let release2 = null;
7263
+ try {
7264
+ if (!existsSync12(path2)) {
7265
+ await writeFile6(path2, "", "utf8");
7266
+ }
7267
+ release2 = await lockfile.lock(path2, { retries: 5, retryWait: 50 });
7268
+ let prevHash = null;
7269
+ let seq = 1;
7270
+ if (existsSync12(path2)) {
7271
+ const raw = (await readFile11(path2, "utf8")).trimEnd();
7272
+ if (raw.length > 0) {
7273
+ const lines = raw.split("\n");
7274
+ const lastLine = lines[lines.length - 1];
7275
+ try {
7276
+ const last = JSON.parse(lastLine);
7277
+ seq = last.seq + 1;
7278
+ prevHash = hashLine(lastLine);
7279
+ } catch {
7280
+ }
7281
+ }
7282
+ }
7283
+ const entry = {
7284
+ seq,
7285
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7286
+ content,
7287
+ prev_hash: prevHash
7288
+ };
7289
+ const line = JSON.stringify(entry);
7290
+ await appendFile(path2, line + "\n", "utf8");
7291
+ return entry;
7292
+ } finally {
7293
+ if (release2) {
7294
+ try {
7295
+ await lockfile.unlock(path2, { skipStale: true });
7296
+ } catch {
7297
+ }
7298
+ }
7299
+ }
7300
+ });
7301
+ }
7302
+ /**
7303
+ * Load all history entries from shell_history.jsonl.
7304
+ * Skips malformed lines (logs a warning for each).
7305
+ *
7306
+ * // TODO: implement tail-read for large history files to avoid loading entire file into memory
7307
+ */
7308
+ async loadHistoryEntries() {
7309
+ await this._ready;
7310
+ const path2 = join14(this.baseDir, "history", "shell_history.jsonl");
7311
+ if (!existsSync12(path2)) return [];
7312
+ const raw = await readFile11(path2, "utf8");
7313
+ const entries = [];
7314
+ for (const line of raw.split("\n")) {
7315
+ if (!line.trim()) continue;
7316
+ try {
7317
+ entries.push(JSON.parse(line));
7318
+ } catch {
7319
+ console.warn(`[history] Skipped malformed NDJSON line: ${line.slice(0, 80)}`);
7320
+ }
7321
+ }
7322
+ return entries;
7323
+ }
7324
+ /**
7325
+ * Verify the hash chain of shell_history.jsonl.
7326
+ * Returns { ok: true } if intact, or { ok: false, breaks: [...] } listing
7327
+ * each break as { seq, expected, actual }.
7328
+ *
7329
+ * Used by `msapling --verify-history`.
7330
+ */
7331
+ async verifyHistory() {
7332
+ const entries = await this.loadHistoryEntries();
7333
+ const breaks = [];
7334
+ let prevLine = null;
7335
+ for (const entry of entries) {
7336
+ if (prevLine !== null) {
7337
+ const expected = hashLine(prevLine);
7338
+ const actual = entry.prev_hash ?? "";
7339
+ if (expected !== actual) {
7340
+ breaks.push({ seq: entry.seq, expected, actual });
7341
+ }
7342
+ } else if (entry.prev_hash !== null) {
7343
+ breaks.push({ seq: entry.seq, expected: "null", actual: entry.prev_hash });
7344
+ }
7345
+ prevLine = JSON.stringify(entry);
7346
+ }
7347
+ return breaks.length === 0 ? { ok: true } : { ok: false, breaks };
7348
+ }
6879
7349
  /**
6880
7350
  * SYNC-01: Serialised history write with file-based locking.
6881
7351
  * Uses proper-lockfile to coordinate writes across multiple terminal instances
@@ -6886,10 +7356,12 @@ var init_Storage = __esm({
6886
7356
  * 2. Partial writes don't corrupt the JSON (atomic rename)
6887
7357
  */
6888
7358
  async saveHistory(history) {
6889
- const path2 = join13(this.baseDir, "history", "shell_history.json");
7359
+ await this._ready;
7360
+ const path2 = join14(this.baseDir, "history", "shell_history.json");
6890
7361
  let release2;
6891
7362
  try {
6892
- release2 = await lockfile.lock(path2, { retries: 5, retryWait: 50 });
7363
+ if (!existsSync12(path2)) writeFileSync(path2, "[]", "utf8");
7364
+ release2 = await lockfile.lock(path2, { realpath: false, retries: 5, retryWait: 50 });
6893
7365
  await this.historyMutex.run(async () => {
6894
7366
  const tmpPath = `${path2}.tmp`;
6895
7367
  const content = JSON.stringify(history, null, 2);
@@ -6899,8 +7371,8 @@ var init_Storage = __esm({
6899
7371
  } catch (e) {
6900
7372
  try {
6901
7373
  if (existsSync12(tmpPath)) {
6902
- const fs = await import("fs/promises");
6903
- await fs.unlink(tmpPath);
7374
+ const fs3 = await import("fs/promises");
7375
+ await fs3.unlink(tmpPath);
6904
7376
  }
6905
7377
  } catch {
6906
7378
  }
@@ -6921,16 +7393,38 @@ var init_Storage = __esm({
6921
7393
  * SYNC-01: Serialised history read with file-based locking.
6922
7394
  * Reads are gated behind the file lock so a read that overlaps with an
6923
7395
  * in-progress write from another process always sees a complete, valid JSON.
7396
+ *
7397
+ * // TODO: implement tail-read for large history files to avoid loading entire file into memory
6924
7398
  */
6925
7399
  async loadHistory() {
6926
- const path2 = join13(this.baseDir, "history", "shell_history.json");
7400
+ await this._ready;
7401
+ const path2 = join14(this.baseDir, "history", "shell_history.json");
7402
+ if (!existsSync12(path2)) return [];
6927
7403
  let release2;
6928
7404
  try {
6929
- release2 = await lockfile.lock(path2, { retries: 5, retryWait: 50 });
7405
+ release2 = await lockfile.lock(path2, { realpath: false, retries: 5, retryWait: 50 });
6930
7406
  return this.historyMutex.run(async () => {
6931
7407
  if (existsSync12(path2)) {
6932
7408
  const text = await readFile11(path2, "utf8");
6933
- return JSON.parse(text);
7409
+ try {
7410
+ return JSON.parse(text);
7411
+ } catch (parseErr) {
7412
+ const filename = path2.split("/").pop() || "shell_history.json";
7413
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7414
+ const suffix = randomBytes7(4).toString("hex");
7415
+ const corruptBackupPath = join14(
7416
+ this.baseDir,
7417
+ "history",
7418
+ `${filename}.corrupt.${stamp}-${suffix}.bak`
7419
+ );
7420
+ try {
7421
+ renameSync(path2, corruptBackupPath);
7422
+ console.warn(`History file was corrupt; backed up to ${corruptBackupPath}`);
7423
+ } catch (backupErr) {
7424
+ console.error(`Failed to backup corrupt history file: ${backupErr}`);
7425
+ }
7426
+ return [];
7427
+ }
6934
7428
  }
6935
7429
  return [];
6936
7430
  });
@@ -6946,12 +7440,14 @@ var init_Storage = __esm({
6946
7440
  }
6947
7441
  /**
6948
7442
  * SYNC-01: Serialised permissions write with file-based locking.
7443
+ * R20-CLI-2: Typed with PermissionState from Sandbox.ts.
6949
7444
  */
6950
7445
  async savePermissions(permissions) {
6951
- const path2 = join13(this.baseDir, "vault", "permissions.json");
7446
+ const path2 = join14(this.baseDir, "vault", "permissions.json");
6952
7447
  let release2;
6953
7448
  try {
6954
- release2 = await lockfile.lock(path2, { retries: 5, retryWait: 50 });
7449
+ if (!existsSync12(path2)) writeFileSync(path2, "{}", "utf8");
7450
+ release2 = await lockfile.lock(path2, { realpath: false, retries: 5, retryWait: 50 });
6955
7451
  await this.permissionsMutex.run(async () => {
6956
7452
  const tmpPath = `${path2}.tmp`;
6957
7453
  const content = JSON.stringify(permissions, null, 2);
@@ -6961,8 +7457,8 @@ var init_Storage = __esm({
6961
7457
  } catch (e) {
6962
7458
  try {
6963
7459
  if (existsSync12(tmpPath)) {
6964
- const fs = await import("fs/promises");
6965
- await fs.unlink(tmpPath);
7460
+ const fs3 = await import("fs/promises");
7461
+ await fs3.unlink(tmpPath);
6966
7462
  }
6967
7463
  } catch {
6968
7464
  }
@@ -6981,16 +7477,36 @@ var init_Storage = __esm({
6981
7477
  }
6982
7478
  /**
6983
7479
  * SYNC-01: Serialised permissions read with file-based locking.
7480
+ * R20-CLI-2: Typed with PermissionState from Sandbox.ts.
6984
7481
  */
6985
7482
  async loadPermissions() {
6986
- const path2 = join13(this.baseDir, "vault", "permissions.json");
7483
+ const path2 = join14(this.baseDir, "vault", "permissions.json");
7484
+ if (!existsSync12(path2)) return { trustedCommands: [], trustedPaths: [] };
6987
7485
  let release2;
6988
7486
  try {
6989
- release2 = await lockfile.lock(path2, { retries: 5, retryWait: 50 });
7487
+ release2 = await lockfile.lock(path2, { realpath: false, retries: 5, retryWait: 50 });
6990
7488
  return this.permissionsMutex.run(async () => {
6991
7489
  if (existsSync12(path2)) {
6992
7490
  const text = await readFile11(path2, "utf8");
6993
- return JSON.parse(text);
7491
+ try {
7492
+ return JSON.parse(text);
7493
+ } catch (parseErr) {
7494
+ const filename = path2.split("/").pop() || "permissions.json";
7495
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7496
+ const suffix = randomBytes7(4).toString("hex");
7497
+ const corruptBackupPath = join14(
7498
+ this.baseDir,
7499
+ "vault",
7500
+ `${filename}.corrupt.${stamp}-${suffix}.bak`
7501
+ );
7502
+ try {
7503
+ renameSync(path2, corruptBackupPath);
7504
+ console.warn(`Permissions file was corrupt; backed up to ${corruptBackupPath}`);
7505
+ } catch (backupErr) {
7506
+ console.error(`Failed to backup corrupt permissions file: ${backupErr}`);
7507
+ }
7508
+ return { trustedCommands: [], trustedPaths: [] };
7509
+ }
6994
7510
  }
6995
7511
  return { trustedCommands: [], trustedPaths: [] };
6996
7512
  });
@@ -7008,11 +7524,14 @@ var init_Storage = __esm({
7008
7524
  * Backup a file before AI edit.
7009
7525
  * CLI-R11-STORAGE-03: backup artifacts are written with restrictive 0600
7010
7526
  * permissions so restored content is not readable by other OS users.
7527
+ * CLI-ARCH-NO-EPOCH-IN-KEYS-FU-01: uses ISO timestamp + random suffix
7528
+ * instead of epoch milliseconds for better traceability and collision avoidance.
7011
7529
  */
7012
7530
  async backup(filePath, content) {
7013
7531
  const filename = filePath.split("/").pop() || "file";
7014
- const timestamp = Date.now();
7015
- const backupPath = join13(this.baseDir, "backups", `${filename}.${timestamp}.bak`);
7532
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7533
+ const suffix = randomBytes7(4).toString("hex");
7534
+ const backupPath = join14(this.baseDir, "backups", `${filename}.backup-${stamp}-${suffix}.bak`);
7016
7535
  await writeFile6(backupPath, content, "utf8");
7017
7536
  if (process.platform !== "win32") {
7018
7537
  chmodSync(backupPath, 384);
@@ -7025,9 +7544,28 @@ var init_Storage = __esm({
7025
7544
 
7026
7545
  // ../core/src/Settings.ts
7027
7546
  import { homedir as homedir8 } from "os";
7028
- import { join as join14 } from "path";
7547
+ import { join as join15 } from "path";
7029
7548
  import { existsSync as existsSync13 } from "fs";
7549
+ import * as fs from "fs";
7030
7550
  import { readFile as readFile12 } from "fs/promises";
7551
+ import { randomBytes as randomBytes8 } from "crypto";
7552
+ function ensureConfigDir(p) {
7553
+ try {
7554
+ fs.mkdirSync(p, { recursive: true, mode: 448 });
7555
+ } catch (e) {
7556
+ if (e.code === "EEXIST" || e.code === "ENOTDIR") {
7557
+ const stat5 = fs.statSync(p);
7558
+ if (!stat5.isDirectory()) {
7559
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
7560
+ const suffix = randomBytes8(4).toString("hex");
7561
+ fs.renameSync(p, `${p}.broken-${stamp}-${suffix}`);
7562
+ fs.mkdirSync(p, { recursive: true, mode: 448 });
7563
+ }
7564
+ } else {
7565
+ throw e;
7566
+ }
7567
+ }
7568
+ }
7031
7569
  async function readJson(path2) {
7032
7570
  try {
7033
7571
  if (!existsSync13(path2)) return null;
@@ -7085,8 +7623,8 @@ function mergeSettings(base, override) {
7085
7623
  }
7086
7624
  async function loadSettings(cwd = process.cwd(), env = process.env, warn) {
7087
7625
  const sources = [];
7088
- const userPath = join14(homedir8() || ".", ".msapling", "settings.json");
7089
- const projectPath = join14(cwd, ".msapling", "settings.json");
7626
+ const userPath = join15(homedir8() || ".", ".msapling", "settings.json");
7627
+ const projectPath = join15(cwd, ".msapling", "settings.json");
7090
7628
  const [user, project] = await Promise.all([readJson(userPath), readJson(projectPath)]);
7091
7629
  if (user) sources.push(userPath);
7092
7630
  if (project) sources.push(projectPath);
@@ -7249,7 +7787,7 @@ var init_client = __esm({
7249
7787
  if (!this.proc) throw new MCPClientError(`MCP server "${this.name}" not started`);
7250
7788
  const id = this.nextId++;
7251
7789
  const frame = { jsonrpc: "2.0", id, method, params };
7252
- return new Promise((resolve17, reject) => {
7790
+ return new Promise((resolve18, reject) => {
7253
7791
  const timer = setTimeout(() => {
7254
7792
  this.pending.delete(id);
7255
7793
  reject(new MCPClientError(`MCP request ${method} timed out after ${timeoutMs}ms`));
@@ -7257,7 +7795,7 @@ var init_client = __esm({
7257
7795
  this.pending.set(id, {
7258
7796
  resolve: (v) => {
7259
7797
  clearTimeout(timer);
7260
- resolve17(v);
7798
+ resolve18(v);
7261
7799
  },
7262
7800
  reject: (e) => {
7263
7801
  clearTimeout(timer);
@@ -7284,7 +7822,7 @@ var init_client = __esm({
7284
7822
  if (!this.proc?.stdout) return;
7285
7823
  const stdout = this.proc.stdout;
7286
7824
  const decoder = new TextDecoder();
7287
- return new Promise((resolve17) => {
7825
+ return new Promise((resolve18) => {
7288
7826
  stdout.on("data", (chunk) => {
7289
7827
  this.buffer += decoder.decode(chunk, { stream: true });
7290
7828
  let idx;
@@ -7295,8 +7833,8 @@ var init_client = __esm({
7295
7833
  this.handleFrame(line);
7296
7834
  }
7297
7835
  });
7298
- stdout.on("end", () => resolve17());
7299
- stdout.on("error", () => resolve17());
7836
+ stdout.on("end", () => resolve18());
7837
+ stdout.on("error", () => resolve18());
7300
7838
  });
7301
7839
  }
7302
7840
  handleFrame(line) {
@@ -7383,7 +7921,7 @@ var init_Swarm = __esm({
7383
7921
  * Execute a parallel swarm with shadow auditing and optional sub-shell popups.
7384
7922
  */
7385
7923
  async execute(tasks, projectRoot) {
7386
- const promises = tasks.map(async (task) => {
7924
+ const promises2 = tasks.map(async (task) => {
7387
7925
  const audit = await this.shadow.verifyAction(task.prompt, `Swarm Task: ${task.name}`);
7388
7926
  if (!audit.approved) {
7389
7927
  return { taskId: task.id, workerId: "none", response: "", status: "blocked", auditNote: audit.reasoning };
@@ -7405,7 +7943,7 @@ var init_Swarm = __esm({
7405
7943
  return { taskId: task.id, workerId: task.id, response: e.message, status: "failed" };
7406
7944
  }
7407
7945
  });
7408
- return Promise.all(promises);
7946
+ return Promise.all(promises2);
7409
7947
  }
7410
7948
  /**
7411
7949
  * SYNTHESIS: Use a high-reasoning model to merge the safe swarm results.
@@ -7470,29 +8008,34 @@ function isEmailAddress(str) {
7470
8008
  function isTokenLike(str) {
7471
8009
  return str.length >= 20 && /^[A-Za-z0-9_\-\.]+$/.test(str);
7472
8010
  }
8011
+ function setRawModeGuarded(stdin, mode) {
8012
+ try {
8013
+ if (typeof stdin.setRawMode === "function") {
8014
+ stdin.setRawMode(mode);
8015
+ }
8016
+ } catch (e) {
8017
+ }
8018
+ }
7473
8019
  async function promptPassword(prompt) {
7474
- return new Promise((resolve17) => {
8020
+ return new Promise((resolve18) => {
7475
8021
  const stdin = process.stdin;
7476
8022
  const stdout = process.stdout;
7477
8023
  stdout.write(prompt);
7478
- const wasRaw = stdin.isRaw;
7479
- try {
7480
- stdin.setRawMode(true);
7481
- } catch (e) {
7482
- }
8024
+ const wasRaw = stdin.isRaw ?? false;
8025
+ setRawModeGuarded(stdin, true);
7483
8026
  let password = "";
7484
8027
  const onData = (chunk) => {
7485
8028
  const char = chunk.toString();
7486
8029
  if (char === "\n" || char === "\r") {
7487
- stdin.setRawMode(wasRaw);
8030
+ setRawModeGuarded(stdin, wasRaw);
7488
8031
  stdin.removeListener("data", onData);
7489
8032
  stdout.write("\n");
7490
- resolve17(password);
8033
+ resolve18(password);
7491
8034
  } else if (char === "") {
7492
- stdin.setRawMode(wasRaw);
8035
+ setRawModeGuarded(stdin, wasRaw);
7493
8036
  stdin.removeListener("data", onData);
7494
8037
  stdout.write("\n");
7495
- resolve17("");
8038
+ resolve18("");
7496
8039
  } else if (char === "\x7F" || char === "\b") {
7497
8040
  password = password.slice(0, -1);
7498
8041
  } else if (char >= " " && char <= "~") {
@@ -7502,40 +8045,6 @@ async function promptPassword(prompt) {
7502
8045
  stdin.on("data", onData);
7503
8046
  });
7504
8047
  }
7505
- async function promptTotp(prompt) {
7506
- return new Promise((resolve17) => {
7507
- const stdin = process.stdin;
7508
- const stdout = process.stdout;
7509
- stdout.write(prompt);
7510
- const wasRaw = stdin.isRaw;
7511
- try {
7512
- stdin.setRawMode(true);
7513
- } catch (e) {
7514
- }
7515
- let code = "";
7516
- const onData = (chunk) => {
7517
- const char = chunk.toString();
7518
- if (char === "\n" || char === "\r") {
7519
- stdin.setRawMode(wasRaw);
7520
- stdin.removeListener("data", onData);
7521
- stdout.write("\n");
7522
- resolve17(code);
7523
- } else if (char === "") {
7524
- stdin.setRawMode(wasRaw);
7525
- stdin.removeListener("data", onData);
7526
- stdout.write("\n");
7527
- resolve17("");
7528
- } else if (char === "\x7F" || char === "\b") {
7529
- code = code.slice(0, -1);
7530
- } else if (/^\d$/.test(char)) {
7531
- if (code.length < 6) {
7532
- code += char;
7533
- }
7534
- }
7535
- };
7536
- stdin.on("data", onData);
7537
- });
7538
- }
7539
8048
  async function loginWithEmailPassword(email, context) {
7540
8049
  context.addMessage("system", `Logging in as ${email}...`);
7541
8050
  let password = "";
@@ -7552,40 +8061,26 @@ async function loginWithEmailPassword(email, context) {
7552
8061
  try {
7553
8062
  const result = await context.client.loginEmailPassword(email, password);
7554
8063
  password = "";
7555
- if (result.kind === "totp_required") {
7556
- context.addMessage("system", "Two-factor authentication required.");
7557
- let totpCode = "";
7558
- try {
7559
- totpCode = await promptTotp("Enter your 6-digit code: ");
7560
- } catch (err) {
7561
- context.addMessage("error", `TOTP prompt failed: ${err}`);
7562
- return;
7563
- }
7564
- if (!totpCode || totpCode.length !== 6) {
7565
- context.addMessage("error", "Invalid TOTP code (must be 6 digits).");
7566
- return;
7567
- }
7568
- context.client.setToken(result.partialToken);
7569
- try {
7570
- const totpResult = await context.client.verifyLoginTotp(totpCode);
7571
- await context.storage.saveToken(totpResult.token);
7572
- context.client.setToken(totpResult.token);
7573
- await context.refreshOverview();
7574
- context.addMessage("system", `Logged in as ${email} (2FA verified).`);
7575
- } catch (err) {
7576
- context.client.setToken(null);
7577
- context.addMessage("error", `TOTP verification failed: ${err}`);
7578
- }
7579
- } else if (result.kind === "success") {
7580
- await context.storage.saveToken(result.token);
7581
- context.client.setToken(result.token);
7582
- await context.refreshOverview();
7583
- context.addMessage("system", `Logged in as ${email}.`);
7584
- }
8064
+ await context.storage.saveToken(result.token);
8065
+ context.client.setToken(result.token);
8066
+ await context.refreshOverview();
8067
+ context.addMessage("system", `Logged in as ${email}.`);
7585
8068
  } catch (err) {
7586
8069
  context.addMessage("error", `Login failed: ${err}`);
7587
8070
  }
7588
8071
  }
8072
+ async function loginAsGuest(context) {
8073
+ context.addMessage("system", "Signing in as guest...");
8074
+ try {
8075
+ const result = await context.client.loginGuest();
8076
+ await context.storage.saveToken(result.token);
8077
+ context.client.setToken(result.token);
8078
+ await context.refreshOverview();
8079
+ context.addMessage("system", "Signed in as guest (limited tier).");
8080
+ } catch (err) {
8081
+ context.addMessage("error", `Guest login failed: ${err}`);
8082
+ }
8083
+ }
7589
8084
  async function loginWithGithubDevice(context) {
7590
8085
  context.addMessage("system", "Starting GitHub device flow...");
7591
8086
  let deviceResp;
@@ -7609,11 +8104,11 @@ async function loginWithGithubDevice(context) {
7609
8104
  `Open ${verification_uri} in your browser and enter code: ${user_code}
7610
8105
  (Waiting for authorization \u2014 expires in ${expires_in}s)`
7611
8106
  );
7612
- const pollMs = (interval + 1) * 1e3;
8107
+ let pollMs = (interval + 1) * 1e3;
7613
8108
  const deadline = Date.now() + expires_in * 1e3;
7614
8109
  let githubToken = null;
7615
8110
  while (Date.now() < deadline) {
7616
- await new Promise((resolve17) => setTimeout(resolve17, pollMs));
8111
+ await new Promise((resolve18) => setTimeout(resolve18, pollMs));
7617
8112
  let tokenResp;
7618
8113
  try {
7619
8114
  tokenResp = await fetch(GITHUB_TOKEN_URL, {
@@ -7636,7 +8131,8 @@ async function loginWithGithubDevice(context) {
7636
8131
  }
7637
8132
  if (tokenData.error === "authorization_pending") continue;
7638
8133
  if (tokenData.error === "slow_down") {
7639
- await new Promise((resolve17) => setTimeout(resolve17, 5e3));
8134
+ pollMs += 5e3;
8135
+ await new Promise((resolve18) => setTimeout(resolve18, 5e3));
7640
8136
  continue;
7641
8137
  }
7642
8138
  context.addMessage("system", `GitHub auth error: ${tokenData.error_description || tokenData.error}`);
@@ -7676,11 +8172,11 @@ var init_login = __esm({
7676
8172
  init_esm_shims();
7677
8173
  GITHUB_CLIENT_ID = "Ov23liOA9yKFLUEEVY3G";
7678
8174
  GITHUB_DEVICE_CODE_URL = "https://github.com/login/device/code";
7679
- GITHUB_TOKEN_URL = "https://github.com/oauth/access_token";
8175
+ GITHUB_TOKEN_URL = "https://github.com/login/oauth/access_token";
7680
8176
  loginCommand = {
7681
8177
  name: "login",
7682
- args: "[email|github|token]",
7683
- description: "Authenticate: /login your@email.com | /login github | /login <token>",
8178
+ args: "[email|github|guest|token]",
8179
+ description: "Authenticate: /login your@email.com | /login github | /login guest | /login <token>",
7684
8180
  category: "auth",
7685
8181
  handler: async (args2, context) => {
7686
8182
  const arg = args2[0];
@@ -7690,12 +8186,15 @@ var init_login = __esm({
7690
8186
  `Usage:
7691
8187
  /login your@email.com Sign in with email + password
7692
8188
  /login github GitHub device flow
8189
+ /login guest Anonymous guest session (limited tier)
7693
8190
  /login <token> Paste an API token from msapling.com \u2192 Settings \u2192 API Keys`
7694
8191
  );
7695
8192
  return;
7696
8193
  }
7697
8194
  if (arg === "github") {
7698
8195
  await loginWithGithubDevice(context);
8196
+ } else if (arg === "guest") {
8197
+ await loginAsGuest(context);
7699
8198
  } else if (isEmailAddress(arg)) {
7700
8199
  await loginWithEmailPassword(arg, context);
7701
8200
  } else if (isTokenLike(arg)) {
@@ -7706,7 +8205,7 @@ var init_login = __esm({
7706
8205
  } else {
7707
8206
  context.addMessage(
7708
8207
  "error",
7709
- `Unrecognized argument: "${arg}". Expected email, 'github', or token string.`
8208
+ `Unrecognized argument: "${arg}". Expected email, 'github', 'guest', or token string.`
7710
8209
  );
7711
8210
  }
7712
8211
  }
@@ -7745,7 +8244,7 @@ var init_unlock = __esm({
7745
8244
 
7746
8245
  // src/commands/doctor.ts
7747
8246
  import { homedir as homedir9 } from "os";
7748
- import { join as join15 } from "path";
8247
+ import { join as join16 } from "path";
7749
8248
  import { existsSync as existsSync14 } from "fs";
7750
8249
  import { readFile as readFile13 } from "fs/promises";
7751
8250
  async function checkApiHealth(client) {
@@ -7772,7 +8271,7 @@ async function checkAuthStatus(client) {
7772
8271
  }
7773
8272
  }
7774
8273
  async function checkSettingsFile() {
7775
- const settingsPath = join15(homedir9(), ".msapling", "settings.json");
8274
+ const settingsPath = join16(homedir9(), ".msapling", "settings.json");
7776
8275
  try {
7777
8276
  if (!existsSync14(settingsPath)) {
7778
8277
  return { ok: false, message: `Not found: ${settingsPath}` };
@@ -8018,7 +8517,7 @@ var init_clear = __esm({
8018
8517
 
8019
8518
  // src/commands/mode.ts
8020
8519
  import { homedir as homedir10 } from "os";
8021
- import { join as join16 } from "path";
8520
+ import { join as join17 } from "path";
8022
8521
  import { existsSync as existsSync15 } from "fs";
8023
8522
  import { readFile as readFile14, writeFile as writeFile7, mkdir as mkdir7 } from "fs/promises";
8024
8523
  async function persistApprovalMode(mode, ttlMs) {
@@ -8036,7 +8535,7 @@ async function persistApprovalMode(mode, ttlMs) {
8036
8535
  ...ttlMs && { ttlMs }
8037
8536
  };
8038
8537
  existing.approvalMode = entry;
8039
- const settingsDir = join16(homedir10(), ".msapling");
8538
+ const settingsDir = join17(homedir10(), ".msapling");
8040
8539
  if (!existsSync15(settingsDir)) {
8041
8540
  await mkdir7(settingsDir, { recursive: true });
8042
8541
  }
@@ -8049,7 +8548,7 @@ var init_mode = __esm({
8049
8548
  "src/commands/mode.ts"() {
8050
8549
  "use strict";
8051
8550
  init_esm_shims();
8052
- SETTINGS_PATH = join16(homedir10(), ".msapling", "settings.json");
8551
+ SETTINGS_PATH = join17(homedir10(), ".msapling", "settings.json");
8053
8552
  modeCommand = {
8054
8553
  name: "mode",
8055
8554
  args: "[default|plan|acceptEdits|bypassPermissions] [...options]",
@@ -8323,7 +8822,7 @@ var init_compact = __esm({
8323
8822
  });
8324
8823
 
8325
8824
  // src/commands/init.ts
8326
- import { join as join17 } from "path";
8825
+ import { join as join18 } from "path";
8327
8826
  import { existsSync as existsSync16 } from "fs";
8328
8827
  import { writeFile as writeFile8 } from "fs/promises";
8329
8828
  var initCommand;
@@ -8338,7 +8837,7 @@ var init_init = __esm({
8338
8837
  handler: async (args2, context) => {
8339
8838
  try {
8340
8839
  const cwd = process.cwd();
8341
- const path2 = join17(cwd, "MSAPLING.md");
8840
+ const path2 = join18(cwd, "MSAPLING.md");
8342
8841
  if (existsSync16(path2)) {
8343
8842
  context.addMessage("error", "MSAPLING.md already exists in current directory.");
8344
8843
  return;
@@ -8481,12 +8980,12 @@ var init_swarm = __esm({
8481
8980
  import { parse as parseYaml } from "yaml";
8482
8981
  import { existsSync as existsSync18 } from "fs";
8483
8982
  import { readFile as readFile16 } from "fs/promises";
8484
- import { join as join18 } from "path";
8983
+ import { join as join19 } from "path";
8485
8984
  function findRecipe(name, cwd) {
8486
8985
  for (const dir of RECIPE_DIRS) {
8487
8986
  for (const suffix of NAME_SUFFIXES) {
8488
8987
  for (const ext of FILE_EXTS) {
8489
- const p = join18(cwd, dir, `${name}${suffix}${ext}`);
8988
+ const p = join19(cwd, dir, `${name}${suffix}${ext}`);
8490
8989
  if (existsSync18(p)) return p;
8491
8990
  }
8492
8991
  }
@@ -8509,6 +9008,7 @@ var init_recipe = __esm({
8509
9008
  "src/commands/recipe.ts"() {
8510
9009
  "use strict";
8511
9010
  init_esm_shims();
9011
+ init_src3();
8512
9012
  RECIPE_DIRS = [".msapling/recipes", ".claude/recipes", ".gemini/recipes"];
8513
9013
  NAME_SUFFIXES = ["", "-workflow"];
8514
9014
  FILE_EXTS = [".yaml", ".yml"];
@@ -8542,14 +9042,21 @@ var init_recipe = __esm({
8542
9042
  context.addMessage("system", `Usage: /recipe ${name} <prompt> (the prompt fills $SELECTION / $PROMPT in step templates)`);
8543
9043
  return;
8544
9044
  }
9045
+ let text;
8545
9046
  let recipe;
8546
9047
  try {
8547
- const text = await readFile16(path2, "utf8");
9048
+ text = await readFile16(path2, "utf8");
8548
9049
  recipe = parseYaml(text);
8549
9050
  } catch (e) {
8550
9051
  context.addMessage("error", `Failed to load ${path2}: ${e.message}`);
8551
9052
  return;
8552
9053
  }
9054
+ try {
9055
+ const storage = new StorageManager();
9056
+ const hash = await storage.registerRecipe(name, text);
9057
+ context.addMessage("system", `[recipe] Resolved ${name} \u2192 recipe@${hash.slice(0, 12)}\u2026`);
9058
+ } catch {
9059
+ }
8553
9060
  const steps = recipe.steps ?? [];
8554
9061
  if (steps.length === 0) {
8555
9062
  context.addMessage("system", `Recipe '${name}' has no steps.`);
@@ -8592,13 +9099,13 @@ ${rendered}` : rendered;
8592
9099
  });
8593
9100
 
8594
9101
  // src/commands/skill.ts
8595
- import { existsSync as existsSync19, readdirSync as readdirSync2, statSync as statSync4 } from "fs";
9102
+ import { existsSync as existsSync19, readdirSync as readdirSync2, statSync as statSync5 } from "fs";
8596
9103
  import { readFile as readFile17 } from "fs/promises";
8597
- import { join as join19, resolve as resolve14 } from "path";
9104
+ import { join as join20, resolve as resolve14 } from "path";
8598
9105
  function findSkillsRoot(cwd) {
8599
9106
  for (const candidate of SKILLS_DIRS) {
8600
9107
  const full = resolve14(cwd, candidate);
8601
- if (existsSync19(full) && statSync4(full).isDirectory()) return full;
9108
+ if (existsSync19(full) && statSync5(full).isDirectory()) return full;
8602
9109
  }
8603
9110
  return null;
8604
9111
  }
@@ -8611,10 +9118,10 @@ function listAllSkills(root) {
8611
9118
  return out;
8612
9119
  }
8613
9120
  for (const domain of domains) {
8614
- const dir = join19(root, domain);
9121
+ const dir = join20(root, domain);
8615
9122
  let s;
8616
9123
  try {
8617
- s = statSync4(dir);
9124
+ s = statSync5(dir);
8618
9125
  } catch {
8619
9126
  continue;
8620
9127
  }
@@ -8627,7 +9134,7 @@ function listAllSkills(root) {
8627
9134
  }
8628
9135
  for (const f of files) {
8629
9136
  if (!f.endsWith(".md")) continue;
8630
- out.push({ domain, name: f.slice(0, -3), path: join19(dir, f) });
9137
+ out.push({ domain, name: f.slice(0, -3), path: join20(dir, f) });
8631
9138
  }
8632
9139
  }
8633
9140
  return out.sort(
@@ -8717,8 +9224,9 @@ ${prompt}`;
8717
9224
 
8718
9225
  // src/commands/benchmark.ts
8719
9226
  import { homedir as homedir11 } from "os";
8720
- import { join as join20 } from "path";
8721
- import { mkdirSync as mkdirSync3, writeFileSync } from "fs";
9227
+ import { join as join21 } from "path";
9228
+ import { mkdirSync as mkdirSync4 } from "fs";
9229
+ import * as fs2 from "fs";
8722
9230
  function parseArgs(args2) {
8723
9231
  let models = null;
8724
9232
  let rounds = 1;
@@ -8833,10 +9341,10 @@ HW at start: ${hw.cores}-core ${hw.platform} | CPU ${hw.cpuPct}% | RAM ${hw.ramP
8833
9341
  `[HW at run time: CPU ${hwAtEnd.cpuPct}% / RAM ${hwAtEnd.ramPct}% | ${hw.ramGiB} GiB RAM, ${hw.cores} cores]`
8834
9342
  );
8835
9343
  try {
8836
- const dir = join20(homedir11(), ".msapling", "benchmarks");
8837
- mkdirSync3(dir, { recursive: true });
9344
+ const dir = join21(homedir11(), ".msapling", "benchmarks");
9345
+ mkdirSync4(dir, { recursive: true });
8838
9346
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 16);
8839
- const file = join20(dir, `${ts}.json`);
9347
+ const file = join21(dir, `${ts}.json`);
8840
9348
  const run = {
8841
9349
  ts: (/* @__PURE__ */ new Date()).toISOString(),
8842
9350
  rounds,
@@ -8846,7 +9354,7 @@ HW at start: ${hw.cores}-core ${hw.platform} | CPU ${hw.cpuPct}% | RAM ${hw.ramP
8846
9354
  hw_end: hwAtEnd,
8847
9355
  results
8848
9356
  };
8849
- writeFileSync(file, JSON.stringify(run, null, 2));
9357
+ await fs2.promises.writeFile(file, JSON.stringify(run, null, 2));
8850
9358
  context.addMessage("system", `Results saved to ${file}`);
8851
9359
  } catch (e) {
8852
9360
  context.addMessage("system", `Warning: could not save results \u2014 ${e?.message ?? e}`);
@@ -9082,12 +9590,12 @@ var init_theme = __esm({
9082
9590
  });
9083
9591
 
9084
9592
  // src/commands/theme.ts
9085
- import { join as join21 } from "path";
9593
+ import { join as join22 } from "path";
9086
9594
  import { homedir as homedir12 } from "os";
9087
9595
  import { existsSync as existsSync20 } from "fs";
9088
- import { readFile as readFile18, writeFile as writeFile9, mkdir as mkdir8 } from "fs/promises";
9596
+ import { readFile as readFile18, writeFile as writeFile9 } from "fs/promises";
9089
9597
  async function persistTheme(storage, themeName) {
9090
- const settingsPath = join21(homedir12(), ".msapling", "settings.json");
9598
+ const settingsPath = join22(homedir12(), ".msapling", "settings.json");
9091
9599
  let existing = {};
9092
9600
  try {
9093
9601
  if (existsSync20(settingsPath)) {
@@ -9097,7 +9605,7 @@ async function persistTheme(storage, themeName) {
9097
9605
  } catch {
9098
9606
  }
9099
9607
  existing["theme"] = themeName;
9100
- await mkdir8(join21(homedir12(), ".msapling"), { recursive: true });
9608
+ ensureConfigDir(join22(homedir12(), ".msapling"));
9101
9609
  await writeFile9(settingsPath, JSON.stringify(existing, null, 2), "utf8");
9102
9610
  }
9103
9611
  var VALID_THEMES, themeCommand;
@@ -9106,6 +9614,7 @@ var init_theme2 = __esm({
9106
9614
  "use strict";
9107
9615
  init_esm_shims();
9108
9616
  init_theme();
9617
+ init_src3();
9109
9618
  VALID_THEMES = ["diamond", "onyx", "mono"];
9110
9619
  themeCommand = {
9111
9620
  name: "theme",
@@ -9153,7 +9662,7 @@ Available themes: ${VALID_THEMES.join(", ")}`);
9153
9662
  });
9154
9663
 
9155
9664
  // src/commands/version.ts
9156
- import { join as join22 } from "path";
9665
+ import { join as join23 } from "path";
9157
9666
  import { existsSync as existsSync21 } from "fs";
9158
9667
  import { readFile as readFile19 } from "fs/promises";
9159
9668
  function row2(label, value) {
@@ -9182,8 +9691,8 @@ var init_version = __esm({
9182
9691
  category: "debug",
9183
9692
  handler: async (_args, context) => {
9184
9693
  const baseDir = typeof __dirname !== "undefined" ? __dirname : process.cwd();
9185
- const cliPkgPath = join22(baseDir, "..", "..", "package.json");
9186
- const corePkgPath = join22(baseDir, "..", "..", "..", "core", "package.json");
9694
+ const cliPkgPath = join23(baseDir, "..", "..", "package.json");
9695
+ const corePkgPath = join23(baseDir, "..", "..", "..", "core", "package.json");
9187
9696
  const [cliVersion, coreVersion] = await Promise.all([
9188
9697
  readPackageVersion(cliPkgPath),
9189
9698
  readPackageVersion(corePkgPath)
@@ -9208,13 +9717,13 @@ var init_version = __esm({
9208
9717
  });
9209
9718
 
9210
9719
  // src/commands/feedback.ts
9211
- import { join as join23 } from "path";
9720
+ import { join as join24 } from "path";
9212
9721
  import { existsSync as existsSync22 } from "fs";
9213
9722
  import { readFile as readFile20 } from "fs/promises";
9214
9723
  async function readCliVersion() {
9215
9724
  try {
9216
9725
  const baseDir = typeof __dirname !== "undefined" ? __dirname : process.cwd();
9217
- const pkgPath = join23(baseDir, "..", "..", "package.json");
9726
+ const pkgPath = join24(baseDir, "..", "..", "package.json");
9218
9727
  if (!existsSync22(pkgPath)) return "unknown";
9219
9728
  const text = await readFile20(pkgPath, "utf8");
9220
9729
  const json = JSON.parse(text);
@@ -9257,8 +9766,8 @@ var init_feedback = __esm({
9257
9766
 
9258
9767
  // src/commands/export.ts
9259
9768
  import { homedir as homedir13 } from "os";
9260
- import { join as join24 } from "path";
9261
- import { writeFile as writeFile10, mkdir as mkdir9 } from "fs/promises";
9769
+ import { join as join25 } from "path";
9770
+ import { writeFile as writeFile10, mkdir as mkdir8 } from "fs/promises";
9262
9771
  function formatTimestamp(date) {
9263
9772
  return date.toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
9264
9773
  }
@@ -9307,10 +9816,10 @@ var init_export = __esm({
9307
9816
  let outputPath;
9308
9817
  let content;
9309
9818
  if (arg === "" || arg === "json") {
9310
- outputPath = join24(homedir13(), `msapling-export-${timestamp}.json`);
9819
+ outputPath = join25(homedir13(), `msapling-export-${timestamp}.json`);
9311
9820
  content = buildJsonExport(history);
9312
9821
  } else if (arg === "markdown" || arg === "md") {
9313
- outputPath = join24(homedir13(), `msapling-export-${timestamp}.md`);
9822
+ outputPath = join25(homedir13(), `msapling-export-${timestamp}.md`);
9314
9823
  content = buildMarkdownExport(history);
9315
9824
  } else {
9316
9825
  outputPath = arg;
@@ -9322,8 +9831,8 @@ var init_export = __esm({
9322
9831
  }
9323
9832
  }
9324
9833
  try {
9325
- const dir = join24(outputPath, "..");
9326
- await mkdir9(dir, { recursive: true });
9834
+ const dir = join25(outputPath, "..");
9835
+ await mkdir8(dir, { recursive: true });
9327
9836
  await writeFile10(outputPath, content, "utf8");
9328
9837
  context.addMessage("system", `Exported to: ${outputPath}`);
9329
9838
  } catch (e) {
@@ -9518,11 +10027,11 @@ var init_plan = __esm({
9518
10027
 
9519
10028
  // src/commands/note.ts
9520
10029
  import { homedir as homedir14 } from "os";
9521
- import { join as join25 } from "path";
9522
- import { mkdirSync as mkdirSync4, existsSync as existsSync23 } from "fs";
10030
+ import { join as join26 } from "path";
10031
+ import { existsSync as existsSync23 } from "fs";
9523
10032
  import { readFile as readFile21, writeFile as writeFile11 } from "fs/promises";
9524
10033
  function getNotesFilePath() {
9525
- return join25(homedir14(), ".msapling", "notes.json");
10034
+ return join26(homedir14(), ".msapling", "notes.json");
9526
10035
  }
9527
10036
  async function readNotes(filePath = getNotesFilePath()) {
9528
10037
  try {
@@ -9536,8 +10045,8 @@ async function readNotes(filePath = getNotesFilePath()) {
9536
10045
  }
9537
10046
  }
9538
10047
  async function writeNotes(notes, filePath = getNotesFilePath()) {
9539
- const dir = join25(homedir14(), ".msapling");
9540
- mkdirSync4(dir, { recursive: true });
10048
+ const dir = join26(homedir14(), ".msapling");
10049
+ ensureConfigDir(dir);
9541
10050
  await writeFile11(filePath, JSON.stringify(notes, null, 2), "utf8");
9542
10051
  }
9543
10052
  function formatTimestamp2(iso) {
@@ -9681,6 +10190,238 @@ var init_todo = __esm({
9681
10190
  }
9682
10191
  });
9683
10192
 
10193
+ // src/commands/outputStyle.ts
10194
+ import { homedir as homedir15 } from "os";
10195
+ import { join as join27, basename, extname as extname3 } from "path";
10196
+ import { existsSync as existsSync24, mkdirSync as mkdirSync5, readdirSync as readdirSync3, readFileSync, writeFileSync as writeFileSync2 } from "fs";
10197
+ function stylesDir() {
10198
+ return join27(homedir15(), ".msapling", "output-styles");
10199
+ }
10200
+ function activeFile() {
10201
+ return join27(stylesDir(), ".active");
10202
+ }
10203
+ function parseStyleFile(text) {
10204
+ const fm = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
10205
+ if (!fm) {
10206
+ const lines = text.split(/\r?\n/);
10207
+ for (const line of lines) {
10208
+ if (!line.trim()) continue;
10209
+ const desc = line.replace(/^#\s+/, "").trim();
10210
+ return { description: desc || "(user style)", body: text.trim() };
10211
+ }
10212
+ return { description: "(user style)", body: text.trim() };
10213
+ }
10214
+ const meta = fm[1];
10215
+ const body = text.slice(fm[0].length).trim();
10216
+ const descMatch = meta.match(/^description:\s*(.+)$/m);
10217
+ const description = descMatch ? descMatch[1].trim().replace(/^['"]|['"]$/g, "") : "(user style)";
10218
+ return { description, body };
10219
+ }
10220
+ function listUserStyles() {
10221
+ const dir = stylesDir();
10222
+ if (!existsSync24(dir)) return [];
10223
+ const out = [];
10224
+ for (const entry of readdirSync3(dir)) {
10225
+ if (extname3(entry).toLowerCase() !== ".md") continue;
10226
+ const full = join27(dir, entry);
10227
+ try {
10228
+ const text = readFileSync(full, "utf8");
10229
+ const { description, body } = parseStyleFile(text);
10230
+ out.push({
10231
+ name: basename(entry, ".md"),
10232
+ description,
10233
+ body,
10234
+ source: "user",
10235
+ path: full
10236
+ });
10237
+ } catch {
10238
+ }
10239
+ }
10240
+ return out;
10241
+ }
10242
+ function listStyles() {
10243
+ const user = listUserStyles();
10244
+ const userNames = new Set(user.map((s) => s.name));
10245
+ const builtins = BUILTIN_STYLES.filter((s) => !userNames.has(s.name));
10246
+ return [...builtins, ...user];
10247
+ }
10248
+ function findStyle(name) {
10249
+ return listStyles().find((s) => s.name === name) ?? null;
10250
+ }
10251
+ function getActiveStyleName() {
10252
+ try {
10253
+ const f = activeFile();
10254
+ if (!existsSync24(f)) return "default";
10255
+ return readFileSync(f, "utf8").trim() || "default";
10256
+ } catch {
10257
+ return "default";
10258
+ }
10259
+ }
10260
+ function setActiveStyleName(name) {
10261
+ const dir = stylesDir();
10262
+ if (!existsSync24(dir)) mkdirSync5(dir, { recursive: true });
10263
+ writeFileSync2(activeFile(), `${name}
10264
+ `, "utf8");
10265
+ }
10266
+ function getActiveStyle() {
10267
+ const name = getActiveStyleName();
10268
+ return findStyle(name) ?? BUILTIN_STYLES[0];
10269
+ }
10270
+ function createUserStyle(name, description, body) {
10271
+ if (!/^[a-z0-9][a-z0-9_-]*$/i.test(name)) {
10272
+ throw new Error(`Invalid style name "${name}" \u2014 use letters, digits, _ and - only.`);
10273
+ }
10274
+ const dir = stylesDir();
10275
+ if (!existsSync24(dir)) mkdirSync5(dir, { recursive: true });
10276
+ const target = join27(dir, `${name}.md`);
10277
+ const frontmatter = `---
10278
+ description: ${description.replace(/\n/g, " ")}
10279
+ ---
10280
+
10281
+ `;
10282
+ writeFileSync2(target, frontmatter + body.trim() + "\n", "utf8");
10283
+ return target;
10284
+ }
10285
+ var BUILTIN_STYLES, outputStyleCommand;
10286
+ var init_outputStyle = __esm({
10287
+ "src/commands/outputStyle.ts"() {
10288
+ "use strict";
10289
+ init_esm_shims();
10290
+ BUILTIN_STYLES = [
10291
+ {
10292
+ name: "default",
10293
+ description: "Default MSapling behavior \u2014 no extra system prefix.",
10294
+ body: "",
10295
+ source: "builtin",
10296
+ path: null
10297
+ },
10298
+ {
10299
+ name: "concise",
10300
+ description: "Terse, code-first answers; minimal prose.",
10301
+ 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.",
10302
+ source: "builtin",
10303
+ path: null
10304
+ },
10305
+ {
10306
+ name: "explanatory",
10307
+ description: "Walk through reasoning and trade-offs before the answer.",
10308
+ 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.",
10309
+ source: "builtin",
10310
+ path: null
10311
+ },
10312
+ {
10313
+ name: "learning",
10314
+ description: "Teach-by-doing: explain concepts, ask checkpoint questions.",
10315
+ 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.",
10316
+ source: "builtin",
10317
+ path: null
10318
+ }
10319
+ ];
10320
+ outputStyleCommand = {
10321
+ name: "output-style",
10322
+ aliases: ["style"],
10323
+ args: "[list|use <name>|new <name> <description>]",
10324
+ description: "List, switch, or create assistant output styles",
10325
+ category: "model",
10326
+ handler: (args2, context) => {
10327
+ const sub = (args2[0] ?? "list").toLowerCase();
10328
+ if (sub === "list" || sub === "ls") {
10329
+ const styles = listStyles();
10330
+ const active = getActiveStyleName();
10331
+ context.addMessage("system", "Output Styles:");
10332
+ for (const s of styles) {
10333
+ const marker = s.name === active ? "*" : " ";
10334
+ const tag = s.source === "builtin" ? "[builtin]" : "[user]";
10335
+ context.addMessage("system", ` ${marker} ${s.name.padEnd(14)} ${tag} ${s.description}`);
10336
+ }
10337
+ context.addMessage("system", `Active: ${active}`);
10338
+ return;
10339
+ }
10340
+ if (sub === "use") {
10341
+ const name = args2[1];
10342
+ if (!name) {
10343
+ context.addMessage("error", "Usage: /output-style use <name>");
10344
+ return;
10345
+ }
10346
+ const style = findStyle(name);
10347
+ if (!style) {
10348
+ context.addMessage("error", `Unknown style: ${name}. Try /output-style list.`);
10349
+ return;
10350
+ }
10351
+ setActiveStyleName(name);
10352
+ context.addMessage("system", `Active output style: ${name} \u2014 ${style.description}`);
10353
+ return;
10354
+ }
10355
+ if (sub === "new" || sub === "create") {
10356
+ const name = args2[1];
10357
+ const description = args2.slice(2).join(" ");
10358
+ if (!name) {
10359
+ context.addMessage("error", "Usage: /output-style new <name> <description>");
10360
+ return;
10361
+ }
10362
+ try {
10363
+ const path2 = createUserStyle(name, description || `User style: ${name}`, "");
10364
+ context.addMessage(
10365
+ "system",
10366
+ `Created ${path2} \u2014 edit the file to define the system prompt body, then /output-style use ${name}`
10367
+ );
10368
+ } catch (e) {
10369
+ context.addMessage("error", e.message ?? String(e));
10370
+ }
10371
+ return;
10372
+ }
10373
+ if (sub === "show") {
10374
+ const style = getActiveStyle();
10375
+ context.addMessage("system", `Active style: ${style.name} (${style.source})`);
10376
+ context.addMessage("system", `Description: ${style.description}`);
10377
+ if (style.body) {
10378
+ context.addMessage("system", "\u2500\u2500\u2500 body \u2500\u2500\u2500");
10379
+ context.addMessage("system", style.body);
10380
+ } else {
10381
+ context.addMessage("system", "(no system-prompt body \u2014 default behavior)");
10382
+ }
10383
+ return;
10384
+ }
10385
+ context.addMessage(
10386
+ "error",
10387
+ `Unknown subcommand: ${sub}. Try /output-style list|use|new|show`
10388
+ );
10389
+ }
10390
+ };
10391
+ }
10392
+ });
10393
+
10394
+ // src/commands/totp.ts
10395
+ var totpCommand;
10396
+ var init_totp = __esm({
10397
+ "src/commands/totp.ts"() {
10398
+ "use strict";
10399
+ init_esm_shims();
10400
+ totpCommand = {
10401
+ name: "totp",
10402
+ args: "<code>",
10403
+ description: "Step up the current session with a 6-digit TOTP code",
10404
+ category: "auth",
10405
+ handler: async (args2, context) => {
10406
+ const code = (args2[0] || "").trim();
10407
+ if (!/^\d{6}$/.test(code)) {
10408
+ context.addMessage("error", "Usage: /totp <6-digit code>");
10409
+ return;
10410
+ }
10411
+ try {
10412
+ const result = await context.client.verifyLoginTotp(code);
10413
+ await context.storage.saveToken(result.token);
10414
+ context.client.setToken(result.token);
10415
+ await context.refreshOverview();
10416
+ context.addMessage("system", "TOTP verified \u2014 session now fully authenticated.");
10417
+ } catch (err) {
10418
+ context.addMessage("error", `TOTP verification failed: ${err}`);
10419
+ }
10420
+ }
10421
+ };
10422
+ }
10423
+ });
10424
+
9684
10425
  // src/commands/index.ts
9685
10426
  var commands_exports = {};
9686
10427
  __export(commands_exports, {
@@ -9727,6 +10468,8 @@ var init_commands = __esm({
9727
10468
  init_plan();
9728
10469
  init_note();
9729
10470
  init_todo();
10471
+ init_outputStyle();
10472
+ init_totp();
9730
10473
  commands = [
9731
10474
  loginCommand,
9732
10475
  unlockCommand,
@@ -9758,11 +10501,481 @@ var init_commands = __esm({
9758
10501
  shortcutsCommand,
9759
10502
  planCommand,
9760
10503
  noteCommand,
9761
- todoCommand
10504
+ todoCommand,
10505
+ outputStyleCommand,
10506
+ totpCommand
9762
10507
  ];
9763
10508
  }
9764
10509
  });
9765
10510
 
10511
+ // src/runtime/exec.ts
10512
+ var exec_exports = {};
10513
+ __export(exec_exports, {
10514
+ runExec: () => runExec
10515
+ });
10516
+ async function runExec(rawInput) {
10517
+ const trimmed = (rawInput || "").trim();
10518
+ if (!trimmed) {
10519
+ process.stderr.write("msapling --exec: empty command\n");
10520
+ return 2;
10521
+ }
10522
+ const parts = trimmed.split(/\s+/);
10523
+ const name = parts[0];
10524
+ const args2 = parts.slice(1);
10525
+ const cmd = findCommand(name);
10526
+ if (!cmd) {
10527
+ process.stderr.write(`msapling --exec: unknown command "${name}"
10528
+ `);
10529
+ return 2;
10530
+ }
10531
+ const storage = new StorageManager();
10532
+ const token = await storage.loadToken().catch(() => null);
10533
+ const client = new MSaplingClient({
10534
+ apiUrl: process.env.MSAPLING_API_URL,
10535
+ token: token || void 0
10536
+ });
10537
+ const stdout = process.stdout;
10538
+ let mode = "default";
10539
+ let model = process.env.MSAPLING_MODEL || "google/gemini-2.0-flash-001";
10540
+ let projectId = null;
10541
+ let activeChatId = null;
10542
+ const context = {
10543
+ client,
10544
+ storage,
10545
+ activeChatId,
10546
+ setActiveChatId: (id) => {
10547
+ activeChatId = id;
10548
+ },
10549
+ addMessage: (role, content) => {
10550
+ stdout.write(`[${role}] ${content}
10551
+ `);
10552
+ },
10553
+ clearHistory: () => {
10554
+ },
10555
+ setMode: (m) => {
10556
+ mode = m;
10557
+ },
10558
+ getMode: () => mode,
10559
+ setModel: (m) => {
10560
+ model = m;
10561
+ },
10562
+ getModel: () => model,
10563
+ setProjectId: (id) => {
10564
+ projectId = id;
10565
+ },
10566
+ getProjectId: () => projectId,
10567
+ lastCost: 0,
10568
+ sessionCost: 0,
10569
+ refreshOverview: async () => {
10570
+ },
10571
+ exit: () => {
10572
+ }
10573
+ };
10574
+ try {
10575
+ await cmd.handler(args2, context);
10576
+ return 0;
10577
+ } catch (e) {
10578
+ process.stderr.write(`msapling --exec: error: ${e?.message ?? e}
10579
+ `);
10580
+ return 1;
10581
+ }
10582
+ }
10583
+ var init_exec = __esm({
10584
+ "src/runtime/exec.ts"() {
10585
+ "use strict";
10586
+ init_esm_shims();
10587
+ init_src();
10588
+ init_src3();
10589
+ init_commands();
10590
+ }
10591
+ });
10592
+
10593
+ // src/runtime/doctorRedact.ts
10594
+ function redactSecrets(text) {
10595
+ let out = text;
10596
+ for (const [re, replacement] of PATTERNS) {
10597
+ out = out.replace(re, replacement);
10598
+ }
10599
+ return out;
10600
+ }
10601
+ var PATTERNS;
10602
+ var init_doctorRedact = __esm({
10603
+ "src/runtime/doctorRedact.ts"() {
10604
+ "use strict";
10605
+ init_esm_shims();
10606
+ PATTERNS = [
10607
+ // Legacy key=value pairs (from original doctor.ts redactSecrets).
10608
+ [/token[=:]\s*['"]?[a-zA-Z0-9_.-]+['"]?/gi, "token=***"],
10609
+ [/password[=:]\s*['"]?[a-zA-Z0-9_.-]+['"]?/gi, "password=***"],
10610
+ [/api[_-]?key[=:]\s*['"]?[a-zA-Z0-9_.-]+['"]?/gi, "api_key=***"],
10611
+ [/secret[=:]\s*['"]?[a-zA-Z0-9_.-]+['"]?/gi, "secret=***"],
10612
+ [/MSAPLING_TOKEN=.*/gi, "MSAPLING_TOKEN=***"],
10613
+ [/MSAPLING_API_KEY=.*/gi, "MSAPLING_API_KEY=***"],
10614
+ // Bearer tokens (case-insensitive).
10615
+ [/Bearer\s+[A-Za-z0-9._~+/=-]+/gi, "Bearer ***"],
10616
+ // GitHub PATs — keep the prefix so the credential class is identifiable.
10617
+ [/\bghp_[A-Za-z0-9]{20,}/g, "ghp_***"],
10618
+ [/\bghs_[A-Za-z0-9]{20,}/g, "ghs_***"],
10619
+ [/\bgho_[A-Za-z0-9]{20,}/g, "gho_***"],
10620
+ [/\bghu_[A-Za-z0-9]{20,}/g, "ghu_***"],
10621
+ [/\bghr_[A-Za-z0-9]{20,}/g, "ghr_***"],
10622
+ [/\bgithub_pat_[A-Za-z0-9_]{20,}/g, "github_pat_***"],
10623
+ // AWS access key IDs.
10624
+ [/\bAKIA[0-9A-Z]{16}\b/g, "AKIA***"],
10625
+ [/\bASIA[0-9A-Z]{16}\b/g, "ASIA***"],
10626
+ // Slack tokens (xoxa/xoxb/xoxp/xoxr/xoxs/xoxe-...).
10627
+ [/\bxox[abpres]-[A-Za-z0-9-]{10,}/gi, "xox-***"],
10628
+ // Anthropic + OpenAI style. Anthropic first to preserve "sk-ant-" prefix.
10629
+ [/\bsk-ant-[A-Za-z0-9_-]{20,}/g, "sk-ant-***"],
10630
+ [/\bsk-[A-Za-z0-9_-]{20,}/g, "sk-***"],
10631
+ // JWT-ish payload: three base64url segments separated by dots, header starts eyJ.
10632
+ [/\beyJ[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}\.[A-Za-z0-9_-]{5,}/g, "***jwt***"]
10633
+ ];
10634
+ }
10635
+ });
10636
+
10637
+ // src/runtime/doctor.ts
10638
+ var doctor_exports = {};
10639
+ __export(doctor_exports, {
10640
+ runDoctor: () => runDoctor
10641
+ });
10642
+ import { homedir as homedir16, platform as platform3 } from "os";
10643
+ import { join as join28 } from "path";
10644
+ import { existsSync as existsSync26, statSync as statSync6, accessSync } from "fs";
10645
+ import { readdir as readdir3 } from "fs/promises";
10646
+ import { exec } from "child_process";
10647
+ import { promisify } from "util";
10648
+ async function checkNodeVersion() {
10649
+ const version = process.version;
10650
+ const match = version.match(/v(\d+)/);
10651
+ const major = match ? parseInt(match[1], 10) : 0;
10652
+ if (major >= 18) {
10653
+ return {
10654
+ name: "Node version",
10655
+ status: "PASS",
10656
+ message: `${version} (>=18)`
10657
+ };
10658
+ }
10659
+ return {
10660
+ name: "Node version",
10661
+ status: "FAIL",
10662
+ message: `${version} (requires >=18)`,
10663
+ remediation: `Upgrade Node.js to v18+ from https://nodejs.org/`
10664
+ };
10665
+ }
10666
+ async function checkConfigDir() {
10667
+ const configDir = join28(homedir16(), ".msapling");
10668
+ if (!existsSync26(configDir)) {
10669
+ return {
10670
+ name: "Config directory",
10671
+ status: "WARN",
10672
+ message: `${configDir} does not exist`,
10673
+ remediation: `mkdir -p "${configDir}" && chmod 700 "${configDir}"`
10674
+ };
10675
+ }
10676
+ const stats = statSync6(configDir);
10677
+ if (!stats.isDirectory()) {
10678
+ return {
10679
+ name: "Config directory",
10680
+ status: "FAIL",
10681
+ message: `${configDir} exists but is not a directory`,
10682
+ remediation: `rm "${configDir}" && mkdir -p "${configDir}"`
10683
+ };
10684
+ }
10685
+ if (platform3() !== "win32") {
10686
+ const mode = stats.mode & 511;
10687
+ const safe = (mode & 63) === 0;
10688
+ if (!safe) {
10689
+ return {
10690
+ name: "Config directory",
10691
+ status: "WARN",
10692
+ message: `${configDir} has overly permissive perms (${(mode & 511).toString(8)})`,
10693
+ remediation: `chmod 700 "${configDir}"`
10694
+ };
10695
+ }
10696
+ }
10697
+ return {
10698
+ name: "Config directory",
10699
+ status: "PASS",
10700
+ message: `${configDir} exists and is readable`
10701
+ };
10702
+ }
10703
+ async function checkKeytar() {
10704
+ try {
10705
+ const keytar2 = await import("keytar");
10706
+ if (keytar2 && typeof keytar2.getPassword === "function") {
10707
+ return {
10708
+ name: "Keytar native binary",
10709
+ status: "PASS",
10710
+ message: "Keytar loadable and functional"
10711
+ };
10712
+ }
10713
+ } catch (e) {
10714
+ const msg = e instanceof Error ? e.message : String(e);
10715
+ return {
10716
+ name: "Keytar native binary",
10717
+ status: "WARN",
10718
+ message: `Keytar unavailable: ${msg}`,
10719
+ remediation: `Token will be stored in plaintext. Run 'npm rebuild keytar' or 'bun install --force' to rebuild native bindings.`
10720
+ };
10721
+ }
10722
+ return {
10723
+ name: "Keytar native binary",
10724
+ status: "WARN",
10725
+ message: "Keytar check inconclusive",
10726
+ remediation: `Run 'npm rebuild keytar' or 'bun install --force' to rebuild native bindings.`
10727
+ };
10728
+ }
10729
+ async function checkPathConflicts() {
10730
+ const pathEnv = process.env.PATH || "";
10731
+ const paths = pathEnv.split(platform3() === "win32" ? ";" : ":");
10732
+ const conflicts = [];
10733
+ for (const dir of paths) {
10734
+ if (!dir || !existsSync26(dir)) continue;
10735
+ try {
10736
+ const files = await readdir3(dir);
10737
+ for (const file of files) {
10738
+ if (file === "msapling" || file === "msapling.exe" || file === "msapling.py") {
10739
+ const fullPath = join28(dir, file);
10740
+ conflicts.push(fullPath);
10741
+ }
10742
+ }
10743
+ } catch {
10744
+ }
10745
+ }
10746
+ if (conflicts.length === 0) {
10747
+ return {
10748
+ name: "PATH conflicts",
10749
+ status: "PASS",
10750
+ message: "No conflicting msapling binaries found"
10751
+ };
10752
+ }
10753
+ if (conflicts.length === 1) {
10754
+ return {
10755
+ name: "PATH conflicts",
10756
+ status: "PASS",
10757
+ message: `Single msapling binary found: ${conflicts[0]}`
10758
+ };
10759
+ }
10760
+ return {
10761
+ name: "PATH conflicts",
10762
+ status: "WARN",
10763
+ message: `Multiple msapling binaries found in PATH`,
10764
+ remediation: `Found: ${conflicts.join(", ")}. Remove stale copies (especially .py or .exe).`
10765
+ };
10766
+ }
10767
+ async function checkNetworkReach() {
10768
+ const apiUrl = process.env.MSAPLING_API_URL || "https://api.msapling.com";
10769
+ const timeout = 5e3;
10770
+ try {
10771
+ const controller = new AbortController();
10772
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
10773
+ const response = await fetch(`${apiUrl}/api/health`, {
10774
+ method: "HEAD",
10775
+ signal: controller.signal
10776
+ });
10777
+ clearTimeout(timeoutId);
10778
+ if (response.ok) {
10779
+ return {
10780
+ name: "Network reach",
10781
+ status: "PASS",
10782
+ message: `${apiUrl} reachable`
10783
+ };
10784
+ }
10785
+ return {
10786
+ name: "Network reach",
10787
+ status: "WARN",
10788
+ message: `${apiUrl} returned status ${response.status}`,
10789
+ remediation: `Check your network connection or API server status.`
10790
+ };
10791
+ } catch (e) {
10792
+ const msg = e instanceof Error ? e.message : String(e);
10793
+ return {
10794
+ name: "Network reach",
10795
+ status: "WARN",
10796
+ message: `Cannot reach ${apiUrl}: ${msg}`,
10797
+ remediation: `Check your network connection and firewall settings.`
10798
+ };
10799
+ }
10800
+ }
10801
+ async function checkTokenValidity() {
10802
+ try {
10803
+ const keytar2 = await import("keytar");
10804
+ const KEYCHAIN_SERVICE = "msapling-cli";
10805
+ const KEYCHAIN_ACCOUNT = "auth_token";
10806
+ const token = await keytar2.getPassword(KEYCHAIN_SERVICE, KEYCHAIN_ACCOUNT);
10807
+ if (!token) {
10808
+ return {
10809
+ name: "Token validity",
10810
+ status: "WARN",
10811
+ message: "No token stored in keychain",
10812
+ remediation: `Run 'msapling login' to authenticate.`
10813
+ };
10814
+ }
10815
+ if (typeof token === "string" && token.length > 10) {
10816
+ return {
10817
+ name: "Token validity",
10818
+ status: "PASS",
10819
+ message: `Token loaded (${token.length} chars)`
10820
+ };
10821
+ }
10822
+ return {
10823
+ name: "Token validity",
10824
+ status: "WARN",
10825
+ message: "Token present but appears invalid",
10826
+ remediation: `Run 'msapling login' to re-authenticate.`
10827
+ };
10828
+ } catch (e) {
10829
+ const msg = e instanceof Error ? e.message : String(e);
10830
+ return {
10831
+ name: "Token validity",
10832
+ status: "WARN",
10833
+ message: `Cannot check token: ${msg}`,
10834
+ remediation: `Run 'msapling login' to authenticate.`
10835
+ };
10836
+ }
10837
+ }
10838
+ async function checkOsSpecific() {
10839
+ if (platform3() === "win32") {
10840
+ try {
10841
+ const configDir = join28(homedir16(), ".msapling");
10842
+ const longPath = "A".repeat(260);
10843
+ const testPath = join28(configDir, longPath);
10844
+ try {
10845
+ accessSync(configDir);
10846
+ } catch {
10847
+ return {
10848
+ name: "OS-specific (Windows)",
10849
+ status: "WARN",
10850
+ message: "Cannot verify Windows long-path support",
10851
+ remediation: `Enable LongPathsEnabled in Registry or use 'fsutil 8dot3name set C: 0'.`
10852
+ };
10853
+ }
10854
+ return {
10855
+ name: "OS-specific (Windows)",
10856
+ status: "PASS",
10857
+ message: "Windows long-path support accessible"
10858
+ };
10859
+ } catch (e) {
10860
+ return {
10861
+ name: "OS-specific (Windows)",
10862
+ status: "WARN",
10863
+ message: "Windows long-path check failed",
10864
+ remediation: `Enable LongPathsEnabled in Registry or use 'fsutil 8dot3name set C: 0'.`
10865
+ };
10866
+ }
10867
+ }
10868
+ if (platform3() === "darwin") {
10869
+ return {
10870
+ name: "OS-specific (macOS)",
10871
+ status: "PASS",
10872
+ message: "macOS detected"
10873
+ };
10874
+ }
10875
+ if (platform3() === "linux") {
10876
+ try {
10877
+ await import("keytar");
10878
+ return {
10879
+ name: "OS-specific (Linux)",
10880
+ status: "PASS",
10881
+ message: "libsecret/keytar dependencies available"
10882
+ };
10883
+ } catch (e) {
10884
+ return {
10885
+ name: "OS-specific (Linux)",
10886
+ status: "WARN",
10887
+ message: "libsecret may not be installed",
10888
+ remediation: `Install libsecret: sudo apt-get install libsecret-1-dev (Ubuntu/Debian) or dnf install libsecret-devel (Fedora)`
10889
+ };
10890
+ }
10891
+ }
10892
+ return {
10893
+ name: "OS-specific",
10894
+ status: "PASS",
10895
+ message: `${platform3()} detected`
10896
+ };
10897
+ }
10898
+ function formatCheckResult(result, maxLabelWidth) {
10899
+ const indicator2 = result.status === "PASS" ? "\u2713" : result.status === "WARN" ? "\u26A0" : "\u2717";
10900
+ const paddedLabel = result.name.padEnd(maxLabelWidth);
10901
+ const statusStr = `[${indicator2} ${result.status}]`.padEnd(10);
10902
+ let output = ` ${paddedLabel} ${statusStr} ${result.message}`;
10903
+ if (result.remediation) {
10904
+ output += `
10905
+ \u2192 ${result.remediation}`;
10906
+ }
10907
+ return output;
10908
+ }
10909
+ function dumpEnv(debug) {
10910
+ if (!debug) return [];
10911
+ const lines = [];
10912
+ lines.push("");
10913
+ lines.push("\u2500".repeat(60));
10914
+ lines.push("Environment variables:");
10915
+ lines.push("\u2500".repeat(60));
10916
+ const env = { ...process.env };
10917
+ const keys = Object.keys(env).sort();
10918
+ for (const key of keys) {
10919
+ const value = env[key] || "";
10920
+ const redacted = redactSecrets(value);
10921
+ lines.push(`${key}=${redacted}`);
10922
+ }
10923
+ return lines;
10924
+ }
10925
+ async function runDoctor(debug = false) {
10926
+ const output = [];
10927
+ output.push("");
10928
+ output.push("MSapling Doctor \u2014 Health Check");
10929
+ output.push("\u2500".repeat(60));
10930
+ const checks = [];
10931
+ checks.push(await checkNodeVersion());
10932
+ checks.push(await checkConfigDir());
10933
+ checks.push(await checkKeytar());
10934
+ checks.push(await checkPathConflicts());
10935
+ checks.push(await checkNetworkReach());
10936
+ checks.push(await checkTokenValidity());
10937
+ checks.push(await checkOsSpecific());
10938
+ const maxLabelWidth = Math.max(...checks.map((c) => c.name.length));
10939
+ for (const check of checks) {
10940
+ output.push(formatCheckResult(check, maxLabelWidth));
10941
+ }
10942
+ output.push("");
10943
+ output.push("\u2500".repeat(60));
10944
+ const failCount = checks.filter((c) => c.status === "FAIL").length;
10945
+ const warnCount = checks.filter((c) => c.status === "WARN").length;
10946
+ if (failCount === 0 && warnCount === 0) {
10947
+ output.push("All checks passed! \u2713");
10948
+ } else {
10949
+ const msgs = [];
10950
+ if (failCount > 0) msgs.push(`${failCount} failed`);
10951
+ if (warnCount > 0) msgs.push(`${warnCount} warning(s)`);
10952
+ output.push(`Status: ${msgs.join(", ")}`);
10953
+ }
10954
+ output.push("");
10955
+ if (debug) {
10956
+ output.push(...dumpEnv(true));
10957
+ output.push("");
10958
+ }
10959
+ const rendered = output.join("\n");
10960
+ console.log(rendered);
10961
+ return {
10962
+ exitCode: failCount > 0 ? 1 : 0,
10963
+ checks,
10964
+ failCount,
10965
+ warnCount,
10966
+ rendered
10967
+ };
10968
+ }
10969
+ var execAsync;
10970
+ var init_doctor2 = __esm({
10971
+ "src/runtime/doctor.ts"() {
10972
+ "use strict";
10973
+ init_esm_shims();
10974
+ init_doctorRedact();
10975
+ execAsync = promisify(exec);
10976
+ }
10977
+ });
10978
+
9766
10979
  // ../../node_modules/.bun/diff@9.0.0/node_modules/diff/libesm/diff/base.js
9767
10980
  var Diff;
9768
10981
  var init_base = __esm({
@@ -9849,13 +11062,13 @@ var init_base = __esm({
9849
11062
  editLength++;
9850
11063
  };
9851
11064
  if (callback) {
9852
- (function exec() {
11065
+ (function exec2() {
9853
11066
  setTimeout(function() {
9854
11067
  if (editLength > maxEditLength || Date.now() > abortAfterTimestamp) {
9855
11068
  return callback(void 0);
9856
11069
  }
9857
11070
  if (!execEditLength()) {
9858
- exec();
11071
+ exec2();
9859
11072
  }
9860
11073
  }, 0);
9861
11074
  })();
@@ -10909,7 +12122,7 @@ async function mergeToolRegistries(localTools, client) {
10909
12122
  return Array.from(merged.values());
10910
12123
  }
10911
12124
  for (const backendTool of registry.tools) {
10912
- const tier = backendTool.tier_required === "free" || backendTool.tier_required === "pro" ? backendTool.tier_required : "free";
12125
+ const tier = backendTool.tier_required === "free" || backendTool.tier_required === "pro" || backendTool.tier_required === "lifetime" || backendTool.tier_required === "enterprise" ? backendTool.tier_required : "enterprise";
10913
12126
  if (!merged.has(backendTool.name)) {
10914
12127
  merged.set(backendTool.name, {
10915
12128
  name: backendTool.name,
@@ -10939,17 +12152,17 @@ var init_registry_merger = __esm({
10939
12152
 
10940
12153
  // ../core/src/mcp/local_tools.ts
10941
12154
  import { spawn as spawn10 } from "child_process";
10942
- import { readdir as readdir3, stat as stat4, realpath as realpath2 } from "fs/promises";
10943
- import { resolve as resolve15 } from "path";
12155
+ import { readdir as readdir4, stat as stat4, realpath as realpath2 } from "fs/promises";
12156
+ import { resolve as resolve16 } from "path";
10944
12157
  function asResult(text, isError = false) {
10945
12158
  return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
10946
12159
  }
10947
12160
  async function runCommand(command, cwd) {
10948
- return new Promise((resolve17) => {
12161
+ return new Promise((resolve18) => {
10949
12162
  let p;
10950
12163
  const timeout = setTimeout(() => {
10951
12164
  if (p) p.kill();
10952
- resolve17({ stdout: "", stderr: "Command timed out after 30s", exit_code: -1 });
12165
+ resolve18({ stdout: "", stderr: "Command timed out after 30s", exit_code: -1 });
10953
12166
  }, 3e4);
10954
12167
  try {
10955
12168
  p = spawn10("sh", ["-c", command], {
@@ -10967,15 +12180,15 @@ async function runCommand(command, cwd) {
10967
12180
  });
10968
12181
  p.on("error", (e) => {
10969
12182
  clearTimeout(timeout);
10970
- resolve17({ stdout, stderr: stderr + (e?.message ?? ""), exit_code: -1 });
12183
+ resolve18({ stdout, stderr: stderr + (e?.message ?? ""), exit_code: -1 });
10971
12184
  });
10972
12185
  p.on("exit", (code) => {
10973
12186
  clearTimeout(timeout);
10974
- resolve17({ stdout, stderr, exit_code: code });
12187
+ resolve18({ stdout, stderr, exit_code: code });
10975
12188
  });
10976
12189
  } catch (e) {
10977
12190
  clearTimeout(timeout);
10978
- resolve17({
12191
+ resolve18({
10979
12192
  stdout: "",
10980
12193
  stderr: e?.message ?? "Failed to spawn process",
10981
12194
  exit_code: -1
@@ -10985,7 +12198,7 @@ async function runCommand(command, cwd) {
10985
12198
  }
10986
12199
  async function listDirectory(path2, maxEntries = 100) {
10987
12200
  try {
10988
- const entries = await readdir3(path2, { withFileTypes: true });
12201
+ const entries = await readdir4(path2, { withFileTypes: true });
10989
12202
  const result = [];
10990
12203
  for (const entry of entries.slice(0, maxEntries)) {
10991
12204
  const item = {
@@ -11042,7 +12255,7 @@ async function callLocalTool(name, args2, projectRoot) {
11042
12255
  const command = String(args2.command ?? "");
11043
12256
  let cwd = projectRoot;
11044
12257
  if (args2.cwd) {
11045
- cwd = resolve15(projectRoot, String(args2.cwd));
12258
+ cwd = resolve16(projectRoot, String(args2.cwd));
11046
12259
  try {
11047
12260
  const resolvedCwd = await realpath2(cwd);
11048
12261
  const resolvedRoot = await realpath2(projectRoot);
@@ -11079,7 +12292,7 @@ ${res.stderr}`
11079
12292
  return asResult("path is required", true);
11080
12293
  }
11081
12294
  try {
11082
- const resolvedPath = await realpath2(resolve15(projectRoot, pathArg));
12295
+ const resolvedPath = await realpath2(resolve16(projectRoot, pathArg));
11083
12296
  const resolvedRoot = await realpath2(projectRoot);
11084
12297
  if (!resolvedPath.startsWith(resolvedRoot)) {
11085
12298
  return asResult("Error: path attempts to escape project root", true);
@@ -11098,7 +12311,7 @@ ${res.stderr}`
11098
12311
  }
11099
12312
  case "local_glob": {
11100
12313
  const pattern = String(args2.pattern ?? "");
11101
- let cwd = args2.cwd ? resolve15(projectRoot, String(args2.cwd)) : projectRoot;
12314
+ let cwd = args2.cwd ? resolve16(projectRoot, String(args2.cwd)) : projectRoot;
11102
12315
  if (!pattern) {
11103
12316
  return asResult("pattern is required", true);
11104
12317
  }
@@ -11127,7 +12340,7 @@ ${res.stderr}`
11127
12340
  }
11128
12341
  if (path2) {
11129
12342
  try {
11130
- const resolvedPath = await realpath2(resolve15(projectRoot, path2));
12343
+ const resolvedPath = await realpath2(resolve16(projectRoot, path2));
11131
12344
  const resolvedRoot = await realpath2(projectRoot);
11132
12345
  if (!resolvedPath.startsWith(resolvedRoot)) {
11133
12346
  return asResult("Error: path attempts to escape project root", true);
@@ -11147,7 +12360,7 @@ ${res.stderr}`
11147
12360
  return asResult("cwd is required", true);
11148
12361
  }
11149
12362
  try {
11150
- const resolvedCwd = await realpath2(resolve15(projectRoot, cwdArg));
12363
+ const resolvedCwd = await realpath2(resolve16(projectRoot, cwdArg));
11151
12364
  const resolvedRoot = await realpath2(projectRoot);
11152
12365
  if (!resolvedCwd.startsWith(resolvedRoot)) {
11153
12366
  return asResult("Error: cwd attempts to escape project root", true);
@@ -11173,7 +12386,7 @@ ${status.porcelain || "(clean)"}`
11173
12386
  return asResult("cwd is required", true);
11174
12387
  }
11175
12388
  try {
11176
- const resolvedCwd = await realpath2(resolve15(projectRoot, cwdArg));
12389
+ const resolvedCwd = await realpath2(resolve16(projectRoot, cwdArg));
11177
12390
  const resolvedRoot = await realpath2(projectRoot);
11178
12391
  if (!resolvedCwd.startsWith(resolvedRoot)) {
11179
12392
  return asResult("Error: cwd attempts to escape project root", true);
@@ -11288,8 +12501,8 @@ __export(server_exports, {
11288
12501
  runStdio: () => runStdio,
11289
12502
  runStdioWithRegistry: () => runStdioWithRegistry
11290
12503
  });
11291
- import { readdirSync as readdirSync3, readFileSync, statSync as statSync5 } from "fs";
11292
- import { join as join26, relative as relative14, resolve as resolve16 } from "path";
12504
+ import { readdirSync as readdirSync4, readFileSync as readFileSync2, statSync as statSync7 } from "fs";
12505
+ import { join as join29, relative as relative14, resolve as resolve17 } from "path";
11293
12506
  function asResult2(text, isError = false) {
11294
12507
  return { content: [{ type: "text", text }], ...isError ? { isError: true } : {} };
11295
12508
  }
@@ -11302,17 +12515,17 @@ function buildFileTree(root, maxFiles) {
11302
12515
  const dir = queue.shift();
11303
12516
  let entries;
11304
12517
  try {
11305
- entries = readdirSync3(dir);
12518
+ entries = readdirSync4(dir);
11306
12519
  } catch {
11307
12520
  continue;
11308
12521
  }
11309
12522
  for (const name of entries) {
11310
12523
  if (out.length >= maxFiles) break;
11311
12524
  if (SKIP_DIRS2.has(name)) continue;
11312
- const full = join26(dir, name);
12525
+ const full = join29(dir, name);
11313
12526
  let s;
11314
12527
  try {
11315
- s = statSync5(full);
12528
+ s = statSync7(full);
11316
12529
  } catch {
11317
12530
  continue;
11318
12531
  }
@@ -11333,7 +12546,7 @@ function readFilesAsContext(root, files, maxKB) {
11333
12546
  for (const f of files) {
11334
12547
  let body;
11335
12548
  try {
11336
- body = readFileSync(f, "utf8");
12549
+ body = readFileSync2(f, "utf8");
11337
12550
  } catch {
11338
12551
  continue;
11339
12552
  }
@@ -11347,11 +12560,37 @@ ${body}
11347
12560
  }
11348
12561
  return parts.join("\n\n");
11349
12562
  }
12563
+ function installDrainHandlers(server) {
12564
+ const abort = new AbortController();
12565
+ let drainStarted = false;
12566
+ const onSignal = (sig) => {
12567
+ if (drainStarted) return;
12568
+ drainStarted = true;
12569
+ process.stderr.write(`[mcp-server] received ${sig}, starting graceful drain
12570
+ `);
12571
+ server.drain().then((forcedResponses) => {
12572
+ for (const resp of forcedResponses) {
12573
+ try {
12574
+ process.stdout.write(JSON.stringify(resp) + "\n");
12575
+ } catch {
12576
+ }
12577
+ }
12578
+ process.stderr.write("[mcp-server] drain complete, exiting\n");
12579
+ abort.abort();
12580
+ process.exit(0);
12581
+ });
12582
+ };
12583
+ process.on("SIGTERM", () => onSignal("SIGTERM"));
12584
+ process.on("SIGINT", () => onSignal("SIGINT"));
12585
+ return abort;
12586
+ }
11350
12587
  async function runStdio(client) {
11351
12588
  const server = new MCPServer(client);
12589
+ const abort = installDrainHandlers(server);
11352
12590
  let buffer = "";
11353
12591
  const decoder = new TextDecoder();
11354
12592
  for await (const chunk of process.stdin) {
12593
+ if (abort.signal.aborted) break;
11355
12594
  buffer += decoder.decode(chunk, { stream: true });
11356
12595
  let idx;
11357
12596
  while ((idx = buffer.indexOf("\n")) !== -1) {
@@ -11371,9 +12610,11 @@ async function runStdio(client) {
11371
12610
  }
11372
12611
  async function runStdioWithRegistry(client, registryClient) {
11373
12612
  const server = new MCPServer(client, registryClient || client);
12613
+ const abort = installDrainHandlers(server);
11374
12614
  let buffer = "";
11375
12615
  const decoder = new TextDecoder();
11376
12616
  for await (const chunk of process.stdin) {
12617
+ if (abort.signal.aborted) break;
11377
12618
  buffer += decoder.decode(chunk, { stream: true });
11378
12619
  let idx;
11379
12620
  while ((idx = buffer.indexOf("\n")) !== -1) {
@@ -11391,7 +12632,7 @@ async function runStdioWithRegistry(client, registryClient) {
11391
12632
  }
11392
12633
  }
11393
12634
  }
11394
- var TOOLS, PROTOCOL_VERSION2, PRO_TIERS2, TIER_CACHE_TTL_MS, SAFE_PATH_RE, MCPServer;
12635
+ var AP4_MAX_BLOB_BYTES, TOOLS, PROTOCOL_VERSION2, PRO_TIERS2, TIER_CACHE_TTL_MS, DRAIN_TIMEOUT_MS, SAFE_PATH_RE, MCPServer;
11395
12636
  var init_server = __esm({
11396
12637
  "../core/src/mcp/server.ts"() {
11397
12638
  "use strict";
@@ -11400,6 +12641,7 @@ var init_server = __esm({
11400
12641
  init_libesm();
11401
12642
  init_registry_merger();
11402
12643
  init_local_tools();
12644
+ AP4_MAX_BLOB_BYTES = 4 * 1024;
11403
12645
  TOOLS = [
11404
12646
  {
11405
12647
  name: "msapling_chat",
@@ -11518,11 +12760,57 @@ var init_server = __esm({
11518
12760
  tier: "free",
11519
12761
  inputSchema: { type: "object", properties: {} }
11520
12762
  },
12763
+ {
12764
+ name: "msapling_list_wakeups",
12765
+ description: "[Pro] List pending AI wake-ups/scheduled tasks for your account.",
12766
+ tier: "pro",
12767
+ inputSchema: {
12768
+ type: "object",
12769
+ properties: {
12770
+ status: { type: "string", enum: ["pending", "fired", "cancelled", "expired"], description: "Filter by status" },
12771
+ session_id: { type: "string", description: "Filter by chat session ID" }
12772
+ }
12773
+ }
12774
+ },
12775
+ {
12776
+ name: "msapling_schedule_wakeup",
12777
+ description: "[Pro] Schedule a future AI task or follow-up wakeup.",
12778
+ tier: "pro",
12779
+ inputSchema: {
12780
+ type: "object",
12781
+ required: ["delay_seconds", "reason"],
12782
+ properties: {
12783
+ delay_seconds: { type: "number", description: "Delay in seconds from now" },
12784
+ reason: { type: "string", description: "Short description of what the AI should do" },
12785
+ prompt: { type: "string", description: "Specific prompt to process when the wakeup fires" },
12786
+ session_id: { type: "string", description: "Associate with an existing chat session" }
12787
+ }
12788
+ }
12789
+ },
12790
+ {
12791
+ name: "msapling_cancel_wakeup",
12792
+ description: "[Pro] Cancel a pending AI wakeup.",
12793
+ tier: "pro",
12794
+ inputSchema: {
12795
+ type: "object",
12796
+ required: ["id"],
12797
+ properties: {
12798
+ id: { type: "string", description: "The wakeup UUID to cancel" }
12799
+ }
12800
+ }
12801
+ },
12802
+ {
12803
+ name: "msapling_fleet_status",
12804
+ description: "[Pro] View status of your remote agent fleet and active tasks.",
12805
+ tier: "pro",
12806
+ inputSchema: { type: "object", properties: {} }
12807
+ },
11521
12808
  ...LOCAL_TOOLS
11522
12809
  ];
11523
12810
  PROTOCOL_VERSION2 = "2024-11-05";
11524
12811
  PRO_TIERS2 = /* @__PURE__ */ new Set(["pro", "monthly", "lifetime", "enterprise", "admin", "superadmin"]);
11525
12812
  TIER_CACHE_TTL_MS = 6e4;
12813
+ DRAIN_TIMEOUT_MS = 5e3;
11526
12814
  SAFE_PATH_RE = /^[A-Za-z0-9_./\-\\: ]+$/;
11527
12815
  MCPServer = class {
11528
12816
  constructor(client, backendClient) {
@@ -11533,6 +12821,83 @@ var init_server = __esm({
11533
12821
  backendClient;
11534
12822
  tierCache = null;
11535
12823
  registryCache = null;
12824
+ /**
12825
+ * CLIENT-CLI-16: When true, the server is shutting down. New tool calls
12826
+ * are rejected with a JSON-RPC error; only in-flight calls are allowed
12827
+ * to finish.
12828
+ */
12829
+ _isDraining = false;
12830
+ /**
12831
+ * CLIENT-CLI-16: Set of in-flight tool call IDs. Used during draining to
12832
+ * wait for completion before closing the transport. Capped at
12833
+ * MAX_INFLIGHT_TOOLS.
12834
+ */
12835
+ _inflightCalls = /* @__PURE__ */ new Map();
12836
+ /**
12837
+ * CLIENT-CLI-16: Resolved when all in-flight calls complete during drain,
12838
+ * or when the drain timeout fires.
12839
+ */
12840
+ _drainResolve = null;
12841
+ /** CLIENT-CLI-16: Whether the server is currently draining. */
12842
+ get isDraining() {
12843
+ return this._isDraining;
12844
+ }
12845
+ /**
12846
+ * CLIENT-CLI-16: Enter draining mode. Returns a promise that resolves when
12847
+ * all in-flight calls complete or DRAIN_TIMEOUT_MS elapses, whichever
12848
+ * comes first.
12849
+ *
12850
+ * After the promise resolves, any remaining in-flight calls have been
12851
+ * forcefully closed with error responses (written to stdout by the caller).
12852
+ */
12853
+ async drain() {
12854
+ this._isDraining = true;
12855
+ const inflight = this._inflightCalls.size;
12856
+ process.stderr.write(
12857
+ `[mcp-server] draining: ${inflight} in-flight tool call(s)
12858
+ `
12859
+ );
12860
+ if (inflight === 0) {
12861
+ return [];
12862
+ }
12863
+ const forcedResponses = await new Promise((resolve18) => {
12864
+ this._drainResolve = () => resolve18([]);
12865
+ setTimeout(() => {
12866
+ this._drainResolve = null;
12867
+ const remaining = Array.from(this._inflightCalls.values());
12868
+ if (remaining.length === 0) {
12869
+ resolve18([]);
12870
+ return;
12871
+ }
12872
+ process.stderr.write(
12873
+ `[mcp-server] drain timeout: force-closing ${remaining.length} call(s)
12874
+ `
12875
+ );
12876
+ const errorResponses = remaining.map((call) => ({
12877
+ jsonrpc: "2.0",
12878
+ id: call.id,
12879
+ error: {
12880
+ code: -32e3,
12881
+ message: `Server shutdown: tool call "${call.name}" timed out after ${DRAIN_TIMEOUT_MS}ms`
12882
+ }
12883
+ }));
12884
+ this._inflightCalls.clear();
12885
+ resolve18(errorResponses);
12886
+ }, DRAIN_TIMEOUT_MS);
12887
+ });
12888
+ return forcedResponses;
12889
+ }
12890
+ /**
12891
+ * CLIENT-CLI-16: Called when an in-flight tool call completes during drain.
12892
+ * If all calls are done, resolves the drain promise.
12893
+ */
12894
+ _completeInflight(id) {
12895
+ this._inflightCalls.delete(id);
12896
+ if (this._isDraining && this._inflightCalls.size === 0 && this._drainResolve) {
12897
+ this._drainResolve();
12898
+ this._drainResolve = null;
12899
+ }
12900
+ }
11536
12901
  /**
11537
12902
  * Fetch the caller's tier (Pro vs free) and cache it briefly. We avoid a
11538
12903
  * `me()` call per `tools/list` because Claude Code calls tools/list often
@@ -11564,6 +12929,25 @@ var init_server = __esm({
11564
12929
  invalidateTierCache() {
11565
12930
  this.tierCache = null;
11566
12931
  }
12932
+ /**
12933
+ * CLI-R12-20260507-TIER-02: Map tier name to numeric level for comparison.
12934
+ * Higher level = more restrictive/expensive.
12935
+ * free (1) < pro (5) < lifetime (10) < enterprise (100)
12936
+ */
12937
+ tierLevel(tier) {
12938
+ switch (String(tier).toLowerCase()) {
12939
+ case "free":
12940
+ return 1;
12941
+ case "pro":
12942
+ return 5;
12943
+ case "lifetime":
12944
+ return 10;
12945
+ case "enterprise":
12946
+ return 100;
12947
+ default:
12948
+ return 100;
12949
+ }
12950
+ }
11567
12951
  /**
11568
12952
  * Fetch and cache the merged tool registry (local + backend).
11569
12953
  * Called on first tools/list request. Cached for session lifetime.
@@ -11601,30 +12985,62 @@ var init_server = __esm({
11601
12985
  return { jsonrpc: "2.0", id, result: { tools: wireFormat } };
11602
12986
  }
11603
12987
  case "tools/call": {
12988
+ if (this._isDraining) {
12989
+ return {
12990
+ jsonrpc: "2.0",
12991
+ id,
12992
+ error: {
12993
+ code: -32e3,
12994
+ message: "Server shutting down: not accepting new tool calls"
12995
+ }
12996
+ };
12997
+ }
11604
12998
  const name = req.params?.name;
11605
12999
  const args2 = req.params?.arguments ?? {};
11606
- const localTool = TOOLS.find((t) => t.name === name);
11607
- if (localTool) {
11608
- try {
11609
- const result = await this.callTool(name, args2);
11610
- return { jsonrpc: "2.0", id, result };
11611
- } catch (e) {
11612
- const msg = e instanceof MSaplingError ? `[backend ${e.status ?? "?"} ${e.code ?? ""}] ${e.message}` : e?.message ?? "tool call failed";
11613
- return { jsonrpc: "2.0", id, result: asResult2(msg, true) };
13000
+ this._inflightCalls.set(id, {
13001
+ id,
13002
+ name: name ?? "unknown",
13003
+ startedAt: Date.now(),
13004
+ resolve: () => {
11614
13005
  }
11615
- }
11616
- const merged = await this.getMergedRegistry();
11617
- const backendTool = merged.find((t) => t.name === name && t.runs_on === "backend");
11618
- if (backendTool) {
11619
- try {
11620
- const result = await this.invokeBackendTool(name, args2);
11621
- return { jsonrpc: "2.0", id, result };
11622
- } catch (e) {
11623
- const msg = e instanceof MSaplingError ? `[backend ${e.status ?? "?"} ${e.code ?? ""}] ${e.message}` : e?.message ?? "tool call failed";
11624
- return { jsonrpc: "2.0", id, result: asResult2(msg, true) };
13006
+ });
13007
+ try {
13008
+ const localTool = TOOLS.find((t) => t.name === name);
13009
+ if (localTool) {
13010
+ try {
13011
+ const result = await this.callTool(name, args2);
13012
+ const resp = { jsonrpc: "2.0", id, result };
13013
+ return resp;
13014
+ } catch (e) {
13015
+ const msg = e instanceof MSaplingError ? `[backend ${e.status ?? "?"} ${e.code ?? ""}] ${e.message}` : e?.message ?? "tool call failed";
13016
+ return { jsonrpc: "2.0", id, result: asResult2(msg, true) };
13017
+ }
13018
+ }
13019
+ const merged = await this.getMergedRegistry();
13020
+ const backendTool = merged.find((t) => t.name === name && t.runs_on === "backend");
13021
+ if (backendTool) {
13022
+ const isPro = await this.getIsProCached();
13023
+ const toolTierLevel = this.tierLevel(backendTool.tier);
13024
+ const userTierLevel = isPro ? 5 : 1;
13025
+ if (toolTierLevel > userTierLevel) {
13026
+ return {
13027
+ jsonrpc: "2.0",
13028
+ id,
13029
+ result: asResult2(`Access denied: tool "${name}" requires ${backendTool.tier} tier`, true)
13030
+ };
13031
+ }
13032
+ try {
13033
+ const result = await this.invokeBackendTool(name, args2);
13034
+ return { jsonrpc: "2.0", id, result };
13035
+ } catch (e) {
13036
+ const msg = e instanceof MSaplingError ? `[backend ${e.status ?? "?"} ${e.code ?? ""}] ${e.message}` : e?.message ?? "tool call failed";
13037
+ return { jsonrpc: "2.0", id, result: asResult2(msg, true) };
13038
+ }
11625
13039
  }
13040
+ return { jsonrpc: "2.0", id, error: { code: -32601, message: `Unknown tool: ${name}` } };
13041
+ } finally {
13042
+ this._completeInflight(id);
11626
13043
  }
11627
- return { jsonrpc: "2.0", id, error: { code: -32601, message: `Unknown tool: ${name}` } };
11628
13044
  }
11629
13045
  case "shutdown":
11630
13046
  return { jsonrpc: "2.0", id, result: {} };
@@ -11675,12 +13091,30 @@ ${r.response ?? ""}`;
11675
13091
  return asResult2(text);
11676
13092
  }
11677
13093
  case "msapling_diff": {
13094
+ const oldContent = String(args2.old_content ?? "");
13095
+ const newContent = String(args2.new_content ?? "");
13096
+ const oldBytes = Buffer.byteLength(oldContent, "utf8");
13097
+ const newBytes = Buffer.byteLength(newContent, "utf8");
13098
+ if (oldBytes > AP4_MAX_BLOB_BYTES || newBytes > AP4_MAX_BLOB_BYTES) {
13099
+ return asResult2(
13100
+ `AP-4: Blobs exceed 4 KB (old=${oldBytes}B, new=${newBytes}B). Use the backend endpoint: POST /api/projects/:id/diff`,
13101
+ true
13102
+ );
13103
+ }
11678
13104
  const filename = String(args2.filename ?? "file");
11679
- const patch = createPatch(filename, String(args2.old_content ?? ""), String(args2.new_content ?? ""), "", "");
13105
+ const patch = createPatch(filename, oldContent, newContent, "", "");
11680
13106
  return asResult2(patch);
11681
13107
  }
11682
13108
  case "msapling_apply_diff": {
11683
- const applied = applyPatch(String(args2.original_content ?? ""), String(args2.diff_text ?? ""));
13109
+ const originalContent = String(args2.original_content ?? "");
13110
+ const originalBytes = Buffer.byteLength(originalContent, "utf8");
13111
+ if (originalBytes > AP4_MAX_BLOB_BYTES) {
13112
+ return asResult2(
13113
+ `AP-4: Blob exceeds 4 KB (${originalBytes}B). Use the backend endpoint: POST /api/projects/:id/apply`,
13114
+ true
13115
+ );
13116
+ }
13117
+ const applied = applyPatch(originalContent, String(args2.diff_text ?? ""));
11684
13118
  if (applied === false) {
11685
13119
  return asResult2("Diff did not apply cleanly (hunks rejected).", true);
11686
13120
  }
@@ -11726,7 +13160,7 @@ ${r.response ?? ""}`;
11726
13160
  return asResult2(JSON.stringify(result));
11727
13161
  }
11728
13162
  case "msapling_project_context": {
11729
- const root = resolve16(String(args2.path ?? "."));
13163
+ const root = resolve17(String(args2.path ?? "."));
11730
13164
  const maxFiles = Number.isFinite(args2.max_files) ? Number(args2.max_files) : 30;
11731
13165
  const maxKB = Number.isFinite(args2.max_file_size_kb) ? Number(args2.max_file_size_kb) : 50;
11732
13166
  const files = buildFileTree(root, maxFiles);
@@ -11777,6 +13211,36 @@ ${text}`);
11777
13211
  return asResult2(`Available models (${models.length} total, showing first 30):
11778
13212
  ${lines.join("\n")}`);
11779
13213
  }
13214
+ case "msapling_list_wakeups": {
13215
+ const isPro = await this.getIsProCached();
13216
+ if (!isPro) return asResult2("msapling_list_wakeups requires a Pro subscription.", true);
13217
+ const data = await this.client.listWakeups(args2);
13218
+ return asResult2(JSON.stringify(data, null, 2));
13219
+ }
13220
+ case "msapling_schedule_wakeup": {
13221
+ const isPro = await this.getIsProCached();
13222
+ if (!isPro) return asResult2("msapling_schedule_wakeup requires a Pro subscription.", true);
13223
+ const data = await this.client.createWakeup({
13224
+ delay_seconds: Number(args2.delay_seconds),
13225
+ reason: String(args2.reason ?? ""),
13226
+ prompt: args2.prompt ? String(args2.prompt) : void 0,
13227
+ session_id: args2.session_id ? String(args2.session_id) : void 0
13228
+ });
13229
+ return asResult2(JSON.stringify(data, null, 2));
13230
+ }
13231
+ case "msapling_cancel_wakeup": {
13232
+ const isPro = await this.getIsProCached();
13233
+ if (!isPro) return asResult2("msapling_cancel_wakeup requires a Pro subscription.", true);
13234
+ await this.client.cancelWakeup(String(args2.id));
13235
+ return asResult2("Wakeup cancelled");
13236
+ }
13237
+ case "msapling_fleet_status": {
13238
+ const isPro = await this.getIsProCached();
13239
+ if (!isPro) return asResult2("msapling_fleet_status requires a Pro subscription.", true);
13240
+ const status = await this.client.getFleetStatus();
13241
+ const tasks = await this.client.getActiveTasks();
13242
+ return asResult2(JSON.stringify({ status, tasks }, null, 2));
13243
+ }
11780
13244
  // Local MCP tools (filesystem, shell, git)
11781
13245
  case "local_run_command":
11782
13246
  case "local_list_directory":
@@ -11822,7 +13286,10 @@ init_esm_shims();
11822
13286
  import { Box, Text } from "ink";
11823
13287
  import { jsx, jsxs } from "react/jsx-runtime";
11824
13288
  var Header = () => /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [
11825
- /* @__PURE__ */ jsx(Text, { bold: true, color: "cyan", children: "\u25CF MSapling CLI v2.0.0" }),
13289
+ /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
13290
+ "\u25CF MSapling CLI v",
13291
+ "2.3.6-beta.11"
13292
+ ] }),
11826
13293
  /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Platinum Tier Architecture" }) })
11827
13294
  ] });
11828
13295
 
@@ -12308,9 +13775,9 @@ ${prompt}` : prompt;
12308
13775
  if (proc.stderr) proc.stderr.on("data", (chunk) => {
12309
13776
  stderr += chunk.toString();
12310
13777
  });
12311
- await new Promise((resolve17, reject) => {
13778
+ await new Promise((resolve18, reject) => {
12312
13779
  proc.on("close", (code) => {
12313
- if (code === 0 || code === null) resolve17();
13780
+ if (code === 0 || code === null) resolve18();
12314
13781
  else reject(new Error(`Process exited with code ${code}`));
12315
13782
  });
12316
13783
  proc.on("error", reject);
@@ -12337,9 +13804,9 @@ ${prompt}` : prompt;
12337
13804
  for (const mention of fileMentions) {
12338
13805
  const filePath = mention.slice(1);
12339
13806
  try {
12340
- const { existsSync: existsSync25 } = await import("fs");
13807
+ const { existsSync: existsSync27 } = await import("fs");
12341
13808
  const { readFile: readFile23 } = await import("fs/promises");
12342
- if (existsSync25(filePath)) {
13809
+ if (existsSync27(filePath)) {
12343
13810
  const content = await readFile23(filePath, "utf8");
12344
13811
  const MAX_LEN = 32768;
12345
13812
  const truncated = content.length > MAX_LEN ? content.slice(0, MAX_LEN) + "\n...[TRUNCATED]" : content;
@@ -12392,7 +13859,66 @@ ${finalCmd}`;
12392
13859
  init_esm_shims();
12393
13860
  init_src3();
12394
13861
  import { readFile as readFile22 } from "fs/promises";
12395
- import { existsSync as existsSync24 } from "fs";
13862
+ import { existsSync as existsSync25 } from "fs";
13863
+
13864
+ // src/state/parseApprovalMode.ts
13865
+ init_esm_shims();
13866
+ var VALID_MODES = [
13867
+ "default",
13868
+ "plan",
13869
+ "acceptEdits",
13870
+ "bypassPermissions"
13871
+ ];
13872
+ function parseApprovalMode(raw, now) {
13873
+ if (!raw || typeof raw !== "object") {
13874
+ return { kind: "invalid", error: "settings file is not a JSON object" };
13875
+ }
13876
+ const block = raw.approvalMode;
13877
+ if (block === void 0) return { kind: "absent" };
13878
+ if (typeof block === "string") {
13879
+ if (!VALID_MODES.includes(block)) {
13880
+ return { kind: "invalid", error: `unknown approvalMode "${block}"` };
13881
+ }
13882
+ return { kind: "ok", mode: block };
13883
+ }
13884
+ if (typeof block !== "object" || block === null) {
13885
+ return {
13886
+ kind: "invalid",
13887
+ error: `approvalMode must be a string or object, got ${typeof block}`
13888
+ };
13889
+ }
13890
+ const obj = block;
13891
+ const mode = obj.mode;
13892
+ if (typeof mode !== "string" || !VALID_MODES.includes(mode)) {
13893
+ return { kind: "invalid", error: `approvalMode.mode invalid: ${JSON.stringify(mode)}` };
13894
+ }
13895
+ if (obj.ttlMs === void 0 && obj.timestamp === void 0) {
13896
+ return { kind: "ok", mode };
13897
+ }
13898
+ if (typeof obj.ttlMs !== "number" || !Number.isFinite(obj.ttlMs) || obj.ttlMs <= 0) {
13899
+ return {
13900
+ kind: "invalid",
13901
+ error: `approvalMode.ttlMs must be a positive number, got ${JSON.stringify(obj.ttlMs)}`
13902
+ };
13903
+ }
13904
+ if (typeof obj.timestamp !== "number" || !Number.isFinite(obj.timestamp) || obj.timestamp <= 0) {
13905
+ return {
13906
+ kind: "invalid",
13907
+ error: `approvalMode.timestamp must be a positive number, got ${JSON.stringify(obj.timestamp)}`
13908
+ };
13909
+ }
13910
+ if (mode !== "bypassPermissions") {
13911
+ return { kind: "ok", mode };
13912
+ }
13913
+ const age = now - obj.timestamp;
13914
+ if (age > obj.ttlMs) {
13915
+ return { kind: "expired", mode: "default", agedMs: age };
13916
+ }
13917
+ const remainingMs = obj.ttlMs - age;
13918
+ return { kind: "ok", mode: "bypassPermissions", ttl: { remainingMs } };
13919
+ }
13920
+
13921
+ // src/state/initSession.ts
12396
13922
  async function initSession(ctx) {
12397
13923
  try {
12398
13924
  const { settings } = await loadSettings(
@@ -12406,38 +13932,40 @@ async function initSession(ctx) {
12406
13932
  ctx.setShellEscapeEnabled(settings.shellEscapeEnabled !== false);
12407
13933
  }
12408
13934
  try {
12409
- const { homedir: homedir15 } = await import("os");
12410
- const { join: join27 } = await import("path");
12411
- const userSettingsPath = join27(homedir15(), ".msapling", "settings.json");
12412
- if (existsSync24(userSettingsPath)) {
13935
+ const { homedir: homedir17 } = await import("os");
13936
+ const { join: join31 } = await import("path");
13937
+ const userSettingsPath = join31(homedir17(), ".msapling", "settings.json");
13938
+ if (existsSync25(userSettingsPath)) {
12413
13939
  const userText = await readFile22(userSettingsPath, "utf8");
12414
- const userJson = JSON.parse(userText);
12415
- const validModes = ["default", "plan", "acceptEdits", "bypassPermissions"];
13940
+ let parsed;
13941
+ try {
13942
+ parsed = JSON.parse(userText);
13943
+ } catch (e) {
13944
+ ctx.addMessage("system", `\u26A0 ~/.msapling/settings.json is not valid JSON: ${e.message}. Using default mode.`);
13945
+ parsed = {};
13946
+ }
13947
+ const result = parseApprovalMode(parsed, Date.now());
12416
13948
  let modeToApply = "default";
12417
- const approvalMode = userJson.approvalMode;
12418
- if (typeof approvalMode === "object" && approvalMode !== null) {
12419
- const { mode, timestamp, ttlMs } = approvalMode;
12420
- if (mode && validModes.includes(mode)) {
12421
- if (mode === "bypassPermissions" && ttlMs && timestamp) {
12422
- const now = Date.now();
12423
- const age = now - timestamp;
12424
- if (age > ttlMs) {
12425
- ctx.addMessage("system", "\u26A0 bypassPermissions TTL expired. Reverting to default mode.");
12426
- modeToApply = "default";
13949
+ switch (result.kind) {
13950
+ case "absent":
13951
+ break;
13952
+ case "invalid":
13953
+ ctx.addMessage("system", `\u26A0 Persisted approvalMode invalid (${result.error}). Reverting to default mode.`);
13954
+ break;
13955
+ case "expired":
13956
+ ctx.addMessage("system", "\u26A0 bypassPermissions TTL expired. Reverting to default mode.");
13957
+ break;
13958
+ case "ok":
13959
+ modeToApply = result.mode;
13960
+ if (result.mode === "bypassPermissions") {
13961
+ if (result.ttl) {
13962
+ const mins = (result.ttl.remainingMs / 1e3 / 60).toFixed(1);
13963
+ ctx.addMessage("system", `\u26A0 bypassPermissions active (expires in ~${mins} min). Use /mode default to re-enable controls.`);
12427
13964
  } else {
12428
- modeToApply = mode;
12429
- const remaining = ttlMs - age;
12430
- ctx.addMessage("system", `\u26A0 bypassPermissions active (expires in ~${(remaining / 1e3 / 60).toFixed(1)} min). Use /mode default to re-enable controls.`);
13965
+ ctx.addMessage("system", "\u26A0 bypassPermissions active (no TTL). Use /mode default to re-enable controls.");
12431
13966
  }
12432
- } else {
12433
- modeToApply = mode;
12434
13967
  }
12435
- }
12436
- } else if (typeof approvalMode === "string" && validModes.includes(approvalMode)) {
12437
- modeToApply = approvalMode;
12438
- if (modeToApply === "bypassPermissions") {
12439
- ctx.addMessage("system", "\u26A0 bypassPermissions active (no TTL). Use /mode default to re-enable controls.");
12440
- }
13968
+ break;
12441
13969
  }
12442
13970
  ctx.setMode(modeToApply);
12443
13971
  }
@@ -12500,8 +14028,8 @@ var App = ({ compact: compact2 = false }) => {
12500
14028
  const storage = useRef(new StorageManager()).current;
12501
14029
  const client = useRef(new MSaplingClient()).current;
12502
14030
  const requestApproval = useCallback((request) => {
12503
- return new Promise((resolve17) => {
12504
- setPendingApproval({ request, resolve: resolve17 });
14031
+ return new Promise((resolve18) => {
14032
+ setPendingApproval({ request, resolve: resolve18 });
12505
14033
  });
12506
14034
  }, []);
12507
14035
  const agent = useRef(new Agent(client, process.cwd(), requestApproval)).current;
@@ -12703,21 +14231,69 @@ var App = ({ compact: compact2 = false }) => {
12703
14231
 
12704
14232
  // src/runtime/bootstrap.ts
12705
14233
  init_esm_shims();
14234
+ import { readFileSync as readFileSync3 } from "fs";
14235
+ import { fileURLToPath as fileURLToPath2 } from "url";
14236
+ import { dirname as dirname3, join as join30 } from "path";
14237
+ function readCliVersion2() {
14238
+ const here = dirname3(fileURLToPath2(import.meta.url));
14239
+ for (const rel of ["../package.json", "../../package.json"]) {
14240
+ try {
14241
+ const pkg = JSON.parse(readFileSync3(join30(here, rel), "utf8"));
14242
+ if (pkg.name && pkg.version) {
14243
+ return { name: pkg.name, version: pkg.version };
14244
+ }
14245
+ } catch {
14246
+ }
14247
+ }
14248
+ return { name: "@mtreeai/msapling-cli", version: "unknown" };
14249
+ }
14250
+ var CLI_PKG = readCliVersion2();
12706
14251
  function handleCliArgs(args2) {
12707
14252
  if (args2.includes("--version") || args2.includes("-v")) {
12708
- console.log("@msapling/cli 2.3.3");
14253
+ console.log(`${CLI_PKG.name} ${CLI_PKG.version}`);
12709
14254
  process.exit(0);
12710
14255
  }
12711
14256
  if (args2.includes("--help") || args2.includes("-h")) {
12712
14257
  console.log("msapling \u2014 MSapling CLI (React/Ink)");
12713
14258
  console.log("Usage: msapling start interactive REPL");
12714
14259
  console.log(" msapling --compact start REPL in compact mode (no footer, thin separators)");
14260
+ console.log(' msapling --exec "<cmd>" run one slash command non-interactively and exit');
12715
14261
  console.log(" msapling mcp serve run as MCP stdio server (Claude Code / Cursor / Windsurf integration)");
14262
+ console.log(" msapling doctor run diagnostic health checks");
14263
+ console.log(" msapling doctor --debug run doctor with full environment dump");
12716
14264
  console.log(" msapling --version print version and exit");
12717
14265
  console.log(" msapling --help print this message");
12718
14266
  console.log("Inside the REPL: type /help for slash-command help.");
12719
14267
  process.exit(0);
12720
14268
  }
14269
+ const execIdx = args2.findIndex((a) => a === "--exec" || a.startsWith("--exec="));
14270
+ if (execIdx >= 0) {
14271
+ const eqArg = args2[execIdx];
14272
+ const cmdStr = eqArg.startsWith("--exec=") ? eqArg.slice("--exec=".length) : args2[execIdx + 1] ?? "";
14273
+ (async () => {
14274
+ const { runExec: runExec2 } = await Promise.resolve().then(() => (init_exec(), exec_exports));
14275
+ const exitCode = await runExec2(cmdStr);
14276
+ process.exit(exitCode);
14277
+ })().catch((e) => {
14278
+ process.stderr.write(`[msapling-exec] fatal: ${e?.message ?? e}
14279
+ `);
14280
+ process.exit(1);
14281
+ });
14282
+ return false;
14283
+ }
14284
+ if (args2[0] === "doctor") {
14285
+ (async () => {
14286
+ const { runDoctor: runDoctor2 } = await Promise.resolve().then(() => (init_doctor2(), doctor_exports));
14287
+ const debug = args2.includes("--debug");
14288
+ const result = await runDoctor2(debug);
14289
+ process.exit(result.exitCode);
14290
+ })().catch((e) => {
14291
+ process.stderr.write(`[msapling-doctor] fatal: ${e?.message ?? e}
14292
+ `);
14293
+ process.exit(1);
14294
+ });
14295
+ return false;
14296
+ }
12721
14297
  if (args2[0] === "mcp" && args2[1] === "serve") {
12722
14298
  (async () => {
12723
14299
  const { MSaplingClient: MSaplingClient2 } = await Promise.resolve().then(() => (init_src(), src_exports));