ctxloom-pro 1.2.5 → 1.2.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -166,12 +166,12 @@ var init_VectorStore = __esm({
166
166
  // server/index.ts
167
167
  import express from "express";
168
168
  import cors from "cors";
169
- import path43 from "path";
169
+ import path44 from "path";
170
170
  import fs32 from "fs";
171
171
  import { fileURLToPath as fileURLToPath2 } from "url";
172
172
 
173
173
  // server/loader.ts
174
- import path37 from "path";
174
+ import path38 from "path";
175
175
 
176
176
  // ../../packages/core/src/graph/DependencyGraph.ts
177
177
  import fs7 from "fs";
@@ -327,7 +327,7 @@ var GrammarLoader = class {
327
327
  const url = entry.downloadUrl?.trim() ? entry.downloadUrl : `${this.cdn}/${entry.npmPackage}@${entry.version}/${entry.wasmFile}`;
328
328
  const dest = path.join(this.cacheDir, entry.wasmFile);
329
329
  logger.info("Downloading grammar", { language, url, source: entry.downloadUrl?.trim() ? "custom" : "cdn" });
330
- fs.mkdirSync(this.cacheDir, { recursive: true });
330
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
331
331
  await this.download(url, dest);
332
332
  if (entry.sha256 && !this.skipVerify) {
333
333
  await this.verifyHash(dest, entry.sha256, language);
@@ -341,6 +341,12 @@ var GrammarLoader = class {
341
341
  return new Promise((resolve, reject) => {
342
342
  const tmp = dest + ".tmp";
343
343
  const file = fs.createWriteStream(tmp);
344
+ const onFileError = (err) => {
345
+ file.destroy();
346
+ fs.rmSync(tmp, { force: true });
347
+ reject(err);
348
+ };
349
+ file.on("error", onFileError);
344
350
  const request = https.get(url, (response) => {
345
351
  if (response.statusCode === 301 || response.statusCode === 302) {
346
352
  const location = response.headers.location;
@@ -370,11 +376,6 @@ var GrammarLoader = class {
370
376
  fs.rmSync(tmp, { force: true });
371
377
  reject(err);
372
378
  });
373
- file.on("error", (err) => {
374
- file.destroy();
375
- fs.rmSync(tmp, { force: true });
376
- reject(err);
377
- });
378
379
  file.on("finish", () => {
379
380
  fs.renameSync(tmp, dest);
380
381
  resolve();
@@ -1001,15 +1002,14 @@ var ASTParser = class {
1001
1002
  return;
1002
1003
  }
1003
1004
  case "import_declaration": {
1005
+ const specs = [];
1004
1006
  const walkImport = (n) => {
1005
1007
  if (n.type === "import_spec") {
1006
1008
  const pathNode = n.childForFieldName?.("path");
1007
1009
  if (pathNode) {
1008
1010
  const spec = pathNode.text.replace(/^"|"$/g, "");
1009
- nodes.push({
1010
- type: "import",
1011
+ specs.push({
1011
1012
  name: spec,
1012
- source: spec,
1013
1013
  startLine: n.startPosition.row + 1,
1014
1014
  endLine: n.endPosition.row + 1
1015
1015
  });
@@ -1020,6 +1020,25 @@ var ASTParser = class {
1020
1020
  }
1021
1021
  };
1022
1022
  walkImport(node);
1023
+ if (specs.length > 0) {
1024
+ const firstSpec = specs[0];
1025
+ nodes.push({
1026
+ type: "import",
1027
+ name: firstSpec.name,
1028
+ source: firstSpec.name,
1029
+ startLine: node.startPosition.row + 1,
1030
+ endLine: node.endPosition.row + 1
1031
+ });
1032
+ }
1033
+ for (const spec of specs) {
1034
+ nodes.push({
1035
+ type: "import",
1036
+ name: spec.name,
1037
+ source: spec.name,
1038
+ startLine: spec.startLine,
1039
+ endLine: spec.endLine
1040
+ });
1041
+ }
1023
1042
  return;
1024
1043
  }
1025
1044
  }
@@ -1341,6 +1360,24 @@ var ASTParser = class {
1341
1360
  const lines = source.split("\n");
1342
1361
  const walk = (node) => {
1343
1362
  switch (node.type) {
1363
+ case "call": {
1364
+ const methodNode = node.childForFieldName?.("method") ?? node.children.find((c) => c?.type === "identifier");
1365
+ const name = methodNode?.text ?? "";
1366
+ if (name === "require" || name === "require_relative" || name === "load" || name === "autoload") {
1367
+ const argsNode = node.childForFieldName?.("arguments") ?? node.children.find((c) => c?.type === "argument_list");
1368
+ const firstStringArg = argsNode?.children.find((c) => c?.type === "string" || c?.type === "simple_symbol");
1369
+ const spec = firstStringArg?.text.replace(/^['":]+|['"]+$/g, "") ?? "";
1370
+ nodes.push({
1371
+ type: "import",
1372
+ name: spec,
1373
+ source: spec,
1374
+ startLine: node.startPosition.row + 1,
1375
+ endLine: node.endPosition.row + 1
1376
+ });
1377
+ return;
1378
+ }
1379
+ break;
1380
+ }
1344
1381
  case "method":
1345
1382
  case "singleton_method": {
1346
1383
  const nameNode = node.childForFieldName?.("name") ?? node.children.find((c) => c?.type === "identifier");
@@ -1628,9 +1665,18 @@ var ASTParser = class {
1628
1665
  const walk = (node) => {
1629
1666
  switch (node.type) {
1630
1667
  case "import_or_export": {
1631
- const uriNode = node.children.find((c) => c?.type === "uri");
1668
+ const findUri = (n) => {
1669
+ if (n.type === "uri") return n;
1670
+ for (const c of n.children) {
1671
+ if (!c) continue;
1672
+ const hit = findUri(c);
1673
+ if (hit) return hit;
1674
+ }
1675
+ return void 0;
1676
+ };
1677
+ const uriNode = findUri(node);
1632
1678
  const uri = uriNode?.text?.replace(/['"]/g, "") ?? "";
1633
- if (uri.startsWith(".")) {
1679
+ if (uri) {
1634
1680
  nodes.push({
1635
1681
  type: "import",
1636
1682
  name: uri,
@@ -3135,8 +3181,8 @@ var CoChangeIndex = class _CoChangeIndex {
3135
3181
  if (event.isBulk || event.isMerge) return;
3136
3182
  const paths = event.files.map((f) => f.path);
3137
3183
  if (paths.length === 0) return;
3138
- for (const path44 of paths) {
3139
- this.nodeCounts.set(path44, (this.nodeCounts.get(path44) ?? 0) + 1);
3184
+ for (const path45 of paths) {
3185
+ this.nodeCounts.set(path45, (this.nodeCounts.get(path45) ?? 0) + 1);
3140
3186
  }
3141
3187
  for (let i = 0; i < paths.length; i++) {
3142
3188
  for (let j = i + 1; j < paths.length; j++) {
@@ -3283,8 +3329,8 @@ var ChurnIndex = class _ChurnIndex {
3283
3329
  */
3284
3330
  snapshot() {
3285
3331
  const nodes = {};
3286
- for (const [path44, raw] of this.nodes) {
3287
- nodes[path44] = {
3332
+ for (const [path45, raw] of this.nodes) {
3333
+ nodes[path45] = {
3288
3334
  commits: raw.commits,
3289
3335
  churnLines: raw.churnLines,
3290
3336
  bugCommits: raw.bugCommits,
@@ -3299,8 +3345,8 @@ var ChurnIndex = class _ChurnIndex {
3299
3345
  */
3300
3346
  static load(s) {
3301
3347
  const idx = new _ChurnIndex();
3302
- for (const [path44, raw] of Object.entries(s.nodes)) {
3303
- idx.nodes.set(path44, {
3348
+ for (const [path45, raw] of Object.entries(s.nodes)) {
3349
+ idx.nodes.set(path45, {
3304
3350
  commits: raw.commits,
3305
3351
  churnLines: raw.churnLines,
3306
3352
  bugCommits: raw.bugCommits,
@@ -3313,8 +3359,8 @@ var ChurnIndex = class _ChurnIndex {
3313
3359
  // -------------------------------------------------------------------------
3314
3360
  // Private helpers
3315
3361
  // -------------------------------------------------------------------------
3316
- getOrCreate(path44) {
3317
- const existing = this.nodes.get(path44);
3362
+ getOrCreate(path45) {
3363
+ const existing = this.nodes.get(path45);
3318
3364
  if (existing !== void 0) return existing;
3319
3365
  const fresh = {
3320
3366
  commits: 0,
@@ -3323,7 +3369,7 @@ var ChurnIndex = class _ChurnIndex {
3323
3369
  authorCounts: {},
3324
3370
  lastTouch: 0
3325
3371
  };
3326
- this.nodes.set(path44, fresh);
3372
+ this.nodes.set(path45, fresh);
3327
3373
  return fresh;
3328
3374
  }
3329
3375
  };
@@ -3404,12 +3450,12 @@ var OwnershipIndex = class _OwnershipIndex {
3404
3450
  */
3405
3451
  snapshot() {
3406
3452
  const nodes = {};
3407
- for (const [path44, raw] of this.nodes) {
3453
+ for (const [path45, raw] of this.nodes) {
3408
3454
  const authorWeights = {};
3409
3455
  for (const [email, entry] of Object.entries(raw.authorWeights)) {
3410
3456
  authorWeights[email] = { ...entry };
3411
3457
  }
3412
- nodes[path44] = { authorWeights, lastTouch: raw.lastTouch };
3458
+ nodes[path45] = { authorWeights, lastTouch: raw.lastTouch };
3413
3459
  }
3414
3460
  return { version: 1, nodes };
3415
3461
  }
@@ -3418,23 +3464,23 @@ var OwnershipIndex = class _OwnershipIndex {
3418
3464
  */
3419
3465
  static load(s) {
3420
3466
  const idx = new _OwnershipIndex();
3421
- for (const [path44, raw] of Object.entries(s.nodes)) {
3467
+ for (const [path45, raw] of Object.entries(s.nodes)) {
3422
3468
  const authorWeights = {};
3423
3469
  for (const [email, entry] of Object.entries(raw.authorWeights)) {
3424
3470
  authorWeights[email] = { ...entry };
3425
3471
  }
3426
- idx.nodes.set(path44, { authorWeights, lastTouch: raw.lastTouch });
3472
+ idx.nodes.set(path45, { authorWeights, lastTouch: raw.lastTouch });
3427
3473
  }
3428
3474
  return idx;
3429
3475
  }
3430
3476
  // -------------------------------------------------------------------------
3431
3477
  // Private helpers
3432
3478
  // -------------------------------------------------------------------------
3433
- getOrCreate(path44) {
3434
- const existing = this.nodes.get(path44);
3479
+ getOrCreate(path45) {
3480
+ const existing = this.nodes.get(path45);
3435
3481
  if (existing !== void 0) return existing;
3436
3482
  const fresh = { authorWeights: {}, lastTouch: 0 };
3437
- this.nodes.set(path44, fresh);
3483
+ this.nodes.set(path45, fresh);
3438
3484
  return fresh;
3439
3485
  }
3440
3486
  };
@@ -4672,8 +4718,8 @@ function getErrorMap() {
4672
4718
 
4673
4719
  // ../../node_modules/zod/v3/helpers/parseUtil.js
4674
4720
  var makeIssue = (params) => {
4675
- const { data, path: path44, errorMaps, issueData } = params;
4676
- const fullPath = [...path44, ...issueData.path || []];
4721
+ const { data, path: path45, errorMaps, issueData } = params;
4722
+ const fullPath = [...path45, ...issueData.path || []];
4677
4723
  const fullIssue = {
4678
4724
  ...issueData,
4679
4725
  path: fullPath
@@ -4789,11 +4835,11 @@ var errorUtil;
4789
4835
 
4790
4836
  // ../../node_modules/zod/v3/types.js
4791
4837
  var ParseInputLazyPath = class {
4792
- constructor(parent, value, path44, key) {
4838
+ constructor(parent, value, path45, key) {
4793
4839
  this._cachedPath = [];
4794
4840
  this.parent = parent;
4795
4841
  this.data = value;
4796
- this._path = path44;
4842
+ this._path = path45;
4797
4843
  this._key = key;
4798
4844
  }
4799
4845
  get path() {
@@ -8244,16 +8290,29 @@ init_logger();
8244
8290
 
8245
8291
  // ../../packages/core/src/tools/search.ts
8246
8292
  init_embedder();
8293
+
8294
+ // ../../packages/core/src/budget/budget.ts
8295
+ init_logger();
8296
+
8297
+ // ../../packages/core/src/tools/search.ts
8247
8298
  var Schema = external_exports.object({
8248
8299
  query: external_exports.string().describe("Search query \u2014 natural language or code fragment"),
8249
8300
  limit: external_exports.number().max(100).optional().default(10).describe("Maximum results to return"),
8250
- project_root: ProjectRootField
8301
+ project_root: ProjectRootField,
8302
+ // ─── Phase B2 budget surface ──
8303
+ max_response_tokens: external_exports.number().int().positive().optional().describe("Soft response budget. Default: 4000 (when budget surface is opted into). Over-budget rebuilds the result list without the content snippets (paths + scores only)."),
8304
+ on_budget_exceeded: external_exports.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) drops snippets; 'truncate' slices the raw XML; 'error' throws."),
8305
+ response_format: external_exports.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces the path-and-score-only view; 'full'/'auto' lets the budget decide.")
8251
8306
  });
8252
8307
 
8253
8308
  // ../../packages/core/src/tools/file.ts
8254
8309
  var Schema2 = external_exports.object({
8255
8310
  path: external_exports.string().describe("Relative path to the file"),
8256
- project_root: ProjectRootField
8311
+ project_root: ProjectRootField,
8312
+ // ─── Phase B2 budget surface (all optional; back-compat preserved) ──
8313
+ max_response_tokens: external_exports.number().int().positive().optional().describe("Soft response budget in tokens. Falls back to a skeleton when exceeded. Default: 8000 (when budget surface is opted into)."),
8314
+ on_budget_exceeded: external_exports.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when the response would exceed max_response_tokens. 'skeleton' (default) substitutes a Skeletonizer signature view; 'truncate' slices the raw text; 'error' throws a structured error with token counts so the caller can re-ask."),
8315
+ response_format: external_exports.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces a Skeletonizer view regardless of budget; 'full'/'auto' lets the budget decide.")
8257
8316
  });
8258
8317
 
8259
8318
  // ../../packages/core/src/tools/context-packet.ts
@@ -8261,7 +8320,11 @@ import path16 from "path";
8261
8320
  var Schema3 = external_exports.object({
8262
8321
  target_file: external_exports.string().describe("Relative path to the primary file"),
8263
8322
  mode: external_exports.enum(["edit", "read"]).optional().default("edit").describe("Context mode"),
8264
- project_root: ProjectRootField
8323
+ project_root: ProjectRootField,
8324
+ // ─── Phase B2 budget surface ──
8325
+ max_response_tokens: external_exports.number().int().positive().optional().describe("Soft response budget. Default: 6000 (when budget surface is opted into). Over-budget rebuilds the packet with the primary file replaced by its skeleton."),
8326
+ on_budget_exceeded: external_exports.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) re-renders the packet with the primary file skeletonized; 'truncate' slices the raw envelope; 'error' throws."),
8327
+ response_format: external_exports.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces the skeletonized-primary packet; 'full'/'auto' lets the budget decide.")
8265
8328
  });
8266
8329
 
8267
8330
  // ../../packages/core/src/tools/findCallers.ts
@@ -8279,7 +8342,11 @@ var Schema4 = external_exports.object({
8279
8342
  // ../../packages/core/src/tools/definition.ts
8280
8343
  var Schema5 = external_exports.object({
8281
8344
  symbol: external_exports.string().describe("Symbol name to look up"),
8282
- project_root: ProjectRootField
8345
+ project_root: ProjectRootField,
8346
+ // ─── Phase B2 budget surface (all optional; back-compat preserved) ──
8347
+ max_response_tokens: external_exports.number().int().positive().optional().describe("Soft response budget in tokens. Default: 2000 (when budget surface is opted into). No skeleton fallback for this tool \u2014 the response is structural metadata; over-budget falls back to truncation."),
8348
+ on_budget_exceeded: external_exports.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton'/'truncate' both slice the XML (no file context to skeletonize from); 'error' throws."),
8349
+ response_format: external_exports.enum(["full", "skeleton", "auto"]).optional().describe("'full'/'auto' default. 'skeleton' is accepted for consistency but produces the same output as 'full' here \u2014 the response is already a compact symbol list.")
8283
8350
  });
8284
8351
 
8285
8352
  // ../../packages/core/src/tools/rules.ts
@@ -8399,7 +8466,11 @@ var Schema15 = external_exports.object({
8399
8466
  detail_level: external_exports.enum(["standard", "minimal"]).default("standard").describe(
8400
8467
  '"standard" (default) lists each written page with size. "minimal" returns counts only.'
8401
8468
  ),
8402
- project_root: ProjectRootField
8469
+ project_root: ProjectRootField,
8470
+ // ─── Phase B2 budget surface ──
8471
+ max_response_tokens: external_exports.number().int().positive().optional().describe("Soft response budget. Default: 12000 (when opted in). Over-budget re-renders at detail_level=minimal (counts only) \u2014 the wiki files themselves are unaffected."),
8472
+ on_budget_exceeded: external_exports.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) downgrades to minimal output; 'truncate' slices; 'error' throws."),
8473
+ response_format: external_exports.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces minimal output regardless of budget; 'full'/'auto' lets the budget decide.")
8403
8474
  });
8404
8475
 
8405
8476
  // ../../packages/core/src/tools/graph-export.ts
@@ -8427,7 +8498,11 @@ var Schema17 = external_exports.object({
8427
8498
  max_diff_lines: external_exports.number().min(10).max(2e3).optional().default(300).describe(
8428
8499
  "Max diff lines to include per file (default: 300)"
8429
8500
  ),
8430
- project_root: ProjectRootField
8501
+ project_root: ProjectRootField,
8502
+ // ─── Phase B2 budget surface ──
8503
+ max_response_tokens: external_exports.number().int().positive().optional().describe("Soft response budget. Default: 8000 (when opted in). Over-budget re-renders without <skeleton> blocks and without the transitive_importers section \u2014 keeps diffs, direct importers, and call sites."),
8504
+ on_budget_exceeded: external_exports.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) drops skeletons + transitive importers; 'truncate' slices; 'error' throws."),
8505
+ response_format: external_exports.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces the lighter view; 'full'/'auto' lets the budget decide.")
8431
8506
  });
8432
8507
 
8433
8508
  // ../../packages/core/src/tools/refactor-preview.ts
@@ -8439,7 +8514,11 @@ var Schema18 = external_exports.object({
8439
8514
  max_files: external_exports.number().min(1).max(200).optional().default(50).describe(
8440
8515
  "Maximum number of files to scan for occurrences (default: 50)"
8441
8516
  ),
8442
- project_root: ProjectRootField
8517
+ project_root: ProjectRootField,
8518
+ // ─── Phase B2 budget surface ──
8519
+ max_response_tokens: external_exports.number().int().positive().optional().describe("Soft response budget. Default: 4000 (when opted in). Over-budget drops the per-change before/after lines; keeps the file+occurrence summary so callers can decide which files to drill into."),
8520
+ on_budget_exceeded: external_exports.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) drops change details; 'truncate' slices; 'error' throws."),
8521
+ response_format: external_exports.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces the summary-only view; 'full'/'auto' lets the budget decide.")
8443
8522
  });
8444
8523
 
8445
8524
  // ../../packages/core/src/tools/execution-flow.ts
@@ -8452,7 +8531,11 @@ var Schema19 = external_exports.object({
8452
8531
  max_nodes: external_exports.number().min(1).max(200).optional().default(50).describe(
8453
8532
  "Max total steps to include in output (default: 50)"
8454
8533
  ),
8455
- project_root: ProjectRootField
8534
+ project_root: ProjectRootField,
8535
+ // ─── Phase B2 budget surface ──
8536
+ max_response_tokens: external_exports.number().int().positive().optional().describe("Soft response budget. Default: 4000 (when opted in). No skeleton fallback \u2014 response is already a bounded step list; over-budget falls through to truncation."),
8537
+ on_budget_exceeded: external_exports.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton'/'truncate' both slice; 'error' throws."),
8538
+ response_format: external_exports.enum(["full", "skeleton", "auto"]).optional().describe("'full'/'auto' default; 'skeleton' same output.")
8456
8539
  });
8457
8540
 
8458
8541
  // ../../packages/core/src/tools/cross-repo-search.ts
@@ -8469,7 +8552,11 @@ var Schema20 = external_exports.object({
8469
8552
  repos: external_exports.array(external_exports.string()).optional().describe(
8470
8553
  "Specific repo root paths to search. Omit to search all registered repos."
8471
8554
  ),
8472
- project_root: ProjectRootField
8555
+ project_root: ProjectRootField,
8556
+ // ─── Phase B2 budget surface ──
8557
+ max_response_tokens: external_exports.number().int().positive().optional().describe("Soft response budget. Default: 4000 (when opted in). Over-budget drops content snippets (repo + path + score only)."),
8558
+ on_budget_exceeded: external_exports.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) drops content snippets; 'truncate' slices; 'error' throws."),
8559
+ response_format: external_exports.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces the path-and-score-only view; 'full'/'auto' lets the budget decide.")
8473
8560
  });
8474
8561
 
8475
8562
  // ../../packages/core/src/tools/apply-refactor.ts
@@ -8484,7 +8571,11 @@ var Schema21 = external_exports.object({
8484
8571
  max_files: external_exports.number().min(1).max(200).optional().default(50).describe(
8485
8572
  "Maximum candidate files to process (default: 50)"
8486
8573
  ),
8487
- project_root: ProjectRootField
8574
+ project_root: ProjectRootField,
8575
+ // ─── Phase B2 budget surface ──
8576
+ max_response_tokens: external_exports.number().int().positive().optional().describe("Soft response budget. Default: 2000 (when opted in). No skeleton fallback \u2014 response is already compact; over-budget falls through to truncation."),
8577
+ on_budget_exceeded: external_exports.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton'/'truncate' both slice the XML; 'error' throws."),
8578
+ response_format: external_exports.enum(["full", "skeleton", "auto"]).optional().describe("'full'/'auto' default; 'skeleton' produces the same output.")
8488
8579
  });
8489
8580
 
8490
8581
  // ../../packages/core/src/tools/detect-changes.ts
@@ -8511,7 +8602,11 @@ var Schema23 = external_exports.object({
8511
8602
  case_sensitive: external_exports.boolean().optional().default(false),
8512
8603
  limit: external_exports.number().min(1).max(100).optional().default(20),
8513
8604
  context_lines: external_exports.number().min(0).max(5).optional().default(1),
8514
- project_root: ProjectRootField
8605
+ project_root: ProjectRootField,
8606
+ // ─── Phase B2 budget surface ──
8607
+ max_response_tokens: external_exports.number().int().positive().optional().describe("Soft response budget. Default: 4000 (when budget surface is opted into). Over-budget rebuilds the result list without match snippets (paths + match counts only)."),
8608
+ on_budget_exceeded: external_exports.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton' (default) drops snippets; 'truncate' slices the raw XML; 'error' throws."),
8609
+ response_format: external_exports.enum(["full", "skeleton", "auto"]).optional().describe("'skeleton' forces the path-and-count-only view; 'full'/'auto' lets the budget decide.")
8515
8610
  });
8516
8611
 
8517
8612
  // ../../packages/core/src/tools/suggested-questions.ts
@@ -8564,7 +8659,11 @@ var schema3 = external_exports.object({
8564
8659
  limit: external_exports.number().int().min(1).max(200).default(30).describe(
8565
8660
  "Maximum results to return (default: 30)."
8566
8661
  ),
8567
- project_root: ProjectRootField
8662
+ project_root: ProjectRootField,
8663
+ // ─── Phase B2 budget surface ──
8664
+ max_response_tokens: external_exports.number().int().positive().optional().describe("Soft response budget. Default: 2000 (when opted in). No skeleton fallback \u2014 response is already structural; over-budget falls through to truncation."),
8665
+ on_budget_exceeded: external_exports.enum(["skeleton", "truncate", "error"]).optional().describe("Behavior when over budget. 'skeleton'/'truncate' both slice the XML; 'error' throws."),
8666
+ response_format: external_exports.enum(["full", "skeleton", "auto"]).optional().describe("'full'/'auto' default; 'skeleton' produces the same output (response is already compact).")
8568
8667
  });
8569
8668
 
8570
8669
  // ../../packages/core/src/tools/git-coupling.ts
@@ -11268,15 +11367,15 @@ import os2 from "os";
11268
11367
  import { readFileSync as readFileSync2 } from "fs";
11269
11368
 
11270
11369
  // ../../packages/core/src/license/index.ts
11271
- import os3 from "os";
11370
+ import os5 from "os";
11272
11371
 
11273
11372
  // ../../packages/core/src/license/DistinctIdStore.ts
11274
11373
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
11275
11374
  import path32 from "path";
11276
- import os4 from "os";
11375
+ import os3 from "os";
11277
11376
  var UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
11278
11377
  function distinctIdPath(home) {
11279
- return path32.join(home ?? os4.homedir(), ".ctxloom", "distinct_id");
11378
+ return path32.join(home ?? os3.homedir(), ".ctxloom", "distinct_id");
11280
11379
  }
11281
11380
  function isValidV4(id) {
11282
11381
  return typeof id === "string" && UUID_V4_REGEX.test(id);
@@ -11295,7 +11394,7 @@ function getOrCreateDistinctId(home) {
11295
11394
  }
11296
11395
  const record = {
11297
11396
  id: crypto.randomUUID(),
11298
- alias_pending: os4.hostname()
11397
+ alias_pending: os3.hostname()
11299
11398
  };
11300
11399
  mkdirSync2(path32.dirname(filePath), { recursive: true });
11301
11400
  writeFileSync2(filePath, JSON.stringify(record), { mode: 384 });
@@ -11323,7 +11422,7 @@ function resolveTelemetryLevel() {
11323
11422
  }
11324
11423
  var TELEMETRY_LEVEL = resolveTelemetryLevel();
11325
11424
  var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
11326
- var CTXLOOM_VERSION = "1.2.5".length > 0 ? "1.2.5" : "dev";
11425
+ var CTXLOOM_VERSION = "1.2.7".length > 0 ? "1.2.7" : "dev";
11327
11426
  var POSTHOG_HOST = "https://eu.i.posthog.com";
11328
11427
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
11329
11428
  var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
@@ -11454,28 +11553,33 @@ function parseStack(stack) {
11454
11553
  }).filter((f) => f !== null).slice(0, 20);
11455
11554
  }
11456
11555
 
11457
- // ../../packages/core/src/license/TelemetryNotice.ts
11556
+ // ../../packages/core/src/license/FunnelMilestones.ts
11458
11557
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
11459
11558
  import path33 from "path";
11460
- import os5 from "os";
11559
+ import os4 from "os";
11560
+
11561
+ // ../../packages/core/src/license/TelemetryNotice.ts
11562
+ import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
11563
+ import path34 from "path";
11564
+ import os6 from "os";
11461
11565
 
11462
11566
  // ../../packages/core/src/server/ProjectState.ts
11463
- import path35 from "path";
11567
+ import path36 from "path";
11464
11568
 
11465
11569
  // ../../packages/core/src/server/projectId.ts
11466
11570
  import crypto5 from "crypto";
11467
- import path34 from "path";
11571
+ import path35 from "path";
11468
11572
 
11469
11573
  // ../../packages/core/src/server/ProjectStateManager.ts
11470
11574
  init_logger();
11471
11575
 
11472
11576
  // ../../packages/core/src/server/resolveProjectRoot.ts
11473
11577
  import fs29 from "fs";
11474
- import path36 from "path";
11578
+ import path37 from "path";
11475
11579
 
11476
11580
  // server/loader.ts
11477
11581
  async function loadContext(root) {
11478
- const absRoot = path37.resolve(root);
11582
+ const absRoot = path38.resolve(root);
11479
11583
  const overlay = new GitOverlayStore(absRoot);
11480
11584
  const gitEnabled = await overlay.loadSnapshot();
11481
11585
  const graph = new DependencyGraph();
@@ -11729,20 +11833,20 @@ function buildOwnershipRouter(ctx) {
11729
11833
  // server/routes/file.ts
11730
11834
  import { Router as Router7 } from "express";
11731
11835
  import fs30 from "fs/promises";
11732
- import path38 from "path";
11836
+ import path39 from "path";
11733
11837
  function buildFileRouter(ctx) {
11734
11838
  const router = Router7();
11735
11839
  router.get("/", async (req, res) => {
11736
11840
  const rel = req.query.path;
11737
11841
  if (!rel) return res.status(400).json({ error: "missing path" });
11738
- const abs = path38.resolve(ctx.root, rel);
11739
- const rootBoundary = ctx.root.endsWith(path38.sep) ? ctx.root : ctx.root + path38.sep;
11842
+ const abs = path39.resolve(ctx.root, rel);
11843
+ const rootBoundary = ctx.root.endsWith(path39.sep) ? ctx.root : ctx.root + path39.sep;
11740
11844
  if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
11741
11845
  return res.status(403).json({ error: "forbidden" });
11742
11846
  }
11743
11847
  try {
11744
11848
  const content = await fs30.readFile(abs, "utf-8");
11745
- const ext = path38.extname(abs).slice(1);
11849
+ const ext = path39.extname(abs).slice(1);
11746
11850
  res.json({ content, lines: content.split("\n").length, ext });
11747
11851
  } catch {
11748
11852
  res.status(404).json({ error: "not found" });
@@ -11754,7 +11858,7 @@ function buildFileRouter(ctx) {
11754
11858
  // server/routes/open.ts
11755
11859
  import { Router as Router8 } from "express";
11756
11860
  import { execFile as execFile2 } from "child_process";
11757
- import path39 from "path";
11861
+ import path40 from "path";
11758
11862
  function tryOpen(bin, abs) {
11759
11863
  return new Promise((resolve) => {
11760
11864
  execFile2(bin, [abs], { timeout: 5e3 }, (err) => resolve(!err));
@@ -11765,8 +11869,8 @@ function buildOpenRouter(ctx) {
11765
11869
  router.post("/", async (req, res) => {
11766
11870
  const rel = req.body?.path;
11767
11871
  if (!rel || typeof rel !== "string") return res.status(400).json({ error: "missing path" });
11768
- const abs = path39.resolve(ctx.root, rel);
11769
- const rootBoundary = ctx.root.endsWith(path39.sep) ? ctx.root : ctx.root + path39.sep;
11872
+ const abs = path40.resolve(ctx.root, rel);
11873
+ const rootBoundary = ctx.root.endsWith(path40.sep) ? ctx.root : ctx.root + path40.sep;
11770
11874
  if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
11771
11875
  return res.status(403).json({ error: "forbidden" });
11772
11876
  }
@@ -11778,7 +11882,7 @@ function buildOpenRouter(ctx) {
11778
11882
 
11779
11883
  // server/routes/tokens.ts
11780
11884
  import { Router as Router9 } from "express";
11781
- import path40 from "path";
11885
+ import path41 from "path";
11782
11886
  import fs31 from "fs";
11783
11887
  var CHARS_PER_TOKEN = 4;
11784
11888
  var cache = null;
@@ -11794,7 +11898,7 @@ function buildTokensRouter(ctx) {
11794
11898
  let fullChars = 0;
11795
11899
  let skeletonChars = 0;
11796
11900
  for (const file of files) {
11797
- const absPath = path40.join(ctx.root, file);
11901
+ const absPath = path41.join(ctx.root, file);
11798
11902
  try {
11799
11903
  const content = fs31.readFileSync(absPath, "utf-8");
11800
11904
  fullChars += content.length;
@@ -11897,21 +12001,21 @@ function buildFileTrendsRouter(ctx) {
11897
12001
 
11898
12002
  // server/routes/projects.ts
11899
12003
  import { Router as Router12 } from "express";
11900
- import path42 from "path";
12004
+ import path43 from "path";
11901
12005
 
11902
12006
  // server/projects.ts
11903
- import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
11904
- import os6 from "os";
11905
- import path41 from "path";
12007
+ import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
12008
+ import os7 from "os";
12009
+ import path42 from "path";
11906
12010
  import crypto6 from "crypto";
11907
- var HOME = os6.homedir();
11908
- var REGISTRY_PATH = path41.join(HOME, ".ctxloom", "repos.json");
12011
+ var HOME = os7.homedir();
12012
+ var REGISTRY_PATH = path42.join(HOME, ".ctxloom", "repos.json");
11909
12013
  function slugFor(root) {
11910
- const abs = path41.resolve(root);
12014
+ const abs = path42.resolve(root);
11911
12015
  return crypto6.createHash("sha1").update(abs).digest("hex").slice(0, 12);
11912
12016
  }
11913
12017
  function readRegistry() {
11914
- if (!existsSync4(REGISTRY_PATH)) return [];
12018
+ if (!existsSync5(REGISTRY_PATH)) return [];
11915
12019
  try {
11916
12020
  const raw = readFileSync4(REGISTRY_PATH, "utf-8");
11917
12021
  const parsed = JSON.parse(raw);
@@ -11922,27 +12026,27 @@ function readRegistry() {
11922
12026
  }
11923
12027
  }
11924
12028
  function listProjects(defaultRoot) {
11925
- const absDefault = path41.resolve(defaultRoot);
12029
+ const absDefault = path42.resolve(defaultRoot);
11926
12030
  const out = [
11927
12031
  {
11928
12032
  slug: slugFor(absDefault),
11929
- name: path41.basename(absDefault) || absDefault,
12033
+ name: path42.basename(absDefault) || absDefault,
11930
12034
  root: absDefault,
11931
12035
  isDefault: true,
11932
- hasSnapshot: existsSync4(path41.join(absDefault, ".ctxloom"))
12036
+ hasSnapshot: existsSync5(path42.join(absDefault, ".ctxloom"))
11933
12037
  }
11934
12038
  ];
11935
12039
  const seen = /* @__PURE__ */ new Set([absDefault]);
11936
12040
  for (const entry of readRegistry()) {
11937
- const abs = path41.resolve(entry.root);
12041
+ const abs = path42.resolve(entry.root);
11938
12042
  if (seen.has(abs)) continue;
11939
12043
  seen.add(abs);
11940
12044
  const item = {
11941
12045
  slug: slugFor(abs),
11942
- name: entry.name ?? (path41.basename(abs) || abs),
12046
+ name: entry.name ?? (path42.basename(abs) || abs),
11943
12047
  root: abs,
11944
12048
  isDefault: false,
11945
- hasSnapshot: existsSync4(path41.join(abs, ".ctxloom"))
12049
+ hasSnapshot: existsSync5(path42.join(abs, ".ctxloom"))
11946
12050
  };
11947
12051
  if (entry.alias !== void 0) item.alias = entry.alias;
11948
12052
  out.push(item);
@@ -11995,7 +12099,7 @@ function buildProjectsRouter(deps) {
11995
12099
  } catch (err) {
11996
12100
  const detail = err instanceof Error ? err.message : String(err);
11997
12101
  res.status(500).json({
11998
- error: `failed to switch to ${path42.basename(target.root)}: ${detail}`
12102
+ error: `failed to switch to ${path43.basename(target.root)}: ${detail}`
11999
12103
  });
12000
12104
  }
12001
12105
  });
@@ -12052,7 +12156,7 @@ function buildTelemetryRouter() {
12052
12156
  }
12053
12157
 
12054
12158
  // server/index.ts
12055
- var __dirname2 = path43.dirname(fileURLToPath2(import.meta.url));
12159
+ var __dirname2 = path44.dirname(fileURLToPath2(import.meta.url));
12056
12160
  async function startDashboard(options) {
12057
12161
  const { root, port, open } = options;
12058
12162
  console.log(`ctxloom dashboard \u2014 loading context from ${root}...`);
@@ -12111,7 +12215,7 @@ async function startDashboard(options) {
12111
12215
  }
12112
12216
  activeWatcher = null;
12113
12217
  }
12114
- const snapshotDir = path43.join(targetRoot, ".ctxloom");
12218
+ const snapshotDir = path44.join(targetRoot, ".ctxloom");
12115
12219
  try {
12116
12220
  activeWatcher = fs32.watch(snapshotDir, (_event, filename) => {
12117
12221
  if (!filename || !filename.includes("snapshot")) return;
@@ -12142,12 +12246,12 @@ async function startDashboard(options) {
12142
12246
  attachSnapshotWatcher(newRoot);
12143
12247
  }
12144
12248
  }));
12145
- const clientDist = path43.join(__dirname2, "../dashboard/client");
12146
- const clientDistExists = fs32.existsSync(path43.join(clientDist, "index.html"));
12249
+ const clientDist = path44.join(__dirname2, "../dashboard/client");
12250
+ const clientDistExists = fs32.existsSync(path44.join(clientDist, "index.html"));
12147
12251
  if (clientDistExists) {
12148
12252
  app.use(express.static(clientDist, { dotfiles: "allow" }));
12149
12253
  app.get(/.*/, (_req, res) => {
12150
- res.sendFile(path43.join(clientDist, "index.html"), { dotfiles: "allow" });
12254
+ res.sendFile(path44.join(clientDist, "index.html"), { dotfiles: "allow" });
12151
12255
  });
12152
12256
  } else {
12153
12257
  app.get(/^\/(?!api\/).*/, (_req, res) => {