ctxloom-pro 1.4.0 → 1.5.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,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.4.0`) 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.5.1`) 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.4.0
349
+ - run: npm install -g ctxloom-pro@1.5.1
350
350
  - run: ctxloom index
351
351
  - run: ctxloom rules check --json
352
352
  ```
@@ -8288,9 +8288,6 @@ var ProjectRootField = external_exports.string().optional().describe(PROJECT_ROO
8288
8288
  // ../../packages/core/src/tools/registry.ts
8289
8289
  init_logger();
8290
8290
 
8291
- // ../../packages/core/src/tools/search.ts
8292
- init_embedder();
8293
-
8294
8291
  // ../../packages/core/src/budget/budget.ts
8295
8292
  init_logger();
8296
8293
 
@@ -8301,7 +8298,21 @@ import os2 from "os";
8301
8298
  import path16 from "path";
8302
8299
  var DEFAULT_TELEMETRY_DIR = path16.join(os2.homedir(), ".ctxloom", "telemetry");
8303
8300
 
8301
+ // ../../packages/core/src/budget/learnedSuggestions.ts
8302
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
8303
+
8304
+ // ../../packages/core/src/budget/taskBudget.ts
8305
+ var OVER_BUDGET_ARG_OVERRIDES = Object.freeze({
8306
+ // Budget-surface tools (the 12 source-returning ones).
8307
+ max_response_tokens: 200,
8308
+ response_format: "skeleton",
8309
+ on_budget_exceeded: "skeleton",
8310
+ // Tools with detail_level (hub_nodes, bridge_nodes, etc).
8311
+ detail_level: "minimal"
8312
+ });
8313
+
8304
8314
  // ../../packages/core/src/tools/search.ts
8315
+ init_embedder();
8305
8316
  var Schema = external_exports.object({
8306
8317
  query: external_exports.string().describe("Search query \u2014 natural language or code fragment"),
8307
8318
  limit: external_exports.number().max(100).optional().default(10).describe("Maximum results to return"),
@@ -11441,7 +11452,7 @@ function resolveTelemetryLevel() {
11441
11452
  }
11442
11453
  var TELEMETRY_LEVEL = resolveTelemetryLevel();
11443
11454
  var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
11444
- var CTXLOOM_VERSION = "1.4.0".length > 0 ? "1.4.0" : "dev";
11455
+ var CTXLOOM_VERSION = "1.5.1".length > 0 ? "1.5.1" : "dev";
11445
11456
  var POSTHOG_HOST = "https://eu.i.posthog.com";
11446
11457
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
11447
11458
  var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
@@ -11601,6 +11612,72 @@ import fs31 from "fs";
11601
11612
  import path39 from "path";
11602
11613
 
11603
11614
  // ../../packages/core/src/install/templates.ts
11615
+ var RULES_BLOCK_CONTENT = `## MCP Tools: ctxloom
11616
+
11617
+ **IMPORTANT: This project has a knowledge graph. ALWAYS use the
11618
+ ctxloom MCP tools BEFORE Grep/Glob/Read to explore the codebase.**
11619
+ The graph is faster, cheaper (fewer tokens), and gives you
11620
+ structural context (callers, dependents, test coverage) that file
11621
+ scanning cannot.
11622
+
11623
+ ### Start every workflow with \`ctx_get_minimal_context\`
11624
+
11625
+ The first MCP call into ctxloom should always be
11626
+ \`ctx_get_minimal_context(task="<what you're about to do>")\`. It
11627
+ returns ~150 tokens of orientation plus a task-aware
11628
+ \`suggested_first_tool\` you should call next instead of guessing.
11629
+
11630
+ ### When to use graph tools FIRST
11631
+
11632
+ - **Code review**: \`ctx_detect_changes\` + \`ctx_get_review_context\`
11633
+ instead of reading whole files
11634
+ - **Understanding impact**: \`ctx_blast_radius\` + \`ctx_get_affected_flows\`
11635
+ instead of manually tracing imports
11636
+ - **Refactor planning**: \`ctx_get_call_graph(direction: 'callers')\`
11637
+ + \`ctx_refactor_preview\` before any rename
11638
+ - **Architecture questions**: \`ctx_architecture_overview\`,
11639
+ \`ctx_community_list\`, \`ctx_hub_nodes\`
11640
+ - **Finding code**: \`ctx_search\` or \`ctx_full_text_search\` instead
11641
+ of \`Grep\`
11642
+
11643
+ Fall back to Grep/Glob/Read **only** when the graph doesn't cover
11644
+ what you need.
11645
+
11646
+ ### Follow the \`next_tool_suggestions\` in every response
11647
+
11648
+ Every budget-wrapped ctxloom response includes
11649
+ \`meta.next_tool_suggestions\` \u2014 author-curated follow-ups with
11650
+ \`why\` reasoning and \`estimated_tokens\` per entry. Pick from
11651
+ those instead of guessing.
11652
+
11653
+ ### Token-budget protocol
11654
+
11655
+ - Target: \u22648 tool calls per task, \u22642000 total tokens of graph context
11656
+ - Pass \`max_response_tokens\` on calls that might return large
11657
+ responses; the budget surface returns skeletons instead of dumping
11658
+ - Use \`response_format: 'skeleton'\` when you know you only need
11659
+ signatures, not bodies
11660
+
11661
+ ### Key tools at a glance
11662
+
11663
+ | Tool | Use when |
11664
+ |------|----------|
11665
+ | \`ctx_get_minimal_context\` | START HERE \u2014 orientation anchor |
11666
+ | \`ctx_detect_changes\` | Reviewing code changes; risk-scored |
11667
+ | \`ctx_get_review_context\` | Token-efficient review snippets |
11668
+ | \`ctx_blast_radius\` | Blast radius of a change |
11669
+ | \`ctx_get_affected_flows\` | Execution paths impacted |
11670
+ | \`ctx_get_call_graph\` | Callers / callees of a symbol |
11671
+ | \`ctx_search\` / \`ctx_full_text_search\` | Find code |
11672
+ | \`ctx_architecture_overview\` | High-level codebase map |
11673
+ | \`ctx_refactor_preview\` / \`ctx_apply_refactor\` | Plan a rename |
11674
+
11675
+ ### Hooks keep the graph fresh
11676
+
11677
+ \`ctxloom init\` installed a PostToolUse hook on \`Write|Edit\` that
11678
+ runs \`ctxloom update --incremental --quiet\` \u2014 so the graph is
11679
+ always up to date when you query it. No "did the index update yet?"
11680
+ guessing.`;
11604
11681
  var SESSION_START_HEADER = `#!/usr/bin/env bash
11605
11682
  # ctxloom \u2014 agent-harness session-start hook
11606
11683
  # Generated by \`ctxloom init\`. Re-run \`ctxloom init\` to update.
@@ -11640,6 +11717,69 @@ EOF
11640
11717
  fi
11641
11718
  `;
11642
11719
  var SESSION_START_FULL = SESSION_START_HEADER + "\n" + SESSION_START_BODY;
11720
+ function plainBody() {
11721
+ return RULES_BLOCK_CONTENT.replace(/^##\s+/gm, "").replace(/^###\s+/gm, "").replace(/^####\s+/gm, "").replace(/\*\*(.*?)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1");
11722
+ }
11723
+ var CURSOR_HEADER = `# ctxloom \u2014 Cursor agent rules
11724
+ # Generated by \`ctxloom init --host=cursor\`. Re-run to update.
11725
+
11726
+ `;
11727
+ var AIDER_HEADER = `# Project Conventions
11728
+
11729
+ Generated by \`ctxloom init --host=aider\`. Re-run to update.
11730
+
11731
+ `;
11732
+ var COPILOT_HEADER = `# ctxloom \u2014 GitHub Copilot instructions
11733
+
11734
+ Generated by \`ctxloom init --host=copilot\`. Re-run to update.
11735
+
11736
+ `;
11737
+ var WINDSURF_HEADER = `# ctxloom \u2014 Windsurf agent rules
11738
+ # Generated by \`ctxloom init --host=windsurf\`. Re-run to update.
11739
+
11740
+ `;
11741
+ var HOST_ADAPTERS = [
11742
+ // Note: claude / agents / gemini are NOT defined as HostAdapters in
11743
+ // this list — they predate the matrix and live in their own writeRulesBlock
11744
+ // path with HMAC-wrapped Markdown blocks. Future refactor may unify.
11745
+ {
11746
+ id: "cursor",
11747
+ path: ".cursorrules",
11748
+ defaultEnabled: false,
11749
+ render: () => CURSOR_HEADER + plainBody() + "\n",
11750
+ isCanonical(current) {
11751
+ return current === this.render();
11752
+ }
11753
+ },
11754
+ {
11755
+ id: "aider",
11756
+ path: "CONVENTIONS.md",
11757
+ defaultEnabled: false,
11758
+ render: () => AIDER_HEADER + RULES_BLOCK_CONTENT + "\n",
11759
+ isCanonical(current) {
11760
+ return current === this.render();
11761
+ }
11762
+ },
11763
+ {
11764
+ id: "copilot",
11765
+ path: ".github/copilot-instructions.md",
11766
+ defaultEnabled: false,
11767
+ render: () => COPILOT_HEADER + RULES_BLOCK_CONTENT + "\n",
11768
+ isCanonical(current) {
11769
+ return current === this.render();
11770
+ }
11771
+ },
11772
+ {
11773
+ id: "windsurf",
11774
+ path: ".windsurfrules",
11775
+ defaultEnabled: false,
11776
+ render: () => WINDSURF_HEADER + plainBody() + "\n",
11777
+ isCanonical(current) {
11778
+ return current === this.render();
11779
+ }
11780
+ }
11781
+ ];
11782
+ var SUPPORTED_HOST_IDS = HOST_ADAPTERS.map((h) => h.id);
11643
11783
 
11644
11784
  // ../../packages/core/src/install/hmacBlock.ts
11645
11785
  import crypto6 from "crypto";
@@ -6,7 +6,8 @@ import {
6
6
  generateEmbedding
7
7
  } from "./chunk-UVR65QBJ.js";
8
8
  import {
9
- diskSink
9
+ diskSink,
10
+ readEvents
10
11
  } from "./chunk-5I6CJITG.js";
11
12
  import {
12
13
  logger
@@ -4590,29 +4591,110 @@ function registerStatusTool(registry, ctx) {
4590
4591
  );
4591
4592
  }
4592
4593
 
4593
- // packages/core/src/tools/registry.ts
4594
- var ToolRegistry = class {
4595
- tools = /* @__PURE__ */ new Map();
4596
- register(name, schema4, handler) {
4597
- this.tools.set(name, { schema: schema4, handler });
4594
+ // packages/core/src/budget/learnedSuggestions.ts
4595
+ var DEFAULT_WINDOW_DAYS2 = 14;
4596
+ var SESSION_GAP_MS = 9e4;
4597
+ var MIN_SAMPLES_PER_PAIR = 3;
4598
+ var TOP_K = 3;
4599
+ var CACHE_TTL_MS = 60 * 60 * 1e3;
4600
+ var _cache = null;
4601
+ function __resetLearnedSuggestionsCacheForTests() {
4602
+ _cache = null;
4603
+ }
4604
+ function clampTokens(n) {
4605
+ if (!Number.isFinite(n)) return 0;
4606
+ return Math.max(0, Math.min(1e5, Math.round(n)));
4607
+ }
4608
+ function learnSuggestionsFromTelemetry(opts = {}) {
4609
+ const windowDays = opts.windowDays ?? DEFAULT_WINDOW_DAYS2;
4610
+ const sessionGapMs = opts.sessionGapMs ?? SESSION_GAP_MS;
4611
+ const minSamples = opts.minSamples ?? MIN_SAMPLES_PER_PAIR;
4612
+ const allowlist = opts.registeredTools;
4613
+ const events = opts.events ?? safeReadEvents(windowDays);
4614
+ if (events.length === 0) return {};
4615
+ const sorted = events.map((e) => ({ ts: Date.parse(e.ts), tool: e.tool })).filter((e) => Number.isFinite(e.ts) && typeof e.tool === "string" && e.tool.length > 0).sort((a, b) => a.ts - b.ts);
4616
+ const transitions = /* @__PURE__ */ new Map();
4617
+ const tokenSums = /* @__PURE__ */ new Map();
4618
+ let prevTool = null;
4619
+ let prevTs = -Infinity;
4620
+ for (const e of sorted) {
4621
+ if (prevTool && e.tool !== prevTool && e.ts - prevTs <= sessionGapMs) {
4622
+ let row = transitions.get(prevTool);
4623
+ if (!row) {
4624
+ row = /* @__PURE__ */ new Map();
4625
+ transitions.set(prevTool, row);
4626
+ }
4627
+ row.set(e.tool, (row.get(e.tool) ?? 0) + 1);
4628
+ }
4629
+ prevTool = e.tool;
4630
+ prevTs = e.ts;
4631
+ }
4632
+ for (const raw of events) {
4633
+ const tok = raw.original_tokens;
4634
+ if (typeof tok === "number" && Number.isFinite(tok)) {
4635
+ const acc = tokenSums.get(raw.tool) ?? { sum: 0, n: 0 };
4636
+ acc.sum += clampTokens(tok);
4637
+ acc.n += 1;
4638
+ tokenSums.set(raw.tool, acc);
4639
+ }
4598
4640
  }
4599
- list() {
4600
- return Array.from(this.tools.values()).map((t) => t.schema);
4641
+ const out = {};
4642
+ for (const [from, row] of transitions) {
4643
+ if (allowlist && !allowlist.has(from)) continue;
4644
+ const candidates = [];
4645
+ for (const [to, count] of row) {
4646
+ if (count < minSamples) continue;
4647
+ if (allowlist && !allowlist.has(to)) continue;
4648
+ const tokenAcc = tokenSums.get(to);
4649
+ const avgTokens = tokenAcc && tokenAcc.n > 0 ? tokenAcc.sum / tokenAcc.n : 0;
4650
+ candidates.push({
4651
+ tool: to,
4652
+ why: `Learned from telemetry: ${count} agents followed ${from} with ${to}.`,
4653
+ estimated_tokens: clampTokens(avgTokens)
4654
+ });
4655
+ }
4656
+ if (candidates.length === 0) continue;
4657
+ candidates.sort((a, b) => {
4658
+ const matchA = a.why.match(/(\d+) agents/);
4659
+ const matchB = b.why.match(/(\d+) agents/);
4660
+ const ca = matchA ? parseInt(matchA[1], 10) : 0;
4661
+ const cb = matchB ? parseInt(matchB[1], 10) : 0;
4662
+ return cb - ca;
4663
+ });
4664
+ out[from] = candidates.slice(0, TOP_K);
4601
4665
  }
4602
- async dispatch(name, args) {
4603
- const def = this.tools.get(name);
4604
- if (!def) throw new Error(`Unknown tool: ${name}`);
4605
- const projectRoot = args && typeof args === "object" && "project_root" in args ? args.project_root : void 0;
4606
- logger.debug("tool.dispatch", { tool: name, project_root: projectRoot });
4607
- return def.handler(args);
4666
+ return out;
4667
+ }
4668
+ function safeReadEvents(windowDays) {
4669
+ try {
4670
+ const until = /* @__PURE__ */ new Date();
4671
+ const since = new Date(until.getTime() - windowDays * 24 * 60 * 60 * 1e3);
4672
+ return readEvents({ since, until });
4673
+ } catch {
4674
+ return [];
4608
4675
  }
4609
- has(name) {
4610
- return this.tools.has(name);
4676
+ }
4677
+ function getLearnedRules(opts = {}) {
4678
+ if (_cache && _cache.expiresAt > Date.now()) {
4679
+ return filterRulesByAllowlist(_cache.rules, opts.registeredTools);
4611
4680
  }
4612
- };
4613
-
4614
- // packages/core/src/tools/search.ts
4615
- import { z as z3 } from "zod";
4681
+ const unfilteredRules = learnSuggestionsFromTelemetry({
4682
+ ...opts,
4683
+ registeredTools: void 0
4684
+ });
4685
+ _cache = { rules: unfilteredRules, expiresAt: Date.now() + CACHE_TTL_MS };
4686
+ return filterRulesByAllowlist(unfilteredRules, opts.registeredTools);
4687
+ }
4688
+ function filterRulesByAllowlist(rules, allowlist) {
4689
+ if (!allowlist) return rules;
4690
+ const out = {};
4691
+ for (const [from, suggestions] of Object.entries(rules)) {
4692
+ if (!allowlist.has(from)) continue;
4693
+ const kept = suggestions.filter((s) => allowlist.has(s.tool));
4694
+ if (kept.length > 0) out[from] = kept;
4695
+ }
4696
+ return out;
4697
+ }
4616
4698
 
4617
4699
  // packages/core/src/budget/nextToolSuggestions.ts
4618
4700
  var STATIC_RULES = {
@@ -4923,6 +5005,17 @@ function clampEstimate(n) {
4923
5005
  return Math.max(TOKEN_ESTIMATE_MIN, Math.min(TOKEN_ESTIMATE_MAX, Math.round(n)));
4924
5006
  }
4925
5007
  function suggestNext(fromTool, registeredTools) {
5008
+ if (process.env.CTXLOOM_LEARNED_SUGGESTIONS === "1") {
5009
+ const learned = getLearnedRules({ registeredTools })[fromTool];
5010
+ if (learned && learned.length > 0) {
5011
+ return learned.slice(0, 3).map((s) => ({
5012
+ tool: s.tool,
5013
+ args: s.args,
5014
+ why: s.why,
5015
+ estimated_tokens: clampEstimate(s.estimated_tokens)
5016
+ }));
5017
+ }
5018
+ }
4926
5019
  const raw = STATIC_RULES[fromTool] ?? [];
4927
5020
  const filtered = registeredTools ? raw.filter((s) => registeredTools.has(s.tool)) : raw;
4928
5021
  return filtered.slice(0, 3).map((s) => ({
@@ -5141,7 +5234,159 @@ function wrapResponse(result) {
5141
5234
  return JSON.stringify(envelope);
5142
5235
  }
5143
5236
 
5237
+ // packages/core/src/budget/taskBudget.ts
5238
+ var DEFAULT_MAX_CALLS = 8;
5239
+ var DEFAULT_RESET_GAP_MS = 9e4;
5240
+ var ENV_VAR = "CTXLOOM_TASK_TOOL_BUDGET";
5241
+ function parseEnvBudget() {
5242
+ const raw = process.env[ENV_VAR];
5243
+ if (!raw) return null;
5244
+ const n = parseInt(raw, 10);
5245
+ if (!Number.isFinite(n) || n <= 0) return null;
5246
+ return n;
5247
+ }
5248
+ var TaskBudgetTracker = class {
5249
+ state = /* @__PURE__ */ new Map();
5250
+ maxCalls;
5251
+ resetGapMs;
5252
+ constructor(opts = {}) {
5253
+ this.maxCalls = opts.maxCalls ?? parseEnvBudget() ?? DEFAULT_MAX_CALLS;
5254
+ this.resetGapMs = opts.resetGapMs ?? DEFAULT_RESET_GAP_MS;
5255
+ }
5256
+ /**
5257
+ * Record a tool call against the budget. Returns the enforcement
5258
+ * decision the dispatch layer should act on.
5259
+ *
5260
+ * @param sessionId — opaque session identifier. Currently a
5261
+ * single global key ('process'); reserved for future multi-
5262
+ * session enforcement.
5263
+ * @param now — milliseconds since epoch. Test hook; defaults to
5264
+ * `Date.now()`.
5265
+ */
5266
+ recordCall(sessionId = "process", now = Date.now()) {
5267
+ if (isBudgetDisabled()) {
5268
+ return {
5269
+ overBudget: false,
5270
+ callCount: 0,
5271
+ maxCalls: this.maxCalls,
5272
+ firstBreach: false
5273
+ };
5274
+ }
5275
+ const existing = this.state.get(sessionId);
5276
+ if (existing && now - existing.lastCallTs > this.resetGapMs) {
5277
+ const fresh = { count: 1, lastCallTs: now, breachEmitted: false };
5278
+ this.state.set(sessionId, fresh);
5279
+ return {
5280
+ overBudget: false,
5281
+ callCount: 1,
5282
+ maxCalls: this.maxCalls,
5283
+ firstBreach: false
5284
+ };
5285
+ }
5286
+ const next = (existing?.count ?? 0) + 1;
5287
+ const wasBreached = existing?.breachEmitted ?? false;
5288
+ const overBudget = next > this.maxCalls;
5289
+ const firstBreach = overBudget && !wasBreached;
5290
+ this.state.set(sessionId, {
5291
+ count: next,
5292
+ lastCallTs: now,
5293
+ breachEmitted: wasBreached || firstBreach
5294
+ });
5295
+ return { overBudget, callCount: next, maxCalls: this.maxCalls, firstBreach };
5296
+ }
5297
+ /**
5298
+ * Test-only: drop all state. Lets tests run in isolation without
5299
+ * depending on order.
5300
+ *
5301
+ * @internal
5302
+ */
5303
+ reset() {
5304
+ this.state.clear();
5305
+ }
5306
+ /**
5307
+ * Test/diagnostic — current call count for the default session.
5308
+ * @internal
5309
+ */
5310
+ __getCount(sessionId = "process") {
5311
+ return this.state.get(sessionId)?.count ?? 0;
5312
+ }
5313
+ };
5314
+ var _singleton = null;
5315
+ function getTaskBudgetTracker() {
5316
+ if (!_singleton) _singleton = new TaskBudgetTracker();
5317
+ return _singleton;
5318
+ }
5319
+ function __resetTaskBudgetTrackerForTests() {
5320
+ _singleton = null;
5321
+ }
5322
+ var OVER_BUDGET_ARG_OVERRIDES = Object.freeze({
5323
+ // Budget-surface tools (the 12 source-returning ones).
5324
+ max_response_tokens: 200,
5325
+ response_format: "skeleton",
5326
+ on_budget_exceeded: "skeleton",
5327
+ // Tools with detail_level (hub_nodes, bridge_nodes, etc).
5328
+ detail_level: "minimal"
5329
+ });
5330
+ function applyOverBudgetOverrides(args) {
5331
+ if (!args || typeof args !== "object") {
5332
+ return { ...OVER_BUDGET_ARG_OVERRIDES };
5333
+ }
5334
+ return { ...args, ...OVER_BUDGET_ARG_OVERRIDES };
5335
+ }
5336
+ function emitTaskBudgetBreached(toolName, callCount, maxCalls) {
5337
+ emitTelemetry({
5338
+ event: "mcp.task_budget.exceeded",
5339
+ tool: toolName,
5340
+ calls: callCount,
5341
+ budget: maxCalls
5342
+ });
5343
+ }
5344
+
5345
+ // packages/core/src/tools/registry.ts
5346
+ var TASK_BUDGET_EXEMPT = /* @__PURE__ */ new Set([
5347
+ "ctx_get_minimal_context",
5348
+ "ctx_status",
5349
+ "ctx_get_workflow",
5350
+ "ctx_get_rules",
5351
+ "ctx_suggested_questions"
5352
+ ]);
5353
+ var ToolRegistry = class {
5354
+ tools = /* @__PURE__ */ new Map();
5355
+ register(name, schema4, handler) {
5356
+ this.tools.set(name, { schema: schema4, handler });
5357
+ }
5358
+ list() {
5359
+ return Array.from(this.tools.values()).map((t) => t.schema);
5360
+ }
5361
+ async dispatch(name, args) {
5362
+ const def = this.tools.get(name);
5363
+ if (!def) throw new Error(`Unknown tool: ${name}`);
5364
+ const projectRoot = args && typeof args === "object" && "project_root" in args ? args.project_root : void 0;
5365
+ logger.debug("tool.dispatch", { tool: name, project_root: projectRoot });
5366
+ let dispatchArgs = args;
5367
+ if (!TASK_BUDGET_EXEMPT.has(name)) {
5368
+ const decision = getTaskBudgetTracker().recordCall();
5369
+ if (decision.overBudget) {
5370
+ dispatchArgs = applyOverBudgetOverrides(args);
5371
+ if (decision.firstBreach) {
5372
+ logger.warn("task tool budget exceeded \u2014 auto-throttling responses to skeleton/minimal", {
5373
+ tool: name,
5374
+ calls: decision.callCount,
5375
+ budget: decision.maxCalls
5376
+ });
5377
+ emitTaskBudgetBreached(name, decision.callCount, decision.maxCalls);
5378
+ }
5379
+ }
5380
+ }
5381
+ return def.handler(dispatchArgs);
5382
+ }
5383
+ has(name) {
5384
+ return this.tools.has(name);
5385
+ }
5386
+ };
5387
+
5144
5388
  // packages/core/src/tools/search.ts
5389
+ import { z as z3 } from "zod";
5145
5390
  var DEFAULT_MAX_RESPONSE_TOKENS = 4e3;
5146
5391
  var Schema = z3.object({
5147
5392
  query: z3.string().describe("Search query \u2014 natural language or code fragment"),
@@ -8897,7 +9142,7 @@ function computeTopHubs(graph) {
8897
9142
  reason: s.inDeg > s.outDeg ? "fan_in" : s.outDeg > s.inDeg ? "fan_out" : "bridge"
8898
9143
  }));
8899
9144
  }
8900
- var CACHE_TTL_MS = 1e4;
9145
+ var CACHE_TTL_MS2 = 1e4;
8901
9146
  var responseCache = /* @__PURE__ */ new Map();
8902
9147
  function cacheKey(projectRoot, task) {
8903
9148
  return `${projectRoot}|${task ?? ""}`;
@@ -8912,7 +9157,7 @@ function cacheGet(key) {
8912
9157
  return e.body;
8913
9158
  }
8914
9159
  function cachePut(key, body) {
8915
- responseCache.set(key, { expiresAt: Date.now() + CACHE_TTL_MS, body });
9160
+ responseCache.set(key, { expiresAt: Date.now() + CACHE_TTL_MS2, body });
8916
9161
  }
8917
9162
  function sanitizeTask(raw) {
8918
9163
  if (raw == null) return void 0;
@@ -9960,7 +10205,7 @@ var TELEMETRY_DISABLED = TELEMETRY_LEVEL === "off";
9960
10205
  function getTelemetryLevel() {
9961
10206
  return TELEMETRY_LEVEL;
9962
10207
  }
9963
- var CTXLOOM_VERSION = "1.4.0".length > 0 ? "1.4.0" : "dev";
10208
+ var CTXLOOM_VERSION = "1.5.1".length > 0 ? "1.5.1" : "dev";
9964
10209
  var POSTHOG_HOST = "https://eu.i.posthog.com";
9965
10210
  var POSTHOG_KEY = process.env["POSTHOG_API_KEY"] ?? (true ? "phc_CiDkmFLcZ2K6uCpcoSUQLmFrnnUvsyXGhSxopX5TVKE6" : "");
9966
10211
  var SENTRY_DSN = process.env["SENTRY_DSN"] ?? (true ? "https://81c94a0f04a8e242dee493ac1e17f733@o4508531702497280.ingest.de.sentry.io/4511256875368528" : "");
@@ -10711,6 +10956,72 @@ var CTXLOOM_HOOK_ENTRIES = {
10711
10956
  ]
10712
10957
  }
10713
10958
  };
10959
+ function plainBody() {
10960
+ return RULES_BLOCK_CONTENT.replace(/^##\s+/gm, "").replace(/^###\s+/gm, "").replace(/^####\s+/gm, "").replace(/\*\*(.*?)\*\*/g, "$1").replace(/`([^`]+)`/g, "$1");
10961
+ }
10962
+ var CURSOR_HEADER = `# ctxloom \u2014 Cursor agent rules
10963
+ # Generated by \`ctxloom init --host=cursor\`. Re-run to update.
10964
+
10965
+ `;
10966
+ var AIDER_HEADER = `# Project Conventions
10967
+
10968
+ Generated by \`ctxloom init --host=aider\`. Re-run to update.
10969
+
10970
+ `;
10971
+ var COPILOT_HEADER = `# ctxloom \u2014 GitHub Copilot instructions
10972
+
10973
+ Generated by \`ctxloom init --host=copilot\`. Re-run to update.
10974
+
10975
+ `;
10976
+ var WINDSURF_HEADER = `# ctxloom \u2014 Windsurf agent rules
10977
+ # Generated by \`ctxloom init --host=windsurf\`. Re-run to update.
10978
+
10979
+ `;
10980
+ var HOST_ADAPTERS = [
10981
+ // Note: claude / agents / gemini are NOT defined as HostAdapters in
10982
+ // this list — they predate the matrix and live in their own writeRulesBlock
10983
+ // path with HMAC-wrapped Markdown blocks. Future refactor may unify.
10984
+ {
10985
+ id: "cursor",
10986
+ path: ".cursorrules",
10987
+ defaultEnabled: false,
10988
+ render: () => CURSOR_HEADER + plainBody() + "\n",
10989
+ isCanonical(current) {
10990
+ return current === this.render();
10991
+ }
10992
+ },
10993
+ {
10994
+ id: "aider",
10995
+ path: "CONVENTIONS.md",
10996
+ defaultEnabled: false,
10997
+ render: () => AIDER_HEADER + RULES_BLOCK_CONTENT + "\n",
10998
+ isCanonical(current) {
10999
+ return current === this.render();
11000
+ }
11001
+ },
11002
+ {
11003
+ id: "copilot",
11004
+ path: ".github/copilot-instructions.md",
11005
+ defaultEnabled: false,
11006
+ render: () => COPILOT_HEADER + RULES_BLOCK_CONTENT + "\n",
11007
+ isCanonical(current) {
11008
+ return current === this.render();
11009
+ }
11010
+ },
11011
+ {
11012
+ id: "windsurf",
11013
+ path: ".windsurfrules",
11014
+ defaultEnabled: false,
11015
+ render: () => WINDSURF_HEADER + plainBody() + "\n",
11016
+ isCanonical(current) {
11017
+ return current === this.render();
11018
+ }
11019
+ }
11020
+ ];
11021
+ function getHostAdapter(id) {
11022
+ return HOST_ADAPTERS.find((h) => h.id === id);
11023
+ }
11024
+ var SUPPORTED_HOST_IDS = HOST_ADAPTERS.map((h) => h.id);
10714
11025
 
10715
11026
  // packages/core/src/install/hmacBlock.ts
10716
11027
  import crypto6 from "crypto";
@@ -11159,6 +11470,9 @@ function installHarness(opts = {}) {
11159
11470
  const hooksJson = writeHooksJson(projectRoot, { dryRun, warnings });
11160
11471
  const sessionStartSh = writeSessionStartScript(projectRoot, { dryRun });
11161
11472
  const skills = CTXLOOM_SKILLS.map((s) => writeSkill(projectRoot, s, { dryRun }));
11473
+ const extraHosts = resolveExtraHosts(opts.extraHosts ?? [], warnings).map(
11474
+ (adapter) => Object.assign(writeHostAdapter(projectRoot, adapter, { dryRun }), { hostId: adapter.id })
11475
+ );
11162
11476
  return {
11163
11477
  projectRoot,
11164
11478
  claudeMd,
@@ -11167,13 +11481,36 @@ function installHarness(opts = {}) {
11167
11481
  hooksJson,
11168
11482
  sessionStartSh,
11169
11483
  skills,
11484
+ extraHosts,
11170
11485
  warnings
11171
11486
  };
11172
11487
  }
11488
+ function resolveExtraHosts(ids, warnings) {
11489
+ const requested = /* @__PURE__ */ new Set();
11490
+ for (const raw of ids) {
11491
+ const id = raw.trim().toLowerCase();
11492
+ if (id === "") continue;
11493
+ if (id === "all") {
11494
+ for (const a of HOST_ADAPTERS) requested.add(a.id);
11495
+ continue;
11496
+ }
11497
+ if (!getHostAdapter(id)) {
11498
+ warnings.push(
11499
+ `Unknown --host id "${id}" \u2014 ignored. Supported: ${HOST_ADAPTERS.map((a) => a.id).join(", ")}, or "all".`
11500
+ );
11501
+ continue;
11502
+ }
11503
+ requested.add(id);
11504
+ }
11505
+ return HOST_ADAPTERS.filter((a) => requested.has(a.id));
11506
+ }
11173
11507
  function safeJoin(root, name) {
11174
11508
  const target = path36.resolve(root, name);
11175
11509
  const rootResolved = path36.resolve(root);
11176
- if (!target.startsWith(rootResolved + path36.sep) && target !== rootResolved) {
11510
+ const caseFold = process.platform === "darwin" || process.platform === "win32";
11511
+ const t = caseFold ? target.toLowerCase() : target;
11512
+ const r = caseFold ? rootResolved.toLowerCase() : rootResolved;
11513
+ if (!t.startsWith(r + path36.sep) && t !== r) {
11177
11514
  throw new Error(`installHarness: refusing to write outside project root: ${target}`);
11178
11515
  }
11179
11516
  return target;
@@ -11262,6 +11599,30 @@ function isCtxloomEntry(entry, expectedMatcher) {
11262
11599
  return cmd.includes("ctxloom") || cmd.includes(".claude/hooks/session-start.sh");
11263
11600
  });
11264
11601
  }
11602
+ function writeHostAdapter(projectRoot, adapter, opts) {
11603
+ const filePath = safeJoin(projectRoot, adapter.path);
11604
+ const dir = path36.dirname(filePath);
11605
+ const existed = fs28.existsSync(filePath);
11606
+ const rendered = adapter.render();
11607
+ let alreadyCorrect = false;
11608
+ if (existed) {
11609
+ const current = fs28.readFileSync(filePath, "utf-8");
11610
+ if (adapter.isCanonical(current)) {
11611
+ alreadyCorrect = true;
11612
+ }
11613
+ }
11614
+ if (!opts.dryRun && !alreadyCorrect) {
11615
+ fs28.mkdirSync(dir, { recursive: true });
11616
+ fs28.writeFileSync(filePath, rendered, "utf-8");
11617
+ }
11618
+ return {
11619
+ path: filePath,
11620
+ created: !existed,
11621
+ updated: existed && !alreadyCorrect,
11622
+ alreadyCorrect,
11623
+ dryRun: opts.dryRun
11624
+ };
11625
+ }
11265
11626
  function writeSkill(projectRoot, skill, opts) {
11266
11627
  const dir = safeJoin(projectRoot, `.claude/skills/${skill.name}`);
11267
11628
  const filePath = safeJoin(projectRoot, skillFilePath(skill.name));
@@ -11347,6 +11708,15 @@ export {
11347
11708
  loadFileRiskHistory,
11348
11709
  Skeletonizer,
11349
11710
  renderStatusXml,
11711
+ __resetLearnedSuggestionsCacheForTests,
11712
+ learnSuggestionsFromTelemetry,
11713
+ getLearnedRules,
11714
+ TaskBudgetTracker,
11715
+ getTaskBudgetTracker,
11716
+ __resetTaskBudgetTrackerForTests,
11717
+ OVER_BUDGET_ARG_OVERRIDES,
11718
+ applyOverBudgetOverrides,
11719
+ emitTaskBudgetBreached,
11350
11720
  ToolRegistry,
11351
11721
  detectChanges,
11352
11722
  getImpactRadius,
@@ -11419,6 +11789,9 @@ export {
11419
11789
  RULES_BLOCK_CONTENT,
11420
11790
  SESSION_START_FULL,
11421
11791
  CTXLOOM_HOOK_ENTRIES,
11792
+ HOST_ADAPTERS,
11793
+ getHostAdapter,
11794
+ SUPPORTED_HOST_IDS,
11422
11795
  DEFAULT_HMAC_KEY,
11423
11796
  resolveHmacKey,
11424
11797
  computeBlockHmac,
@@ -11430,4 +11803,4 @@ export {
11430
11803
  skillFilePath,
11431
11804
  installHarness
11432
11805
  };
11433
- //# sourceMappingURL=chunk-7GZVGIQL.js.map
11806
+ //# sourceMappingURL=chunk-D3RJQLX6.js.map
package/dist/index.js CHANGED
@@ -49,7 +49,7 @@ import {
49
49
  validateDefaultRoot,
50
50
  wrapWithIndexingEnvelope,
51
51
  writeCODEOWNERS
52
- } from "./chunk-7GZVGIQL.js";
52
+ } from "./chunk-D3RJQLX6.js";
53
53
  import {
54
54
  VectorStore
55
55
  } from "./chunk-DVI2RWJR.js";
@@ -1019,7 +1019,7 @@ try {
1019
1019
  } catch {
1020
1020
  }
1021
1021
  var args = process.argv.slice(2);
1022
- var ctxloomVersion = "1.4.0".length > 0 ? "1.4.0" : "dev";
1022
+ var ctxloomVersion = "1.5.1".length > 0 ? "1.5.1" : "dev";
1023
1023
  if (args.includes("--version") || args.includes("-v")) {
1024
1024
  process.stdout.write(`ctxloom ${ctxloomVersion}
1025
1025
  `);
@@ -1092,7 +1092,7 @@ async function checkLicense() {
1092
1092
  if (command !== void 0 && LICENSE_GATE_BYPASS_COMMANDS.has(command)) return;
1093
1093
  const ciKey = process.env["CTXLOOM_LICENSE_KEY"];
1094
1094
  if (ciKey) {
1095
- const { ApiClient } = await import("./src-CH2OSHKF.js");
1095
+ const { ApiClient } = await import("./src-FU53WQZC.js");
1096
1096
  const client = new ApiClient(process.env["CTXLOOM_API_BASE"]);
1097
1097
  try {
1098
1098
  const result = await client.validate(ciKey, "ci-ephemeral");
@@ -1447,6 +1447,15 @@ async function main() {
1447
1447
  const skipHarness = process.argv.includes("--skip-harness");
1448
1448
  const dryRun = process.argv.includes("--dry-run");
1449
1449
  const force = process.argv.includes("--force");
1450
+ const extraHosts = [];
1451
+ for (let i = 0; i < process.argv.length; i++) {
1452
+ const arg = process.argv[i];
1453
+ if (arg.startsWith("--host=")) {
1454
+ extraHosts.push(...arg.slice("--host=".length).split(",").map((s) => s.trim()));
1455
+ } else if (arg === "--host" && i + 1 < process.argv.length) {
1456
+ extraHosts.push(...process.argv[i + 1].split(",").map((s) => s.trim()));
1457
+ }
1458
+ }
1450
1459
  try {
1451
1460
  const result = runInit(initRoot);
1452
1461
  const mcpLabel = result.mcpJson.created ? `${style.bold("Created")} ${result.mcpJson.path}` : result.mcpJson.merged ? `${style.bold("Merged ctxloom entry into")} ${result.mcpJson.path}` : `${style.dim("Already up to date:")} ${result.mcpJson.path}`;
@@ -1461,15 +1470,16 @@ async function main() {
1461
1470
  }
1462
1471
  if (!skipHarness) {
1463
1472
  process.stdout.write("\n");
1464
- const { installHarness } = await import("./src-CH2OSHKF.js");
1465
- const h = installHarness({ cwd: initRoot, dryRun, force });
1473
+ const { installHarness } = await import("./src-FU53WQZC.js");
1474
+ const h = installHarness({ cwd: initRoot, dryRun, force, extraHosts });
1466
1475
  const harnessFiles = [
1467
1476
  h.claudeMd,
1468
1477
  h.agentsMd,
1469
1478
  h.geminiMd,
1470
1479
  h.hooksJson,
1471
1480
  h.sessionStartSh,
1472
- ...h.skills
1481
+ ...h.skills,
1482
+ ...h.extraHosts
1473
1483
  ];
1474
1484
  for (const fr of harnessFiles) {
1475
1485
  const rel = path4.relative(initRoot, fr.path);
@@ -1523,7 +1533,7 @@ async function main() {
1523
1533
  process.exit(1);
1524
1534
  }
1525
1535
  if (alias !== void 0) {
1526
- const { validateAlias } = await import("./src-CH2OSHKF.js");
1536
+ const { validateAlias } = await import("./src-FU53WQZC.js");
1527
1537
  const v = validateAlias(alias);
1528
1538
  if (!v.ok) {
1529
1539
  console.error(`[ctxloom] Invalid alias: ${v.reason}`);
@@ -1787,7 +1797,7 @@ Suggested reviewers for ${files.length} file(s):`);
1787
1797
  process.stderr.write("[ctxloom] --limit must be a non-negative integer (0 for unlimited)\n");
1788
1798
  process.exit(2);
1789
1799
  }
1790
- const { loadRulesConfig, RulesChecker, formatText, formatJson, RulesConfigError } = await import("./src-CH2OSHKF.js");
1800
+ const { loadRulesConfig, RulesChecker, formatText, formatJson, RulesConfigError } = await import("./src-FU53WQZC.js");
1791
1801
  let config;
1792
1802
  try {
1793
1803
  config = await loadRulesConfig(root);
@@ -1811,7 +1821,7 @@ Suggested reviewers for ${files.length} file(s):`);
1811
1821
  }
1812
1822
  let graph;
1813
1823
  if (useSnapshot) {
1814
- const { DependencyGraph: DG } = await import("./src-CH2OSHKF.js");
1824
+ const { DependencyGraph: DG } = await import("./src-FU53WQZC.js");
1815
1825
  graph = new DG();
1816
1826
  const loaded = await graph.loadSnapshotOnly(root);
1817
1827
  if (!loaded) {
@@ -1820,7 +1830,7 @@ Suggested reviewers for ${files.length} file(s):`);
1820
1830
  }
1821
1831
  } else {
1822
1832
  process.stderr.write("[ctxloom] Building dependency graph...\n");
1823
- const { ASTParser: ASTParser2, DependencyGraph: DependencyGraph2 } = await import("./src-CH2OSHKF.js");
1833
+ const { ASTParser: ASTParser2, DependencyGraph: DependencyGraph2 } = await import("./src-FU53WQZC.js");
1824
1834
  let parser;
1825
1835
  try {
1826
1836
  parser = new ASTParser2();
@@ -24,11 +24,13 @@ import {
24
24
  GoModuleResolver,
25
25
  GrammarLoader,
26
26
  GraphExporter,
27
+ HOST_ADAPTERS,
27
28
  InvalidKeyError,
28
29
  LicenseRequiredError,
29
30
  LicenseRevokedError,
30
31
  LicenseStore,
31
32
  NetworkError,
33
+ OVER_BUDGET_ARG_OVERRIDES,
32
34
  OwnershipIndex,
33
35
  PathValidator,
34
36
  ProjectStateManager,
@@ -42,14 +44,19 @@ import {
42
44
  SCORE_FLOOR,
43
45
  SESSION_START_FULL,
44
46
  SILO_BUS_FACTOR,
47
+ SUPPORTED_HOST_IDS,
45
48
  SeatLimitError,
46
49
  Skeletonizer,
50
+ TaskBudgetTracker,
47
51
  ToolRegistry,
48
52
  TrialUnavailableError,
49
53
  TsConfigPathsResolver,
50
54
  WikiGenerator,
55
+ __resetLearnedSuggestionsCacheForTests,
56
+ __resetTaskBudgetTrackerForTests,
51
57
  activateLicense,
52
58
  aliasNotFoundError,
59
+ applyOverBudgetOverrides,
53
60
  assignLabelsByPercentile,
54
61
  buildBlastRadiusXml,
55
62
  buildCodeownersBlock,
@@ -62,6 +69,7 @@ import {
62
69
  deactivateLicense,
63
70
  detectChanges,
64
71
  disposeProjectState,
72
+ emitTaskBudgetBreached,
65
73
  ensureVectorsInitialized,
66
74
  extractBlock,
67
75
  extractImports,
@@ -73,14 +81,18 @@ import {
73
81
  formatJson,
74
82
  formatText,
75
83
  generateCODEOWNERS,
84
+ getHostAdapter,
76
85
  getImpactRadius,
86
+ getLearnedRules,
77
87
  getLicenseInfo,
78
88
  getOrCreateDistinctId,
89
+ getTaskBudgetTracker,
79
90
  getTelemetryLevel,
80
91
  hashProjectRoot,
81
92
  installHarness,
82
93
  isActive,
83
94
  isSiloed,
95
+ learnSuggestionsFromTelemetry,
84
96
  listNamedSnapshots,
85
97
  loadFileRiskHistory,
86
98
  loadReviewConfig,
@@ -117,7 +129,7 @@ import {
117
129
  wrapBlock,
118
130
  wrapWithIndexingEnvelope,
119
131
  writeCODEOWNERS
120
- } from "./chunk-7GZVGIQL.js";
132
+ } from "./chunk-D3RJQLX6.js";
121
133
  import {
122
134
  VectorStore
123
135
  } from "./chunk-DVI2RWJR.js";
@@ -158,11 +170,13 @@ export {
158
170
  GoModuleResolver,
159
171
  GrammarLoader,
160
172
  GraphExporter,
173
+ HOST_ADAPTERS,
161
174
  InvalidKeyError,
162
175
  LicenseRequiredError,
163
176
  LicenseRevokedError,
164
177
  LicenseStore,
165
178
  NetworkError,
179
+ OVER_BUDGET_ARG_OVERRIDES,
166
180
  OwnershipIndex,
167
181
  PathValidator,
168
182
  ProjectStateManager,
@@ -176,15 +190,20 @@ export {
176
190
  SCORE_FLOOR,
177
191
  SESSION_START_FULL,
178
192
  SILO_BUS_FACTOR,
193
+ SUPPORTED_HOST_IDS,
179
194
  SeatLimitError,
180
195
  Skeletonizer,
196
+ TaskBudgetTracker,
181
197
  ToolRegistry,
182
198
  TrialUnavailableError,
183
199
  TsConfigPathsResolver,
184
200
  VectorStore,
185
201
  WikiGenerator,
202
+ __resetLearnedSuggestionsCacheForTests,
203
+ __resetTaskBudgetTrackerForTests,
186
204
  activateLicense,
187
205
  aliasNotFoundError,
206
+ applyOverBudgetOverrides,
188
207
  assignLabelsByPercentile,
189
208
  buildBlastRadiusXml,
190
209
  buildCodeownersBlock,
@@ -198,6 +217,7 @@ export {
198
217
  deactivateLicense,
199
218
  detectChanges,
200
219
  disposeProjectState,
220
+ emitTaskBudgetBreached,
201
221
  ensureVectorsInitialized,
202
222
  extractBlock,
203
223
  extractImports,
@@ -210,15 +230,19 @@ export {
210
230
  formatText,
211
231
  generateCODEOWNERS,
212
232
  generateEmbedding,
233
+ getHostAdapter,
213
234
  getImpactRadius,
235
+ getLearnedRules,
214
236
  getLicenseInfo,
215
237
  getOrCreateDistinctId,
238
+ getTaskBudgetTracker,
216
239
  getTelemetryLevel,
217
240
  hashProjectRoot,
218
241
  indexDirectory,
219
242
  installHarness,
220
243
  isActive,
221
244
  isSiloed,
245
+ learnSuggestionsFromTelemetry,
222
246
  listNamedSnapshots,
223
247
  loadFileRiskHistory,
224
248
  loadReviewConfig,
@@ -257,4 +281,4 @@ export {
257
281
  wrapWithIndexingEnvelope,
258
282
  writeCODEOWNERS
259
283
  };
260
- //# sourceMappingURL=src-CH2OSHKF.js.map
284
+ //# sourceMappingURL=src-FU53WQZC.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ctxloom-pro",
3
- "version": "1.4.0",
3
+ "version": "1.5.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",