ctxloom-pro 1.3.1 → 1.4.0

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,7 +47,7 @@ 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.
50
+ > **For local trial / dev use the unpinned command above is fine.** For unattended CI usage, pin to the exact version (`ctxloom-pro@1.4.0`) so future CLI releases don't silently desync your agent-spec coverage — see the workflow example below.
51
51
 
52
52
  ### 2 — Start your free trial (once per email)
53
53
 
@@ -346,7 +346,7 @@ jobs:
346
346
  # Exact pin (not `@^1`) so future CLI releases that add/remove MCP
347
347
  # tools don't silently desync your reviewer-agent specs. Bump on
348
348
  # every release; see CHANGELOG.md for the live version table.
349
- - run: npm install -g ctxloom-pro@1.3.1
349
+ - run: npm install -g ctxloom-pro@1.4.0
350
350
  - run: ctxloom index
351
351
  - run: ctxloom rules check --json
352
352
  ```
@@ -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 path45 from "path";
170
- import fs33 from "fs";
169
+ import path46 from "path";
170
+ import fs34 from "fs";
171
171
  import { fileURLToPath as fileURLToPath2 } from "url";
172
172
 
173
173
  // server/loader.ts
174
- import path39 from "path";
174
+ import path40 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 path46 of paths) {
3185
- this.nodeCounts.set(path46, (this.nodeCounts.get(path46) ?? 0) + 1);
3184
+ for (const path47 of paths) {
3185
+ this.nodeCounts.set(path47, (this.nodeCounts.get(path47) ?? 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 [path46, raw] of this.nodes) {
3333
- nodes[path46] = {
3332
+ for (const [path47, raw] of this.nodes) {
3333
+ nodes[path47] = {
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 [path46, raw] of Object.entries(s.nodes)) {
3349
- idx.nodes.set(path46, {
3348
+ for (const [path47, raw] of Object.entries(s.nodes)) {
3349
+ idx.nodes.set(path47, {
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(path46) {
3363
- const existing = this.nodes.get(path46);
3362
+ getOrCreate(path47) {
3363
+ const existing = this.nodes.get(path47);
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(path46, fresh);
3372
+ this.nodes.set(path47, 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 [path46, raw] of this.nodes) {
3453
+ for (const [path47, 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[path46] = { authorWeights, lastTouch: raw.lastTouch };
3458
+ nodes[path47] = { 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 [path46, raw] of Object.entries(s.nodes)) {
3467
+ for (const [path47, 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(path46, { authorWeights, lastTouch: raw.lastTouch });
3472
+ idx.nodes.set(path47, { authorWeights, lastTouch: raw.lastTouch });
3473
3473
  }
3474
3474
  return idx;
3475
3475
  }
3476
3476
  // -------------------------------------------------------------------------
3477
3477
  // Private helpers
3478
3478
  // -------------------------------------------------------------------------
3479
- getOrCreate(path46) {
3480
- const existing = this.nodes.get(path46);
3479
+ getOrCreate(path47) {
3480
+ const existing = this.nodes.get(path47);
3481
3481
  if (existing !== void 0) return existing;
3482
3482
  const fresh = { authorWeights: {}, lastTouch: 0 };
3483
- this.nodes.set(path46, fresh);
3483
+ this.nodes.set(path47, 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: path46, errorMaps, issueData } = params;
4722
- const fullPath = [...path46, ...issueData.path || []];
4721
+ const { data, path: path47, errorMaps, issueData } = params;
4722
+ const fullPath = [...path47, ...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, path46, key) {
4838
+ constructor(parent, value, path47, key) {
4839
4839
  this._cachedPath = [];
4840
4840
  this.parent = parent;
4841
4841
  this.data = value;
4842
- this._path = path46;
4842
+ this._path = path47;
4843
4843
  this._key = key;
4844
4844
  }
4845
4845
  get path() {
@@ -11318,6 +11318,18 @@ var Schema29 = external_exports.object({
11318
11318
  project_root: ProjectRootField
11319
11319
  });
11320
11320
 
11321
+ // ../../packages/core/src/tools/minimal-context.ts
11322
+ import { execSync } from "child_process";
11323
+ var Schema30 = external_exports.object({
11324
+ task: external_exports.string().max(200).optional().describe(
11325
+ "Free-text description of what you're about to do (e.g. 'review PR 142', 'rename emitTelemetry'). The tool routes by regex to the most-fitting suggested-first-tool. Capped at 200 chars; control characters stripped."
11326
+ ),
11327
+ project_root: ProjectRootField,
11328
+ max_response_tokens: external_exports.number().int().positive().optional(),
11329
+ on_budget_exceeded: external_exports.enum(["skeleton", "truncate", "error"]).optional(),
11330
+ response_format: external_exports.enum(["full", "skeleton", "auto"]).optional()
11331
+ });
11332
+
11321
11333
  // ../../packages/core/src/tools/ruleManager.ts
11322
11334
  import fs25 from "fs";
11323
11335
  import path27 from "path";
@@ -11429,7 +11441,7 @@ function resolveTelemetryLevel() {
11429
11441
  }
11430
11442
  var TELEMETRY_LEVEL = resolveTelemetryLevel();
11431
11443
  var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
11432
- var CTXLOOM_VERSION = "1.3.1".length > 0 ? "1.3.1" : "dev";
11444
+ var CTXLOOM_VERSION = "1.4.0".length > 0 ? "1.4.0" : "dev";
11433
11445
  var POSTHOG_HOST = "https://eu.i.posthog.com";
11434
11446
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
11435
11447
  var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
@@ -11584,9 +11596,57 @@ init_logger();
11584
11596
  import fs30 from "fs";
11585
11597
  import path38 from "path";
11586
11598
 
11599
+ // ../../packages/core/src/install/installer.ts
11600
+ import fs31 from "fs";
11601
+ import path39 from "path";
11602
+
11603
+ // ../../packages/core/src/install/templates.ts
11604
+ var SESSION_START_HEADER = `#!/usr/bin/env bash
11605
+ # ctxloom \u2014 agent-harness session-start hook
11606
+ # Generated by \`ctxloom init\`. Re-run \`ctxloom init\` to update.
11607
+ # Manual edits will be overwritten on the next install.
11608
+
11609
+ set -e
11610
+ `;
11611
+ var SESSION_START_BODY = `DB=".ctxloom/graph.db"
11612
+
11613
+ if [ -f "$DB" ]; then
11614
+ # \`ctxloom status --json\` is cached + sub-100ms \u2014 keeps the hook
11615
+ # under its 2s timeout even on cold disk.
11616
+ STATS=$(ctxloom status --json 2>/dev/null || echo '{"nodes":0,"edges":0}')
11617
+ NODES=$(echo "$STATS" | grep -oE '"nodes":\\s*[0-9]+' | grep -oE '[0-9]+' || echo "?")
11618
+ EDGES=$(echo "$STATS" | grep -oE '"edges":\\s*[0-9]+' | grep -oE '[0-9]+' || echo "?")
11619
+
11620
+ cat <<EOF
11621
+ [ctxloom] Knowledge graph ready (\${NODES} nodes, \${EDGES} edges).
11622
+
11623
+ Start every workflow with \\\`ctx_get_minimal_context(task="...")\\\`.
11624
+ It returns ~150 tokens of orientation + a task-aware
11625
+ suggested_first_tool. Follow the meta.next_tool_suggestions on
11626
+ every response.
11627
+
11628
+ Prefer ctxloom MCP tools over Grep/Glob/Read:
11629
+ - ctx_detect_changes for code review
11630
+ - ctx_blast_radius / ctx_get_call_graph before refactoring
11631
+ - ctx_architecture_overview for orientation
11632
+ EOF
11633
+ else
11634
+ cat <<EOF
11635
+ [ctxloom] No knowledge graph found here.
11636
+ Run: ctxloom build
11637
+
11638
+ Then restart this session to enable graph-powered queries.
11639
+ EOF
11640
+ fi
11641
+ `;
11642
+ var SESSION_START_FULL = SESSION_START_HEADER + "\n" + SESSION_START_BODY;
11643
+
11644
+ // ../../packages/core/src/install/hmacBlock.ts
11645
+ import crypto6 from "crypto";
11646
+
11587
11647
  // server/loader.ts
11588
11648
  async function loadContext(root) {
11589
- const absRoot = path39.resolve(root);
11649
+ const absRoot = path40.resolve(root);
11590
11650
  const overlay = new GitOverlayStore(absRoot);
11591
11651
  const gitEnabled = await overlay.loadSnapshot();
11592
11652
  const graph = new DependencyGraph();
@@ -11839,21 +11899,21 @@ function buildOwnershipRouter(ctx) {
11839
11899
 
11840
11900
  // server/routes/file.ts
11841
11901
  import { Router as Router7 } from "express";
11842
- import fs31 from "fs/promises";
11843
- import path40 from "path";
11902
+ import fs32 from "fs/promises";
11903
+ import path41 from "path";
11844
11904
  function buildFileRouter(ctx) {
11845
11905
  const router = Router7();
11846
11906
  router.get("/", async (req, res) => {
11847
11907
  const rel = req.query.path;
11848
11908
  if (!rel) return res.status(400).json({ error: "missing path" });
11849
- const abs = path40.resolve(ctx.root, rel);
11850
- const rootBoundary = ctx.root.endsWith(path40.sep) ? ctx.root : ctx.root + path40.sep;
11909
+ const abs = path41.resolve(ctx.root, rel);
11910
+ const rootBoundary = ctx.root.endsWith(path41.sep) ? ctx.root : ctx.root + path41.sep;
11851
11911
  if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
11852
11912
  return res.status(403).json({ error: "forbidden" });
11853
11913
  }
11854
11914
  try {
11855
- const content = await fs31.readFile(abs, "utf-8");
11856
- const ext = path40.extname(abs).slice(1);
11915
+ const content = await fs32.readFile(abs, "utf-8");
11916
+ const ext = path41.extname(abs).slice(1);
11857
11917
  res.json({ content, lines: content.split("\n").length, ext });
11858
11918
  } catch {
11859
11919
  res.status(404).json({ error: "not found" });
@@ -11865,7 +11925,7 @@ function buildFileRouter(ctx) {
11865
11925
  // server/routes/open.ts
11866
11926
  import { Router as Router8 } from "express";
11867
11927
  import { execFile as execFile2 } from "child_process";
11868
- import path41 from "path";
11928
+ import path42 from "path";
11869
11929
  function tryOpen(bin, abs) {
11870
11930
  return new Promise((resolve) => {
11871
11931
  execFile2(bin, [abs], { timeout: 5e3 }, (err) => resolve(!err));
@@ -11876,8 +11936,8 @@ function buildOpenRouter(ctx) {
11876
11936
  router.post("/", async (req, res) => {
11877
11937
  const rel = req.body?.path;
11878
11938
  if (!rel || typeof rel !== "string") return res.status(400).json({ error: "missing path" });
11879
- const abs = path41.resolve(ctx.root, rel);
11880
- const rootBoundary = ctx.root.endsWith(path41.sep) ? ctx.root : ctx.root + path41.sep;
11939
+ const abs = path42.resolve(ctx.root, rel);
11940
+ const rootBoundary = ctx.root.endsWith(path42.sep) ? ctx.root : ctx.root + path42.sep;
11881
11941
  if (abs !== ctx.root && !abs.startsWith(rootBoundary)) {
11882
11942
  return res.status(403).json({ error: "forbidden" });
11883
11943
  }
@@ -11889,8 +11949,8 @@ function buildOpenRouter(ctx) {
11889
11949
 
11890
11950
  // server/routes/tokens.ts
11891
11951
  import { Router as Router9 } from "express";
11892
- import path42 from "path";
11893
- import fs32 from "fs";
11952
+ import path43 from "path";
11953
+ import fs33 from "fs";
11894
11954
  var CHARS_PER_TOKEN = 4;
11895
11955
  var cache = null;
11896
11956
  function buildTokensRouter(ctx) {
@@ -11905,9 +11965,9 @@ function buildTokensRouter(ctx) {
11905
11965
  let fullChars = 0;
11906
11966
  let skeletonChars = 0;
11907
11967
  for (const file of files) {
11908
- const absPath = path42.join(ctx.root, file);
11968
+ const absPath = path43.join(ctx.root, file);
11909
11969
  try {
11910
- const content = fs32.readFileSync(absPath, "utf-8");
11970
+ const content = fs33.readFileSync(absPath, "utf-8");
11911
11971
  fullChars += content.length;
11912
11972
  const skeleton = await skeletonizer.skeletonize(absPath);
11913
11973
  skeletonChars += skeleton.length;
@@ -12008,18 +12068,18 @@ function buildFileTrendsRouter(ctx) {
12008
12068
 
12009
12069
  // server/routes/projects.ts
12010
12070
  import { Router as Router12 } from "express";
12011
- import path44 from "path";
12071
+ import path45 from "path";
12012
12072
 
12013
12073
  // server/projects.ts
12014
12074
  import { existsSync as existsSync5, readFileSync as readFileSync4 } from "fs";
12015
12075
  import os8 from "os";
12016
- import path43 from "path";
12017
- import crypto6 from "crypto";
12076
+ import path44 from "path";
12077
+ import crypto7 from "crypto";
12018
12078
  var HOME = os8.homedir();
12019
- var REGISTRY_PATH = path43.join(HOME, ".ctxloom", "repos.json");
12079
+ var REGISTRY_PATH = path44.join(HOME, ".ctxloom", "repos.json");
12020
12080
  function slugFor(root) {
12021
- const abs = path43.resolve(root);
12022
- return crypto6.createHash("sha1").update(abs).digest("hex").slice(0, 12);
12081
+ const abs = path44.resolve(root);
12082
+ return crypto7.createHash("sha1").update(abs).digest("hex").slice(0, 12);
12023
12083
  }
12024
12084
  function readRegistry() {
12025
12085
  if (!existsSync5(REGISTRY_PATH)) return [];
@@ -12033,27 +12093,27 @@ function readRegistry() {
12033
12093
  }
12034
12094
  }
12035
12095
  function listProjects(defaultRoot) {
12036
- const absDefault = path43.resolve(defaultRoot);
12096
+ const absDefault = path44.resolve(defaultRoot);
12037
12097
  const out = [
12038
12098
  {
12039
12099
  slug: slugFor(absDefault),
12040
- name: path43.basename(absDefault) || absDefault,
12100
+ name: path44.basename(absDefault) || absDefault,
12041
12101
  root: absDefault,
12042
12102
  isDefault: true,
12043
- hasSnapshot: existsSync5(path43.join(absDefault, ".ctxloom"))
12103
+ hasSnapshot: existsSync5(path44.join(absDefault, ".ctxloom"))
12044
12104
  }
12045
12105
  ];
12046
12106
  const seen = /* @__PURE__ */ new Set([absDefault]);
12047
12107
  for (const entry of readRegistry()) {
12048
- const abs = path43.resolve(entry.root);
12108
+ const abs = path44.resolve(entry.root);
12049
12109
  if (seen.has(abs)) continue;
12050
12110
  seen.add(abs);
12051
12111
  const item = {
12052
12112
  slug: slugFor(abs),
12053
- name: entry.name ?? (path43.basename(abs) || abs),
12113
+ name: entry.name ?? (path44.basename(abs) || abs),
12054
12114
  root: abs,
12055
12115
  isDefault: false,
12056
- hasSnapshot: existsSync5(path43.join(abs, ".ctxloom"))
12116
+ hasSnapshot: existsSync5(path44.join(abs, ".ctxloom"))
12057
12117
  };
12058
12118
  if (entry.alias !== void 0) item.alias = entry.alias;
12059
12119
  out.push(item);
@@ -12106,7 +12166,7 @@ function buildProjectsRouter(deps) {
12106
12166
  } catch (err) {
12107
12167
  const detail = err instanceof Error ? err.message : String(err);
12108
12168
  res.status(500).json({
12109
- error: `failed to switch to ${path44.basename(target.root)}: ${detail}`
12169
+ error: `failed to switch to ${path45.basename(target.root)}: ${detail}`
12110
12170
  });
12111
12171
  }
12112
12172
  });
@@ -12163,7 +12223,7 @@ function buildTelemetryRouter() {
12163
12223
  }
12164
12224
 
12165
12225
  // server/index.ts
12166
- var __dirname2 = path45.dirname(fileURLToPath2(import.meta.url));
12226
+ var __dirname2 = path46.dirname(fileURLToPath2(import.meta.url));
12167
12227
  async function startDashboard(options) {
12168
12228
  const { root, port, open } = options;
12169
12229
  console.log(`ctxloom dashboard \u2014 loading context from ${root}...`);
@@ -12222,9 +12282,9 @@ async function startDashboard(options) {
12222
12282
  }
12223
12283
  activeWatcher = null;
12224
12284
  }
12225
- const snapshotDir = path45.join(targetRoot, ".ctxloom");
12285
+ const snapshotDir = path46.join(targetRoot, ".ctxloom");
12226
12286
  try {
12227
- activeWatcher = fs33.watch(snapshotDir, (_event, filename) => {
12287
+ activeWatcher = fs34.watch(snapshotDir, (_event, filename) => {
12228
12288
  if (!filename || !filename.includes("snapshot")) return;
12229
12289
  if (debounce) clearTimeout(debounce);
12230
12290
  debounce = setTimeout(async () => {
@@ -12253,12 +12313,12 @@ async function startDashboard(options) {
12253
12313
  attachSnapshotWatcher(newRoot);
12254
12314
  }
12255
12315
  }));
12256
- const clientDist = path45.join(__dirname2, "../dashboard/client");
12257
- const clientDistExists = fs33.existsSync(path45.join(clientDist, "index.html"));
12316
+ const clientDist = path46.join(__dirname2, "../dashboard/client");
12317
+ const clientDistExists = fs34.existsSync(path46.join(clientDist, "index.html"));
12258
12318
  if (clientDistExists) {
12259
12319
  app.use(express.static(clientDist, { dotfiles: "allow" }));
12260
12320
  app.get(/.*/, (_req, res) => {
12261
- res.sendFile(path45.join(clientDist, "index.html"), { dotfiles: "allow" });
12321
+ res.sendFile(path46.join(clientDist, "index.html"), { dotfiles: "allow" });
12262
12322
  });
12263
12323
  } else {
12264
12324
  app.get(/^\/(?!api\/).*/, (_req, res) => {