@massu/core 1.6.2 → 1.7.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/dist/cli.js CHANGED
@@ -9542,14 +9542,33 @@ function domainFromWorkspace(pkg) {
9542
9542
  allowedImportsFrom: []
9543
9543
  };
9544
9544
  }
9545
- function topLevelSrcSubdirs(root) {
9546
- const srcDir = join5(root, "src");
9547
- if (!existsSync7(srcDir)) return [];
9548
- try {
9549
- return readdirSync5(srcDir, { withFileTypes: true }).filter((e2) => e2.isDirectory() && !IGNORED_SUBDIRS.has(e2.name)).map((e2) => e2.name).sort();
9550
- } catch {
9551
- return [];
9545
+ function topLevelSrcSubdirs(root, sourceDirs) {
9546
+ const effective = sourceDirs.length > 0 ? sourceDirs : ["src"];
9547
+ const seen = /* @__PURE__ */ new Set();
9548
+ for (const rel of effective) {
9549
+ const abs = join5(root, rel);
9550
+ if (!existsSync7(abs)) continue;
9551
+ try {
9552
+ for (const e2 of readdirSync5(abs, { withFileTypes: true })) {
9553
+ if (!e2.isDirectory()) continue;
9554
+ if (IGNORED_SUBDIRS.has(e2.name)) continue;
9555
+ seen.add(e2.name);
9556
+ }
9557
+ } catch {
9558
+ }
9559
+ }
9560
+ return Array.from(seen).sort();
9561
+ }
9562
+ function flattenSourceDirs(sourceDirs) {
9563
+ const flat = /* @__PURE__ */ new Set();
9564
+ for (const entry of Object.values(sourceDirs)) {
9565
+ if (!entry) continue;
9566
+ for (const dir of entry.source_dirs) {
9567
+ if (dir === "." || dir === "") continue;
9568
+ flat.add(dir);
9569
+ }
9552
9570
  }
9571
+ return Array.from(flat);
9553
9572
  }
9554
9573
  function inferDomains(projectRoot, monorepo, sourceDirs) {
9555
9574
  const domains = [];
@@ -9558,7 +9577,8 @@ function inferDomains(projectRoot, monorepo, sourceDirs) {
9558
9577
  domains.push(domainFromWorkspace(pkg));
9559
9578
  }
9560
9579
  } else {
9561
- const subdirs = topLevelSrcSubdirs(projectRoot);
9580
+ const flat = flattenSourceDirs(sourceDirs);
9581
+ const subdirs = topLevelSrcSubdirs(projectRoot, flat);
9562
9582
  for (const s of subdirs) {
9563
9583
  domains.push({
9564
9584
  name: titleCase(s),
@@ -13780,6 +13800,9 @@ function annotateToolDefinitions(defs) {
13780
13800
  };
13781
13801
  });
13782
13802
  }
13803
+ function isCloudFeatureAvailable() {
13804
+ return getConfig().cloud?.enabled === true;
13805
+ }
13783
13806
  async function validateLicense(apiKey) {
13784
13807
  const keyHash = createHash4("sha256").update(apiKey).digest("hex");
13785
13808
  const memDb = getMemoryDb();
@@ -26723,7 +26746,8 @@ function getToolDefinitions() {
26723
26746
  ...getSecurityToolDefinitions(),
26724
26747
  ...getDependencyToolDefinitions(),
26725
26748
  // Enterprise layer (team knowledge, regression detection)
26726
- ...getTeamToolDefinitions(),
26749
+ // P-A-003: team tools are cloud-gated — only listed when cloud.enabled is true.
26750
+ ...isCloudFeatureAvailable() ? getTeamToolDefinitions() : [],
26727
26751
  ...getRegressionToolDefinitions(),
26728
26752
  // Knowledge layer (indexed .claude/ knowledge — rules, patterns, incidents)
26729
26753
  ...getKnowledgeToolDefinitions(),
@@ -26927,7 +26951,7 @@ async function handleToolCall(name, args2, dataDb, codegraphDb) {
26927
26951
  memDb.close();
26928
26952
  }
26929
26953
  }
26930
- if (isTeamTool(name)) {
26954
+ if (isTeamTool(name) && isCloudFeatureAvailable()) {
26931
26955
  const memDb = getMemoryDb();
26932
26956
  try {
26933
26957
  return handleTeamToolCall(name, args2, memDb);
@@ -27759,111 +27783,163 @@ var init_tool_db_needs = __esm({
27759
27783
  }
27760
27784
  });
27761
27785
 
27762
- // src/server.ts
27763
- var server_exports = {};
27764
- import { readFileSync as readFileSync39 } from "fs";
27765
- import { resolve as resolve32, dirname as dirname16 } from "path";
27766
- import { fileURLToPath as fileURLToPath4 } from "url";
27767
- function resolveDbsForTool(toolName) {
27768
- const needs = getToolDbNeeds(toolName, getConfig().toolPrefix);
27769
- let dataDbResolved;
27770
- let codegraphDbResolved;
27771
- if (needs.includes("data")) {
27772
- if (!dataDbCache) dataDbCache = getDataDb();
27773
- dataDbResolved = dataDbCache;
27774
- }
27775
- if (needs.includes("codegraph")) {
27776
- if (!codegraphDbCache) codegraphDbCache = getCodeGraphDb();
27777
- codegraphDbResolved = codegraphDbCache;
27778
- }
27779
- return { needs, dataDb: dataDbResolved, codegraphDb: codegraphDbResolved };
27780
- }
27781
- async function handleRequest(request) {
27782
- const { method, params, id } = request;
27783
- switch (method) {
27784
- case "initialize": {
27785
- return {
27786
- jsonrpc: "2.0",
27787
- id: id ?? null,
27788
- result: {
27789
- protocolVersion: "2024-11-05",
27790
- capabilities: {
27791
- tools: {}
27792
- },
27793
- serverInfo: {
27794
- name: getConfig().toolPrefix || "massu",
27795
- version: PKG_VERSION
27796
- }
27797
- }
27798
- };
27799
- }
27800
- case "notifications/initialized": {
27801
- return { jsonrpc: "2.0", id: id ?? null, result: {} };
27802
- }
27803
- case "tools/list": {
27804
- const tools = getToolDefinitions();
27805
- return {
27806
- jsonrpc: "2.0",
27807
- id: id ?? null,
27808
- result: { tools }
27809
- };
27810
- }
27811
- case "tools/call": {
27812
- const toolName = params?.name;
27813
- const toolArgs = params?.arguments ?? {};
27814
- try {
27815
- const { dataDb: lDb, codegraphDb: cgDb } = resolveDbsForTool(toolName);
27816
- const result = await handleToolCall(toolName, toolArgs, lDb, cgDb);
27786
+ // src/server-dispatch.ts
27787
+ function createDispatcher(options) {
27788
+ let codegraphDbCache = null;
27789
+ let dataDbCache = null;
27790
+ function resolveDbsForTool(toolName) {
27791
+ const needs = getToolDbNeeds(toolName, getConfig().toolPrefix);
27792
+ let dataDbResolved;
27793
+ let codegraphDbResolved;
27794
+ if (needs.includes("data")) {
27795
+ if (!dataDbCache) dataDbCache = getDataDb();
27796
+ dataDbResolved = dataDbCache;
27797
+ }
27798
+ if (needs.includes("codegraph")) {
27799
+ if (!codegraphDbCache) codegraphDbCache = getCodeGraphDb();
27800
+ codegraphDbResolved = codegraphDbCache;
27801
+ }
27802
+ return { needs, dataDb: dataDbResolved, codegraphDb: codegraphDbResolved };
27803
+ }
27804
+ async function handleRequest(request) {
27805
+ const { method, params, id } = request;
27806
+ switch (method) {
27807
+ case "initialize": {
27817
27808
  return {
27818
27809
  jsonrpc: "2.0",
27819
27810
  id: id ?? null,
27820
- result
27811
+ result: {
27812
+ protocolVersion: "2024-11-05",
27813
+ capabilities: { tools: {} },
27814
+ serverInfo: {
27815
+ name: getConfig().toolPrefix || "massu",
27816
+ version: options.serverInfoVersion
27817
+ }
27818
+ }
27821
27819
  };
27822
- } catch (err) {
27823
- if (err instanceof CodegraphDbNotInitializedError) {
27824
- return {
27825
- jsonrpc: "2.0",
27826
- id: id ?? null,
27827
- error: {
27828
- code: -32001,
27829
- message: `Tool requires CodeGraph database which is not initialized for this repo`,
27830
- data: {
27831
- remedy: "npx @colbymchenry/codegraph@0.7.4 init . && npx @colbymchenry/codegraph@0.7.4 index .",
27832
- codegraphDbPath: err.dbPath,
27833
- tool: toolName
27820
+ }
27821
+ case "notifications/initialized": {
27822
+ return { jsonrpc: "2.0", id: id ?? null, result: {} };
27823
+ }
27824
+ case "tools/list": {
27825
+ const tools = getToolDefinitions();
27826
+ return { jsonrpc: "2.0", id: id ?? null, result: { tools } };
27827
+ }
27828
+ case "tools/call": {
27829
+ const toolName = params?.name ?? "";
27830
+ const toolArgs = params?.arguments ?? {};
27831
+ try {
27832
+ const { dataDb, codegraphDb } = resolveDbsForTool(toolName);
27833
+ const result = await handleToolCall(toolName, toolArgs, dataDb, codegraphDb);
27834
+ return { jsonrpc: "2.0", id: id ?? null, result };
27835
+ } catch (err) {
27836
+ if (err instanceof CodegraphDbNotInitializedError) {
27837
+ return {
27838
+ jsonrpc: "2.0",
27839
+ id: id ?? null,
27840
+ error: {
27841
+ code: -32001,
27842
+ message: "Tool requires CodeGraph database which is not initialized for this repo",
27843
+ data: {
27844
+ remedy: "npx @colbymchenry/codegraph@0.7.4 init . && npx @colbymchenry/codegraph@0.7.4 index .",
27845
+ codegraphDbPath: err.dbPath,
27846
+ tool: toolName
27847
+ }
27834
27848
  }
27835
- }
27836
- };
27837
- }
27838
- if (err instanceof UnknownToolError) {
27839
- return {
27840
- jsonrpc: "2.0",
27841
- id: id ?? null,
27842
- error: {
27843
- code: -32602,
27844
- message: `Unknown tool: ${err.toolName}`,
27845
- data: {
27846
- remedy: "Tool not registered in TOOL_DB_NEEDS manifest. See packages/core/src/tool-db-needs.ts.",
27847
- tool: toolName
27849
+ };
27850
+ }
27851
+ if (err instanceof UnknownToolError) {
27852
+ return {
27853
+ jsonrpc: "2.0",
27854
+ id: id ?? null,
27855
+ error: {
27856
+ code: -32602,
27857
+ message: `Unknown tool: ${err.toolName}`,
27858
+ data: {
27859
+ remedy: "Tool not registered in TOOL_DB_NEEDS manifest. See packages/core/src/tool-db-needs.ts.",
27860
+ tool: toolName
27861
+ }
27848
27862
  }
27849
- }
27850
- };
27863
+ };
27864
+ }
27865
+ throw err;
27851
27866
  }
27852
- throw err;
27867
+ }
27868
+ case "ping": {
27869
+ return { jsonrpc: "2.0", id: id ?? null, result: {} };
27870
+ }
27871
+ default: {
27872
+ return {
27873
+ jsonrpc: "2.0",
27874
+ id: id ?? null,
27875
+ error: { code: -32601, message: `Method not found: ${method}` }
27876
+ };
27853
27877
  }
27854
27878
  }
27855
- case "ping": {
27856
- return { jsonrpc: "2.0", id: id ?? null, result: {} };
27879
+ }
27880
+ async function processLine(line) {
27881
+ const trimmed = line.trim();
27882
+ if (!trimmed) return null;
27883
+ let request;
27884
+ try {
27885
+ request = JSON.parse(trimmed);
27886
+ } catch (parseError) {
27887
+ return {
27888
+ response: {
27889
+ jsonrpc: "2.0",
27890
+ id: null,
27891
+ error: {
27892
+ code: -32700,
27893
+ message: `Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`
27894
+ }
27895
+ },
27896
+ emit: true
27897
+ };
27857
27898
  }
27858
- default: {
27899
+ try {
27900
+ const response = await handleRequest(request);
27901
+ return { response, emit: request.id !== void 0 };
27902
+ } catch (error) {
27859
27903
  return {
27860
- jsonrpc: "2.0",
27861
- id: id ?? null,
27862
- error: { code: -32601, message: `Method not found: ${method}` }
27904
+ response: {
27905
+ jsonrpc: "2.0",
27906
+ id: request.id ?? null,
27907
+ error: {
27908
+ code: -32603,
27909
+ message: `Internal error: ${error instanceof Error ? error.message : String(error)}`
27910
+ }
27911
+ },
27912
+ emit: true
27863
27913
  };
27864
27914
  }
27865
27915
  }
27916
+ function closeCachedDbs() {
27917
+ if (codegraphDbCache) {
27918
+ codegraphDbCache.close();
27919
+ codegraphDbCache = null;
27920
+ }
27921
+ if (dataDbCache) {
27922
+ dataDbCache.close();
27923
+ dataDbCache = null;
27924
+ }
27925
+ }
27926
+ return { handleRequest, processLine, closeCachedDbs };
27866
27927
  }
27928
+ var init_server_dispatch = __esm({
27929
+ "src/server-dispatch.ts"() {
27930
+ "use strict";
27931
+ init_db();
27932
+ init_config();
27933
+ init_tools();
27934
+ init_tool_db_needs();
27935
+ }
27936
+ });
27937
+
27938
+ // src/server.ts
27939
+ var server_exports = {};
27940
+ import { readFileSync as readFileSync39 } from "fs";
27941
+ import { resolve as resolve32, dirname as dirname16 } from "path";
27942
+ import { fileURLToPath as fileURLToPath4 } from "url";
27867
27943
  function pruneMemoryOnStartup() {
27868
27944
  try {
27869
27945
  const memDb = getMemoryDb();
@@ -27887,16 +27963,13 @@ function pruneMemoryOnStartup() {
27887
27963
  );
27888
27964
  }
27889
27965
  }
27890
- var __dirname4, PKG_VERSION, codegraphDbCache, dataDbCache, buffer;
27966
+ var __dirname4, PKG_VERSION, dispatcher, buffer;
27891
27967
  var init_server = __esm({
27892
27968
  "src/server.ts"() {
27893
27969
  "use strict";
27894
- init_db();
27895
- init_config();
27896
- init_tools();
27897
27970
  init_memory_db();
27898
27971
  init_license();
27899
- init_tool_db_needs();
27972
+ init_server_dispatch();
27900
27973
  __dirname4 = dirname16(fileURLToPath4(import.meta.url));
27901
27974
  PKG_VERSION = (() => {
27902
27975
  try {
@@ -27906,8 +27979,7 @@ var init_server = __esm({
27906
27979
  return "0.0.0";
27907
27980
  }
27908
27981
  })();
27909
- codegraphDbCache = null;
27910
- dataDbCache = null;
27982
+ dispatcher = createDispatcher({ serverInfoVersion: PKG_VERSION });
27911
27983
  pruneMemoryOnStartup();
27912
27984
  getCurrentTier().then((tier) => {
27913
27985
  process.stderr.write(`massu: License tier: ${tier}
@@ -27924,46 +27996,16 @@ var init_server = __esm({
27924
27996
  buffer += chunk;
27925
27997
  let newlineIndex;
27926
27998
  while ((newlineIndex = buffer.indexOf("\n")) !== -1) {
27927
- const line = buffer.slice(0, newlineIndex).trim();
27999
+ const line = buffer.slice(0, newlineIndex);
27928
28000
  buffer = buffer.slice(newlineIndex + 1);
27929
- if (!line) continue;
27930
- let request = null;
27931
- try {
27932
- request = JSON.parse(line);
27933
- } catch (parseError) {
27934
- const errorResponse = {
27935
- jsonrpc: "2.0",
27936
- id: null,
27937
- error: {
27938
- code: -32700,
27939
- message: `Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`
27940
- }
27941
- };
27942
- process.stdout.write(JSON.stringify(errorResponse) + "\n");
27943
- continue;
27944
- }
27945
- try {
27946
- const response = await handleRequest(request);
27947
- if (request.id !== void 0) {
27948
- const responseStr = JSON.stringify(response);
27949
- process.stdout.write(responseStr + "\n");
27950
- }
27951
- } catch (error) {
27952
- const errorResponse = {
27953
- jsonrpc: "2.0",
27954
- id: request.id ?? null,
27955
- error: {
27956
- code: -32603,
27957
- message: `Internal error: ${error instanceof Error ? error.message : String(error)}`
27958
- }
27959
- };
27960
- process.stdout.write(JSON.stringify(errorResponse) + "\n");
28001
+ const result = await dispatcher.processLine(line);
28002
+ if (result && result.emit) {
28003
+ process.stdout.write(JSON.stringify(result.response) + "\n");
27961
28004
  }
27962
28005
  }
27963
28006
  });
27964
28007
  process.stdin.on("end", () => {
27965
- if (codegraphDbCache) codegraphDbCache.close();
27966
- if (dataDbCache) dataDbCache.close();
28008
+ dispatcher.closeCachedDbs();
27967
28009
  process.exit(0);
27968
28010
  });
27969
28011
  process.on("uncaughtException", (error) => {
@@ -8360,14 +8360,33 @@ function domainFromWorkspace(pkg) {
8360
8360
  allowedImportsFrom: []
8361
8361
  };
8362
8362
  }
8363
- function topLevelSrcSubdirs(root) {
8364
- const srcDir = join3(root, "src");
8365
- if (!existsSync5(srcDir)) return [];
8366
- try {
8367
- return readdirSync3(srcDir, { withFileTypes: true }).filter((e) => e.isDirectory() && !IGNORED_SUBDIRS.has(e.name)).map((e) => e.name).sort();
8368
- } catch {
8369
- return [];
8363
+ function topLevelSrcSubdirs(root, sourceDirs) {
8364
+ const effective = sourceDirs.length > 0 ? sourceDirs : ["src"];
8365
+ const seen = /* @__PURE__ */ new Set();
8366
+ for (const rel of effective) {
8367
+ const abs = join3(root, rel);
8368
+ if (!existsSync5(abs)) continue;
8369
+ try {
8370
+ for (const e of readdirSync3(abs, { withFileTypes: true })) {
8371
+ if (!e.isDirectory()) continue;
8372
+ if (IGNORED_SUBDIRS.has(e.name)) continue;
8373
+ seen.add(e.name);
8374
+ }
8375
+ } catch {
8376
+ }
8377
+ }
8378
+ return Array.from(seen).sort();
8379
+ }
8380
+ function flattenSourceDirs(sourceDirs) {
8381
+ const flat = /* @__PURE__ */ new Set();
8382
+ for (const entry of Object.values(sourceDirs)) {
8383
+ if (!entry) continue;
8384
+ for (const dir of entry.source_dirs) {
8385
+ if (dir === "." || dir === "") continue;
8386
+ flat.add(dir);
8387
+ }
8370
8388
  }
8389
+ return Array.from(flat);
8371
8390
  }
8372
8391
  function inferDomains(projectRoot, monorepo, sourceDirs) {
8373
8392
  const domains = [];
@@ -8376,7 +8395,8 @@ function inferDomains(projectRoot, monorepo, sourceDirs) {
8376
8395
  domains.push(domainFromWorkspace(pkg));
8377
8396
  }
8378
8397
  } else {
8379
- const subdirs = topLevelSrcSubdirs(projectRoot);
8398
+ const flat = flattenSourceDirs(sourceDirs);
8399
+ const subdirs = topLevelSrcSubdirs(projectRoot, flat);
8380
8400
  for (const s of subdirs) {
8381
8401
  domains.push({
8382
8402
  name: titleCase(s),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "1.6.2",
3
+ "version": "1.7.0",
4
4
  "type": "module",
5
5
  "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 55+ workflow commands, 11 agents, 20+ patterns",
6
6
  "main": "src/server.ts",
@@ -67,17 +67,69 @@ function domainFromWorkspace(pkg: WorkspacePackage): DomainConfig {
67
67
  };
68
68
  }
69
69
 
70
- function topLevelSrcSubdirs(root: string): string[] {
71
- const srcDir = join(root, 'src');
72
- if (!existsSync(srcDir)) return [];
73
- try {
74
- return readdirSync(srcDir, { withFileTypes: true })
75
- .filter((e) => e.isDirectory() && !IGNORED_SUBDIRS.has(e.name))
76
- .map((e) => e.name)
77
- .sort();
78
- } catch {
79
- return [];
70
+ /**
71
+ * Enumerate domain candidates under each detected source directory.
72
+ *
73
+ * The `sourceDirs` argument is the flattened, unique list of relative
74
+ * source paths produced upstream by the source-dir detector
75
+ * (`detectSourceDirs` in `source-dir-detector.ts`). For each path that
76
+ * exists under `root`, this function lists immediate subdirectories as
77
+ * candidate domain names. Hardcoded `src/` lookup was removed (plan
78
+ * `plan-1.7.0-cohesive-cleanup` P-B-002) — the function now consumes
79
+ * the detection pipeline's output verbatim, so projects whose source
80
+ * lives at non-`src/` paths (e.g. `lib/`, `apps/<x>/src/`) are no
81
+ * longer silently dropped.
82
+ *
83
+ * Empty `sourceDirs` is treated as a legacy single-repo `src/` lookup
84
+ * to preserve behavior for callers that pre-date the source-dir
85
+ * pipeline (CLI / test harnesses that hand-wire `inferDomains`).
86
+ *
87
+ * Returns deduplicated subdir names sorted alphabetically.
88
+ */
89
+ function topLevelSrcSubdirs(root: string, sourceDirs: readonly string[]): string[] {
90
+ const effective = sourceDirs.length > 0 ? sourceDirs : ['src'];
91
+ const seen = new Set<string>();
92
+ for (const rel of effective) {
93
+ const abs = join(root, rel);
94
+ if (!existsSync(abs)) continue;
95
+ try {
96
+ for (const e of readdirSync(abs, { withFileTypes: true })) {
97
+ if (!e.isDirectory()) continue;
98
+ if (IGNORED_SUBDIRS.has(e.name)) continue;
99
+ seen.add(e.name);
100
+ }
101
+ } catch {
102
+ // skip directories that cannot be read; do not throw.
103
+ }
104
+ }
105
+ return Array.from(seen).sort();
106
+ }
107
+
108
+ /**
109
+ * Flatten a `SourceDirMap` into a unique, deduplicated list of relative
110
+ * source paths across all detected languages.
111
+ *
112
+ * Drops the root sentinels `.` and `''` — those are emitted by the
113
+ * source-dir-detector when source files live directly at the project
114
+ * root (e.g. Django's `manage.py` or Swift's `Package.swift`). Treating
115
+ * them as enumerable source dirs causes spurious top-level directory
116
+ * inclusion (Tests/, Sources/, etc.), which collides with the
117
+ * language-fallback path in `inferDomains`. Root-source repos rely on
118
+ * the language-fallback path to emit `{Python}` / `{Swift}` domains,
119
+ * NOT a fan-out of every root subdirectory.
120
+ *
121
+ * Order is not guaranteed — callers that need determinism must sort.
122
+ */
123
+ function flattenSourceDirs(sourceDirs: SourceDirMap): string[] {
124
+ const flat = new Set<string>();
125
+ for (const entry of Object.values(sourceDirs)) {
126
+ if (!entry) continue;
127
+ for (const dir of entry.source_dirs) {
128
+ if (dir === '.' || dir === '') continue;
129
+ flat.add(dir);
130
+ }
80
131
  }
132
+ return Array.from(flat);
81
133
  }
82
134
 
83
135
  /**
@@ -100,8 +152,11 @@ export function inferDomains(
100
152
  domains.push(domainFromWorkspace(pkg));
101
153
  }
102
154
  } else {
103
- // Single repo: suggest one domain per top-level src/<subdir>/ if src/ exists.
104
- const subdirs = topLevelSrcSubdirs(projectRoot);
155
+ // Single repo: suggest one domain per top-level <sourceDir>/<subdir>/ for
156
+ // every detected source dir (formerly hardcoded to `src/` only — see
157
+ // P-B-002 in plan-1.7.0-cohesive-cleanup).
158
+ const flat = flattenSourceDirs(sourceDirs);
159
+ const subdirs = topLevelSrcSubdirs(projectRoot, flat);
105
160
  for (const s of subdirs) {
106
161
  domains.push({
107
162
  name: titleCase(s),
package/src/license.ts CHANGED
@@ -203,6 +203,26 @@ export function annotateToolDefinitions(defs: ToolDefinition[]): ToolDefinition[
203
203
  });
204
204
  }
205
205
 
206
+ // ============================================================
207
+ // plan-1.7.0-cohesive-cleanup P-A-003: Cloud feature availability gate
208
+ // ============================================================
209
+
210
+ /**
211
+ * Whether cloud-gated tool surfaces (team knowledge, etc.) are exposed.
212
+ *
213
+ * Returns true ONLY when `massu.config.yaml` opts the workspace into the
214
+ * cloud feature surface via `cloud.enabled: true`. Defaults to false for
215
+ * fresh installs (the schema's `enabled` default is `false`).
216
+ *
217
+ * This is distinct from {@link isLicenseTool} (which matches tool NAMES);
218
+ * `isCloudFeatureAvailable` is a runtime feature-availability check used
219
+ * by `tools.ts` to gate team-tool registration and routing at the
220
+ * tools-list and dispatch boundaries.
221
+ */
222
+ export function isCloudFeatureAvailable(): boolean {
223
+ return getConfig().cloud?.enabled === true;
224
+ }
225
+
206
226
  // ============================================================
207
227
  // P3-005/P3-006/P3-007/P3-013: License validation & caching
208
228
  // ============================================================
@@ -1,4 +1,4 @@
1
- // AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-11T05:57:55.925Z.
1
+ // AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-12T04:00:39.397Z.
2
2
  // Source pem: packages/core/security/registry-pubkey.pem
3
3
  // RAW-bytes sha256: 3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c
4
4
  // DO NOT EDIT — regenerate via `node scripts/bundle-pubkey.mjs` or
@@ -0,0 +1,225 @@
1
+ // Copyright (c) 2026 Massu. All rights reserved.
2
+ // Licensed under BSL 1.1 - see LICENSE file for details.
3
+
4
+ /**
5
+ * MCP server dispatch logic — pure, factory-based, no module-level mutable state.
6
+ *
7
+ * Production (`server.ts`) calls `createDispatcher()` once at startup and wires
8
+ * its `processLine` into stdin. Tests call `createDispatcher()` per test for
9
+ * fresh DB cache state (no test bleed).
10
+ *
11
+ * Three error envelopes live here (CR-12 / plan-1.6.2-server-lazy-db-deps):
12
+ * -32001 Tool needs CodeGraph but `.codegraph/codegraph.db` is missing
13
+ * (CodegraphDbNotInitializedError → structured remedy data)
14
+ * -32602 Tool not registered in `TOOL_DB_NEEDS` manifest
15
+ * (UnknownToolError → points at tool-db-needs.ts)
16
+ * -32603 Other internal errors raised by handleToolCall — request id
17
+ * is preserved (NOT id:null, which is reserved for -32700 parse
18
+ * failures per JSON-RPC §5.1).
19
+ */
20
+
21
+ import type Database from 'better-sqlite3';
22
+ import { getCodeGraphDb, getDataDb, CodegraphDbNotInitializedError } from './db.ts';
23
+ import { getConfig } from './config.ts';
24
+ import { getToolDefinitions, handleToolCall } from './tools.ts';
25
+ import { getToolDbNeeds, UnknownToolError, type DbNeed } from './tool-db-needs.ts';
26
+
27
+ export interface JsonRpcRequest {
28
+ jsonrpc: '2.0';
29
+ id?: number | string;
30
+ method: string;
31
+ params?: Record<string, unknown>;
32
+ }
33
+
34
+ export interface JsonRpcResponse {
35
+ jsonrpc: '2.0';
36
+ id: number | string | null;
37
+ result?: unknown;
38
+ error?: { code: number; message: string; data?: unknown };
39
+ }
40
+
41
+ /** Per-line dispatch result. `emit=false` when the request was a notification (no id). */
42
+ export interface ProcessLineResult {
43
+ response: JsonRpcResponse;
44
+ emit: boolean;
45
+ }
46
+
47
+ export interface DispatcherOptions {
48
+ /** Version string surfaced in `initialize.result.serverInfo.version`. */
49
+ serverInfoVersion: string;
50
+ }
51
+
52
+ export interface Dispatcher {
53
+ handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse>;
54
+ processLine(line: string): Promise<ProcessLineResult | null>;
55
+ closeCachedDbs(): void;
56
+ }
57
+
58
+ export function createDispatcher(options: DispatcherOptions): Dispatcher {
59
+ let codegraphDbCache: Database.Database | null = null;
60
+ let dataDbCache: Database.Database | null = null;
61
+
62
+ function resolveDbsForTool(toolName: string): {
63
+ needs: readonly DbNeed[];
64
+ dataDb?: Database.Database;
65
+ codegraphDb?: Database.Database;
66
+ } {
67
+ const needs = getToolDbNeeds(toolName, getConfig().toolPrefix);
68
+
69
+ let dataDbResolved: Database.Database | undefined;
70
+ let codegraphDbResolved: Database.Database | undefined;
71
+
72
+ if (needs.includes('data')) {
73
+ if (!dataDbCache) dataDbCache = getDataDb();
74
+ dataDbResolved = dataDbCache;
75
+ }
76
+
77
+ if (needs.includes('codegraph')) {
78
+ // Throws CodegraphDbNotInitializedError when .codegraph/codegraph.db is missing.
79
+ if (!codegraphDbCache) codegraphDbCache = getCodeGraphDb();
80
+ codegraphDbResolved = codegraphDbCache;
81
+ }
82
+
83
+ return { needs, dataDb: dataDbResolved, codegraphDb: codegraphDbResolved };
84
+ }
85
+
86
+ async function handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {
87
+ const { method, params, id } = request;
88
+
89
+ switch (method) {
90
+ case 'initialize': {
91
+ return {
92
+ jsonrpc: '2.0',
93
+ id: id ?? null,
94
+ result: {
95
+ protocolVersion: '2024-11-05',
96
+ capabilities: { tools: {} },
97
+ serverInfo: {
98
+ name: getConfig().toolPrefix || 'massu',
99
+ version: options.serverInfoVersion,
100
+ },
101
+ },
102
+ };
103
+ }
104
+
105
+ case 'notifications/initialized': {
106
+ return { jsonrpc: '2.0', id: id ?? null, result: {} };
107
+ }
108
+
109
+ case 'tools/list': {
110
+ const tools = getToolDefinitions();
111
+ return { jsonrpc: '2.0', id: id ?? null, result: { tools } };
112
+ }
113
+
114
+ case 'tools/call': {
115
+ const toolName = (params as { name?: string })?.name ?? '';
116
+ const toolArgs = (params as { arguments?: Record<string, unknown> })?.arguments ?? {};
117
+
118
+ try {
119
+ const { dataDb, codegraphDb } = resolveDbsForTool(toolName);
120
+ const result = await handleToolCall(toolName, toolArgs, dataDb, codegraphDb);
121
+ return { jsonrpc: '2.0', id: id ?? null, result };
122
+ } catch (err) {
123
+ if (err instanceof CodegraphDbNotInitializedError) {
124
+ return {
125
+ jsonrpc: '2.0',
126
+ id: id ?? null,
127
+ error: {
128
+ code: -32001,
129
+ message: 'Tool requires CodeGraph database which is not initialized for this repo',
130
+ data: {
131
+ remedy: 'npx @colbymchenry/codegraph@0.7.4 init . && npx @colbymchenry/codegraph@0.7.4 index .',
132
+ codegraphDbPath: err.dbPath,
133
+ tool: toolName,
134
+ },
135
+ },
136
+ };
137
+ }
138
+ if (err instanceof UnknownToolError) {
139
+ return {
140
+ jsonrpc: '2.0',
141
+ id: id ?? null,
142
+ error: {
143
+ code: -32602,
144
+ message: `Unknown tool: ${err.toolName}`,
145
+ data: {
146
+ remedy: 'Tool not registered in TOOL_DB_NEEDS manifest. See packages/core/src/tool-db-needs.ts.',
147
+ tool: toolName,
148
+ },
149
+ },
150
+ };
151
+ }
152
+ throw err;
153
+ }
154
+ }
155
+
156
+ case 'ping': {
157
+ return { jsonrpc: '2.0', id: id ?? null, result: {} };
158
+ }
159
+
160
+ default: {
161
+ return {
162
+ jsonrpc: '2.0',
163
+ id: id ?? null,
164
+ error: { code: -32601, message: `Method not found: ${method}` },
165
+ };
166
+ }
167
+ }
168
+ }
169
+
170
+ async function processLine(line: string): Promise<ProcessLineResult | null> {
171
+ const trimmed = line.trim();
172
+ if (!trimmed) return null;
173
+
174
+ let request: JsonRpcRequest;
175
+ try {
176
+ request = JSON.parse(trimmed) as JsonRpcRequest;
177
+ } catch (parseError) {
178
+ // JSON-RPC §5.1: parse failure → -32700 + id:null (no id is extractable).
179
+ return {
180
+ response: {
181
+ jsonrpc: '2.0',
182
+ id: null,
183
+ error: {
184
+ code: -32700,
185
+ message: `Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
186
+ },
187
+ },
188
+ emit: true,
189
+ };
190
+ }
191
+
192
+ try {
193
+ const response = await handleRequest(request);
194
+ // Notifications (no id) MUST NOT receive a response per JSON-RPC §4.1.
195
+ return { response, emit: request.id !== undefined };
196
+ } catch (error) {
197
+ // Request-processing failure: -32603 with the request id preserved.
198
+ // Specific subclasses (-32001/-32602) are handled inside tools/call.
199
+ return {
200
+ response: {
201
+ jsonrpc: '2.0',
202
+ id: request.id ?? null,
203
+ error: {
204
+ code: -32603,
205
+ message: `Internal error: ${error instanceof Error ? error.message : String(error)}`,
206
+ },
207
+ },
208
+ emit: true,
209
+ };
210
+ }
211
+ }
212
+
213
+ function closeCachedDbs(): void {
214
+ if (codegraphDbCache) {
215
+ codegraphDbCache.close();
216
+ codegraphDbCache = null;
217
+ }
218
+ if (dataDbCache) {
219
+ dataDbCache.close();
220
+ dataDbCache = null;
221
+ }
222
+ }
223
+
224
+ return { handleRequest, processLine, closeCachedDbs };
225
+ }
package/src/server.ts CHANGED
@@ -14,13 +14,9 @@
14
14
  import { readFileSync } from 'fs';
15
15
  import { resolve, dirname } from 'path';
16
16
  import { fileURLToPath } from 'url';
17
- import type Database from 'better-sqlite3';
18
- import { getCodeGraphDb, getDataDb, CodegraphDbNotInitializedError } from './db.ts';
19
- import { getConfig, getResolvedPaths } from './config.ts';
20
- import { getToolDefinitions, handleToolCall } from './tools.ts';
21
17
  import { getMemoryDb, pruneOldConversationTurns, pruneOldObservations } from './memory-db.ts';
22
18
  import { getCurrentTier } from './license.ts';
23
- import { getToolDbNeeds, UnknownToolError, type DbNeed } from './tool-db-needs.ts';
19
+ import { createDispatcher } from './server-dispatch.ts';
24
20
 
25
21
  const __dirname = dirname(fileURLToPath(import.meta.url));
26
22
  const PKG_VERSION = (() => {
@@ -32,167 +28,7 @@ const PKG_VERSION = (() => {
32
28
  }
33
29
  })();
34
30
 
35
- interface JsonRpcRequest {
36
- jsonrpc: '2.0';
37
- id?: number | string;
38
- method: string;
39
- params?: Record<string, unknown>;
40
- }
41
-
42
- interface JsonRpcResponse {
43
- jsonrpc: '2.0';
44
- id: number | string | null;
45
- result?: unknown;
46
- error?: { code: number; message: string; data?: unknown };
47
- }
48
-
49
- // === Server state: lazy per-tool DB resolution ===
50
- //
51
- // Per plan-1.6.2-server-lazy-db-deps: DBs are opened ONLY when the
52
- // currently-dispatched tool declares it needs them in `TOOL_DB_NEEDS`.
53
- // Connections are cached at module scope so subsequent tool calls reuse
54
- // the open handle without re-opening (CodeGraph is read-only — safe to
55
- // share; Data DB has WAL journal — single-writer is fine).
56
- //
57
- // PRIOR DESIGN (eliminated 2026-05-10): `getDb()` eagerly opened BOTH
58
- // CodeGraph + Data on every `tools/call`, even for memory/audit/knowledge
59
- // tools that don't need codegraph. Missing `.codegraph/codegraph.db`
60
- // broke ALL tools. See `docs/plans/2026-05-10-server-lazy-db-deps.md`.
61
-
62
- let codegraphDbCache: Database.Database | null = null;
63
- let dataDbCache: Database.Database | null = null;
64
-
65
- /**
66
- * Resolve the SQLite connections a tool needs, opening cached singletons
67
- * lazily. Memory DB and Knowledge DB are opened per-call by their routed
68
- * handlers (existing pattern in tools.ts) — only CodeGraph + Data are
69
- * cached here.
70
- *
71
- * @throws {CodegraphDbNotInitializedError} when tool needs codegraph but
72
- * `.codegraph/codegraph.db` is missing. Caller (handleRequest) catches
73
- * and translates to a structured `-32001` JSON-RPC error.
74
- */
75
- function resolveDbsForTool(toolName: string): {
76
- needs: readonly DbNeed[];
77
- dataDb?: Database.Database;
78
- codegraphDb?: Database.Database;
79
- } {
80
- const needs = getToolDbNeeds(toolName, getConfig().toolPrefix);
81
-
82
- let dataDbResolved: Database.Database | undefined;
83
- let codegraphDbResolved: Database.Database | undefined;
84
-
85
- if (needs.includes('data')) {
86
- if (!dataDbCache) dataDbCache = getDataDb();
87
- dataDbResolved = dataDbCache;
88
- }
89
-
90
- if (needs.includes('codegraph')) {
91
- if (!codegraphDbCache) codegraphDbCache = getCodeGraphDb(); // throws CodegraphDbNotInitializedError on missing
92
- codegraphDbResolved = codegraphDbCache;
93
- }
94
-
95
- return { needs, dataDb: dataDbResolved, codegraphDb: codegraphDbResolved };
96
- }
97
-
98
- async function handleRequest(request: JsonRpcRequest): Promise<JsonRpcResponse> {
99
- const { method, params, id } = request;
100
-
101
- switch (method) {
102
- case 'initialize': {
103
- return {
104
- jsonrpc: '2.0',
105
- id: id ?? null,
106
- result: {
107
- protocolVersion: '2024-11-05',
108
- capabilities: {
109
- tools: {},
110
- },
111
- serverInfo: {
112
- name: getConfig().toolPrefix || 'massu',
113
- version: PKG_VERSION,
114
- },
115
- },
116
- };
117
- }
118
-
119
- case 'notifications/initialized': {
120
- // Client acknowledges initialization - no response needed for notifications
121
- return { jsonrpc: '2.0', id: id ?? null, result: {} };
122
- }
123
-
124
- case 'tools/list': {
125
- const tools = getToolDefinitions();
126
- return {
127
- jsonrpc: '2.0',
128
- id: id ?? null,
129
- result: { tools },
130
- };
131
- }
132
-
133
- case 'tools/call': {
134
- const toolName = (params as { name: string })?.name;
135
- const toolArgs = (params as { arguments?: Record<string, unknown> })?.arguments ?? {};
136
-
137
- // Lazy per-tool DB resolution. Throws if tool needs codegraph and
138
- // .codegraph/codegraph.db is missing; caught below and translated
139
- // to a structured -32001 error preserving the request id.
140
- try {
141
- const { dataDb: lDb, codegraphDb: cgDb } = resolveDbsForTool(toolName);
142
- const result = await handleToolCall(toolName, toolArgs, lDb, cgDb);
143
- return {
144
- jsonrpc: '2.0',
145
- id: id ?? null,
146
- result,
147
- };
148
- } catch (err) {
149
- if (err instanceof CodegraphDbNotInitializedError) {
150
- return {
151
- jsonrpc: '2.0',
152
- id: id ?? null,
153
- error: {
154
- code: -32001,
155
- message: `Tool requires CodeGraph database which is not initialized for this repo`,
156
- data: {
157
- remedy: 'npx @colbymchenry/codegraph@0.7.4 init . && npx @colbymchenry/codegraph@0.7.4 index .',
158
- codegraphDbPath: err.dbPath,
159
- tool: toolName,
160
- },
161
- },
162
- };
163
- }
164
- if (err instanceof UnknownToolError) {
165
- return {
166
- jsonrpc: '2.0',
167
- id: id ?? null,
168
- error: {
169
- code: -32602,
170
- message: `Unknown tool: ${err.toolName}`,
171
- data: {
172
- remedy: 'Tool not registered in TOOL_DB_NEEDS manifest. See packages/core/src/tool-db-needs.ts.',
173
- tool: toolName,
174
- },
175
- },
176
- };
177
- }
178
- // Other errors propagate to the outer catch in the stdio handler
179
- throw err;
180
- }
181
- }
182
-
183
- case 'ping': {
184
- return { jsonrpc: '2.0', id: id ?? null, result: {} };
185
- }
186
-
187
- default: {
188
- return {
189
- jsonrpc: '2.0',
190
- id: id ?? null,
191
- error: { code: -32601, message: `Method not found: ${method}` },
192
- };
193
- }
194
- }
195
- }
31
+ const dispatcher = createDispatcher({ serverInfoVersion: PKG_VERSION });
196
32
 
197
33
  // === Startup: prune stale memory data (non-blocking) ===
198
34
 
@@ -244,62 +80,20 @@ process.stdin.on('data', async (chunk: string) => {
244
80
  // Process complete messages (newline-delimited JSON-RPC)
245
81
  let newlineIndex: number;
246
82
  while ((newlineIndex = buffer.indexOf('\n')) !== -1) {
247
- const line = buffer.slice(0, newlineIndex).trim();
83
+ const line = buffer.slice(0, newlineIndex);
248
84
  buffer = buffer.slice(newlineIndex + 1);
249
85
 
250
- if (!line) continue;
251
-
252
- // Two-phase error handling: separate JSON-parse failures (genuine
253
- // -32700) from request-processing failures (-32603 Internal error,
254
- // preserving the request id when parseable).
255
- let request: JsonRpcRequest | null = null;
256
- try {
257
- request = JSON.parse(line) as JsonRpcRequest;
258
- } catch (parseError) {
259
- // Real JSON parse failure — -32700 per JSON-RPC §5.1, id MUST be null
260
- // because we couldn't extract one.
261
- const errorResponse: JsonRpcResponse = {
262
- jsonrpc: '2.0',
263
- id: null,
264
- error: {
265
- code: -32700,
266
- message: `Parse error: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
267
- },
268
- };
269
- process.stdout.write(JSON.stringify(errorResponse) + '\n');
270
- continue;
271
- }
272
-
273
- try {
274
- const response = await handleRequest(request);
275
- // Don't send responses for notifications (no id)
276
- if (request.id !== undefined) {
277
- const responseStr = JSON.stringify(response);
278
- process.stdout.write(responseStr + '\n');
279
- }
280
- } catch (error) {
281
- // Request-processing failure — propagate the request id (not null).
282
- // -32603 Internal error per JSON-RPC §5.1. Specific subclasses
283
- // (codegraph-not-init, unknown-tool) are caught earlier in the
284
- // tools/call handler and translated to structured -32001/-32602.
285
- const errorResponse: JsonRpcResponse = {
286
- jsonrpc: '2.0',
287
- id: request.id ?? null,
288
- error: {
289
- code: -32603,
290
- message: `Internal error: ${error instanceof Error ? error.message : String(error)}`,
291
- },
292
- };
293
- process.stdout.write(JSON.stringify(errorResponse) + '\n');
86
+ const result = await dispatcher.processLine(line);
87
+ if (result && result.emit) {
88
+ process.stdout.write(JSON.stringify(result.response) + '\n');
294
89
  }
295
90
  }
296
91
  });
297
92
 
298
93
  process.stdin.on('end', () => {
299
- // Clean up cached DB connections (Memory + Knowledge are per-call,
300
- // already closed in their routing branches).
301
- if (codegraphDbCache) codegraphDbCache.close();
302
- if (dataDbCache) dataDbCache.close();
94
+ // Close cached CodeGraph + Data connections. Memory + Knowledge are
95
+ // per-call (closed inside their routing branches in tools.ts).
96
+ dispatcher.closeCachedDbs();
303
97
  process.exit(0);
304
98
  });
305
99
 
package/src/tools.ts CHANGED
@@ -38,7 +38,7 @@ import { getKnowledgeToolDefinitions, isKnowledgeTool, handleKnowledgeToolCall }
38
38
  import { getKnowledgeDb } from './knowledge-db.ts';
39
39
  import { getPythonToolDefinitions, isPythonTool, handlePythonToolCall } from './python-tools.ts';
40
40
  import { getConfig, getProjectRoot, getResolvedPaths } from './config.ts';
41
- import { getCurrentTier, getToolTier, isToolAllowed, annotateToolDefinitions, getLicenseToolDefinitions, isLicenseTool, handleLicenseToolCall } from './license.ts';
41
+ import { getCurrentTier, getToolTier, isToolAllowed, annotateToolDefinitions, getLicenseToolDefinitions, isLicenseTool, handleLicenseToolCall, isCloudFeatureAvailable } from './license.ts';
42
42
 
43
43
  export interface ToolDefinition {
44
44
  name: string;
@@ -172,7 +172,8 @@ export function getToolDefinitions(): ToolDefinition[] {
172
172
  ...getSecurityToolDefinitions(),
173
173
  ...getDependencyToolDefinitions(),
174
174
  // Enterprise layer (team knowledge, regression detection)
175
- ...getTeamToolDefinitions(),
175
+ // P-A-003: team tools are cloud-gated — only listed when cloud.enabled is true.
176
+ ...(isCloudFeatureAvailable() ? getTeamToolDefinitions() : []),
176
177
  ...getRegressionToolDefinitions(),
177
178
  // Knowledge layer (indexed .claude/ knowledge — rules, patterns, incidents)
178
179
  ...getKnowledgeToolDefinitions(),
@@ -410,7 +411,9 @@ export async function handleToolCall(
410
411
  }
411
412
 
412
413
  // Route enterprise layer tools
413
- if (isTeamTool(name)) {
414
+ // P-A-003: team-tool dispatch gated on cloud availability; if a stale client
415
+ // still has the tool name cached, fall through to the unknown-tool branch.
416
+ if (isTeamTool(name) && isCloudFeatureAvailable()) {
414
417
  const memDb = getMemoryDb();
415
418
  try { return handleTeamToolCall(name, args, memDb); }
416
419
  finally { memDb.close(); }