ctxloom-pro 1.2.7 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,6 +47,8 @@ The full first-run flow is **one install + one trial + one init per project.** E
47
47
  npm install -g ctxloom-pro
48
48
  ```
49
49
 
50
+ > **For local trial / dev use the unpinned command above is fine.** For unattended CI usage, pin to the exact version (`ctxloom-pro@1.3.1`) so future CLI releases don't silently desync your agent-spec coverage — see the workflow example below.
51
+
50
52
  ### 2 — Start your free trial (once per email)
51
53
 
52
54
  ```bash
@@ -341,7 +343,10 @@ jobs:
341
343
  - uses: actions/checkout@v4
342
344
  - uses: actions/setup-node@v4
343
345
  with: { node-version: '20' }
344
- - run: npm install -g ctxloom-pro
346
+ # Exact pin (not `@^1`) so future CLI releases that add/remove MCP
347
+ # tools don't silently desync your reviewer-agent specs. Bump on
348
+ # every release; see CHANGELOG.md for the live version table.
349
+ - run: npm install -g ctxloom-pro@1.3.1
345
350
  - run: ctxloom index
346
351
  - run: ctxloom rules check --json
347
352
  ```
@@ -502,6 +507,83 @@ Pass `--no-git` to disable the overlay entirely. Tools degrade gracefully — th
502
507
 
503
508
  ---
504
509
 
510
+ ## Response Budgets (v1.2.7+)
511
+
512
+ Twelve source-returning tools accept a server-enforced **token budget**. When a response would exceed the budget, the server auto-substitutes a lighter form (Skeletonizer signature view, summary-only XML, or paths-without-snippets) instead of dumping 50KB of source into your context window.
513
+
514
+ ### Opting in
515
+
516
+ Pass any of these three optional fields to any of the 12 supported tools:
517
+
518
+ ```json
519
+ {
520
+ "max_response_tokens": 4000,
521
+ "on_budget_exceeded": "skeleton",
522
+ "response_format": "auto"
523
+ }
524
+ ```
525
+
526
+ | Field | Values | Default |
527
+ |---|---|---|
528
+ | `max_response_tokens` | positive integer | per-tool (see below) |
529
+ | `on_budget_exceeded` | `"skeleton"` \| `"truncate"` \| `"error"` | `"skeleton"` |
530
+ | `response_format` | `"full"` \| `"skeleton"` \| `"auto"` | `"auto"` |
531
+
532
+ **Back-compat:** when none of these fields are passed, the tool returns its raw response unchanged. Existing callers see zero behavior change.
533
+
534
+ ### Response envelope
535
+
536
+ When you opt in, the response is wrapped in a JSON envelope:
537
+
538
+ ```json
539
+ {
540
+ "data": "<the actual tool output — XML, text, or whatever the tool returns>",
541
+ "meta": {
542
+ "format": "full" | "skeleton" | "truncated",
543
+ "original_tokens_est": 8400,
544
+ "returned_tokens_est": 1600,
545
+ "fallback_reason": null | "budget_exceeded" | "minified_input" | "size_cap" | "skeleton_failed"
546
+ }
547
+ }
548
+ ```
549
+
550
+ ### Supported tools + default budgets
551
+
552
+ Defaults activate only when you opt in (any of the 3 fields above) without specifying `max_response_tokens` explicitly.
553
+
554
+ | Tool | Default | Skeleton fallback |
555
+ |---|---:|---|
556
+ | `ctx_get_file` | 8000 | Skeletonizer view of the file (~90% reduction on TS) |
557
+ | `ctx_get_context_packet` | 6000 | Re-render with the primary file skeletonized |
558
+ | `ctx_get_definition` | 2000 | none — truncate-only (already structural) |
559
+ | `ctx_git_diff_review` | 8000 | Drop `<skeleton>` blocks + omit transitive importers |
560
+ | `ctx_search` | 4000 | Drop content snippets (paths + scores only) |
561
+ | `ctx_full_text_search` | 4000 | Drop match snippets (paths + match counts only) |
562
+ | `ctx_wiki_generate` | 12000 | Downgrade to `detail_level=minimal` |
563
+ | `ctx_find_large_functions` | 2000 | none — truncate-only |
564
+ | `ctx_apply_refactor` | 2000 | none — truncate-only |
565
+ | `ctx_refactor_preview` | 4000 | Drop per-change before/after, keep file summary |
566
+ | `ctx_cross_repo_search` | 4000 | Drop content snippets |
567
+ | `ctx_execution_flow` | 4000 | none — truncate-only |
568
+
569
+ Defaults are **provisional** (derived from the issue's initial table); a future release will re-derive them from real per-tool p75 telemetry once enough usage data accumulates.
570
+
571
+ ### Token estimator
572
+
573
+ Default = `chars / 4` — within ±10% of GPT/Claude tokenizers on code with zero tokenization cost. Pluggable per-tool via the `estimator` option on `BudgetOptions` for callers that need accuracy-critical estimation (e.g. tiktoken).
574
+
575
+ ### Kill switch
576
+
577
+ Set `CTXLOOM_DISABLE_BUDGET=1` in the environment to silently ignore every `max_response_tokens` arg server-wide. Tools behave exactly as in pre-v1.2.7. Documented escape hatch for the soak period.
578
+
579
+ ### Telemetry
580
+
581
+ Set `CTXLOOM_TELEMETRY_LEVEL=full` to emit structured `mcp.budget.exceeded` and `mcp.fallback.used` events to stderr. Useful for tuning defaults against your own usage patterns.
582
+
583
+ > **Note:** `CTXLOOM_TELEMETRY_LEVEL` is also consumed by the license / PostHog telemetry layer (see [Telemetry](#telemetry) below) which only recognizes `all` / `error` / `off`. `full` is a separate, **additive** level — it enables budget-event emission *without narrowing* PostHog scope. To narrow PostHog telemetry, set the variable to `error` or `off`; those values disable budget events as a side effect.
584
+
585
+ ---
586
+
505
587
  ## CLI Commands
506
588
 
507
589
  ```
@@ -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 path44 from "path";
170
- import fs32 from "fs";
169
+ import path45 from "path";
170
+ import fs33 from "fs";
171
171
  import { fileURLToPath as fileURLToPath2 } from "url";
172
172
 
173
173
  // server/loader.ts
174
- import path38 from "path";
174
+ import path39 from "path";
175
175
 
176
176
  // ../../packages/core/src/graph/DependencyGraph.ts
177
177
  import fs7 from "fs";
@@ -3181,8 +3181,8 @@ var CoChangeIndex = class _CoChangeIndex {
3181
3181
  if (event.isBulk || event.isMerge) return;
3182
3182
  const paths = event.files.map((f) => f.path);
3183
3183
  if (paths.length === 0) return;
3184
- for (const path45 of paths) {
3185
- this.nodeCounts.set(path45, (this.nodeCounts.get(path45) ?? 0) + 1);
3184
+ for (const path46 of paths) {
3185
+ this.nodeCounts.set(path46, (this.nodeCounts.get(path46) ?? 0) + 1);
3186
3186
  }
3187
3187
  for (let i = 0; i < paths.length; i++) {
3188
3188
  for (let j = i + 1; j < paths.length; j++) {
@@ -3329,8 +3329,8 @@ var ChurnIndex = class _ChurnIndex {
3329
3329
  */
3330
3330
  snapshot() {
3331
3331
  const nodes = {};
3332
- for (const [path45, raw] of this.nodes) {
3333
- nodes[path45] = {
3332
+ for (const [path46, raw] of this.nodes) {
3333
+ nodes[path46] = {
3334
3334
  commits: raw.commits,
3335
3335
  churnLines: raw.churnLines,
3336
3336
  bugCommits: raw.bugCommits,
@@ -3345,8 +3345,8 @@ var ChurnIndex = class _ChurnIndex {
3345
3345
  */
3346
3346
  static load(s) {
3347
3347
  const idx = new _ChurnIndex();
3348
- for (const [path45, raw] of Object.entries(s.nodes)) {
3349
- idx.nodes.set(path45, {
3348
+ for (const [path46, raw] of Object.entries(s.nodes)) {
3349
+ idx.nodes.set(path46, {
3350
3350
  commits: raw.commits,
3351
3351
  churnLines: raw.churnLines,
3352
3352
  bugCommits: raw.bugCommits,
@@ -3359,8 +3359,8 @@ var ChurnIndex = class _ChurnIndex {
3359
3359
  // -------------------------------------------------------------------------
3360
3360
  // Private helpers
3361
3361
  // -------------------------------------------------------------------------
3362
- getOrCreate(path45) {
3363
- const existing = this.nodes.get(path45);
3362
+ getOrCreate(path46) {
3363
+ const existing = this.nodes.get(path46);
3364
3364
  if (existing !== void 0) return existing;
3365
3365
  const fresh = {
3366
3366
  commits: 0,
@@ -3369,7 +3369,7 @@ var ChurnIndex = class _ChurnIndex {
3369
3369
  authorCounts: {},
3370
3370
  lastTouch: 0
3371
3371
  };
3372
- this.nodes.set(path45, fresh);
3372
+ this.nodes.set(path46, fresh);
3373
3373
  return fresh;
3374
3374
  }
3375
3375
  };
@@ -3450,12 +3450,12 @@ var OwnershipIndex = class _OwnershipIndex {
3450
3450
  */
3451
3451
  snapshot() {
3452
3452
  const nodes = {};
3453
- for (const [path45, raw] of this.nodes) {
3453
+ for (const [path46, raw] of this.nodes) {
3454
3454
  const authorWeights = {};
3455
3455
  for (const [email, entry] of Object.entries(raw.authorWeights)) {
3456
3456
  authorWeights[email] = { ...entry };
3457
3457
  }
3458
- nodes[path45] = { authorWeights, lastTouch: raw.lastTouch };
3458
+ nodes[path46] = { authorWeights, lastTouch: raw.lastTouch };
3459
3459
  }
3460
3460
  return { version: 1, nodes };
3461
3461
  }
@@ -3464,23 +3464,23 @@ var OwnershipIndex = class _OwnershipIndex {
3464
3464
  */
3465
3465
  static load(s) {
3466
3466
  const idx = new _OwnershipIndex();
3467
- for (const [path45, raw] of Object.entries(s.nodes)) {
3467
+ for (const [path46, raw] of Object.entries(s.nodes)) {
3468
3468
  const authorWeights = {};
3469
3469
  for (const [email, entry] of Object.entries(raw.authorWeights)) {
3470
3470
  authorWeights[email] = { ...entry };
3471
3471
  }
3472
- idx.nodes.set(path45, { authorWeights, lastTouch: raw.lastTouch });
3472
+ idx.nodes.set(path46, { authorWeights, lastTouch: raw.lastTouch });
3473
3473
  }
3474
3474
  return idx;
3475
3475
  }
3476
3476
  // -------------------------------------------------------------------------
3477
3477
  // Private helpers
3478
3478
  // -------------------------------------------------------------------------
3479
- getOrCreate(path45) {
3480
- const existing = this.nodes.get(path45);
3479
+ getOrCreate(path46) {
3480
+ const existing = this.nodes.get(path46);
3481
3481
  if (existing !== void 0) return existing;
3482
3482
  const fresh = { authorWeights: {}, lastTouch: 0 };
3483
- this.nodes.set(path45, fresh);
3483
+ this.nodes.set(path46, fresh);
3484
3484
  return fresh;
3485
3485
  }
3486
3486
  };
@@ -4718,8 +4718,8 @@ function getErrorMap() {
4718
4718
 
4719
4719
  // ../../node_modules/zod/v3/helpers/parseUtil.js
4720
4720
  var makeIssue = (params) => {
4721
- const { data, path: path45, errorMaps, issueData } = params;
4722
- const fullPath = [...path45, ...issueData.path || []];
4721
+ const { data, path: path46, errorMaps, issueData } = params;
4722
+ const fullPath = [...path46, ...issueData.path || []];
4723
4723
  const fullIssue = {
4724
4724
  ...issueData,
4725
4725
  path: fullPath
@@ -4835,11 +4835,11 @@ var errorUtil;
4835
4835
 
4836
4836
  // ../../node_modules/zod/v3/types.js
4837
4837
  var ParseInputLazyPath = class {
4838
- constructor(parent, value, path45, key) {
4838
+ constructor(parent, value, path46, key) {
4839
4839
  this._cachedPath = [];
4840
4840
  this.parent = parent;
4841
4841
  this.data = value;
4842
- this._path = path45;
4842
+ this._path = path46;
4843
4843
  this._key = key;
4844
4844
  }
4845
4845
  get path() {
@@ -8294,6 +8294,13 @@ init_embedder();
8294
8294
  // ../../packages/core/src/budget/budget.ts
8295
8295
  init_logger();
8296
8296
 
8297
+ // ../../packages/core/src/budget/eventCollector.ts
8298
+ init_logger();
8299
+ import fs16 from "fs";
8300
+ import os2 from "os";
8301
+ import path16 from "path";
8302
+ var DEFAULT_TELEMETRY_DIR = path16.join(os2.homedir(), ".ctxloom", "telemetry");
8303
+
8297
8304
  // ../../packages/core/src/tools/search.ts
8298
8305
  var Schema = external_exports.object({
8299
8306
  query: external_exports.string().describe("Search query \u2014 natural language or code fragment"),
@@ -8316,7 +8323,7 @@ var Schema2 = external_exports.object({
8316
8323
  });
8317
8324
 
8318
8325
  // ../../packages/core/src/tools/context-packet.ts
8319
- import path16 from "path";
8326
+ import path17 from "path";
8320
8327
  var Schema3 = external_exports.object({
8321
8328
  target_file: external_exports.string().describe("Relative path to the primary file"),
8322
8329
  mode: external_exports.enum(["edit", "read"]).optional().default("edit").describe("Context mode"),
@@ -8328,7 +8335,7 @@ var Schema3 = external_exports.object({
8328
8335
  });
8329
8336
 
8330
8337
  // ../../packages/core/src/tools/findCallers.ts
8331
- import path17 from "path";
8338
+ import path18 from "path";
8332
8339
 
8333
8340
  // ../../packages/core/src/tools/call-graph.ts
8334
8341
  var Schema4 = external_exports.object({
@@ -8429,7 +8436,7 @@ var Schema12 = external_exports.object({
8429
8436
  });
8430
8437
 
8431
8438
  // ../../packages/core/src/tools/knowledge-gaps.ts
8432
- import path18 from "path";
8439
+ import path19 from "path";
8433
8440
  var Schema13 = external_exports.object({
8434
8441
  min_importers: external_exports.number().min(1).max(50).optional().default(3).describe(
8435
8442
  "Minimum importers to qualify as an untested hub (default: 3)"
@@ -8458,7 +8465,7 @@ var Schema14 = external_exports.object({
8458
8465
  });
8459
8466
 
8460
8467
  // ../../packages/core/src/tools/wiki-generate.ts
8461
- import fs16 from "fs";
8468
+ import fs17 from "fs";
8462
8469
  var Schema15 = external_exports.object({
8463
8470
  force: external_exports.boolean().optional().default(false).describe(
8464
8471
  "Regenerate all pages even if content unchanged (default: false)"
@@ -8506,8 +8513,8 @@ var Schema17 = external_exports.object({
8506
8513
  });
8507
8514
 
8508
8515
  // ../../packages/core/src/tools/refactor-preview.ts
8509
- import fs17 from "fs";
8510
- import path19 from "path";
8516
+ import fs18 from "fs";
8517
+ import path20 from "path";
8511
8518
  var Schema18 = external_exports.object({
8512
8519
  symbol: external_exports.string().min(1).describe("Symbol name to rename (exact match, case-sensitive)"),
8513
8520
  new_name: external_exports.string().min(1).describe("New name for the symbol"),
@@ -8542,8 +8549,8 @@ var Schema19 = external_exports.object({
8542
8549
  init_embedder();
8543
8550
  init_VectorStore();
8544
8551
  init_logger();
8545
- import fs18 from "fs";
8546
- import path20 from "path";
8552
+ import fs19 from "fs";
8553
+ import path21 from "path";
8547
8554
  var Schema20 = external_exports.object({
8548
8555
  query: external_exports.string().min(1).describe("Search query \u2014 natural language or code fragment"),
8549
8556
  limit: external_exports.number().min(1).max(100).optional().default(10).describe(
@@ -8560,8 +8567,8 @@ var Schema20 = external_exports.object({
8560
8567
  });
8561
8568
 
8562
8569
  // ../../packages/core/src/tools/apply-refactor.ts
8563
- import fs19 from "fs";
8564
- import path21 from "path";
8570
+ import fs20 from "fs";
8571
+ import path22 from "path";
8565
8572
  var Schema21 = external_exports.object({
8566
8573
  symbol: external_exports.string().min(1).describe("Symbol name to rename (exact, case-sensitive)"),
8567
8574
  new_name: external_exports.string().min(1).describe("New name for the symbol"),
@@ -8594,8 +8601,8 @@ var Schema22 = external_exports.object({
8594
8601
  });
8595
8602
 
8596
8603
  // ../../packages/core/src/tools/full-text-search.ts
8597
- import fs20 from "fs";
8598
- import path22 from "path";
8604
+ import fs21 from "fs";
8605
+ import path23 from "path";
8599
8606
  var Schema23 = external_exports.object({
8600
8607
  query: external_exports.string().min(1).describe("Search term \u2014 literal or /regex/"),
8601
8608
  mode: external_exports.enum(["hybrid", "keyword", "semantic"]).optional().default("hybrid"),
@@ -8627,8 +8634,8 @@ var Schema25 = external_exports.object({
8627
8634
  });
8628
8635
 
8629
8636
  // ../../packages/core/src/tools/graph-snapshot.ts
8630
- import fs21 from "fs";
8631
- import path23 from "path";
8637
+ import fs22 from "fs";
8638
+ import path24 from "path";
8632
8639
  var schema = external_exports.object({
8633
8640
  name: external_exports.string().min(1).max(64).regex(/^[\w.-]+$/, "Name may only contain letters, digits, dots, underscores, hyphens").describe(
8634
8641
  'Snapshot name (e.g. "before-refactor", "v1.0"). Used as the filename.'
@@ -8640,8 +8647,8 @@ var schema = external_exports.object({
8640
8647
  });
8641
8648
 
8642
8649
  // ../../packages/core/src/tools/graph-diff.ts
8643
- import fs22 from "fs";
8644
- import path24 from "path";
8650
+ import fs23 from "fs";
8651
+ import path25 from "path";
8645
8652
  var schema2 = external_exports.object({
8646
8653
  baseline: external_exports.string().min(1).describe('Name of the baseline snapshot (the "before" state).'),
8647
8654
  current: external_exports.string().min(1).describe('Name of the current snapshot (the "after" state).'),
@@ -8682,8 +8689,8 @@ var Schema27 = external_exports.object({
8682
8689
  });
8683
8690
 
8684
8691
  // ../../packages/core/src/rules/loadConfig.ts
8685
- import fs23 from "fs/promises";
8686
- import path25 from "path";
8692
+ import fs24 from "fs/promises";
8693
+ import path26 from "path";
8687
8694
 
8688
8695
  // ../../node_modules/js-yaml/dist/js-yaml.mjs
8689
8696
  function isNothing(subject) {
@@ -11312,24 +11319,24 @@ var Schema29 = external_exports.object({
11312
11319
  });
11313
11320
 
11314
11321
  // ../../packages/core/src/tools/ruleManager.ts
11315
- import fs24 from "fs";
11316
- import path26 from "path";
11317
-
11318
- // ../../packages/core/src/review/AuthorResolver.ts
11319
- import fs25 from "fs/promises";
11322
+ import fs25 from "fs";
11320
11323
  import path27 from "path";
11321
11324
 
11322
- // ../../packages/core/src/review/CodeownersWriter.ts
11325
+ // ../../packages/core/src/review/AuthorResolver.ts
11323
11326
  import fs26 from "fs/promises";
11324
11327
  import path28 from "path";
11325
11328
 
11326
- // ../../packages/core/src/review/loadConfig.ts
11329
+ // ../../packages/core/src/review/CodeownersWriter.ts
11327
11330
  import fs27 from "fs/promises";
11328
11331
  import path29 from "path";
11329
11332
 
11330
- // ../../packages/core/src/security/PathValidator.ts
11333
+ // ../../packages/core/src/review/loadConfig.ts
11334
+ import fs28 from "fs/promises";
11331
11335
  import path30 from "path";
11332
- import fs28 from "fs";
11336
+
11337
+ // ../../packages/core/src/security/PathValidator.ts
11338
+ import path31 from "path";
11339
+ import fs29 from "fs";
11333
11340
  var MAX_FILE_SIZE = 5 * 1024 * 1024;
11334
11341
 
11335
11342
  // ../../packages/core/src/index.ts
@@ -11340,7 +11347,7 @@ init_logger();
11340
11347
 
11341
11348
  // ../../packages/core/src/license/LicenseStore.ts
11342
11349
  import { readFileSync, writeFileSync, unlinkSync, mkdirSync, chmodSync, existsSync } from "fs";
11343
- import path31 from "path";
11350
+ import path32 from "path";
11344
11351
 
11345
11352
  // ../../packages/core/src/license/types.ts
11346
11353
  var FINGERPRINT_RE = /^sha256:[0-9a-f]{64}$/;
@@ -11363,19 +11370,19 @@ var API_BASE = process.env["CTXLOOM_API_BASE"] ?? "https://api.ctxloom.com";
11363
11370
 
11364
11371
  // ../../packages/core/src/license/Fingerprint.ts
11365
11372
  import crypto4 from "crypto";
11366
- import os2 from "os";
11373
+ import os3 from "os";
11367
11374
  import { readFileSync as readFileSync2 } from "fs";
11368
11375
 
11369
11376
  // ../../packages/core/src/license/index.ts
11370
- import os5 from "os";
11377
+ import os6 from "os";
11371
11378
 
11372
11379
  // ../../packages/core/src/license/DistinctIdStore.ts
11373
11380
  import { readFileSync as readFileSync3, writeFileSync as writeFileSync2, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "fs";
11374
- import path32 from "path";
11375
- import os3 from "os";
11381
+ import path33 from "path";
11382
+ import os4 from "os";
11376
11383
  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;
11377
11384
  function distinctIdPath(home) {
11378
- return path32.join(home ?? os3.homedir(), ".ctxloom", "distinct_id");
11385
+ return path33.join(home ?? os4.homedir(), ".ctxloom", "distinct_id");
11379
11386
  }
11380
11387
  function isValidV4(id) {
11381
11388
  return typeof id === "string" && UUID_V4_REGEX.test(id);
@@ -11394,9 +11401,9 @@ function getOrCreateDistinctId(home) {
11394
11401
  }
11395
11402
  const record = {
11396
11403
  id: crypto.randomUUID(),
11397
- alias_pending: os3.hostname()
11404
+ alias_pending: os4.hostname()
11398
11405
  };
11399
- mkdirSync2(path32.dirname(filePath), { recursive: true });
11406
+ mkdirSync2(path33.dirname(filePath), { recursive: true });
11400
11407
  writeFileSync2(filePath, JSON.stringify(record), { mode: 384 });
11401
11408
  return record;
11402
11409
  }
@@ -11422,7 +11429,7 @@ function resolveTelemetryLevel() {
11422
11429
  }
11423
11430
  var TELEMETRY_LEVEL = resolveTelemetryLevel();
11424
11431
  var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
11425
- var CTXLOOM_VERSION = "1.2.7".length > 0 ? "1.2.7" : "dev";
11432
+ var CTXLOOM_VERSION = "1.3.1".length > 0 ? "1.3.1" : "dev";
11426
11433
  var POSTHOG_HOST = "https://eu.i.posthog.com";
11427
11434
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
11428
11435
  var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
@@ -11555,31 +11562,31 @@ function parseStack(stack) {
11555
11562
 
11556
11563
  // ../../packages/core/src/license/FunnelMilestones.ts
11557
11564
  import { existsSync as existsSync3, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3 } from "fs";
11558
- import path33 from "path";
11559
- import os4 from "os";
11565
+ import path34 from "path";
11566
+ import os5 from "os";
11560
11567
 
11561
11568
  // ../../packages/core/src/license/TelemetryNotice.ts
11562
11569
  import { existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync4 } from "fs";
11563
- import path34 from "path";
11564
- import os6 from "os";
11570
+ import path35 from "path";
11571
+ import os7 from "os";
11565
11572
 
11566
11573
  // ../../packages/core/src/server/ProjectState.ts
11567
- import path36 from "path";
11574
+ import path37 from "path";
11568
11575
 
11569
11576
  // ../../packages/core/src/server/projectId.ts
11570
11577
  import crypto5 from "crypto";
11571
- import path35 from "path";
11578
+ import path36 from "path";
11572
11579
 
11573
11580
  // ../../packages/core/src/server/ProjectStateManager.ts
11574
11581
  init_logger();
11575
11582
 
11576
11583
  // ../../packages/core/src/server/resolveProjectRoot.ts
11577
- import fs29 from "fs";
11578
- import path37 from "path";
11584
+ import fs30 from "fs";
11585
+ import path38 from "path";
11579
11586
 
11580
11587
  // server/loader.ts
11581
11588
  async function loadContext(root) {
11582
- const absRoot = path38.resolve(root);
11589
+ const absRoot = path39.resolve(root);
11583
11590
  const overlay = new GitOverlayStore(absRoot);
11584
11591
  const gitEnabled = await overlay.loadSnapshot();
11585
11592
  const graph = new DependencyGraph();
@@ -11832,21 +11839,21 @@ function buildOwnershipRouter(ctx) {
11832
11839
 
11833
11840
  // server/routes/file.ts
11834
11841
  import { Router as Router7 } from "express";
11835
- import fs30 from "fs/promises";
11836
- import path39 from "path";
11842
+ import fs31 from "fs/promises";
11843
+ import path40 from "path";
11837
11844
  function buildFileRouter(ctx) {
11838
11845
  const router = Router7();
11839
11846
  router.get("/", async (req, res) => {
11840
11847
  const rel = req.query.path;
11841
11848
  if (!rel) return res.status(400).json({ error: "missing path" });
11842
- const abs = path39.resolve(ctx.root, rel);
11843
- const rootBoundary = ctx.root.endsWith(path39.sep) ? ctx.root : ctx.root + path39.sep;
11849
+ const abs = path40.resolve(ctx.root, rel);
11850
+ const rootBoundary = ctx.root.endsWith(path40.sep) ? ctx.root : ctx.root + path40.sep;
11844
11851
  if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
11845
11852
  return res.status(403).json({ error: "forbidden" });
11846
11853
  }
11847
11854
  try {
11848
- const content = await fs30.readFile(abs, "utf-8");
11849
- const ext = path39.extname(abs).slice(1);
11855
+ const content = await fs31.readFile(abs, "utf-8");
11856
+ const ext = path40.extname(abs).slice(1);
11850
11857
  res.json({ content, lines: content.split("\n").length, ext });
11851
11858
  } catch {
11852
11859
  res.status(404).json({ error: "not found" });
@@ -11858,7 +11865,7 @@ function buildFileRouter(ctx) {
11858
11865
  // server/routes/open.ts
11859
11866
  import { Router as Router8 } from "express";
11860
11867
  import { execFile as execFile2 } from "child_process";
11861
- import path40 from "path";
11868
+ import path41 from "path";
11862
11869
  function tryOpen(bin, abs) {
11863
11870
  return new Promise((resolve) => {
11864
11871
  execFile2(bin, [abs], { timeout: 5e3 }, (err) => resolve(!err));
@@ -11869,8 +11876,8 @@ function buildOpenRouter(ctx) {
11869
11876
  router.post("/", async (req, res) => {
11870
11877
  const rel = req.body?.path;
11871
11878
  if (!rel || typeof rel !== "string") return res.status(400).json({ error: "missing path" });
11872
- const abs = path40.resolve(ctx.root, rel);
11873
- const rootBoundary = ctx.root.endsWith(path40.sep) ? ctx.root : ctx.root + path40.sep;
11879
+ const abs = path41.resolve(ctx.root, rel);
11880
+ const rootBoundary = ctx.root.endsWith(path41.sep) ? ctx.root : ctx.root + path41.sep;
11874
11881
  if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
11875
11882
  return res.status(403).json({ error: "forbidden" });
11876
11883
  }
@@ -11882,8 +11889,8 @@ function buildOpenRouter(ctx) {
11882
11889
 
11883
11890
  // server/routes/tokens.ts
11884
11891
  import { Router as Router9 } from "express";
11885
- import path41 from "path";
11886
- import fs31 from "fs";
11892
+ import path42 from "path";
11893
+ import fs32 from "fs";
11887
11894
  var CHARS_PER_TOKEN = 4;
11888
11895
  var cache = null;
11889
11896
  function buildTokensRouter(ctx) {
@@ -11898,9 +11905,9 @@ function buildTokensRouter(ctx) {
11898
11905
  let fullChars = 0;
11899
11906
  let skeletonChars = 0;
11900
11907
  for (const file of files) {
11901
- const absPath = path41.join(ctx.root, file);
11908
+ const absPath = path42.join(ctx.root, file);
11902
11909
  try {
11903
- const content = fs31.readFileSync(absPath, "utf-8");
11910
+ const content = fs32.readFileSync(absPath, "utf-8");
11904
11911
  fullChars += content.length;
11905
11912
  const skeleton = await skeletonizer.skeletonize(absPath);
11906
11913
  skeletonChars += skeleton.length;
@@ -12001,17 +12008,17 @@ function buildFileTrendsRouter(ctx) {
12001
12008
 
12002
12009
  // server/routes/projects.ts
12003
12010
  import { Router as Router12 } from "express";
12004
- import path43 from "path";
12011
+ import path44 from "path";
12005
12012
 
12006
12013
  // server/projects.ts
12007
12014
  import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
12008
- import os7 from "os";
12009
- import path42 from "path";
12015
+ import os8 from "os";
12016
+ import path43 from "path";
12010
12017
  import crypto6 from "crypto";
12011
- var HOME = os7.homedir();
12012
- var REGISTRY_PATH = path42.join(HOME, ".ctxloom", "repos.json");
12018
+ var HOME = os8.homedir();
12019
+ var REGISTRY_PATH = path43.join(HOME, ".ctxloom", "repos.json");
12013
12020
  function slugFor(root) {
12014
- const abs = path42.resolve(root);
12021
+ const abs = path43.resolve(root);
12015
12022
  return crypto6.createHash("sha1").update(abs).digest("hex").slice(0, 12);
12016
12023
  }
12017
12024
  function readRegistry() {
@@ -12026,27 +12033,27 @@ function readRegistry() {
12026
12033
  }
12027
12034
  }
12028
12035
  function listProjects(defaultRoot) {
12029
- const absDefault = path42.resolve(defaultRoot);
12036
+ const absDefault = path43.resolve(defaultRoot);
12030
12037
  const out = [
12031
12038
  {
12032
12039
  slug: slugFor(absDefault),
12033
- name: path42.basename(absDefault) || absDefault,
12040
+ name: path43.basename(absDefault) || absDefault,
12034
12041
  root: absDefault,
12035
12042
  isDefault: true,
12036
- hasSnapshot: existsSync5(path42.join(absDefault, ".ctxloom"))
12043
+ hasSnapshot: existsSync5(path43.join(absDefault, ".ctxloom"))
12037
12044
  }
12038
12045
  ];
12039
12046
  const seen = /* @__PURE__ */ new Set([absDefault]);
12040
12047
  for (const entry of readRegistry()) {
12041
- const abs = path42.resolve(entry.root);
12048
+ const abs = path43.resolve(entry.root);
12042
12049
  if (seen.has(abs)) continue;
12043
12050
  seen.add(abs);
12044
12051
  const item = {
12045
12052
  slug: slugFor(abs),
12046
- name: entry.name ?? (path42.basename(abs) || abs),
12053
+ name: entry.name ?? (path43.basename(abs) || abs),
12047
12054
  root: abs,
12048
12055
  isDefault: false,
12049
- hasSnapshot: existsSync5(path42.join(abs, ".ctxloom"))
12056
+ hasSnapshot: existsSync5(path43.join(abs, ".ctxloom"))
12050
12057
  };
12051
12058
  if (entry.alias !== void 0) item.alias = entry.alias;
12052
12059
  out.push(item);
@@ -12099,7 +12106,7 @@ function buildProjectsRouter(deps) {
12099
12106
  } catch (err) {
12100
12107
  const detail = err instanceof Error ? err.message : String(err);
12101
12108
  res.status(500).json({
12102
- error: `failed to switch to ${path43.basename(target.root)}: ${detail}`
12109
+ error: `failed to switch to ${path44.basename(target.root)}: ${detail}`
12103
12110
  });
12104
12111
  }
12105
12112
  });
@@ -12156,7 +12163,7 @@ function buildTelemetryRouter() {
12156
12163
  }
12157
12164
 
12158
12165
  // server/index.ts
12159
- var __dirname2 = path44.dirname(fileURLToPath2(import.meta.url));
12166
+ var __dirname2 = path45.dirname(fileURLToPath2(import.meta.url));
12160
12167
  async function startDashboard(options) {
12161
12168
  const { root, port, open } = options;
12162
12169
  console.log(`ctxloom dashboard \u2014 loading context from ${root}...`);
@@ -12215,9 +12222,9 @@ async function startDashboard(options) {
12215
12222
  }
12216
12223
  activeWatcher = null;
12217
12224
  }
12218
- const snapshotDir = path44.join(targetRoot, ".ctxloom");
12225
+ const snapshotDir = path45.join(targetRoot, ".ctxloom");
12219
12226
  try {
12220
- activeWatcher = fs32.watch(snapshotDir, (_event, filename) => {
12227
+ activeWatcher = fs33.watch(snapshotDir, (_event, filename) => {
12221
12228
  if (!filename || !filename.includes("snapshot")) return;
12222
12229
  if (debounce) clearTimeout(debounce);
12223
12230
  debounce = setTimeout(async () => {
@@ -12246,12 +12253,12 @@ async function startDashboard(options) {
12246
12253
  attachSnapshotWatcher(newRoot);
12247
12254
  }
12248
12255
  }));
12249
- const clientDist = path44.join(__dirname2, "../dashboard/client");
12250
- const clientDistExists = fs32.existsSync(path44.join(clientDist, "index.html"));
12256
+ const clientDist = path45.join(__dirname2, "../dashboard/client");
12257
+ const clientDistExists = fs33.existsSync(path45.join(clientDist, "index.html"));
12251
12258
  if (clientDistExists) {
12252
12259
  app.use(express.static(clientDist, { dotfiles: "allow" }));
12253
12260
  app.get(/.*/, (_req, res) => {
12254
- res.sendFile(path44.join(clientDist, "index.html"), { dotfiles: "allow" });
12261
+ res.sendFile(path45.join(clientDist, "index.html"), { dotfiles: "allow" });
12255
12262
  });
12256
12263
  } else {
12257
12264
  app.get(/^\/(?!api\/).*/, (_req, res) => {
@@ -0,0 +1,116 @@
1
+ // packages/core/src/utils/stats.ts
2
+ function percentile(values, p) {
3
+ if (values.length === 0) return null;
4
+ const sorted = [...values].sort((a, b) => a - b);
5
+ const idx = Math.floor((sorted.length - 1) * p);
6
+ return sorted[idx];
7
+ }
8
+
9
+ // packages/core/src/budget/budgetStats.ts
10
+ function summarize(events, windowStart, windowEnd) {
11
+ const byTool = /* @__PURE__ */ new Map();
12
+ for (const e of events) {
13
+ const existing = byTool.get(e.tool);
14
+ if (existing) existing.push(e);
15
+ else byTool.set(e.tool, [e]);
16
+ }
17
+ const fallbackTable = [];
18
+ const distributionTable = [];
19
+ for (const [tool, bucket] of byTool) {
20
+ const fallbackUsed = bucket.filter((e) => e.event === "mcp.fallback.used");
21
+ if (fallbackUsed.length > 0) {
22
+ let skeleton = 0, truncate = 0, error = 0;
23
+ for (const e of fallbackUsed) {
24
+ const mode = typeof e.mode === "string" ? e.mode : "";
25
+ if (mode === "skeleton" || mode === "skeleton+truncate") skeleton++;
26
+ else if (mode === "truncate" || mode === "truncate-fallback") truncate++;
27
+ else if (mode === "error") error++;
28
+ }
29
+ const total = skeleton + truncate + error;
30
+ if (total > 0) {
31
+ fallbackTable.push({
32
+ tool,
33
+ breaches: fallbackUsed.length,
34
+ skeletonPct: Math.round(skeleton / total * 100),
35
+ truncatePct: Math.round(truncate / total * 100),
36
+ errorPct: Math.round(error / total * 100)
37
+ });
38
+ }
39
+ }
40
+ const tokens = bucket.filter((e) => e.event === "mcp.budget.exceeded").map((e) => typeof e.original_tokens === "number" ? e.original_tokens : null).filter((n) => n !== null);
41
+ if (tokens.length > 0) {
42
+ distributionTable.push({
43
+ tool,
44
+ n: tokens.length,
45
+ min: Math.min(...tokens),
46
+ p50: percentile(tokens, 0.5),
47
+ p75: percentile(tokens, 0.75),
48
+ p95: percentile(tokens, 0.95),
49
+ max: Math.max(...tokens)
50
+ });
51
+ }
52
+ }
53
+ fallbackTable.sort((a, b) => a.tool.localeCompare(b.tool));
54
+ distributionTable.sort((a, b) => a.tool.localeCompare(b.tool));
55
+ return {
56
+ windowStart: windowStart.toISOString(),
57
+ windowEnd: windowEnd.toISOString(),
58
+ totalEvents: events.length,
59
+ fallbackTable,
60
+ distributionTable
61
+ };
62
+ }
63
+ function renderSummary(s) {
64
+ const lines = [];
65
+ const startDate = s.windowStart.slice(0, 10);
66
+ const endDate = s.windowEnd.slice(0, 10);
67
+ lines.push(`Budget event summary (${startDate} \u2192 ${endDate}, ${s.totalEvents} events)`);
68
+ lines.push("");
69
+ if (s.totalEvents === 0) {
70
+ lines.push("No events in window. Either:");
71
+ lines.push(" - No budget breaches occurred (everything fit under budgets)");
72
+ lines.push(` - CTXLOOM_TELEMETRY_LEVEL is not set to "full" in the MCP server's env`);
73
+ lines.push(" - No tool calls have opted into the budget surface yet");
74
+ return lines.join("\n");
75
+ }
76
+ lines.push("Fallback distribution per tool");
77
+ lines.push("");
78
+ if (s.fallbackTable.length === 0) {
79
+ lines.push(" (no fallback events recorded in window)");
80
+ } else {
81
+ lines.push("| Tool | Breaches | Skeleton % | Truncate % | Error % |");
82
+ lines.push("|----------------------------|---------:|-----------:|-----------:|--------:|");
83
+ for (const r of s.fallbackTable) {
84
+ lines.push(
85
+ `| ${r.tool.padEnd(26)} | ${String(r.breaches).padStart(8)} | ${String(r.skeletonPct).padStart(9)}% | ${String(r.truncatePct).padStart(9)}% | ${String(r.errorPct).padStart(6)}% |`
86
+ );
87
+ }
88
+ }
89
+ lines.push("");
90
+ lines.push("Original-token distribution per tool (over-budget calls only)");
91
+ lines.push("");
92
+ if (s.distributionTable.length === 0) {
93
+ lines.push(" (no budget-breach events recorded in window)");
94
+ } else {
95
+ lines.push("| Tool | n | min | p50 | p75 | p95 | max |");
96
+ lines.push("|----------------------------|----:|-------:|-------:|-------:|-------:|-------:|");
97
+ for (const r of s.distributionTable) {
98
+ lines.push(
99
+ `| ${r.tool.padEnd(26)} | ${String(r.n).padStart(3)} | ${fmt(r.min)} | ${fmt(r.p50)} | ${fmt(r.p75)} | ${fmt(r.p95)} | ${fmt(r.max)} |`
100
+ );
101
+ }
102
+ lines.push("");
103
+ lines.push("The **p75** column is the input for per-tool default budget tuning");
104
+ lines.push("(packages/core/src/tools/*.ts \u2192 DEFAULT_MAX_RESPONSE_TOKENS).");
105
+ }
106
+ return lines.join("\n");
107
+ }
108
+ function fmt(n) {
109
+ return n === null ? " \u2014" : String(n).padStart(6);
110
+ }
111
+ export {
112
+ percentile,
113
+ renderSummary,
114
+ summarize
115
+ };
116
+ //# sourceMappingURL=budgetStats-TURA232F.js.map
@@ -0,0 +1,99 @@
1
+ import {
2
+ logger
3
+ } from "./chunk-TYDMSHV7.js";
4
+
5
+ // packages/core/src/budget/eventCollector.ts
6
+ import fs from "fs";
7
+ import os from "os";
8
+ import path from "path";
9
+ var DEFAULT_TELEMETRY_DIR = path.join(os.homedir(), ".ctxloom", "telemetry");
10
+ function telemetryDir() {
11
+ const raw = process.env.CTXLOOM_TELEMETRY_DIR ?? DEFAULT_TELEMETRY_DIR;
12
+ if (raw.includes("..") || !path.isAbsolute(raw)) {
13
+ if (!telemetryDirWarned) {
14
+ telemetryDirWarned = true;
15
+ logger.warn('CTXLOOM_TELEMETRY_DIR rejected \u2014 must be an absolute path with no ".." segments; using default', {
16
+ rejected: raw,
17
+ fallback: DEFAULT_TELEMETRY_DIR
18
+ });
19
+ }
20
+ return DEFAULT_TELEMETRY_DIR;
21
+ }
22
+ return path.resolve(raw);
23
+ }
24
+ var telemetryDirWarned = false;
25
+ function filenameForDate(date) {
26
+ const y = date.getUTCFullYear();
27
+ const m = String(date.getUTCMonth() + 1).padStart(2, "0");
28
+ const d = String(date.getUTCDate()).padStart(2, "0");
29
+ return `budget-events-${y}-${m}-${d}.jsonl`;
30
+ }
31
+ function appendEvent(event, now = /* @__PURE__ */ new Date()) {
32
+ try {
33
+ const dir = telemetryDir();
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ const file = path.join(dir, filenameForDate(now));
36
+ const persisted = { ts: now.toISOString(), ...event };
37
+ fs.appendFileSync(file, JSON.stringify(persisted) + "\n", "utf-8");
38
+ } catch (err) {
39
+ if (!appendFailureWarned) {
40
+ appendFailureWarned = true;
41
+ logger.warn("telemetry sink append failed (further failures suppressed)", {
42
+ error: err instanceof Error ? err.message : String(err)
43
+ });
44
+ }
45
+ }
46
+ }
47
+ var appendFailureWarned = false;
48
+ function __resetTelemetryWarnFlagsForTests() {
49
+ telemetryDirWarned = false;
50
+ appendFailureWarned = false;
51
+ }
52
+ function readEvents(opts = {}) {
53
+ const until = opts.until ?? /* @__PURE__ */ new Date();
54
+ const since = opts.since ?? new Date(until.getTime() - 14 * 24 * 60 * 60 * 1e3);
55
+ const dir = telemetryDir();
56
+ if (!fs.existsSync(dir)) return [];
57
+ const out = [];
58
+ for (let cursor = new Date(Date.UTC(since.getUTCFullYear(), since.getUTCMonth(), since.getUTCDate())); cursor.getTime() <= until.getTime(); cursor = new Date(cursor.getTime() + 24 * 60 * 60 * 1e3)) {
59
+ const file = path.join(dir, filenameForDate(cursor));
60
+ if (!fs.existsSync(file)) continue;
61
+ const text = fs.readFileSync(file, "utf-8");
62
+ for (const line of text.split("\n")) {
63
+ if (line.trim() === "") continue;
64
+ let parsed;
65
+ try {
66
+ parsed = JSON.parse(line);
67
+ } catch {
68
+ continue;
69
+ }
70
+ if (!isPersistedEvent(parsed)) continue;
71
+ const eventTs = new Date(parsed.ts).getTime();
72
+ if (!Number.isFinite(eventTs)) continue;
73
+ if (eventTs < since.getTime() || eventTs > until.getTime()) continue;
74
+ if (opts.tool && parsed.tool !== opts.tool) continue;
75
+ out.push(parsed);
76
+ }
77
+ }
78
+ return out;
79
+ }
80
+ function isPersistedEvent(v) {
81
+ if (!v || typeof v !== "object") return false;
82
+ const o = v;
83
+ return typeof o.ts === "string" && typeof o.event === "string" && typeof o.tool === "string";
84
+ }
85
+ var diskSink = {
86
+ append(event) {
87
+ appendEvent(event);
88
+ }
89
+ };
90
+
91
+ export {
92
+ telemetryDir,
93
+ filenameForDate,
94
+ appendEvent,
95
+ __resetTelemetryWarnFlagsForTests,
96
+ readEvents,
97
+ diskSink
98
+ };
99
+ //# sourceMappingURL=chunk-5I6CJITG.js.map
@@ -5,6 +5,9 @@ import {
5
5
  collectFiles,
6
6
  generateEmbedding
7
7
  } from "./chunk-UVR65QBJ.js";
8
+ import {
9
+ diskSink
10
+ } from "./chunk-5I6CJITG.js";
8
11
  import {
9
12
  logger
10
13
  } from "./chunk-TYDMSHV7.js";
@@ -4634,13 +4637,18 @@ function readBudgetArgs(args) {
4634
4637
  function isBudgetDisabled() {
4635
4638
  return process.env.CTXLOOM_DISABLE_BUDGET === "1";
4636
4639
  }
4637
- function emitTelemetry(event) {
4640
+ function emitTelemetry(event, sink = diskSink) {
4638
4641
  if (process.env.CTXLOOM_TELEMETRY_LEVEL !== "full") return;
4639
4642
  logger.info(event.event, event);
4643
+ try {
4644
+ sink.append(event);
4645
+ } catch {
4646
+ }
4640
4647
  }
4641
4648
  async function enforceBudget(opts) {
4642
4649
  const { full, args, toolName, defaultMaxTokens, skeletonProducer } = opts;
4643
4650
  const estimate = opts.estimator ?? defaultTokenEstimator;
4651
+ const sink = opts.sink ?? opts.ctx?.telemetrySink ?? diskSink;
4644
4652
  const originalTokens = estimate(full);
4645
4653
  if (isBudgetDisabled()) {
4646
4654
  return {
@@ -4706,7 +4714,7 @@ async function enforceBudget(opts) {
4706
4714
  original_tokens: originalTokens,
4707
4715
  budget,
4708
4716
  ratio: originalTokens / budget
4709
- });
4717
+ }, sink);
4710
4718
  const mode = args.on_budget_exceeded ?? "skeleton";
4711
4719
  if (mode === "error") {
4712
4720
  const err = new Error(
@@ -4725,7 +4733,7 @@ async function enforceBudget(opts) {
4725
4733
  tool: toolName,
4726
4734
  fallback_reason: "budget_exceeded",
4727
4735
  mode: "truncate"
4728
- });
4736
+ }, sink);
4729
4737
  return {
4730
4738
  text: sliced2,
4731
4739
  meta: {
@@ -4745,7 +4753,7 @@ async function enforceBudget(opts) {
4745
4753
  tool: toolName,
4746
4754
  fallback_reason: "budget_exceeded",
4747
4755
  mode: "skeleton"
4748
- });
4756
+ }, sink);
4749
4757
  return {
4750
4758
  text: skeleton,
4751
4759
  meta: {
@@ -4762,7 +4770,7 @@ async function enforceBudget(opts) {
4762
4770
  tool: toolName,
4763
4771
  fallback_reason: "budget_exceeded",
4764
4772
  mode: "skeleton+truncate"
4765
- });
4773
+ }, sink);
4766
4774
  return {
4767
4775
  text: slicedSk,
4768
4776
  meta: {
@@ -4779,7 +4787,7 @@ async function enforceBudget(opts) {
4779
4787
  tool: toolName,
4780
4788
  fallback_reason: "skeleton_failed",
4781
4789
  mode: "truncate-fallback"
4782
- });
4790
+ }, sink);
4783
4791
  return {
4784
4792
  text: sliced,
4785
4793
  meta: {
@@ -4887,6 +4895,7 @@ function registerSearchTool(registry, ctx) {
4887
4895
  if (!hasBudgetArgs(args)) return full;
4888
4896
  const skeletonProducer = async () => renderResults(parsed.query, ranked, false);
4889
4897
  const result = await enforceBudget({
4898
+ ctx,
4890
4899
  full,
4891
4900
  args: readBudgetArgs(args),
4892
4901
  toolName: "ctx_search",
@@ -4946,6 +4955,7 @@ function registerFileTool(registry, ctx) {
4946
4955
  const absPath = validator.validate(parsed.path);
4947
4956
  const skeletonizer = await ctx.getSkeletonizer(parsed.project_root);
4948
4957
  const result = await enforceBudget({
4958
+ ctx,
4949
4959
  full,
4950
4960
  args: readBudgetArgs(args),
4951
4961
  toolName: "ctx_get_file",
@@ -5054,6 +5064,7 @@ ${sk}`;
5054
5064
  return renderPacket({ ...parts, primaryContent: primarySkeleton });
5055
5065
  };
5056
5066
  const result = await enforceBudget({
5067
+ ctx,
5057
5068
  full,
5058
5069
  args: readBudgetArgs(args),
5059
5070
  toolName: "ctx_get_context_packet",
@@ -5217,6 +5228,7 @@ function registerDefinitionTool(registry, ctx) {
5217
5228
  }
5218
5229
  if (!hasBudgetArgs(args)) return full;
5219
5230
  const result = await enforceBudget({
5231
+ ctx,
5220
5232
  full,
5221
5233
  args: readBudgetArgs(args),
5222
5234
  toolName: "ctx_get_definition",
@@ -6258,6 +6270,7 @@ function registerWikiGenerateTool(registry, ctx) {
6258
6270
  const full = detail_level === "minimal" ? renderMinimal() : renderStandard();
6259
6271
  if (!hasBudgetArgs(args)) return full;
6260
6272
  const budgetResult = await enforceBudget({
6273
+ ctx,
6261
6274
  full,
6262
6275
  args: readBudgetArgs(args),
6263
6276
  toolName: "ctx_wiki_generate",
@@ -6405,6 +6418,7 @@ function registerGitDiffReviewTool(registry, ctx) {
6405
6418
  const maybeBudget = async (full2, skeletonProducer) => {
6406
6419
  if (!hasBudgetArgs(args)) return full2;
6407
6420
  const result = await enforceBudget({
6421
+ ctx,
6408
6422
  full: full2,
6409
6423
  args: readBudgetArgs(args),
6410
6424
  toolName: "ctx_git_diff_review",
@@ -6622,6 +6636,7 @@ function registerRefactorPreviewTool(registry, ctx) {
6622
6636
  const full = render(true);
6623
6637
  if (!hasBudgetArgs(args)) return full;
6624
6638
  const result = await enforceBudget({
6639
+ ctx,
6625
6640
  full,
6626
6641
  args: readBudgetArgs(args),
6627
6642
  toolName: "ctx_refactor_preview",
@@ -6743,6 +6758,7 @@ function registerExecutionFlowTool(registry, ctx) {
6743
6758
  const maybeBudget = async (full) => {
6744
6759
  if (!hasBudgetArgs(args)) return full;
6745
6760
  const result = await enforceBudget({
6761
+ ctx,
6746
6762
  full,
6747
6763
  args: readBudgetArgs(args),
6748
6764
  toolName: "ctx_execution_flow",
@@ -6899,7 +6915,7 @@ var Schema20 = z22.object({
6899
6915
  function escapeXML19(text) {
6900
6916
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6901
6917
  }
6902
- function registerCrossRepoSearchTool(registry, _ctx, registryFilePath) {
6918
+ function registerCrossRepoSearchTool(registry, ctx, registryFilePath) {
6903
6919
  const repoRegistryPath = registryFilePath ?? path18.join(process.env.HOME ?? process.env.USERPROFILE ?? "", ".ctxloom", "repos.json");
6904
6920
  registry.register(
6905
6921
  "ctx_cross_repo_search",
@@ -7007,6 +7023,7 @@ function registerCrossRepoSearchTool(registry, _ctx, registryFilePath) {
7007
7023
  const full = render(true);
7008
7024
  if (!hasBudgetArgs(args)) return full;
7009
7025
  const result = await enforceBudget({
7026
+ ctx,
7010
7027
  full,
7011
7028
  args: readBudgetArgs(args),
7012
7029
  toolName: "ctx_cross_repo_search",
@@ -7116,6 +7133,7 @@ function registerApplyRefactorTool(registry, ctx) {
7116
7133
  const full = xml.join("\n");
7117
7134
  if (!hasBudgetArgs(args)) return full;
7118
7135
  const result = await enforceBudget({
7136
+ ctx,
7119
7137
  full,
7120
7138
  args: readBudgetArgs(args),
7121
7139
  toolName: "ctx_apply_refactor",
@@ -7333,6 +7351,7 @@ function registerFullTextSearchTool(registry, ctx) {
7333
7351
  const maybeBudget = async (full, skeletonProducer) => {
7334
7352
  if (!hasBudgetArgs(args)) return full;
7335
7353
  const result = await enforceBudget({
7354
+ ctx,
7336
7355
  full,
7337
7356
  args: readBudgetArgs(args),
7338
7357
  toolName: "ctx_full_text_search",
@@ -7897,6 +7916,7 @@ function registerFindLargeFunctionsTool(registry, ctx) {
7897
7916
  }
7898
7917
  if (!hasBudgetArgs(args)) return full;
7899
7918
  const result = await enforceBudget({
7919
+ ctx,
7900
7920
  full,
7901
7921
  args: readBudgetArgs(args),
7902
7922
  toolName: "ctx_find_large_functions",
@@ -9342,7 +9362,7 @@ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
9342
9362
  function getTelemetryLevel() {
9343
9363
  return TELEMETRY_LEVEL;
9344
9364
  }
9345
- var CTXLOOM_VERSION = "1.2.7".length > 0 ? "1.2.7" : "dev";
9365
+ var CTXLOOM_VERSION = "1.3.1".length > 0 ? "1.3.1" : "dev";
9346
9366
  var POSTHOG_HOST = "https://eu.i.posthog.com";
9347
9367
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
9348
9368
  var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
@@ -10072,4 +10092,4 @@ export {
10072
10092
  FirstTouchTracker,
10073
10093
  EmittedOnceTracker
10074
10094
  };
10075
- //# sourceMappingURL=chunk-RY3JAC2Q.js.map
10095
+ //# sourceMappingURL=chunk-TIYTPWYN.js.map
@@ -0,0 +1,18 @@
1
+ import {
2
+ __resetTelemetryWarnFlagsForTests,
3
+ appendEvent,
4
+ diskSink,
5
+ filenameForDate,
6
+ readEvents,
7
+ telemetryDir
8
+ } from "./chunk-5I6CJITG.js";
9
+ import "./chunk-TYDMSHV7.js";
10
+ export {
11
+ __resetTelemetryWarnFlagsForTests,
12
+ appendEvent,
13
+ diskSink,
14
+ filenameForDate,
15
+ readEvents,
16
+ telemetryDir
17
+ };
18
+ //# sourceMappingURL=eventCollector-QSRBVUDF.js.map
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ import {
49
49
  validateDefaultRoot,
50
50
  wrapWithIndexingEnvelope,
51
51
  writeCODEOWNERS
52
- } from "./chunk-RY3JAC2Q.js";
52
+ } from "./chunk-TIYTPWYN.js";
53
53
  import {
54
54
  VectorStore
55
55
  } from "./chunk-DVI2RWJR.js";
@@ -57,6 +57,7 @@ import {
57
57
  generateEmbedding,
58
58
  indexDirectory
59
59
  } from "./chunk-UVR65QBJ.js";
60
+ import "./chunk-5I6CJITG.js";
60
61
  import {
61
62
  logger
62
63
  } from "./chunk-TYDMSHV7.js";
@@ -1018,7 +1019,7 @@ try {
1018
1019
  } catch {
1019
1020
  }
1020
1021
  var args = process.argv.slice(2);
1021
- var ctxloomVersion = "1.2.7".length > 0 ? "1.2.7" : "dev";
1022
+ var ctxloomVersion = "1.3.1".length > 0 ? "1.3.1" : "dev";
1022
1023
  if (args.includes("--version") || args.includes("-v")) {
1023
1024
  process.stdout.write(`ctxloom ${ctxloomVersion}
1024
1025
  `);
@@ -1086,12 +1087,12 @@ function buildActivityFromOverlay(store) {
1086
1087
  lastCommitTimestamp
1087
1088
  }));
1088
1089
  }
1089
- var LICENSE_GATE_BYPASS_COMMANDS = /* @__PURE__ */ new Set(["trial", "activate", "deactivate", "status", "--help"]);
1090
+ var LICENSE_GATE_BYPASS_COMMANDS = /* @__PURE__ */ new Set(["trial", "activate", "deactivate", "status", "budget-stats", "--help"]);
1090
1091
  async function checkLicense() {
1091
1092
  if (command !== void 0 && LICENSE_GATE_BYPASS_COMMANDS.has(command)) return;
1092
1093
  const ciKey = process.env["CTXLOOM_LICENSE_KEY"];
1093
1094
  if (ciKey) {
1094
- const { ApiClient } = await import("./src-3ZB6BHFW.js");
1095
+ const { ApiClient } = await import("./src-KTFHRVTO.js");
1095
1096
  const client = new ApiClient(process.env["CTXLOOM_API_BASE"]);
1096
1097
  try {
1097
1098
  const result = await client.validate(ciKey, "ci-ephemeral");
@@ -1496,7 +1497,7 @@ async function main() {
1496
1497
  process.exit(1);
1497
1498
  }
1498
1499
  if (alias !== void 0) {
1499
- const { validateAlias } = await import("./src-3ZB6BHFW.js");
1500
+ const { validateAlias } = await import("./src-KTFHRVTO.js");
1500
1501
  const v = validateAlias(alias);
1501
1502
  if (!v.ok) {
1502
1503
  console.error(`[ctxloom] Invalid alias: ${v.reason}`);
@@ -1569,6 +1570,23 @@ async function main() {
1569
1570
  }
1570
1571
  break;
1571
1572
  }
1573
+ case "budget-stats": {
1574
+ const windowArg = args.find((a) => a.startsWith("--window="))?.split("=")[1] ?? "14d";
1575
+ const toolArg = args.find((a) => a.startsWith("--tool="))?.split("=")[1];
1576
+ const days = parseInt(windowArg.replace(/d$/, ""), 10);
1577
+ if (!Number.isFinite(days) || days <= 0) {
1578
+ console.error(`[ctxloom] Invalid --window=${windowArg} \u2014 expected an integer day count like 14d`);
1579
+ process.exit(1);
1580
+ }
1581
+ const until = /* @__PURE__ */ new Date();
1582
+ const since = new Date(until.getTime() - days * 24 * 60 * 60 * 1e3);
1583
+ const { readEvents } = await import("./eventCollector-QSRBVUDF.js");
1584
+ const { summarize, renderSummary } = await import("./budgetStats-TURA232F.js");
1585
+ const events = readEvents({ since, until, tool: toolArg });
1586
+ const summary = summarize(events, since, until);
1587
+ console.log(renderSummary(summary));
1588
+ break;
1589
+ }
1572
1590
  case "dashboard": {
1573
1591
  const port = Number(
1574
1592
  args.find((a) => a.startsWith("--port="))?.split("=")[1] ?? "7842"
@@ -1743,7 +1761,7 @@ Suggested reviewers for ${files.length} file(s):`);
1743
1761
  process.stderr.write("[ctxloom] --limit must be a non-negative integer (0 for unlimited)\n");
1744
1762
  process.exit(2);
1745
1763
  }
1746
- const { loadRulesConfig, RulesChecker, formatText, formatJson, RulesConfigError } = await import("./src-3ZB6BHFW.js");
1764
+ const { loadRulesConfig, RulesChecker, formatText, formatJson, RulesConfigError } = await import("./src-KTFHRVTO.js");
1747
1765
  let config;
1748
1766
  try {
1749
1767
  config = await loadRulesConfig(root);
@@ -1767,7 +1785,7 @@ Suggested reviewers for ${files.length} file(s):`);
1767
1785
  }
1768
1786
  let graph;
1769
1787
  if (useSnapshot) {
1770
- const { DependencyGraph: DG } = await import("./src-3ZB6BHFW.js");
1788
+ const { DependencyGraph: DG } = await import("./src-KTFHRVTO.js");
1771
1789
  graph = new DG();
1772
1790
  const loaded = await graph.loadSnapshotOnly(root);
1773
1791
  if (!loaded) {
@@ -1776,7 +1794,7 @@ Suggested reviewers for ${files.length} file(s):`);
1776
1794
  }
1777
1795
  } else {
1778
1796
  process.stderr.write("[ctxloom] Building dependency graph...\n");
1779
- const { ASTParser: ASTParser2, DependencyGraph: DependencyGraph2 } = await import("./src-3ZB6BHFW.js");
1797
+ const { ASTParser: ASTParser2, DependencyGraph: DependencyGraph2 } = await import("./src-KTFHRVTO.js");
1780
1798
  let parser;
1781
1799
  try {
1782
1800
  parser = new ASTParser2();
@@ -1828,6 +1846,9 @@ Usage:
1828
1846
  ctxloom dashboard Start the web dashboard (port 7842)
1829
1847
  ctxloom dashboard --port=N Start on custom port
1830
1848
  ctxloom dashboard --open Open browser automatically
1849
+ ctxloom budget-stats Aggregate Phase B budget events (per-tool p50/p75/p95)
1850
+ ctxloom budget-stats --window=Nd Lookback window in days (default: 14)
1851
+ ctxloom budget-stats --tool=NAME Restrict to one tool
1831
1852
  ctxloom review-suggest [files] Suggest reviewers from ownership index
1832
1853
  ctxloom authors-sync Map git emails to GitHub handles (needs GITHUB_TOKEN)
1833
1854
  ctxloom rules check Check architecture rules (.ctxloom/rules.yml)
@@ -103,7 +103,7 @@ import {
103
103
  validateDefaultRoot,
104
104
  wrapWithIndexingEnvelope,
105
105
  writeCODEOWNERS
106
- } from "./chunk-RY3JAC2Q.js";
106
+ } from "./chunk-TIYTPWYN.js";
107
107
  import {
108
108
  VectorStore
109
109
  } from "./chunk-DVI2RWJR.js";
@@ -113,6 +113,7 @@ import {
113
113
  generateEmbedding,
114
114
  indexDirectory
115
115
  } from "./chunk-UVR65QBJ.js";
116
+ import "./chunk-5I6CJITG.js";
116
117
  import {
117
118
  logger
118
119
  } from "./chunk-TYDMSHV7.js";
@@ -228,4 +229,4 @@ export {
228
229
  wrapWithIndexingEnvelope,
229
230
  writeCODEOWNERS
230
231
  };
231
- //# sourceMappingURL=src-3ZB6BHFW.js.map
232
+ //# sourceMappingURL=src-KTFHRVTO.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctxloom-pro",
3
- "version": "1.2.7",
3
+ "version": "1.3.1",
4
4
  "description": "ctxloom — The Universal Code Context Engine. A local-first MCP server providing intelligent code context via hybrid Vector + AST + Graph search with Skeletonization (92% token reduction).",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",