@mtreeai/msapling-cli 2.3.6-beta.48 → 2.3.6-beta.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +655 -115
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -5397,17 +5397,17 @@ async function backupFile(absPath) {
5397
5397
  try {
5398
5398
  const { readFile: readFile30, writeFile: writeFile19, mkdir: mkdir10 } = await import("fs/promises");
5399
5399
  const { homedir: homedir25 } = await import("os");
5400
- const { join: join41 } = await import("path");
5400
+ const { join: join42 } = await import("path");
5401
5401
  const filename = absPath.split(/[\\/]/).pop() ?? "file";
5402
5402
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
5403
5403
  const suffix = randomBytes7(4).toString("hex");
5404
- const backupPath = join41(
5404
+ const backupPath = join42(
5405
5405
  homedir25(),
5406
5406
  ".msapling",
5407
5407
  "backups",
5408
5408
  `${filename}.backup-${stamp}-${suffix}.bak`
5409
5409
  );
5410
- await mkdir10(join41(homedir25(), ".msapling", "backups"), { recursive: true });
5410
+ await mkdir10(join42(homedir25(), ".msapling", "backups"), { recursive: true });
5411
5411
  const content = await readFile30(absPath, "utf8");
5412
5412
  await writeFile19(backupPath, content, "utf8");
5413
5413
  return backupPath;
@@ -6017,10 +6017,10 @@ var init_Sandbox = __esm({
6017
6017
  * to a minimal platform default so subprocess spawn never sees an empty PATH.
6018
6018
  */
6019
6019
  static curatePath(rawPath) {
6020
- const sep4 = process.platform === "win32" ? ";" : ":";
6021
- const entries = (rawPath ?? "").split(sep4);
6020
+ const sep5 = process.platform === "win32" ? ";" : ":";
6021
+ const entries = (rawPath ?? "").split(sep5);
6022
6022
  const kept = entries.filter((e) => _Sandbox.isSafePathEntry(e));
6023
- if (kept.length > 0) return kept.join(sep4);
6023
+ if (kept.length > 0) return kept.join(sep5);
6024
6024
  return process.platform === "win32" ? "C:\\Windows\\System32;C:\\Windows" : "/usr/local/bin:/usr/bin:/bin";
6025
6025
  }
6026
6026
  getRestrictedEnv(pathOverride) {
@@ -6594,6 +6594,124 @@ var init_ResourceGovernor = __esm({
6594
6594
  }
6595
6595
  });
6596
6596
 
6597
+ // ../core/src/SessionStats.ts
6598
+ function getSessionStats() {
6599
+ if (!singleton) singleton = new SessionStats();
6600
+ return singleton;
6601
+ }
6602
+ function _resetSessionStatsSingleton() {
6603
+ singleton = null;
6604
+ }
6605
+ function fmtDuration(ms) {
6606
+ const s = Math.floor(ms / 1e3);
6607
+ const h = Math.floor(s / 3600);
6608
+ const m = Math.floor(s % 3600 / 60);
6609
+ const sec = s % 60;
6610
+ const parts = [];
6611
+ if (h) parts.push(`${h}h`);
6612
+ if (m || h) parts.push(`${m}m`);
6613
+ parts.push(`${sec}s`);
6614
+ return parts.join(" ");
6615
+ }
6616
+ function renderSessionStats(snap) {
6617
+ const lines = [];
6618
+ lines.push("=== Session Stats ===");
6619
+ lines.push(` Duration: ${fmtDuration(snap.wallMs)}`);
6620
+ const toolNames = Object.keys(snap.tools).sort();
6621
+ lines.push("");
6622
+ lines.push(` Tool calls: ${snap.totalToolCalls}`);
6623
+ if (toolNames.length === 0) {
6624
+ lines.push(" (no tools invoked yet)");
6625
+ } else {
6626
+ for (const name of toolNames) {
6627
+ const t = snap.tools[name];
6628
+ const avg = t.count > 0 ? t.totalMs / t.count : 0;
6629
+ const errStr = t.errors > 0 ? `, ${t.errors} err` : "";
6630
+ lines.push(
6631
+ ` - ${name}: ${t.count}\xD7 (avg ${avg.toFixed(0)}ms, total ${t.totalMs.toFixed(0)}ms${errStr})`
6632
+ );
6633
+ }
6634
+ }
6635
+ const a = snap.approvals;
6636
+ if (a.approved || a.denied || a.autoApproved) {
6637
+ lines.push("");
6638
+ lines.push(" Approvals:");
6639
+ lines.push(` - approved: ${a.approved}`);
6640
+ lines.push(` - denied: ${a.denied}`);
6641
+ lines.push(` - auto (trusted):${a.autoApproved}`);
6642
+ }
6643
+ const u = snap.usage;
6644
+ const totalTokens = u.promptTokens + u.completionTokens;
6645
+ lines.push("");
6646
+ lines.push(" Usage:");
6647
+ lines.push(` - turns: ${u.turns}`);
6648
+ lines.push(` - prompt tokens: ${u.promptTokens.toLocaleString()}`);
6649
+ lines.push(` - output tokens: ${u.completionTokens.toLocaleString()}`);
6650
+ lines.push(` - total tokens: ${totalTokens.toLocaleString()}`);
6651
+ lines.push(` - cost (USD): $${u.costUsd.toFixed(4)}`);
6652
+ if (u.turns > 0) {
6653
+ lines.push(` - avg cost/turn: $${(u.costUsd / u.turns).toFixed(4)}`);
6654
+ }
6655
+ return lines;
6656
+ }
6657
+ var SessionStats, singleton;
6658
+ var init_SessionStats = __esm({
6659
+ "../core/src/SessionStats.ts"() {
6660
+ "use strict";
6661
+ init_esm_shims();
6662
+ SessionStats = class {
6663
+ startedAt = Date.now();
6664
+ tools = /* @__PURE__ */ new Map();
6665
+ usage = { costUsd: 0, promptTokens: 0, completionTokens: 0, turns: 0 };
6666
+ approvals = { approved: 0, denied: 0, autoApproved: 0 };
6667
+ /** Record a single tool invocation's outcome + latency. */
6668
+ recordTool(name, latencyMs, isError) {
6669
+ const cur = this.tools.get(name) ?? { count: 0, errors: 0, totalMs: 0 };
6670
+ cur.count += 1;
6671
+ if (isError) cur.errors += 1;
6672
+ cur.totalMs += Math.max(0, latencyMs);
6673
+ this.tools.set(name, cur);
6674
+ }
6675
+ /** Record an approval-gate outcome for a tool call. */
6676
+ recordApproval(kind) {
6677
+ this.approvals[kind] += 1;
6678
+ }
6679
+ /** Record backend usage from a completed agent turn. */
6680
+ recordUsage(costUsd, promptTokens, completionTokens) {
6681
+ if (Number.isFinite(costUsd)) this.usage.costUsd += costUsd;
6682
+ if (Number.isFinite(promptTokens)) this.usage.promptTokens += promptTokens;
6683
+ if (Number.isFinite(completionTokens)) this.usage.completionTokens += completionTokens;
6684
+ this.usage.turns += 1;
6685
+ }
6686
+ /** Immutable snapshot for rendering. */
6687
+ snapshot() {
6688
+ const tools = {};
6689
+ let totalToolCalls = 0;
6690
+ for (const [name, s] of this.tools) {
6691
+ tools[name] = { ...s };
6692
+ totalToolCalls += s.count;
6693
+ }
6694
+ return {
6695
+ startedAt: this.startedAt,
6696
+ wallMs: Date.now() - this.startedAt,
6697
+ tools,
6698
+ totalToolCalls,
6699
+ usage: { ...this.usage },
6700
+ approvals: { ...this.approvals }
6701
+ };
6702
+ }
6703
+ /** Reset everything (used by tests and a potential /stats reset). */
6704
+ reset() {
6705
+ this.startedAt = Date.now();
6706
+ this.tools.clear();
6707
+ this.usage = { costUsd: 0, promptTokens: 0, completionTokens: 0, turns: 0 };
6708
+ this.approvals = { approved: 0, denied: 0, autoApproved: 0 };
6709
+ }
6710
+ };
6711
+ singleton = null;
6712
+ }
6713
+ });
6714
+
6597
6715
  // ../core/src/agent/ToolExecutor.ts
6598
6716
  import { readFile as readFile10 } from "fs/promises";
6599
6717
  var APPROVAL_GATED, ToolExecutor;
@@ -6628,6 +6746,7 @@ var init_ToolExecutor = __esm({
6628
6746
  init_ShadowService();
6629
6747
  init_Hooks();
6630
6748
  init_ResourceGovernor();
6749
+ init_SessionStats();
6631
6750
  APPROVAL_GATED = /* @__PURE__ */ new Set(["run_command", "bash_command", "bash_background", "edit_file", "write_file", "patch_file", "multi_edit_file", "notebook_edit_cell", "move_file", "delete_file", "open_sub_shell"]);
6632
6751
  ToolExecutor = class {
6633
6752
  tools = /* @__PURE__ */ new Map();
@@ -6941,8 +7060,10 @@ ${blocker.stderr || "(empty)"}`,
6941
7060
  reason: `${toolName} requested by agent in '${this.mode}' mode`
6942
7061
  });
6943
7062
  if (decision === "no") {
7063
+ getSessionStats().recordApproval("denied");
6944
7064
  return { content: `User denied ${toolName} approval.`, isError: true };
6945
7065
  }
7066
+ getSessionStats().recordApproval("approved");
6946
7067
  if (decision === "always") {
6947
7068
  if (this.trustStore) {
6948
7069
  this.trustStore.add(cmdKey).catch(() => {
@@ -6951,6 +7072,8 @@ ${blocker.stderr || "(empty)"}`,
6951
7072
  this.sessionTrust.add(cmdKey);
6952
7073
  }
6953
7074
  }
7075
+ } else {
7076
+ getSessionStats().recordApproval("autoApproved");
6954
7077
  }
6955
7078
  }
6956
7079
  if (toolName === "run_command" || toolName === "bash_command" || toolName === "bash_background" || toolName === "edit_file" || toolName === "write_file" || toolName === "patch_file" || toolName === "multi_edit_file" || toolName === "notebook_edit_cell" || toolName === "move_file" || toolName === "delete_file") {
@@ -6982,10 +7105,20 @@ Please approve the diff in the UI to sync this change locally.`
6982
7105
  const governor = await getGlobalGovernor();
6983
7106
  await governor.acquireTool();
6984
7107
  let result;
7108
+ const startedAt = Date.now();
7109
+ let threw = false;
6985
7110
  try {
6986
7111
  result = await tool.execute(args2, projectRoot);
7112
+ } catch (e) {
7113
+ threw = true;
7114
+ throw e;
6987
7115
  } finally {
6988
7116
  governor.releaseTool();
7117
+ getSessionStats().recordTool(
7118
+ toolName,
7119
+ Date.now() - startedAt,
7120
+ threw || !!(result && result.isError)
7121
+ );
6989
7122
  }
6990
7123
  if (this.hooks) {
6991
7124
  this.hooks.fire({
@@ -7194,13 +7327,14 @@ async function loadProjectConfig(cwd = process.cwd()) {
7194
7327
  const [user, project] = await Promise.all([findUserConfig(), findProjectConfig(cwd)]);
7195
7328
  return { user, project, combined: buildCombined(user, project) };
7196
7329
  }
7197
- var FILENAMES, TRUNCATE_AT;
7330
+ var FILENAMES, TRUNCATE_AT, PROJECT_CONFIG_FILENAMES;
7198
7331
  var init_ProjectConfig = __esm({
7199
7332
  "../core/src/ProjectConfig.ts"() {
7200
7333
  "use strict";
7201
7334
  init_esm_shims();
7202
7335
  FILENAMES = ["MSAPLING.md", "CLAUDE.md", "GEMINI.md", "AGENTS.md"];
7203
7336
  TRUNCATE_AT = 32e3;
7337
+ PROJECT_CONFIG_FILENAMES = FILENAMES;
7204
7338
  }
7205
7339
  });
7206
7340
 
@@ -9217,8 +9351,10 @@ __export(src_exports2, {
9217
9351
  MultiEditFileTool: () => MultiEditFileTool,
9218
9352
  NotebookEditTool: () => NotebookEditTool,
9219
9353
  NotebookReadTool: () => NotebookReadTool,
9354
+ PROJECT_CONFIG_FILENAMES: () => PROJECT_CONFIG_FILENAMES,
9220
9355
  PatchFileTool: () => PatchFileTool,
9221
9356
  ReadBackgroundShellTool: () => ReadBackgroundShellTool,
9357
+ SessionStats: () => SessionStats,
9222
9358
  StorageManager: () => StorageManager,
9223
9359
  SwarmManager: () => SwarmManager,
9224
9360
  TOOL_NAME_ALIASES: () => TOOL_NAME_ALIASES,
@@ -9230,6 +9366,7 @@ __export(src_exports2, {
9230
9366
  WebFetchTool: () => WebFetchTool,
9231
9367
  WebSearchTool: () => WebSearchTool,
9232
9368
  WriteFileTool: () => WriteFileTool,
9369
+ _resetSessionStatsSingleton: () => _resetSessionStatsSingleton,
9233
9370
  _setBackupDirOverride: () => _setBackupDirOverride,
9234
9371
  backupDir: () => backupDir,
9235
9372
  buildCompactionPrompt: () => buildCompactionPrompt,
@@ -9243,6 +9380,7 @@ __export(src_exports2, {
9243
9380
  formatNotebookHeader: () => formatNotebookHeader,
9244
9381
  formatTodos: () => formatTodos,
9245
9382
  getOrCreateJournalKey: () => getOrCreateJournalKey,
9383
+ getSessionStats: () => getSessionStats,
9246
9384
  initJournalEncryption: () => initJournalEncryption,
9247
9385
  listCheckpoints: () => listCheckpoints,
9248
9386
  loadNamedAgents: () => loadNamedAgents,
@@ -9254,6 +9392,7 @@ __export(src_exports2, {
9254
9392
  parseAgentFile: () => parseAgentFile,
9255
9393
  parseToolsValue: () => parseToolsValue,
9256
9394
  recordBackup: () => recordBackup,
9395
+ renderSessionStats: () => renderSessionStats,
9257
9396
  restoreCheckpoint: () => restoreCheckpoint,
9258
9397
  takeSnapshot: () => takeSnapshot
9259
9398
  });
@@ -9272,6 +9411,7 @@ var init_src3 = __esm({
9272
9411
  init_Storage();
9273
9412
  init_journalCrypto();
9274
9413
  init_Mutex();
9414
+ init_SessionStats();
9275
9415
  init_ProjectConfig();
9276
9416
  init_Settings();
9277
9417
  init_client();
@@ -11347,40 +11487,405 @@ var init_compact = __esm({
11347
11487
  }
11348
11488
  });
11349
11489
 
11490
+ // src/runtime/codebaseScan.ts
11491
+ import { existsSync as existsSync23, readFileSync as readFileSync4, readdirSync as readdirSync3, statSync as statSync7 } from "fs";
11492
+ import { join as join27, relative as relative16, sep as sep4, basename as basename3 } from "path";
11493
+ function parseGitignore(content) {
11494
+ const patterns = content.split(/\r?\n/).map((l) => l.trim()).filter((l) => l && !l.startsWith("#") && !l.startsWith("!"));
11495
+ const matchers = patterns.map((raw) => {
11496
+ let p = raw;
11497
+ const dirOnly = p.endsWith("/");
11498
+ if (dirOnly) p = p.slice(0, -1);
11499
+ const anchored = p.startsWith("/");
11500
+ if (anchored) p = p.slice(1);
11501
+ const re = new RegExp(
11502
+ "^" + p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "\0").replace(/\*/g, "[^/]*").replace(//g, ".*").replace(/\?/g, "[^/]") + "$"
11503
+ );
11504
+ return { re, dirOnly, anchored, base: p };
11505
+ });
11506
+ return (relPath, isDir) => {
11507
+ const posix = relPath.split(sep4).join("/");
11508
+ for (const m of matchers) {
11509
+ if (m.dirOnly && !isDir) continue;
11510
+ if (m.anchored) {
11511
+ if (m.re.test(posix)) return true;
11512
+ } else {
11513
+ if (m.re.test(posix)) return true;
11514
+ if (m.re.test(basename3(posix))) return true;
11515
+ const segs = posix.split("/");
11516
+ for (let i = 0; i < segs.length; i++) {
11517
+ if (m.re.test(segs[i])) return true;
11518
+ }
11519
+ }
11520
+ }
11521
+ return false;
11522
+ };
11523
+ }
11524
+ function loadIgnore(root) {
11525
+ try {
11526
+ const gi = join27(root, ".gitignore");
11527
+ if (existsSync23(gi)) {
11528
+ return parseGitignore(readFileSync4(gi, "utf8"));
11529
+ }
11530
+ } catch {
11531
+ }
11532
+ return () => false;
11533
+ }
11534
+ function readJsonSafe(path2) {
11535
+ try {
11536
+ return JSON.parse(readFileSync4(path2, "utf8"));
11537
+ } catch {
11538
+ return null;
11539
+ }
11540
+ }
11541
+ function detectStacks(root) {
11542
+ const found = [];
11543
+ const seen = /* @__PURE__ */ new Set();
11544
+ for (const probe of STACK_PROBES) {
11545
+ const hit = probe.detect(root);
11546
+ if (hit && !seen.has(hit.name)) {
11547
+ seen.add(hit.name);
11548
+ found.push(hit);
11549
+ }
11550
+ }
11551
+ return found;
11552
+ }
11553
+ function inferCommands(root, stacks) {
11554
+ const cmds = { extra: {} };
11555
+ for (const stack of stacks) {
11556
+ if (stack.manifest === "package.json") {
11557
+ const pkg = readJsonSafe(join27(root, "package.json"));
11558
+ const scripts = pkg?.scripts ?? {};
11559
+ const pm = stack.packageManager ?? "npm";
11560
+ const runScript = (name) => pm === "npm" ? `npm run ${name}` : `${pm} run ${name}`;
11561
+ if (scripts.build) cmds.build = runScript("build");
11562
+ if (scripts.test) cmds.test = pm === "npm" ? "npm test" : `${pm} test`;
11563
+ if (scripts.start) cmds.run = pm === "npm" ? "npm start" : `${pm} start`;
11564
+ else if (scripts.dev) cmds.run = runScript("dev");
11565
+ if (scripts.lint) cmds.lint = runScript("lint");
11566
+ for (const name of ["typecheck", "format", "e2e", "coverage"]) {
11567
+ if (scripts[name]) cmds.extra[name] = runScript(name);
11568
+ }
11569
+ } else if (stack.name === "Python") {
11570
+ cmds.test = cmds.test ?? "pytest";
11571
+ if (stack.packageManager === "poetry") cmds.run = cmds.run ?? "poetry run python -m <module>";
11572
+ } else if (stack.name === "Go") {
11573
+ cmds.build = cmds.build ?? "go build ./...";
11574
+ cmds.test = cmds.test ?? "go test ./...";
11575
+ cmds.run = cmds.run ?? "go run .";
11576
+ } else if (stack.name === "Rust") {
11577
+ cmds.build = cmds.build ?? "cargo build";
11578
+ cmds.test = cmds.test ?? "cargo test";
11579
+ cmds.run = cmds.run ?? "cargo run";
11580
+ } else if (stack.name === "Java / Maven") {
11581
+ cmds.build = cmds.build ?? "mvn package";
11582
+ cmds.test = cmds.test ?? "mvn test";
11583
+ } else if (stack.name === "Java / Kotlin (Gradle)") {
11584
+ cmds.build = cmds.build ?? "gradle build";
11585
+ cmds.test = cmds.test ?? "gradle test";
11586
+ } else if (stack.name === "Ruby") {
11587
+ cmds.test = cmds.test ?? "bundle exec rspec";
11588
+ } else if (stack.name === "PHP") {
11589
+ cmds.test = cmds.test ?? "composer test";
11590
+ }
11591
+ }
11592
+ return cmds;
11593
+ }
11594
+ function detectEntryPoints(root, stacks) {
11595
+ const out = [];
11596
+ for (const stack of stacks) {
11597
+ if (stack.manifest === "package.json") {
11598
+ const pkg = readJsonSafe(join27(root, "package.json"));
11599
+ if (pkg?.main && existsSync23(join27(root, pkg.main))) out.push(pkg.main);
11600
+ if (typeof pkg?.bin === "string" && existsSync23(join27(root, pkg.bin))) out.push(pkg.bin);
11601
+ else if (pkg?.bin && typeof pkg.bin === "object") {
11602
+ for (const v of Object.values(pkg.bin)) {
11603
+ if (typeof v === "string" && existsSync23(join27(root, v))) out.push(v);
11604
+ }
11605
+ }
11606
+ }
11607
+ }
11608
+ for (const c of ENTRY_CANDIDATES) {
11609
+ if (existsSync23(join27(root, c)) && !out.includes(c)) out.push(c);
11610
+ }
11611
+ return out.slice(0, 8);
11612
+ }
11613
+ function scanCodebase(root, opts = {}) {
11614
+ const maxFiles = opts.maxFiles ?? DEFAULT_MAX_FILES;
11615
+ const maxDepth = opts.maxDepth ?? DEFAULT_MAX_DEPTH;
11616
+ const ignored = loadIgnore(root);
11617
+ let fileCount = 0;
11618
+ let dirCount = 0;
11619
+ let truncated = false;
11620
+ const keyDirs = /* @__PURE__ */ new Set();
11621
+ const walk = (dir, depth) => {
11622
+ if (truncated || depth > maxDepth) return;
11623
+ let entries;
11624
+ try {
11625
+ entries = readdirSync3(dir);
11626
+ } catch {
11627
+ return;
11628
+ }
11629
+ for (const name of entries) {
11630
+ if (fileCount >= maxFiles) {
11631
+ truncated = true;
11632
+ return;
11633
+ }
11634
+ const abs = join27(dir, name);
11635
+ let isDir = false;
11636
+ try {
11637
+ isDir = statSync7(abs).isDirectory();
11638
+ } catch {
11639
+ continue;
11640
+ }
11641
+ const rel = relative16(root, abs);
11642
+ if (isDir) {
11643
+ if (ALWAYS_IGNORE_DIRS.has(name)) continue;
11644
+ if (name.startsWith(".") && depth >= 1) continue;
11645
+ if (ignored(rel, true)) continue;
11646
+ dirCount++;
11647
+ if (depth === 0) keyDirs.add(rel.split(sep4).join("/"));
11648
+ walk(abs, depth + 1);
11649
+ } else {
11650
+ if (ignored(rel, false)) continue;
11651
+ fileCount++;
11652
+ }
11653
+ }
11654
+ };
11655
+ walk(root, 0);
11656
+ const stacks = detectStacks(root);
11657
+ const commands2 = inferCommands(root, stacks);
11658
+ const entryPoints = detectEntryPoints(root, stacks);
11659
+ return {
11660
+ root,
11661
+ stacks,
11662
+ keyDirs: Array.from(keyDirs).sort(),
11663
+ entryPoints,
11664
+ commands: commands2,
11665
+ fileCount,
11666
+ dirCount,
11667
+ truncated
11668
+ };
11669
+ }
11670
+ function renderMemoryFile(scan, title = "Project Memory") {
11671
+ const lines = [];
11672
+ lines.push(`# ${title}`);
11673
+ lines.push("");
11674
+ lines.push(
11675
+ "> Auto-generated by `/init` (deep codebase scan). Edit freely \u2014 re-running `/init` refreshes the scanned sections but preserves this file if you have customized it."
11676
+ );
11677
+ lines.push("");
11678
+ lines.push("## Stack");
11679
+ if (scan.stacks.length === 0) {
11680
+ lines.push("- No recognized manifest found (no package.json / pyproject.toml / go.mod / Cargo.toml / etc.).");
11681
+ } else {
11682
+ for (const s of scan.stacks) {
11683
+ const pm = s.packageManager ? ` (${s.packageManager})` : "";
11684
+ lines.push(`- **${s.name}**${pm} \u2014 detected via \`${s.manifest}\``);
11685
+ }
11686
+ }
11687
+ lines.push("");
11688
+ lines.push("## Commands");
11689
+ const c = scan.commands;
11690
+ const cmdRows = [
11691
+ ["Build", c.build],
11692
+ ["Test", c.test],
11693
+ ["Run", c.run],
11694
+ ["Lint", c.lint]
11695
+ ];
11696
+ const anyCmd = cmdRows.some(([, v]) => v) || Object.keys(c.extra).length > 0;
11697
+ if (!anyCmd) {
11698
+ lines.push("- No build/test/run commands could be inferred from the manifests.");
11699
+ } else {
11700
+ for (const [label, val] of cmdRows) {
11701
+ if (val) lines.push(`- **${label}:** \`${val}\``);
11702
+ }
11703
+ for (const [name, val] of Object.entries(c.extra)) {
11704
+ lines.push(`- **${name}:** \`${val}\``);
11705
+ }
11706
+ }
11707
+ lines.push("");
11708
+ lines.push("## Structure");
11709
+ lines.push(
11710
+ `- ${scan.dirCount} directories, ${scan.fileCount} files scanned${scan.truncated ? " (scan capped \u2014 large repo)" : ""}.`
11711
+ );
11712
+ if (scan.keyDirs.length > 0) {
11713
+ lines.push("- Key directories:");
11714
+ for (const d of scan.keyDirs) lines.push(` - \`${d}/\``);
11715
+ }
11716
+ lines.push("");
11717
+ lines.push("## Entry points");
11718
+ if (scan.entryPoints.length === 0) {
11719
+ lines.push("- No obvious entry point detected.");
11720
+ } else {
11721
+ for (const e of scan.entryPoints) lines.push(`- \`${e}\``);
11722
+ }
11723
+ lines.push("");
11724
+ lines.push("## Guidelines");
11725
+ lines.push("- Follow the existing code style and conventions in this repository.");
11726
+ lines.push("- Run the test command above before considering a change complete.");
11727
+ lines.push("");
11728
+ return lines.join("\n");
11729
+ }
11730
+ var DEFAULT_MAX_FILES, DEFAULT_MAX_DEPTH, ALWAYS_IGNORE_DIRS, STACK_PROBES, ENTRY_CANDIDATES;
11731
+ var init_codebaseScan = __esm({
11732
+ "src/runtime/codebaseScan.ts"() {
11733
+ "use strict";
11734
+ init_esm_shims();
11735
+ DEFAULT_MAX_FILES = 5e3;
11736
+ DEFAULT_MAX_DEPTH = 6;
11737
+ ALWAYS_IGNORE_DIRS = /* @__PURE__ */ new Set([
11738
+ "node_modules",
11739
+ ".git",
11740
+ ".hg",
11741
+ ".svn",
11742
+ "dist",
11743
+ "build",
11744
+ "out",
11745
+ "target",
11746
+ ".next",
11747
+ ".nuxt",
11748
+ ".cache",
11749
+ "coverage",
11750
+ ".venv",
11751
+ "venv",
11752
+ "__pycache__",
11753
+ ".idea",
11754
+ ".vscode",
11755
+ "vendor",
11756
+ ".bun",
11757
+ ".turbo"
11758
+ ]);
11759
+ STACK_PROBES = [
11760
+ {
11761
+ manifest: "package.json",
11762
+ detect: (root) => {
11763
+ const path2 = join27(root, "package.json");
11764
+ if (!existsSync23(path2)) return null;
11765
+ const pkg = readJsonSafe(path2);
11766
+ let pm = "npm";
11767
+ if (existsSync23(join27(root, "bun.lock")) || existsSync23(join27(root, "bun.lockb"))) pm = "bun";
11768
+ else if (existsSync23(join27(root, "pnpm-lock.yaml"))) pm = "pnpm";
11769
+ else if (existsSync23(join27(root, "yarn.lock"))) pm = "yarn";
11770
+ const hasTs = existsSync23(join27(root, "tsconfig.json")) || !!(pkg?.devDependencies?.typescript || pkg?.dependencies?.typescript);
11771
+ return {
11772
+ name: hasTs ? "Node.js / TypeScript" : "Node.js / JavaScript",
11773
+ manifest: "package.json",
11774
+ packageManager: pm
11775
+ };
11776
+ }
11777
+ },
11778
+ {
11779
+ manifest: "pyproject.toml",
11780
+ detect: (root) => {
11781
+ if (!existsSync23(join27(root, "pyproject.toml"))) return null;
11782
+ let pm = "pip";
11783
+ if (existsSync23(join27(root, "poetry.lock"))) pm = "poetry";
11784
+ else if (existsSync23(join27(root, "uv.lock"))) pm = "uv";
11785
+ return { name: "Python", manifest: "pyproject.toml", packageManager: pm };
11786
+ }
11787
+ },
11788
+ {
11789
+ manifest: "requirements.txt",
11790
+ detect: (root) => {
11791
+ if (!existsSync23(join27(root, "requirements.txt"))) return null;
11792
+ return { name: "Python", manifest: "requirements.txt", packageManager: "pip" };
11793
+ }
11794
+ },
11795
+ {
11796
+ manifest: "go.mod",
11797
+ detect: (root) => existsSync23(join27(root, "go.mod")) ? { name: "Go", manifest: "go.mod", packageManager: "go" } : null
11798
+ },
11799
+ {
11800
+ manifest: "Cargo.toml",
11801
+ detect: (root) => existsSync23(join27(root, "Cargo.toml")) ? { name: "Rust", manifest: "Cargo.toml", packageManager: "cargo" } : null
11802
+ },
11803
+ {
11804
+ manifest: "pom.xml",
11805
+ detect: (root) => existsSync23(join27(root, "pom.xml")) ? { name: "Java / Maven", manifest: "pom.xml", packageManager: "maven" } : null
11806
+ },
11807
+ {
11808
+ manifest: "build.gradle",
11809
+ detect: (root) => existsSync23(join27(root, "build.gradle")) || existsSync23(join27(root, "build.gradle.kts")) ? { name: "Java / Kotlin (Gradle)", manifest: "build.gradle", packageManager: "gradle" } : null
11810
+ },
11811
+ {
11812
+ manifest: "Gemfile",
11813
+ detect: (root) => existsSync23(join27(root, "Gemfile")) ? { name: "Ruby", manifest: "Gemfile", packageManager: "bundler" } : null
11814
+ },
11815
+ {
11816
+ manifest: "composer.json",
11817
+ detect: (root) => existsSync23(join27(root, "composer.json")) ? { name: "PHP", manifest: "composer.json", packageManager: "composer" } : null
11818
+ }
11819
+ ];
11820
+ ENTRY_CANDIDATES = [
11821
+ "src/index.ts",
11822
+ "src/index.tsx",
11823
+ "src/index.js",
11824
+ "src/main.ts",
11825
+ "src/main.tsx",
11826
+ "src/main.py",
11827
+ "src/main.rs",
11828
+ "index.ts",
11829
+ "index.js",
11830
+ "main.py",
11831
+ "main.go",
11832
+ "app.py",
11833
+ "manage.py",
11834
+ "cmd/main.go",
11835
+ "src/main/java"
11836
+ ];
11837
+ }
11838
+ });
11839
+
11350
11840
  // src/commands/init.ts
11351
- import { join as join27 } from "path";
11352
- import { existsSync as existsSync23 } from "fs";
11841
+ import { join as join28 } from "path";
11842
+ import { existsSync as existsSync24 } from "fs";
11353
11843
  import { writeFile as writeFile13 } from "fs/promises";
11844
+ function resolveTargetFile(cwd) {
11845
+ for (const filename2 of PROJECT_CONFIG_FILENAMES) {
11846
+ const path2 = join28(cwd, filename2);
11847
+ if (existsSync24(path2)) return { path: path2, filename: filename2, existed: true };
11848
+ }
11849
+ const filename = PROJECT_CONFIG_FILENAMES[0];
11850
+ return { path: join28(cwd, filename), filename, existed: false };
11851
+ }
11354
11852
  var initCommand;
11355
11853
  var init_init = __esm({
11356
11854
  "src/commands/init.ts"() {
11357
11855
  "use strict";
11358
11856
  init_esm_shims();
11857
+ init_src3();
11858
+ init_codebaseScan();
11359
11859
  initCommand = {
11360
11860
  name: "init",
11361
- description: "Create MSAPLING.md (or CLAUDE.md-compat) in cwd with project-detected boilerplate",
11861
+ args: "[--force]",
11862
+ description: "Scan the codebase (stack, commands, structure, entry points) and write/refresh a project-memory file (MSAPLING.md/CLAUDE.md/\u2026)",
11362
11863
  category: "project",
11363
11864
  handler: async (args2, context) => {
11364
11865
  try {
11365
11866
  const cwd = process.cwd();
11366
- const path2 = join27(cwd, "MSAPLING.md");
11367
- if (existsSync23(path2)) {
11368
- context.addMessage("error", "MSAPLING.md already exists in current directory.");
11867
+ const force = args2.includes("--force");
11868
+ const target = resolveTargetFile(cwd);
11869
+ if (target.existed && !force) {
11870
+ context.addMessage(
11871
+ "error",
11872
+ `${target.filename} already exists. Re-run \`/init --force\` to refresh it from a fresh codebase scan.`
11873
+ );
11369
11874
  return;
11370
11875
  }
11371
- const content = `# MSapling Project Configuration
11372
-
11373
- ## Build
11374
- - \`npm run build\` or equivalent.
11375
-
11376
- ## Test
11377
- - \`npm test\` or equivalent.
11378
-
11379
- ## Guidelines
11380
- - Follow existing code style.
11381
- `;
11382
- await writeFile13(path2, content, "utf8");
11383
- context.addMessage("system", `Created MSAPLING.md at ${path2}`);
11876
+ context.addMessage("system", `Scanning codebase at ${cwd} ...`);
11877
+ const scan = scanCodebase(cwd);
11878
+ const title = target.filename === "MSAPLING.md" ? "MSapling Project Memory" : "Project Memory";
11879
+ const content = renderMemoryFile(scan, title);
11880
+ await writeFile13(target.path, content, "utf8");
11881
+ const stackSummary = scan.stacks.length > 0 ? scan.stacks.map((s) => s.name).join(", ") : "unknown stack";
11882
+ context.addMessage(
11883
+ "system",
11884
+ `${target.existed ? "Refreshed" : "Created"} ${target.filename} at ${target.path}
11885
+ Stack: ${stackSummary}
11886
+ Scanned: ${scan.dirCount} dirs, ${scan.fileCount} files${scan.truncated ? " (capped)" : ""}
11887
+ Entry points: ${scan.entryPoints.length > 0 ? scan.entryPoints.join(", ") : "none detected"}`
11888
+ );
11384
11889
  } catch (e) {
11385
11890
  context.addMessage("error", `Failed to initialize project: ${e.message}`);
11386
11891
  }
@@ -11390,7 +11895,7 @@ var init_init = __esm({
11390
11895
  });
11391
11896
 
11392
11897
  // src/commands/review.ts
11393
- import { existsSync as existsSync24 } from "fs";
11898
+ import { existsSync as existsSync25 } from "fs";
11394
11899
  import { readFile as readFile21 } from "fs/promises";
11395
11900
  var reviewCommand;
11396
11901
  var init_review = __esm({
@@ -11410,7 +11915,7 @@ var init_review = __esm({
11410
11915
  }
11411
11916
  let content = "";
11412
11917
  try {
11413
- if (existsSync24(target)) {
11918
+ if (existsSync25(target)) {
11414
11919
  content = await readFile21(target, "utf8");
11415
11920
  } else {
11416
11921
  content = `Review target: ${target}`;
@@ -11504,15 +12009,15 @@ var init_swarm = __esm({
11504
12009
 
11505
12010
  // src/commands/recipe.ts
11506
12011
  import { parse as parseYaml } from "yaml";
11507
- import { existsSync as existsSync25 } from "fs";
12012
+ import { existsSync as existsSync26 } from "fs";
11508
12013
  import { readFile as readFile22 } from "fs/promises";
11509
- import { join as join28 } from "path";
12014
+ import { join as join29 } from "path";
11510
12015
  function findRecipe(name, cwd) {
11511
12016
  for (const dir of RECIPE_DIRS) {
11512
12017
  for (const suffix of NAME_SUFFIXES) {
11513
12018
  for (const ext of FILE_EXTS) {
11514
- const p = join28(cwd, dir, `${name}${suffix}${ext}`);
11515
- if (existsSync25(p)) return p;
12019
+ const p = join29(cwd, dir, `${name}${suffix}${ext}`);
12020
+ if (existsSync26(p)) return p;
11516
12021
  }
11517
12022
  }
11518
12023
  }
@@ -11625,13 +12130,13 @@ ${rendered}` : rendered;
11625
12130
  });
11626
12131
 
11627
12132
  // src/commands/skill.ts
11628
- import { existsSync as existsSync26, readdirSync as readdirSync3, statSync as statSync7 } from "fs";
12133
+ import { existsSync as existsSync27, readdirSync as readdirSync4, statSync as statSync8 } from "fs";
11629
12134
  import { readFile as readFile23 } from "fs/promises";
11630
- import { join as join29, resolve as resolve16 } from "path";
12135
+ import { join as join30, resolve as resolve16 } from "path";
11631
12136
  function findSkillsRoot(cwd) {
11632
12137
  for (const candidate of SKILLS_DIRS) {
11633
12138
  const full = resolve16(cwd, candidate);
11634
- if (existsSync26(full) && statSync7(full).isDirectory()) return full;
12139
+ if (existsSync27(full) && statSync8(full).isDirectory()) return full;
11635
12140
  }
11636
12141
  return null;
11637
12142
  }
@@ -11639,28 +12144,28 @@ function listAllSkills(root) {
11639
12144
  const out = [];
11640
12145
  let domains;
11641
12146
  try {
11642
- domains = readdirSync3(root);
12147
+ domains = readdirSync4(root);
11643
12148
  } catch {
11644
12149
  return out;
11645
12150
  }
11646
12151
  for (const domain of domains) {
11647
- const dir = join29(root, domain);
12152
+ const dir = join30(root, domain);
11648
12153
  let s;
11649
12154
  try {
11650
- s = statSync7(dir);
12155
+ s = statSync8(dir);
11651
12156
  } catch {
11652
12157
  continue;
11653
12158
  }
11654
12159
  if (!s.isDirectory()) continue;
11655
12160
  let files;
11656
12161
  try {
11657
- files = readdirSync3(dir);
12162
+ files = readdirSync4(dir);
11658
12163
  } catch {
11659
12164
  continue;
11660
12165
  }
11661
12166
  for (const f of files) {
11662
12167
  if (!f.endsWith(".md")) continue;
11663
- out.push({ domain, name: f.slice(0, -3), path: join29(dir, f) });
12168
+ out.push({ domain, name: f.slice(0, -3), path: join30(dir, f) });
11664
12169
  }
11665
12170
  }
11666
12171
  return out.sort(
@@ -11750,7 +12255,7 @@ ${prompt4}`;
11750
12255
 
11751
12256
  // src/commands/benchmark.ts
11752
12257
  import { homedir as homedir16 } from "os";
11753
- import { join as join30 } from "path";
12258
+ import { join as join31 } from "path";
11754
12259
  import { mkdirSync as mkdirSync5 } from "fs";
11755
12260
  import * as fs2 from "fs";
11756
12261
  function parseArgs(args2) {
@@ -11767,7 +12272,7 @@ function parseArgs(args2) {
11767
12272
  }
11768
12273
  function formatTable(results) {
11769
12274
  const header = `${"Model".padEnd(40)} ${"TTFT(ms)".padStart(9)} ${"TPS".padStart(7)} ${"Tokens".padStart(8)} ${"Cost($)".padStart(9)}`;
11770
- const sep4 = "-".repeat(header.length);
12275
+ const sep5 = "-".repeat(header.length);
11771
12276
  const rows = results.map((r) => {
11772
12277
  const model = r.model.slice(0, 39).padEnd(40);
11773
12278
  const ttft = r.ttft_ms != null ? r.ttft_ms.toFixed(0).padStart(9) : " - ";
@@ -11777,7 +12282,7 @@ function formatTable(results) {
11777
12282
  const err = r.error ? ` \u26A0 ${r.error}` : "";
11778
12283
  return `${model} ${ttft} ${tps} ${tok} ${cost}${err}`;
11779
12284
  });
11780
- return [sep4, header, sep4, ...rows, sep4].join("\n");
12285
+ return [sep5, header, sep5, ...rows, sep5].join("\n");
11781
12286
  }
11782
12287
  var DEFAULT_PROMPTS, benchmarkCommand;
11783
12288
  var init_benchmark = __esm({
@@ -11870,10 +12375,10 @@ HW at start: ${hw.cores}-core ${hw.platform} | CPU ${hw.cpuPct}% | RAM ${hw.ramP
11870
12375
  `[HW at run time: CPU ${hwAtEnd.cpuPct}% / RAM ${hwAtEnd.ramPct}% | ${hw.ramGiB} GiB RAM, ${hw.cores} cores]`
11871
12376
  );
11872
12377
  try {
11873
- const dir = join30(homedir16(), ".msapling", "benchmarks");
12378
+ const dir = join31(homedir16(), ".msapling", "benchmarks");
11874
12379
  mkdirSync5(dir, { recursive: true });
11875
12380
  const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 16);
11876
- const file = join30(dir, `${ts}.json`);
12381
+ const file = join31(dir, `${ts}.json`);
11877
12382
  const run = {
11878
12383
  ts: (/* @__PURE__ */ new Date()).toISOString(),
11879
12384
  rounds,
@@ -12119,22 +12624,22 @@ var init_theme = __esm({
12119
12624
  });
12120
12625
 
12121
12626
  // src/commands/theme.ts
12122
- import { join as join31 } from "path";
12627
+ import { join as join32 } from "path";
12123
12628
  import { homedir as homedir17 } from "os";
12124
- import { existsSync as existsSync27 } from "fs";
12629
+ import { existsSync as existsSync28 } from "fs";
12125
12630
  import { readFile as readFile24, writeFile as writeFile14 } from "fs/promises";
12126
12631
  async function persistTheme(storage, themeName) {
12127
- const settingsPath = join31(homedir17(), ".msapling", "settings.json");
12632
+ const settingsPath = join32(homedir17(), ".msapling", "settings.json");
12128
12633
  let existing = {};
12129
12634
  try {
12130
- if (existsSync27(settingsPath)) {
12635
+ if (existsSync28(settingsPath)) {
12131
12636
  const text = await readFile24(settingsPath, "utf8");
12132
12637
  if (text.trim()) existing = JSON.parse(text);
12133
12638
  }
12134
12639
  } catch {
12135
12640
  }
12136
12641
  existing["theme"] = themeName;
12137
- ensureConfigDir(join31(homedir17(), ".msapling"));
12642
+ ensureConfigDir(join32(homedir17(), ".msapling"));
12138
12643
  await writeFile14(settingsPath, JSON.stringify(existing, null, 2), "utf8");
12139
12644
  }
12140
12645
  var VALID_THEMES, themeCommand;
@@ -12206,7 +12711,7 @@ var init_version = __esm({
12206
12711
  description: "Show version information for CLI and core packages",
12207
12712
  category: "debug",
12208
12713
  handler: async (_args, context) => {
12209
- const cliVersion = true ? "2.3.6-beta.48" : "(dev)";
12714
+ const cliVersion = true ? "2.3.6-beta.49" : "(dev)";
12210
12715
  const coreVersion = true ? "2.3.6-beta.43" : "(dev)";
12211
12716
  const runtime = process.version;
12212
12717
  context.addMessage("system", "MSapling Version Info");
@@ -12215,7 +12720,7 @@ var init_version = __esm({
12215
12720
  context.addMessage("system", row2("Core (@msapling/core)", coreVersion));
12216
12721
  context.addMessage("system", row2("Runtime (Node/Bun)", runtime));
12217
12722
  try {
12218
- const ts = "2026-06-20T08:17:10.331Z";
12723
+ const ts = "2026-06-20T08:28:18.242Z";
12219
12724
  if (ts && ts !== "__BUILD_TIMESTAMP__") {
12220
12725
  context.addMessage("system", row2("Build Timestamp", ts));
12221
12726
  }
@@ -12228,14 +12733,14 @@ var init_version = __esm({
12228
12733
  });
12229
12734
 
12230
12735
  // src/commands/feedback.ts
12231
- import { join as join32 } from "path";
12232
- import { existsSync as existsSync28 } from "fs";
12736
+ import { join as join33 } from "path";
12737
+ import { existsSync as existsSync29 } from "fs";
12233
12738
  import { readFile as readFile25 } from "fs/promises";
12234
12739
  async function readCliVersion() {
12235
12740
  try {
12236
12741
  const baseDir = typeof __dirname !== "undefined" ? __dirname : process.cwd();
12237
- const pkgPath = join32(baseDir, "..", "..", "package.json");
12238
- if (!existsSync28(pkgPath)) return "unknown";
12742
+ const pkgPath = join33(baseDir, "..", "..", "package.json");
12743
+ if (!existsSync29(pkgPath)) return "unknown";
12239
12744
  const text = await readFile25(pkgPath, "utf8");
12240
12745
  const json = JSON.parse(text);
12241
12746
  return json.version ?? "unknown";
@@ -12277,7 +12782,7 @@ var init_feedback = __esm({
12277
12782
 
12278
12783
  // src/commands/export.ts
12279
12784
  import { homedir as homedir18 } from "os";
12280
- import { join as join33 } from "path";
12785
+ import { join as join34 } from "path";
12281
12786
  import { writeFile as writeFile15, mkdir as mkdir8 } from "fs/promises";
12282
12787
  function formatTimestamp(date) {
12283
12788
  return date.toISOString().replace(/[:.]/g, "-").replace("T", "_").slice(0, 19);
@@ -12327,10 +12832,10 @@ var init_export = __esm({
12327
12832
  let outputPath;
12328
12833
  let content;
12329
12834
  if (arg === "" || arg === "json") {
12330
- outputPath = join33(homedir18(), `msapling-export-${timestamp}.json`);
12835
+ outputPath = join34(homedir18(), `msapling-export-${timestamp}.json`);
12331
12836
  content = buildJsonExport(history);
12332
12837
  } else if (arg === "markdown" || arg === "md") {
12333
- outputPath = join33(homedir18(), `msapling-export-${timestamp}.md`);
12838
+ outputPath = join34(homedir18(), `msapling-export-${timestamp}.md`);
12334
12839
  content = buildMarkdownExport(history);
12335
12840
  } else {
12336
12841
  outputPath = arg;
@@ -12342,7 +12847,7 @@ var init_export = __esm({
12342
12847
  }
12343
12848
  }
12344
12849
  try {
12345
- const dir = join33(outputPath, "..");
12850
+ const dir = join34(outputPath, "..");
12346
12851
  await mkdir8(dir, { recursive: true });
12347
12852
  await writeFile15(outputPath, content, "utf8");
12348
12853
  context.addMessage("system", `Exported to: ${outputPath}`);
@@ -12538,15 +13043,15 @@ var init_plan = __esm({
12538
13043
 
12539
13044
  // src/commands/note.ts
12540
13045
  import { homedir as homedir19 } from "os";
12541
- import { join as join34 } from "path";
12542
- import { existsSync as existsSync29 } from "fs";
13046
+ import { join as join35 } from "path";
13047
+ import { existsSync as existsSync30 } from "fs";
12543
13048
  import { readFile as readFile26, writeFile as writeFile16 } from "fs/promises";
12544
13049
  function getNotesFilePath() {
12545
- return join34(homedir19(), ".msapling", "notes.json");
13050
+ return join35(homedir19(), ".msapling", "notes.json");
12546
13051
  }
12547
13052
  async function readNotes(filePath = getNotesFilePath()) {
12548
13053
  try {
12549
- if (!existsSync29(filePath)) return [];
13054
+ if (!existsSync30(filePath)) return [];
12550
13055
  const raw = await readFile26(filePath, "utf8");
12551
13056
  const parsed = JSON.parse(raw);
12552
13057
  if (!Array.isArray(parsed)) return [];
@@ -12556,7 +13061,7 @@ async function readNotes(filePath = getNotesFilePath()) {
12556
13061
  }
12557
13062
  }
12558
13063
  async function writeNotes(notes, filePath = getNotesFilePath()) {
12559
- const dir = join34(homedir19(), ".msapling");
13064
+ const dir = join35(homedir19(), ".msapling");
12560
13065
  ensureConfigDir(dir);
12561
13066
  await writeFile16(filePath, JSON.stringify(notes, null, 2), "utf8");
12562
13067
  }
@@ -12703,16 +13208,16 @@ var init_todo = __esm({
12703
13208
 
12704
13209
  // src/commands/outputStyle.ts
12705
13210
  import { homedir as homedir20 } from "os";
12706
- import { join as join35, basename as basename3, extname as extname3 } from "path";
12707
- import { existsSync as existsSync30, mkdirSync as mkdirSync6, readdirSync as readdirSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync5 } from "fs";
13211
+ import { join as join36, basename as basename4, extname as extname3 } from "path";
13212
+ import { existsSync as existsSync31, mkdirSync as mkdirSync6, readdirSync as readdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "fs";
12708
13213
  function resolveHome() {
12709
13214
  return process.env.HOME || process.env.USERPROFILE || homedir20();
12710
13215
  }
12711
13216
  function stylesDir() {
12712
- return join35(resolveHome(), ".msapling", "output-styles");
13217
+ return join36(resolveHome(), ".msapling", "output-styles");
12713
13218
  }
12714
13219
  function activeFile() {
12715
- return join35(stylesDir(), ".active");
13220
+ return join36(stylesDir(), ".active");
12716
13221
  }
12717
13222
  function parseStyleFile(text) {
12718
13223
  const fm = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?/);
@@ -12733,16 +13238,16 @@ function parseStyleFile(text) {
12733
13238
  }
12734
13239
  function listUserStyles() {
12735
13240
  const dir = stylesDir();
12736
- if (!existsSync30(dir)) return [];
13241
+ if (!existsSync31(dir)) return [];
12737
13242
  const out = [];
12738
- for (const entry of readdirSync4(dir)) {
13243
+ for (const entry of readdirSync5(dir)) {
12739
13244
  if (extname3(entry).toLowerCase() !== ".md") continue;
12740
- const full = join35(dir, entry);
13245
+ const full = join36(dir, entry);
12741
13246
  try {
12742
- const text = readFileSync4(full, "utf8");
13247
+ const text = readFileSync5(full, "utf8");
12743
13248
  const { description, body } = parseStyleFile(text);
12744
13249
  out.push({
12745
- name: basename3(entry, ".md"),
13250
+ name: basename4(entry, ".md"),
12746
13251
  description,
12747
13252
  body,
12748
13253
  source: "user",
@@ -12765,15 +13270,15 @@ function findStyle(name) {
12765
13270
  function getActiveStyleName() {
12766
13271
  try {
12767
13272
  const f = activeFile();
12768
- if (!existsSync30(f)) return "default";
12769
- return readFileSync4(f, "utf8").trim() || "default";
13273
+ if (!existsSync31(f)) return "default";
13274
+ return readFileSync5(f, "utf8").trim() || "default";
12770
13275
  } catch {
12771
13276
  return "default";
12772
13277
  }
12773
13278
  }
12774
13279
  function setActiveStyleName(name) {
12775
13280
  const dir = stylesDir();
12776
- if (!existsSync30(dir)) mkdirSync6(dir, { recursive: true });
13281
+ if (!existsSync31(dir)) mkdirSync6(dir, { recursive: true });
12777
13282
  writeFileSync5(activeFile(), `${name}
12778
13283
  `, "utf8");
12779
13284
  }
@@ -12786,8 +13291,8 @@ function createUserStyle(name, description, body) {
12786
13291
  throw new Error(`Invalid style name "${name}" \u2014 use letters, digits, _ and - only.`);
12787
13292
  }
12788
13293
  const dir = stylesDir();
12789
- if (!existsSync30(dir)) mkdirSync6(dir, { recursive: true });
12790
- const target = join35(dir, `${name}.md`);
13294
+ if (!existsSync31(dir)) mkdirSync6(dir, { recursive: true });
13295
+ const target = join36(dir, `${name}.md`);
12791
13296
  const frontmatter = `---
12792
13297
  description: ${description.replace(/\n/g, " ")}
12793
13298
  ---
@@ -14204,6 +14709,32 @@ var init_agents = __esm({
14204
14709
  }
14205
14710
  });
14206
14711
 
14712
+ // src/commands/stats.ts
14713
+ var statsCommand;
14714
+ var init_stats = __esm({
14715
+ "src/commands/stats.ts"() {
14716
+ "use strict";
14717
+ init_esm_shims();
14718
+ init_src3();
14719
+ statsCommand = {
14720
+ name: "stats",
14721
+ args: "[--json]",
14722
+ description: "Show this session's tool, token, cost, and approval stats",
14723
+ category: "debug",
14724
+ handler: (args2, context) => {
14725
+ const snap = getSessionStats().snapshot();
14726
+ if (args2.includes("--json")) {
14727
+ context.addMessage("system", JSON.stringify(snap, null, 2));
14728
+ return;
14729
+ }
14730
+ for (const line of renderSessionStats(snap)) {
14731
+ context.addMessage("system", line);
14732
+ }
14733
+ }
14734
+ };
14735
+ }
14736
+ });
14737
+
14207
14738
  // src/commands/index.ts
14208
14739
  var commands_exports = {};
14209
14740
  __export(commands_exports, {
@@ -14278,6 +14809,7 @@ var init_commands = __esm({
14278
14809
  init_rewind();
14279
14810
  init_bashes();
14280
14811
  init_agents();
14812
+ init_stats();
14281
14813
  commands = [
14282
14814
  loginCommand,
14283
14815
  logoutCommand,
@@ -14344,7 +14876,8 @@ var init_commands = __esm({
14344
14876
  remoteAgentCommand,
14345
14877
  rewindCommand,
14346
14878
  bashesCommand,
14347
- agentsCommand
14879
+ agentsCommand,
14880
+ statsCommand
14348
14881
  ];
14349
14882
  }
14350
14883
  });
@@ -14414,15 +14947,15 @@ var exec_exports = {};
14414
14947
  __export(exec_exports, {
14415
14948
  runExec: () => runExec
14416
14949
  });
14417
- import { existsSync as existsSync33 } from "fs";
14950
+ import { existsSync as existsSync34 } from "fs";
14418
14951
  import { readFile as readFile29 } from "fs/promises";
14419
14952
  import { homedir as homedir23 } from "os";
14420
- import { join as join37 } from "path";
14953
+ import { join as join38 } from "path";
14421
14954
  async function loadPersistedSettings() {
14422
14955
  const out = { mode: "default", theme: null };
14423
14956
  try {
14424
- const p = join37(homedir23(), ".msapling", "settings.json");
14425
- if (!existsSync33(p)) return out;
14957
+ const p = join38(homedir23(), ".msapling", "settings.json");
14958
+ if (!existsSync34(p)) return out;
14426
14959
  const raw = JSON.parse(await readFile29(p, "utf8"));
14427
14960
  const parsed = parseApprovalMode(raw, Date.now());
14428
14961
  if (parsed.kind === "ok") out.mode = parsed.mode;
@@ -15281,8 +15814,8 @@ __export(doctor_exports, {
15281
15814
  runDoctor: () => runDoctor
15282
15815
  });
15283
15816
  import { homedir as homedir24, platform as platform4, tmpdir } from "os";
15284
- import { join as join38 } from "path";
15285
- import { existsSync as existsSync34, statSync as statSync8 } from "fs";
15817
+ import { join as join39 } from "path";
15818
+ import { existsSync as existsSync35, statSync as statSync9 } from "fs";
15286
15819
  import { readdir as readdir3, mkdir as mkdir9, rm as rm3 } from "fs/promises";
15287
15820
  import { exec as exec2 } from "child_process";
15288
15821
  import { promisify } from "util";
@@ -15305,8 +15838,8 @@ async function checkNodeVersion() {
15305
15838
  };
15306
15839
  }
15307
15840
  async function checkConfigDir() {
15308
- const configDir = join38(homedir24(), ".msapling");
15309
- if (!existsSync34(configDir)) {
15841
+ const configDir = join39(homedir24(), ".msapling");
15842
+ if (!existsSync35(configDir)) {
15310
15843
  return {
15311
15844
  name: "Config directory",
15312
15845
  status: "WARN",
@@ -15314,7 +15847,7 @@ async function checkConfigDir() {
15314
15847
  remediation: `mkdir -p "${configDir}" && chmod 700 "${configDir}"`
15315
15848
  };
15316
15849
  }
15317
- const stats = statSync8(configDir);
15850
+ const stats = statSync9(configDir);
15318
15851
  if (!stats.isDirectory()) {
15319
15852
  return {
15320
15853
  name: "Config directory",
@@ -15375,7 +15908,7 @@ async function checkPathConflicts() {
15375
15908
  const timedOutDirs = [];
15376
15909
  const DIR_TIMEOUT_MS = 1500;
15377
15910
  for (const dir of paths) {
15378
- if (!dir || !existsSync34(dir)) continue;
15911
+ if (!dir || !existsSync35(dir)) continue;
15379
15912
  try {
15380
15913
  const files = await Promise.race([
15381
15914
  readdir3(dir),
@@ -15388,7 +15921,7 @@ async function checkPathConflicts() {
15388
15921
  ]);
15389
15922
  for (const file of files) {
15390
15923
  if (file === "msapling" || file === "msapling.exe" || file === "msapling.py") {
15391
- const fullPath = join38(dir, file);
15924
+ const fullPath = join39(dir, file);
15392
15925
  conflicts.push(fullPath);
15393
15926
  }
15394
15927
  }
@@ -15506,9 +16039,9 @@ async function checkTokenValidity() {
15506
16039
  }
15507
16040
  async function checkOsSpecific() {
15508
16041
  if (platform4() === "win32") {
15509
- const testDir = join38(tmpdir(), `msapling-longpath-test-${Date.now()}`);
16042
+ const testDir = join39(tmpdir(), `msapling-longpath-test-${Date.now()}`);
15510
16043
  const longDirName = "A".repeat(260);
15511
- const testPath = join38(testDir, longDirName);
16044
+ const testPath = join39(testDir, longDirName);
15512
16045
  try {
15513
16046
  await mkdir9(testDir, { recursive: true });
15514
16047
  try {
@@ -17185,8 +17718,8 @@ var init_libesm = __esm({
17185
17718
  });
17186
17719
 
17187
17720
  // ../core/src/mcp/catalog.ts
17188
- import { readdirSync as readdirSync5, readFileSync as readFileSync5, statSync as statSync9 } from "fs";
17189
- import { join as join39, relative as relative16 } from "path";
17721
+ import { readdirSync as readdirSync6, readFileSync as readFileSync6, statSync as statSync10 } from "fs";
17722
+ import { join as join40, relative as relative17 } from "path";
17190
17723
  function buildFileTree(root, maxFiles) {
17191
17724
  const SKIP_DIRS2 = /* @__PURE__ */ new Set(["node_modules", ".git", "build", "dist", ".venv", "venv", ".next", "__pycache__", ".dart_tool", ".bun", "target"]);
17192
17725
  const SOURCE_EXT = /* @__PURE__ */ new Set([".ts", ".tsx", ".js", ".jsx", ".py", ".go", ".rs", ".java", ".kt", ".swift", ".dart", ".rb", ".cs", ".cpp", ".c", ".h", ".md", ".json", ".yaml", ".yml", ".toml", ".sh", ".sql"]);
@@ -17196,17 +17729,17 @@ function buildFileTree(root, maxFiles) {
17196
17729
  const dir = queue.shift();
17197
17730
  let entries;
17198
17731
  try {
17199
- entries = readdirSync5(dir);
17732
+ entries = readdirSync6(dir);
17200
17733
  } catch {
17201
17734
  continue;
17202
17735
  }
17203
17736
  for (const name of entries) {
17204
17737
  if (out.length >= maxFiles) break;
17205
17738
  if (SKIP_DIRS2.has(name)) continue;
17206
- const full = join39(dir, name);
17739
+ const full = join40(dir, name);
17207
17740
  let s;
17208
17741
  try {
17209
- s = statSync9(full);
17742
+ s = statSync10(full);
17210
17743
  } catch {
17211
17744
  continue;
17212
17745
  }
@@ -17227,12 +17760,12 @@ function readFilesAsContext(root, files, maxKB) {
17227
17760
  for (const f of files) {
17228
17761
  let body;
17229
17762
  try {
17230
- body = readFileSync5(f, "utf8");
17763
+ body = readFileSync6(f, "utf8");
17231
17764
  } catch {
17232
17765
  continue;
17233
17766
  }
17234
17767
  if (body.length > cap) body = body.slice(0, cap) + "\n[...truncated]";
17235
- const rel = relative16(root, f).replace(/\\/g, "/");
17768
+ const rel = relative17(root, f).replace(/\\/g, "/");
17236
17769
  parts.push(`### ${rel}
17237
17770
 
17238
17771
  \`\`\`
@@ -18009,7 +18542,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
18009
18542
  var Header = () => /* @__PURE__ */ jsxs(Box, { borderStyle: "single", borderColor: "cyan", paddingX: 1, marginBottom: 1, children: [
18010
18543
  /* @__PURE__ */ jsxs(Text, { bold: true, color: "cyan", children: [
18011
18544
  "\u25CF MSapling CLI v",
18012
- "2.3.6-beta.48"
18545
+ "2.3.6-beta.49"
18013
18546
  ] }),
18014
18547
  /* @__PURE__ */ jsx(Box, { marginLeft: 2, children: /* @__PURE__ */ jsx(Text, { color: "gray", children: "Platinum Tier Architecture" }) })
18015
18548
  ] });
@@ -18412,6 +18945,7 @@ function createIdleAwarePoll(pollingIntervalRef, lastActivityRef, client, setUse
18412
18945
 
18413
18946
  // src/state/commandHandler.ts
18414
18947
  init_esm_shims();
18948
+ init_src3();
18415
18949
  init_commands();
18416
18950
  init_plan();
18417
18951
  import { spawn as spawn10 } from "child_process";
@@ -18419,14 +18953,14 @@ import { spawn as spawn10 } from "child_process";
18419
18953
  // src/state/persistentState.ts
18420
18954
  init_esm_shims();
18421
18955
  import { homedir as homedir22 } from "os";
18422
- import { join as join36, dirname as dirname5 } from "path";
18423
- import { existsSync as existsSync31, mkdirSync as mkdirSync7 } from "fs";
18956
+ import { join as join37, dirname as dirname5 } from "path";
18957
+ import { existsSync as existsSync32, mkdirSync as mkdirSync7 } from "fs";
18424
18958
  import { readFile as readFile27, writeFile as writeFile17, rename as rename3 } from "fs/promises";
18425
18959
  import { randomBytes as randomBytes15 } from "crypto";
18426
- var STATE_PATH = join36(homedir22(), ".msapling", "state.json");
18960
+ var STATE_PATH = join37(homedir22(), ".msapling", "state.json");
18427
18961
  async function loadPersistentState(statePath = STATE_PATH) {
18428
18962
  try {
18429
- if (!existsSync31(statePath)) return { version: 1 };
18963
+ if (!existsSync32(statePath)) return { version: 1 };
18430
18964
  const text = await readFile27(statePath, "utf8");
18431
18965
  const parsed = JSON.parse(text);
18432
18966
  if (parsed.version !== 1) return { version: 1 };
@@ -18442,7 +18976,7 @@ async function loadPersistentState(statePath = STATE_PATH) {
18442
18976
  async function savePersistentState(state, statePath = STATE_PATH) {
18443
18977
  try {
18444
18978
  const dir = dirname5(statePath);
18445
- if (!existsSync31(dir)) mkdirSync7(dir, { recursive: true });
18979
+ if (!existsSync32(dir)) mkdirSync7(dir, { recursive: true });
18446
18980
  const existing = await loadPersistentState(statePath);
18447
18981
  const merged = {
18448
18982
  version: 1,
@@ -18511,6 +19045,11 @@ ${prompt4}` : prompt4;
18511
19045
  if (usage !== null) {
18512
19046
  ctx.setLastCost(usage.cost_usd);
18513
19047
  ctx.setSessionCost((prev) => prev + usage.cost_usd);
19048
+ getSessionStats().recordUsage(
19049
+ usage.cost_usd,
19050
+ usage.prompt_tokens,
19051
+ usage.completion_tokens
19052
+ );
18514
19053
  }
18515
19054
  ctx.setContextBudgetSnap(snapshotBudget(ctx.agent.getContextBudget()));
18516
19055
  } catch (e) {
@@ -18589,9 +19128,9 @@ ${prompt4}` : prompt4;
18589
19128
  for (const mention of fileMentions) {
18590
19129
  const filePath = mention.slice(1);
18591
19130
  try {
18592
- const { existsSync: existsSync35 } = await import("fs");
19131
+ const { existsSync: existsSync36 } = await import("fs");
18593
19132
  const { readFile: readFile30 } = await import("fs/promises");
18594
- if (existsSync35(filePath)) {
19133
+ if (existsSync36(filePath)) {
18595
19134
  const content = await readFile30(filePath, "utf8");
18596
19135
  const MAX_LEN = 32768;
18597
19136
  const truncated = content.length > MAX_LEN ? content.slice(0, MAX_LEN) + "\n...[TRUNCATED]" : content;
@@ -18617,6 +19156,7 @@ ${finalCmd}`;
18617
19156
  if (usage !== null) {
18618
19157
  ctx.setLastCost(usage.cost_usd);
18619
19158
  ctx.setSessionCost((prev) => prev + usage.cost_usd);
19159
+ getSessionStats().recordUsage(usage.cost_usd, usage.prompt_tokens, usage.completion_tokens);
18620
19160
  }
18621
19161
  ctx.setContextBudgetSnap(snapshotBudget(ctx.agent.getContextBudget()));
18622
19162
  } catch (e) {
@@ -18653,7 +19193,7 @@ init_src3();
18653
19193
  init_src();
18654
19194
  init_parseApprovalMode();
18655
19195
  import { readFile as readFile28 } from "fs/promises";
18656
- import { existsSync as existsSync32 } from "fs";
19196
+ import { existsSync as existsSync33 } from "fs";
18657
19197
  async function initSession(ctx) {
18658
19198
  try {
18659
19199
  const journalEncrypted = await initJournalEncryption();
@@ -18679,9 +19219,9 @@ async function initSession(ctx) {
18679
19219
  }
18680
19220
  try {
18681
19221
  const { homedir: homedir25 } = await import("os");
18682
- const { join: join41 } = await import("path");
18683
- const userSettingsPath = join41(homedir25(), ".msapling", "settings.json");
18684
- if (existsSync32(userSettingsPath)) {
19222
+ const { join: join42 } = await import("path");
19223
+ const userSettingsPath = join42(homedir25(), ".msapling", "settings.json");
19224
+ if (existsSync33(userSettingsPath)) {
18685
19225
  const userText = await readFile28(userSettingsPath, "utf8");
18686
19226
  let parsed;
18687
19227
  try {
@@ -19098,14 +19638,14 @@ var App = ({ compact: compact2 = false, continueSession: continueSession2 = fals
19098
19638
 
19099
19639
  // src/runtime/bootstrap.ts
19100
19640
  init_esm_shims();
19101
- import { readFileSync as readFileSync6 } from "fs";
19641
+ import { readFileSync as readFileSync7 } from "fs";
19102
19642
  import { fileURLToPath as fileURLToPath2 } from "url";
19103
- import { dirname as dirname6, join as join40 } from "path";
19643
+ import { dirname as dirname6, join as join41 } from "path";
19104
19644
  function readCliVersion2() {
19105
19645
  const here = dirname6(fileURLToPath2(import.meta.url));
19106
19646
  for (const rel of ["../package.json", "../../package.json"]) {
19107
19647
  try {
19108
- const pkg = JSON.parse(readFileSync6(join40(here, rel), "utf8"));
19648
+ const pkg = JSON.parse(readFileSync7(join41(here, rel), "utf8"));
19109
19649
  if (pkg.name && pkg.version) {
19110
19650
  return { name: pkg.name, version: pkg.version };
19111
19651
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mtreeai/msapling-cli",
3
- "version": "2.3.6-beta.48",
3
+ "version": "2.3.6-beta.49",
4
4
  "description": "MSapling CLI — React/Ink terminal client for the MSapling backend (chat, projects, MDrive, agent tools). Proprietary; redistribution prohibited.",
5
5
  "license": "UNLICENSED",
6
6
  "author": "MSapling Team",