monomind 1.10.16 → 1.10.17

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "monomind",
3
- "version": "1.10.16",
3
+ "version": "1.10.17",
4
4
  "description": "Monomind - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -23,6 +23,9 @@
23
23
  "packages/@monomind/cli/dist/**/*.d.ts",
24
24
  "!packages/@monomind/cli/dist/**/*.map",
25
25
  "packages/@monomind/cli/bundled-graph/**",
26
+ "packages/@monomind/cli/scripts/**/*.mjs",
27
+ "packages/@monomind/cli/scripts/**/*.js",
28
+ "packages/@monomind/cli/scripts/**/*.sh",
26
29
  "packages/@monomind/cli/package.json",
27
30
  "packages/@monomind/shared/dist/**/*.js",
28
31
  "packages/@monomind/shared/dist/**/*.d.ts",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monoes/monomindcli",
3
- "version": "1.10.16",
3
+ "version": "1.10.17",
4
4
  "type": "module",
5
5
  "description": "Monomind CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",
@@ -0,0 +1,153 @@
1
+ #!/bin/bash
2
+ #
3
+ # Deploy IPFS Node to Google Cloud
4
+ # Provides free IPFS pinning for your users
5
+ #
6
+ # Usage:
7
+ # ./deploy-ipfs-node.sh [PROJECT_ID] [ZONE]
8
+ #
9
+ # Example:
10
+ # ./deploy-ipfs-node.sh my-project us-central1-a
11
+ #
12
+
13
+ set -e
14
+
15
+ PROJECT_ID="${1:-$(gcloud config get-value project)}"
16
+ ZONE="${2:-us-central1-a}"
17
+ INSTANCE_NAME="ipfs-node"
18
+ MACHINE_TYPE="e2-medium" # $25/month, use e2-small for $8/month
19
+ DISK_SIZE="100GB"
20
+
21
+ echo "╔══════════════════════════════════════════════════════════════╗"
22
+ echo "║ IPFS Node Deployment for Monomind ║"
23
+ echo "╚══════════════════════════════════════════════════════════════╝"
24
+ echo ""
25
+ echo "Project: $PROJECT_ID"
26
+ echo "Zone: $ZONE"
27
+ echo "Machine: $MACHINE_TYPE"
28
+ echo "Disk: $DISK_SIZE"
29
+ echo ""
30
+
31
+ # Create firewall rules
32
+ echo "▶ Creating firewall rules..."
33
+ gcloud compute firewall-rules create ipfs-swarm \
34
+ --project="$PROJECT_ID" \
35
+ --allow=tcp:4001,udp:4001 \
36
+ --target-tags=ipfs-node \
37
+ --description="IPFS swarm connections" \
38
+ 2>/dev/null || echo " (firewall rule already exists)"
39
+
40
+ gcloud compute firewall-rules create ipfs-api \
41
+ --project="$PROJECT_ID" \
42
+ --allow=tcp:5001 \
43
+ --target-tags=ipfs-node \
44
+ --source-ranges="0.0.0.0/0" \
45
+ --description="IPFS API (consider restricting)" \
46
+ 2>/dev/null || echo " (firewall rule already exists)"
47
+
48
+ gcloud compute firewall-rules create ipfs-gateway \
49
+ --project="$PROJECT_ID" \
50
+ --allow=tcp:8080 \
51
+ --target-tags=ipfs-node \
52
+ --description="IPFS Gateway" \
53
+ 2>/dev/null || echo " (firewall rule already exists)"
54
+
55
+ # Create startup script
56
+ STARTUP_SCRIPT='#!/bin/bash
57
+ set -e
58
+
59
+ # Install IPFS
60
+ echo "Installing IPFS..."
61
+ wget -q https://dist.ipfs.tech/kubo/v0.24.0/kubo_v0.24.0_linux-amd64.tar.gz
62
+ tar xzf kubo_v0.24.0_linux-amd64.tar.gz
63
+ cd kubo && sudo bash install.sh
64
+ cd .. && rm -rf kubo kubo_v0.24.0_linux-amd64.tar.gz
65
+
66
+ # Create ipfs user
67
+ sudo useradd -m -s /bin/bash ipfs || true
68
+
69
+ # Initialize IPFS
70
+ sudo -u ipfs IPFS_PATH=/home/ipfs/.ipfs ipfs init --profile=server
71
+
72
+ # Configure IPFS for server use
73
+ sudo -u ipfs IPFS_PATH=/home/ipfs/.ipfs ipfs config Addresses.API /ip4/0.0.0.0/tcp/5001
74
+ sudo -u ipfs IPFS_PATH=/home/ipfs/.ipfs ipfs config Addresses.Gateway /ip4/0.0.0.0/tcp/8080
75
+ sudo -u ipfs IPFS_PATH=/home/ipfs/.ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Origin "[\"*\"]"
76
+ sudo -u ipfs IPFS_PATH=/home/ipfs/.ipfs ipfs config --json API.HTTPHeaders.Access-Control-Allow-Methods "[\"PUT\", \"POST\", \"GET\"]"
77
+
78
+ # Set storage limits (adjust as needed)
79
+ sudo -u ipfs IPFS_PATH=/home/ipfs/.ipfs ipfs config Datastore.StorageMax 80GB
80
+
81
+ # Create systemd service
82
+ cat > /etc/systemd/system/ipfs.service << EOF
83
+ [Unit]
84
+ Description=IPFS Daemon
85
+ After=network.target
86
+
87
+ [Service]
88
+ Type=simple
89
+ User=ipfs
90
+ Environment=IPFS_PATH=/home/ipfs/.ipfs
91
+ ExecStart=/usr/local/bin/ipfs daemon --migrate=true
92
+ Restart=on-failure
93
+ RestartSec=10
94
+
95
+ [Install]
96
+ WantedBy=multi-user.target
97
+ EOF
98
+
99
+ # Start IPFS
100
+ systemctl daemon-reload
101
+ systemctl enable ipfs
102
+ systemctl start ipfs
103
+
104
+ echo "IPFS node started successfully!"
105
+ '
106
+
107
+ # Create instance
108
+ echo "▶ Creating VM instance..."
109
+ gcloud compute instances create "$INSTANCE_NAME" \
110
+ --project="$PROJECT_ID" \
111
+ --zone="$ZONE" \
112
+ --machine-type="$MACHINE_TYPE" \
113
+ --image-family=ubuntu-2204-lts \
114
+ --image-project=ubuntu-os-cloud \
115
+ --boot-disk-size="$DISK_SIZE" \
116
+ --boot-disk-type=pd-ssd \
117
+ --tags=ipfs-node \
118
+ --metadata=startup-script="$STARTUP_SCRIPT"
119
+
120
+ # Get external IP
121
+ echo ""
122
+ echo "▶ Waiting for instance to start..."
123
+ sleep 30
124
+
125
+ EXTERNAL_IP=$(gcloud compute instances describe "$INSTANCE_NAME" \
126
+ --project="$PROJECT_ID" \
127
+ --zone="$ZONE" \
128
+ --format='get(networkInterfaces[0].accessConfigs[0].natIP)')
129
+
130
+ echo ""
131
+ echo "═══════════════════════════════════════════════════════════════"
132
+ echo " DEPLOYMENT COMPLETE"
133
+ echo "═══════════════════════════════════════════════════════════════"
134
+ echo ""
135
+ echo " IPFS Node IP: $EXTERNAL_IP"
136
+ echo ""
137
+ echo " Endpoints:"
138
+ echo " API: http://$EXTERNAL_IP:5001"
139
+ echo " Gateway: http://$EXTERNAL_IP:8080"
140
+ echo " Swarm: /ip4/$EXTERNAL_IP/tcp/4001"
141
+ echo ""
142
+ echo " Test commands:"
143
+ echo " curl http://$EXTERNAL_IP:5001/api/v0/id"
144
+ echo " curl http://$EXTERNAL_IP:8080/ipfs/QmYwAPJzv5CZsnA625s3Xf2nemtYgPpHdWEz79ojWnPbdG/readme"
145
+ echo ""
146
+ echo " Configure Monomind CLI:"
147
+ echo " export IPFS_API_URL=http://$EXTERNAL_IP:5001"
148
+ echo ""
149
+ echo " SSH into node:"
150
+ echo " gcloud compute ssh $INSTANCE_NAME --zone=$ZONE"
151
+ echo ""
152
+ echo " Monthly cost estimate: ~\$25-54 depending on usage"
153
+ echo ""
@@ -0,0 +1,55 @@
1
+ #!/bin/bash
2
+ # Publish script for @monomind/cli
3
+ # Publishes to both @monomind/cli@alpha AND monomind@alpha
4
+
5
+ set -e
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
8
+ CLI_DIR="$(dirname "$SCRIPT_DIR")"
9
+
10
+ cd "$CLI_DIR"
11
+
12
+ # Get current version
13
+ VERSION=$(node -p "require('./package.json').version")
14
+ echo "Publishing version: $VERSION"
15
+
16
+ # 1. Publish @monomind/cli with alpha tag
17
+ echo ""
18
+ echo "=== Publishing @monomind/cli@$VERSION (alpha tag) ==="
19
+ npm publish --tag alpha
20
+
21
+ # 2. Publish to monomind with alpha tag
22
+ echo ""
23
+ echo "=== Publishing monomind@$VERSION (alpha tag) ==="
24
+
25
+ # Create temp directory
26
+ TEMP_DIR=$(mktemp -d)
27
+ trap "rm -rf $TEMP_DIR" EXIT
28
+
29
+ # Copy necessary files
30
+ cp -r dist bin src package.json README.md "$TEMP_DIR/"
31
+
32
+ # Change package name to unscoped
33
+ cd "$TEMP_DIR"
34
+ sed -i 's/"name": "@monomind\/cli"/"name": "monomind"/' package.json
35
+
36
+ # Publish with alpha tag
37
+ npm publish --tag alpha
38
+
39
+ echo ""
40
+ echo "=== Updating dist-tags ==="
41
+
42
+ # Update all tags to point to the new version
43
+ npm dist-tag add @monomind/cli@$VERSION alpha
44
+ npm dist-tag add @monomind/cli@$VERSION latest
45
+ npm dist-tag add monomind@$VERSION alpha
46
+ npm dist-tag add monomind@$VERSION latest
47
+
48
+ echo ""
49
+ echo "=== Published successfully ==="
50
+ echo " @monomind/cli@$VERSION (alpha, latest)"
51
+ echo " monomind@$VERSION (alpha, latest)"
52
+ echo ""
53
+ echo "Install with:"
54
+ echo " npx monomind@alpha"
55
+ echo " npx @monomind/cli@latest"
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env bash
2
+ # Syncs distributable .claude assets (skills, commands, agents, helpers) from
3
+ # the monorepo root into this package so they're included in the npm tarball.
4
+ set -e
5
+
6
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7
+ PKG_DIR="$SCRIPT_DIR/.."
8
+ ROOT_DIR="$PKG_DIR/../../.."
9
+ SRC="$ROOT_DIR/.claude"
10
+ DEST="$PKG_DIR/.claude"
11
+
12
+ mkdir -p "$DEST"
13
+
14
+ for dir in skills commands agents helpers; do
15
+ if [ -d "$SRC/$dir" ]; then
16
+ rm -rf "$DEST/$dir"
17
+ # Copy while excluding runtime .monomind subdirectories
18
+ rsync -a --exclude='.monomind/' "$SRC/$dir/" "$DEST/$dir/"
19
+ echo " synced .claude/$dir"
20
+ fi
21
+ done
22
+
23
+ # Bundle @monomind/graph dist for npm installs
24
+ GRAPH_SRC="$ROOT_DIR/packages/@monomind/graph"
25
+ GRAPH_DEST="$PKG_DIR/bundled-graph"
26
+ if [ -d "$GRAPH_SRC/dist" ]; then
27
+ rm -rf "$GRAPH_DEST"
28
+ mkdir -p "$GRAPH_DEST"
29
+ cp -r "$GRAPH_SRC/dist" "$GRAPH_DEST/dist"
30
+ cp "$GRAPH_SRC/package.json" "$GRAPH_DEST/package.json"
31
+ echo " synced bundled-graph"
32
+ fi
33
+
34
+ echo "sync-claude-assets: done"
@@ -0,0 +1,934 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * understand-analyze.mjs — Built-in semantic enrichment engine for monomind:understand
4
+ *
5
+ * Ported from Understand-Anything (understand-anything-plugin/packages/core).
6
+ * Ships inside @monomind/cli — no external plugin needed.
7
+ *
8
+ * Reads file nodes from monograph.db, calls the Anthropic API to generate
9
+ * summaries, tags, complexity, and architectural layers, then writes results
10
+ * back into the DB (and optionally emits a graph.json).
11
+ *
12
+ * Usage:
13
+ * node understand-analyze.mjs [options]
14
+ *
15
+ * Options:
16
+ * --dir <path> Project directory (default: cwd)
17
+ * --db <path> monograph.db path (default: <dir>/.monomind/monograph.db)
18
+ * --output <path> Write a graph.json here (default: <dir>/.understand/knowledge-graph.json)
19
+ * --batch-size <N> Files per LLM batch (default: 5)
20
+ * --max-files <N> Stop after N files (0 = all, default: 0)
21
+ * --dry-run Print what would happen without writing to DB
22
+ * --no-llm Heuristic-only mode (layers + tags from paths, no API calls)
23
+ * --layers-only Skip per-file analysis, only (re-)detect layers
24
+ *
25
+ * Env:
26
+ * ANTHROPIC_API_KEY Required unless --no-llm is set
27
+ */
28
+
29
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
30
+ import { resolve, join, dirname, basename, relative } from 'node:path';
31
+ import { createRequire } from 'node:module';
32
+ import { fileURLToPath } from 'node:url';
33
+ import { execFileSync } from 'node:child_process';
34
+
35
+ const __dir = dirname(fileURLToPath(import.meta.url));
36
+ const CWD = process.cwd();
37
+
38
+ // ── CLI argument helpers ─────────────────────────────────────────────────────
39
+ function argVal(name) {
40
+ const i = process.argv.indexOf('--' + name);
41
+ return i !== -1 ? process.argv[i + 1] : null;
42
+ }
43
+ const hasFlag = (f) => process.argv.includes('--' + f);
44
+
45
+ const projectDir = resolve(argVal('dir') || CWD);
46
+ const dbPathArg = argVal('db') ? resolve(argVal('db')) : join(projectDir, '.monomind', 'monograph.db');
47
+ const outputArg = argVal('output')? resolve(argVal('output')) : join(projectDir, '.understand', 'knowledge-graph.json');
48
+ const batchSize = parseInt(argVal('batch-size') || '5', 10);
49
+ const maxFiles = parseInt(argVal('max-files') || '0', 10);
50
+ const dryRun = hasFlag('dry-run');
51
+ const noLlm = hasFlag('no-llm');
52
+ const layersOnly = hasFlag('layers-only');
53
+ const incremental = hasFlag('incremental');
54
+ const onboard = hasFlag('onboard');
55
+ const onboardOut = argVal('onboard-out') ? resolve(argVal('onboard-out')) : join(projectDir, 'ONBOARDING.md');
56
+
57
+ // ── Resolve @monoes/monograph for DB access ──────────────────────────────────
58
+ function resolveMonograph() {
59
+ const req = createRequire(import.meta.url);
60
+ const candidates = [
61
+ // CLI package's own node_modules (npx / global npm install)
62
+ join(__dir, '..', 'node_modules', '@monoes', 'monograph'),
63
+ // Global npm / homebrew
64
+ (() => { try { return join(req.resolve('npm/bin/npm-cli.js'), '..', '..', '..', '@monoes', 'monograph'); } catch { return null; } })(),
65
+ // Monorepo root
66
+ join(__dir, '..', '..', '..', '..', 'node_modules', '@monoes', 'monograph'),
67
+ // User project
68
+ join(projectDir, 'node_modules', '@monoes', 'monograph'),
69
+ ].filter(Boolean);
70
+
71
+ for (const c of candidates) {
72
+ try {
73
+ if (existsSync(c)) return req(c);
74
+ } catch {}
75
+ }
76
+ // Last resort: require by name (works when installed globally)
77
+ try { return req('@monoes/monograph'); } catch {}
78
+ return null;
79
+ }
80
+
81
+ // ── Heuristic layer detection (ported from understand-anything layer-detector) ─
82
+ const LAYER_PATTERNS = [
83
+ { patterns: ['routes', 'controller', 'handler', 'endpoint', 'api'], name: 'API Layer', description: 'HTTP endpoints, route handlers, and API controllers' },
84
+ { patterns: ['service', 'usecase', 'use-case', 'business'], name: 'Service Layer', description: 'Business logic and application services' },
85
+ { patterns: ['model', 'entity', 'schema', 'database', 'db', 'migration','repository', 'repo'],
86
+ name: 'Data Layer', description: 'Data models, database access, and persistence' },
87
+ { patterns: ['component', 'view', 'page', 'screen', 'layout', 'widget', 'ui'],
88
+ name: 'UI Layer', description: 'User interface components and views' },
89
+ { patterns: ['middleware', 'interceptor', 'guard', 'filter', 'pipe'], name: 'Middleware Layer', description: 'Request/response middleware and interceptors' },
90
+ { patterns: ['client', 'integration', 'external', 'sdk', 'vendor', 'adapter'],
91
+ name: 'External Services', description: 'External service integrations, SDKs, and third-party adapters' },
92
+ { patterns: ['worker', 'job', 'queue', 'cron', 'consumer', 'processor', 'scheduler', 'background'],
93
+ name: 'Background Tasks', description: 'Background workers, job processors, and scheduled tasks' },
94
+ { patterns: ['util', 'helper', 'lib', 'common', 'shared'], name: 'Utility Layer', description: 'Shared utilities, helpers, and common libraries' },
95
+ { patterns: ['test', 'spec', '__test__', '__spec__', '__tests__'], name: 'Test Layer', description: 'Test files and test utilities' },
96
+ { patterns: ['config', 'setting', 'env'], name: 'Configuration Layer', description: 'Application configuration and environment settings' },
97
+ ];
98
+
99
+ function matchFileToLayer(filePath) {
100
+ const norm = filePath.replace(/\\/g, '/').toLowerCase();
101
+ const segments = norm.split('/');
102
+ for (const { patterns, name } of LAYER_PATTERNS) {
103
+ for (const seg of segments) {
104
+ for (const p of patterns) {
105
+ if (seg === p || seg === p + 's') return name;
106
+ }
107
+ }
108
+ }
109
+ return null;
110
+ }
111
+
112
+ function toLayerId(name) {
113
+ return 'layer:' + name.toLowerCase().replace(/\s+/g, '-');
114
+ }
115
+
116
+ function detectLayersHeuristic(fileNodes) {
117
+ const map = new Map(); // layerName → nodeIds[]
118
+ for (const node of fileNodes) {
119
+ const layerName = (node.file_path && matchFileToLayer(node.file_path)) || 'Core';
120
+ if (!map.has(layerName)) map.set(layerName, []);
121
+ map.get(layerName).push(node.id);
122
+ }
123
+ const layers = [];
124
+ for (const [name, nodeIds] of map) {
125
+ const pattern = LAYER_PATTERNS.find(p => p.name === name);
126
+ layers.push({ id: toLayerId(name), name, description: pattern?.description ?? 'Core application files', nodeIds });
127
+ }
128
+ return layers;
129
+ }
130
+
131
+ // ── Anthropic API helpers (raw fetch — no SDK needed) ────────────────────────
132
+ const ANTHROPIC_API_KEY = process.env.ANTHROPIC_API_KEY;
133
+ const ANTHROPIC_URL = 'https://api.anthropic.com/v1/messages';
134
+ const MODEL = 'claude-haiku-4-5-20251001'; // cheapest for bulk analysis
135
+
136
+ async function callClaude(systemPrompt, userPrompt, maxTokens = 1024) {
137
+ const body = JSON.stringify({
138
+ model: MODEL,
139
+ max_tokens: maxTokens,
140
+ system: systemPrompt,
141
+ messages: [{ role: 'user', content: userPrompt }],
142
+ });
143
+ const headers = {
144
+ 'Content-Type': 'application/json',
145
+ 'x-api-key': ANTHROPIC_API_KEY,
146
+ 'anthropic-version': '2023-06-01',
147
+ };
148
+ let lastError;
149
+ for (let attempt = 0; attempt < 3; attempt++) {
150
+ try {
151
+ const resp = await fetch(ANTHROPIC_URL, { method: 'POST', headers, body });
152
+ if (resp.ok) {
153
+ const data = await resp.json();
154
+ return data.content?.[0]?.text ?? '';
155
+ }
156
+ // Retry on 429 (rate limit) and 5xx; fail fast on 4xx
157
+ if (resp.status === 429 || resp.status >= 500) {
158
+ const retryAfter = parseInt(resp.headers.get('retry-after') || '0', 10);
159
+ const backoff = retryAfter > 0 ? retryAfter * 1000 : Math.min(2 ** attempt * 1000, 8000);
160
+ await new Promise(r => setTimeout(r, backoff));
161
+ lastError = new Error(`Anthropic API ${resp.status} (attempt ${attempt + 1}/3)`);
162
+ continue;
163
+ }
164
+ const text = await resp.text();
165
+ throw new Error(`Anthropic API ${resp.status}: ${text.slice(0, 200)}`);
166
+ } catch (e) {
167
+ lastError = e;
168
+ if (attempt === 2) break;
169
+ await new Promise(r => setTimeout(r, Math.min(2 ** attempt * 1000, 4000)));
170
+ }
171
+ }
172
+ throw lastError || new Error('Anthropic API failed after 3 attempts');
173
+ }
174
+
175
+ function parseJson(text) {
176
+ try {
177
+ const fenceMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
178
+ const src = fenceMatch ? fenceMatch[1] : text;
179
+ const objMatch = src.match(/\{[\s\S]*\}|\[[\s\S]*\]/);
180
+ if (objMatch) return JSON.parse(objMatch[0]);
181
+ } catch {}
182
+ return null;
183
+ }
184
+
185
+ // Per-file analysis prompt (ported from llm-analyzer.ts)
186
+ function buildFilePrompt(filePath, content, projectContext) {
187
+ const truncated = content.length > 6000 ? content.slice(0, 6000) + '\n... (truncated)' : content;
188
+ return `You are a code analysis assistant. Analyze the following source file and return a JSON object.
189
+
190
+ Project context: ${projectContext}
191
+
192
+ File: ${filePath}
193
+
194
+ \`\`\`
195
+ ${truncated}
196
+ \`\`\`
197
+
198
+ Return a JSON object with exactly these fields:
199
+ - "fileSummary": A concise summary of what this file does (1-2 sentences).
200
+ - "tags": An array of 2-5 relevant tags (e.g., ["utility", "async", "api"]).
201
+ - "complexity": One of "simple", "moderate", or "complex".
202
+ - "functionSummaries": An object mapping each function/method name to a 1-sentence summary (top 5 only).
203
+ - "classSummaries": An object mapping each class name to a 1-sentence summary.
204
+
205
+ Respond ONLY with the JSON object, no additional text.`;
206
+ }
207
+
208
+ // Batch file analysis prompt (multiple files at once for efficiency)
209
+ function buildBatchPrompt(files, projectContext) {
210
+ const fileBlocks = files.map(({ path, content }) => {
211
+ const truncated = content.length > 2000 ? content.slice(0, 2000) + '\n...' : content;
212
+ return `### ${path}\n\`\`\`\n${truncated}\n\`\`\``;
213
+ }).join('\n\n');
214
+
215
+ return `You are a code analysis assistant. Analyze the following source files and return a JSON object.
216
+
217
+ Project context: ${projectContext}
218
+
219
+ ${fileBlocks}
220
+
221
+ Return a JSON object where each key is the exact file path and the value is:
222
+ - "fileSummary": 1-2 sentence summary of what the file does.
223
+ - "tags": 2-5 relevant tags.
224
+ - "complexity": "simple", "moderate", or "complex".
225
+ - "functionSummaries": object of function name → 1-sentence summary (top 5 per file).
226
+ - "classSummaries": object of class name → 1-sentence summary.
227
+
228
+ Respond ONLY with the JSON object mapping file paths to their analysis.`;
229
+ }
230
+
231
+ // Layer detection prompt (ported from layer-detector.ts)
232
+ function buildLayerPrompt(filePaths) {
233
+ const list = filePaths.slice(0, 200).map(f => ` - ${f}`).join('\n');
234
+ return `You are a software architecture analyst. Given these file paths, identify 3-8 logical architectural layers.
235
+
236
+ ${list}
237
+
238
+ Return a JSON array where each element has:
239
+ - "name": Short layer name (e.g., "API", "Data", "UI")
240
+ - "description": What this layer does (1 sentence)
241
+ - "filePatterns": Path prefixes that belong to this layer (e.g., ["src/routes/", "src/controllers/"])
242
+
243
+ Every file should belong to exactly one layer. Respond ONLY with the JSON array.`;
244
+ }
245
+
246
+ // ── .understandignore support (ported from ignore-filter.ts) ────────────────
247
+ const DEFAULT_IGNORE_PATTERNS = [
248
+ 'node_modules/', '.git/', 'vendor/', 'venv/', '.venv/', '__pycache__/',
249
+ 'dist/', 'build/', 'out/', 'coverage/', '.next/', '.cache/', '.turbo/', 'target/', 'obj/',
250
+ '*.lock', 'package-lock.json', 'yarn.lock', 'pnpm-lock.yaml',
251
+ '*.png', '*.jpg', '*.jpeg', '*.gif', '*.svg', '*.ico', '*.woff', '*.woff2',
252
+ '*.ttf', '*.eot', '*.mp3', '*.mp4', '*.pdf', '*.zip', '*.tar', '*.gz',
253
+ '*.min.js', '*.min.css', '*.map', '*.generated.*',
254
+ '.idea/', '.vscode/', '*.log',
255
+ ];
256
+
257
+ function loadIgnorePatterns(dir) {
258
+ const patterns = [...DEFAULT_IGNORE_PATTERNS];
259
+ const locations = [
260
+ join(dir, '.understand-anything', '.understandignore'),
261
+ join(dir, '.understandignore'),
262
+ ];
263
+ for (const p of locations) {
264
+ if (existsSync(p)) {
265
+ try {
266
+ const lines = readFileSync(p, 'utf-8').split('\n')
267
+ .map(l => l.trim()).filter(l => l && !l.startsWith('#'));
268
+ patterns.push(...lines);
269
+ } catch {}
270
+ }
271
+ }
272
+ return patterns;
273
+ }
274
+
275
+ function makeIgnoreMatcher(patterns) {
276
+ return function isIgnored(filePath) {
277
+ const norm = filePath.replace(/\\/g, '/');
278
+ for (const pat of patterns) {
279
+ if (pat.startsWith('!')) continue; // negation — skip for simplicity
280
+ if (pat.endsWith('/')) {
281
+ // directory pattern
282
+ if (norm.includes('/' + pat.slice(0, -1) + '/') || norm.startsWith(pat)) return true;
283
+ } else if (pat.startsWith('*.')) {
284
+ // extension glob
285
+ if (norm.endsWith(pat.slice(1))) return true;
286
+ } else if (pat.includes('*')) {
287
+ // simple wildcard — match anywhere in path
288
+ const re = new RegExp('^' + pat.replace(/\./g, '\\.').replace(/\*/g, '.*') + '$');
289
+ if (re.test(norm) || re.test(norm.split('/').pop() || '')) return true;
290
+ } else {
291
+ // exact segment or prefix
292
+ if (norm === pat || norm.includes('/' + pat) || norm.startsWith(pat)) return true;
293
+ }
294
+ }
295
+ return false;
296
+ };
297
+ }
298
+
299
+ const _ignorePatterns = loadIgnorePatterns(projectDir);
300
+ const isIgnoredByUser = makeIgnoreMatcher(_ignorePatterns);
301
+
302
+ // ── Incremental mode helpers (ported from staleness.ts) ──────────────────────
303
+ function getChangedFiles(dir, lastHash) {
304
+ const files = new Set();
305
+ function runGit(args) {
306
+ try {
307
+ const out = execFileSync('git', args, { cwd: dir, encoding: 'utf-8' });
308
+ for (const line of out.split('\n')) {
309
+ const trimmed = line.trim();
310
+ if (!trimmed) continue;
311
+ // `git status --porcelain` lines look like " M path/to/file" or "?? path"
312
+ // Strip the leading status code if present
313
+ const path = trimmed.length > 3 && trimmed[2] === ' ' ? trimmed.slice(3) : trimmed;
314
+ files.add(path);
315
+ }
316
+ } catch { /* git not available or no diff */ }
317
+ }
318
+ // Committed changes since last run
319
+ runGit(['diff', `${lastHash}..HEAD`, '--name-only']);
320
+ // Uncommitted working tree changes (staged + unstaged + untracked)
321
+ runGit(['status', '--porcelain']);
322
+ return [...files];
323
+ }
324
+
325
+ function getCurrentCommitHash(dir) {
326
+ try {
327
+ return execFileSync('git', ['rev-parse', 'HEAD'], { cwd: dir, encoding: 'utf-8' }).trim();
328
+ } catch {
329
+ return '';
330
+ }
331
+ }
332
+
333
+ // ── Language detection (slim port of language-registry.ts) ──────────────────
334
+ const LANGUAGE_BY_EXT = {
335
+ '.ts': 'TypeScript', '.tsx': 'TypeScript',
336
+ '.js': 'JavaScript', '.jsx': 'JavaScript', '.mjs': 'JavaScript', '.cjs': 'JavaScript',
337
+ '.py': 'Python', '.pyi': 'Python',
338
+ '.rs': 'Rust', '.go': 'Go',
339
+ '.java': 'Java', '.kt': 'Kotlin', '.scala': 'Scala',
340
+ '.rb': 'Ruby', '.php': 'PHP', '.swift': 'Swift',
341
+ '.c': 'C', '.h': 'C', '.cpp': 'C++', '.cc': 'C++', '.hpp': 'C++', '.hxx': 'C++',
342
+ '.cs': 'C#', '.fs': 'F#',
343
+ '.sh': 'Shell', '.bash': 'Shell', '.zsh': 'Shell',
344
+ '.lua': 'Lua', '.r': 'R', '.dart': 'Dart', '.ex': 'Elixir', '.exs': 'Elixir',
345
+ '.sol': 'Solidity', '.zig': 'Zig', '.nim': 'Nim',
346
+ '.html': 'HTML', '.css': 'CSS', '.scss': 'SCSS', '.vue': 'Vue', '.svelte': 'Svelte',
347
+ };
348
+
349
+ function detectLanguages(fileNodes) {
350
+ const counts = new Map();
351
+ for (const n of fileNodes) {
352
+ if (!n.file_path) continue;
353
+ const lastDot = n.file_path.lastIndexOf('.');
354
+ if (lastDot === -1) continue;
355
+ const ext = n.file_path.slice(lastDot).toLowerCase();
356
+ const lang = LANGUAGE_BY_EXT[ext];
357
+ if (lang) counts.set(lang, (counts.get(lang) || 0) + 1);
358
+ }
359
+ // Return languages with >=3 files, sorted by count desc
360
+ return [...counts.entries()]
361
+ .filter(([, c]) => c >= 3)
362
+ .sort((a, b) => b[1] - a[1])
363
+ .map(([lang]) => lang);
364
+ }
365
+
366
+ // ── Framework detection (slim port of framework-registry.ts) ────────────────
367
+ const FRAMEWORK_SIGNATURES = [
368
+ // [name, manifestFile, keywords[]]
369
+ ['React', 'package.json', ['"react"']],
370
+ ['Next.js', 'package.json', ['"next"']],
371
+ ['Vue', 'package.json', ['"vue"']],
372
+ ['Svelte', 'package.json', ['"svelte"']],
373
+ ['Angular', 'package.json', ['"@angular/core"']],
374
+ ['Express', 'package.json', ['"express"']],
375
+ ['Fastify', 'package.json', ['"fastify"']],
376
+ ['NestJS', 'package.json', ['"@nestjs/core"']],
377
+ ['Vite', 'package.json', ['"vite"']],
378
+ ['Webpack', 'package.json', ['"webpack"']],
379
+ ['TypeScript', 'package.json', ['"typescript"']],
380
+ ['Anthropic SDK','package.json', ['"@anthropic-ai/sdk"']],
381
+ ['Django', 'requirements.txt', ['django']],
382
+ ['Flask', 'requirements.txt', ['flask']],
383
+ ['FastAPI', 'requirements.txt', ['fastapi']],
384
+ ['Rails', 'Gemfile', ['rails']],
385
+ ['Spring Boot', 'pom.xml', ['spring-boot']],
386
+ ['Axum', 'Cargo.toml', ['axum']],
387
+ ['Actix', 'Cargo.toml', ['actix-web']],
388
+ ['Tokio', 'Cargo.toml', ['tokio']],
389
+ ['Gin', 'go.mod', ['gin-gonic/gin']],
390
+ ];
391
+
392
+ function findManifests(dir, manifestName, maxDepth = 4) {
393
+ const fs = createRequire(import.meta.url)('node:fs');
394
+ const results = [];
395
+ function walk(d, depth) {
396
+ if (depth > maxDepth) return;
397
+ let entries;
398
+ try { entries = fs.readdirSync(d, { withFileTypes: true }); }
399
+ catch { return; }
400
+ for (const e of entries) {
401
+ if (e.name === 'node_modules' || e.name === 'dist' || e.name.startsWith('.')) continue;
402
+ const full = join(d, e.name);
403
+ if (e.isDirectory()) walk(full, depth + 1);
404
+ else if (e.name === manifestName) results.push(full);
405
+ }
406
+ }
407
+ walk(dir, 0);
408
+ return results;
409
+ }
410
+
411
+ function detectFrameworks(dir) {
412
+ const detected = [];
413
+ const seen = new Set();
414
+ // Cache manifest contents by name to avoid re-reading
415
+ const manifestCache = new Map();
416
+ for (const [name, manifest, keywords] of FRAMEWORK_SIGNATURES) {
417
+ if (seen.has(name)) continue;
418
+ let manifestPaths = manifestCache.get(manifest);
419
+ if (!manifestPaths) {
420
+ manifestPaths = findManifests(dir, manifest);
421
+ manifestCache.set(manifest, manifestPaths);
422
+ }
423
+ for (const p of manifestPaths) {
424
+ try {
425
+ const content = readFileSync(p, 'utf-8').toLowerCase();
426
+ if (keywords.some(k => content.includes(k.toLowerCase()))) {
427
+ detected.push(name);
428
+ seen.add(name);
429
+ break;
430
+ }
431
+ } catch {}
432
+ }
433
+ }
434
+ return detected;
435
+ }
436
+
437
+ // ── Onboarding guide builder (ported from onboard-builder.ts) ───────────────
438
+ function buildOnboardingGuide(graphJson) {
439
+ const { project, nodes, layers, tour = [] } = graphJson;
440
+ const lines = [];
441
+
442
+ lines.push(`# ${project.name}`);
443
+ lines.push('');
444
+ if (project.description) {
445
+ lines.push(`> ${project.description}`);
446
+ lines.push('');
447
+ }
448
+ lines.push('| | |');
449
+ lines.push('|---|---|');
450
+ if (project.languages?.length) lines.push(`| **Languages** | ${project.languages.join(', ')} |`);
451
+ if (project.frameworks?.length) lines.push(`| **Frameworks** | ${project.frameworks.join(', ')} |`);
452
+ lines.push(`| **Components** | ${nodes.length} nodes |`);
453
+ lines.push(`| **Last Analyzed** | ${project.analyzedAt} |`);
454
+ lines.push('');
455
+
456
+ if (layers.length > 0) {
457
+ lines.push('## Architecture');
458
+ lines.push('');
459
+ lines.push('The project is organized into the following layers:');
460
+ lines.push('');
461
+ const nodeMap = new Map(nodes.map(n => [n.id, n]));
462
+ for (const layer of layers) {
463
+ lines.push(`### ${layer.name}`);
464
+ lines.push('');
465
+ if (layer.description) { lines.push(layer.description); lines.push(''); }
466
+ const members = (layer.nodeIds || []).map(id => nodeMap.get(id)?.name).filter(Boolean);
467
+ if (members.length > 0) { lines.push(`Key components: ${members.slice(0, 8).join(', ')}`); lines.push(''); }
468
+ }
469
+ }
470
+
471
+ if (tour.length > 0) {
472
+ lines.push('## Getting Started');
473
+ lines.push('');
474
+ lines.push('Follow this guided tour to understand the codebase:');
475
+ lines.push('');
476
+ const nodeMap = new Map(nodes.map(n => [n.id, n]));
477
+ for (const step of tour) {
478
+ lines.push(`### ${step.order}. ${step.title}`);
479
+ lines.push('');
480
+ lines.push(step.description);
481
+ lines.push('');
482
+ const stepNodes = (step.nodeIds || []).map(id => nodeMap.get(id)).filter(Boolean);
483
+ if (stepNodes.length > 0) {
484
+ lines.push('**Files to look at:**');
485
+ for (const node of stepNodes) {
486
+ if (node.filePath) lines.push(`- \`${node.filePath}\` — ${node.summary}`);
487
+ }
488
+ lines.push('');
489
+ }
490
+ }
491
+ }
492
+
493
+ const FILE_MAP_LIMIT = 50;
494
+ const HOTSPOT_LIMIT = 20;
495
+
496
+ const fileNodes = nodes.filter(n => n.type === 'file' && n.filePath && n.summary);
497
+ if (fileNodes.length > 0) {
498
+ lines.push('## File Map');
499
+ lines.push('');
500
+ if (fileNodes.length > FILE_MAP_LIMIT) {
501
+ lines.push(`Showing ${FILE_MAP_LIMIT} of ${fileNodes.length} analyzed files. See \`.understand/knowledge-graph.json\` for the full list.`);
502
+ lines.push('');
503
+ }
504
+ lines.push('| File | Purpose | Complexity |');
505
+ lines.push('|------|---------|------------|');
506
+ for (const node of fileNodes.slice(0, FILE_MAP_LIMIT)) {
507
+ const summary = (node.summary || '').replace(/\|/g, '\\|');
508
+ lines.push(`| \`${node.filePath}\` | ${summary} | ${node.complexity || 'moderate'} |`);
509
+ }
510
+ lines.push('');
511
+ }
512
+
513
+ const complexNodes = nodes.filter(n => n.complexity === 'complex');
514
+ if (complexNodes.length > 0) {
515
+ lines.push('## Complexity Hotspots');
516
+ lines.push('');
517
+ lines.push('These components are the most complex and deserve extra attention:');
518
+ if (complexNodes.length > HOTSPOT_LIMIT) {
519
+ lines.push('');
520
+ lines.push(`Showing top ${HOTSPOT_LIMIT} of ${complexNodes.length} complex components.`);
521
+ }
522
+ lines.push('');
523
+ for (const node of complexNodes.slice(0, HOTSPOT_LIMIT)) {
524
+ lines.push(`- **${node.name}** (${node.type}): ${node.summary || ''}`);
525
+ }
526
+ lines.push('');
527
+ }
528
+
529
+ lines.push('---');
530
+ lines.push('');
531
+ lines.push(`*Generated by [monomind](https://github.com/nokhodian/monomind) from knowledge graph v${graphJson.version}*`);
532
+ lines.push('');
533
+
534
+ return lines.join('\n');
535
+ }
536
+
537
+ // ── File reading with graceful skip ─────────────────────────────────────────
538
+ function readFileSafe(absPath) {
539
+ try {
540
+ const content = readFileSync(absPath, 'utf-8');
541
+ return content;
542
+ } catch {
543
+ return null;
544
+ }
545
+ }
546
+
547
+ const SKIP_EXTENSIONS = new Set([
548
+ '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp',
549
+ '.pdf', '.zip', '.tar', '.gz', '.wasm', '.map',
550
+ '.lock', '.lockb', '.db', '.sqlite', '.bin', '.exe',
551
+ ]);
552
+ function shouldAnalyze(filePath) {
553
+ if (!filePath) return false;
554
+ const ext = '.' + filePath.split('.').pop()?.toLowerCase();
555
+ if (SKIP_EXTENSIONS.has(ext)) return false;
556
+ if (isIgnoredByUser(filePath)) return false;
557
+ return true;
558
+ }
559
+
560
+ // ── Main ─────────────────────────────────────────────────────────────────────
561
+ async function main() {
562
+ console.log('[understand] Starting semantic enrichment for', projectDir);
563
+
564
+ if (!existsSync(dbPathArg)) {
565
+ console.error('[understand] monograph.db not found at', dbPathArg);
566
+ console.error('[understand] Build the graph first: npx monomind monograph build');
567
+ process.exit(1);
568
+ }
569
+
570
+ const mg = resolveMonograph();
571
+ if (!mg) {
572
+ console.error('[understand] Cannot find @monoes/monograph. Run: npm install @monoes/monograph');
573
+ process.exit(1);
574
+ }
575
+
576
+ // Retry openDb against SQLite BUSY when monograph is building in the background
577
+ let db;
578
+ for (let attempt = 0; attempt < 5; attempt++) {
579
+ try { db = mg.openDb(dbPathArg); break; }
580
+ catch (e) {
581
+ if (attempt === 4) throw e;
582
+ const msg = String(e?.message || e);
583
+ if (!/busy|locked/i.test(msg)) throw e;
584
+ console.log(`[understand] monograph.db busy, retrying in ${(attempt + 1) * 2}s...`);
585
+ await new Promise(r => setTimeout(r, (attempt + 1) * 2000));
586
+ }
587
+ }
588
+
589
+ // Ensure properties column exists
590
+ try { db.prepare(`ALTER TABLE nodes ADD COLUMN properties TEXT`).run(); } catch {}
591
+ try { db.prepare(`CREATE TABLE IF NOT EXISTS communities (id INTEGER PRIMARY KEY, label TEXT, size INTEGER NOT NULL DEFAULT 0, cohesion_score REAL NOT NULL DEFAULT 0.0)`).run(); } catch {}
592
+
593
+ // ── Load all file nodes ──────────────────────────────────────────────────
594
+ let fileNodes = db.prepare(`SELECT id, name, file_path, properties FROM nodes WHERE label = 'File' AND file_path IS NOT NULL`).all();
595
+ console.log(`[understand] Found ${fileNodes.length} file nodes in DB`);
596
+
597
+ // ── Layers-only mode ─────────────────────────────────────────────────────
598
+ if (layersOnly) {
599
+ console.log('[understand] Layers-only mode — skipping per-file analysis');
600
+ await detectAndWriteLayers(db, fileNodes, noLlm, dryRun);
601
+ mg.closeDb(db);
602
+ console.log('[understand] Done (layers-only)');
603
+ return;
604
+ }
605
+
606
+ // ── Incremental mode: only re-analyze changed files ─────────────────────
607
+ let changedFileSet = null; // null means "analyze all"
608
+ if (incremental) {
609
+ let lastHash = '';
610
+ try {
611
+ const row = db.prepare(`SELECT value FROM index_meta WHERE key = 'ua_last_commit'`).get();
612
+ lastHash = row?.value || '';
613
+ } catch {}
614
+ if (lastHash) {
615
+ const changed = getChangedFiles(projectDir, lastHash);
616
+ if (changed.length > 0) {
617
+ changedFileSet = new Set(changed);
618
+ console.log(`[understand] Incremental mode: ${changed.length} files changed since ${lastHash.slice(0, 8)}`);
619
+ } else {
620
+ console.log('[understand] Incremental mode: no changes detected since last run — skipping analysis');
621
+ mg.closeDb(db);
622
+ return;
623
+ }
624
+ } else {
625
+ console.log('[understand] Incremental mode: no previous commit hash found — running full analysis');
626
+ }
627
+ }
628
+
629
+ // ── Filter files that need analysis ─────────────────────────────────────
630
+ const toAnalyze = fileNodes.filter(n => {
631
+ if (!shouldAnalyze(n.file_path)) return false;
632
+ if (changedFileSet) {
633
+ // Match by relative or absolute path
634
+ const rel = n.file_path.startsWith('/') ? relative(projectDir, n.file_path) : n.file_path;
635
+ return changedFileSet.has(rel) || changedFileSet.has(n.file_path);
636
+ }
637
+ return true;
638
+ });
639
+ const limit = maxFiles > 0 ? Math.min(maxFiles, toAnalyze.length) : toAnalyze.length;
640
+ const batch = toAnalyze.slice(0, limit);
641
+ console.log(`[understand] Analyzing ${batch.length} files (${toAnalyze.length - batch.length} skipped/already enriched)`);
642
+
643
+ if (!ANTHROPIC_API_KEY && !noLlm) {
644
+ console.warn('[understand] ANTHROPIC_API_KEY not set — falling back to --no-llm heuristic mode');
645
+ // Fall through to heuristic only
646
+ }
647
+
648
+ const useLlm = !noLlm && !!ANTHROPIC_API_KEY;
649
+
650
+ // ── Get project context for better prompts ────────────────────────────────
651
+ let projectContext = `Project directory: ${basename(projectDir)}`;
652
+ try {
653
+ const pkgPath = join(projectDir, 'package.json');
654
+ if (existsSync(pkgPath)) {
655
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
656
+ projectContext = `${pkg.name || basename(projectDir)}: ${pkg.description || ''}`.trim();
657
+ }
658
+ } catch {}
659
+
660
+ // ── Per-file LLM analysis ────────────────────────────────────────────────
661
+ const analysisMap = {}; // file_path → { fileSummary, tags, complexity, functionSummaries, classSummaries }
662
+
663
+ if (useLlm && batch.length > 0) {
664
+ const system = 'You are an expert code analysis assistant. Always respond with valid JSON only.';
665
+ const chunks = [];
666
+ for (let i = 0; i < batch.length; i += batchSize) chunks.push(batch.slice(i, i + batchSize));
667
+
668
+ let done = 0;
669
+ for (const chunk of chunks) {
670
+ // Read file contents
671
+ const files = chunk
672
+ .map(n => {
673
+ const absPath = n.file_path.startsWith('/') ? n.file_path : join(projectDir, n.file_path);
674
+ const content = readFileSafe(absPath);
675
+ return content ? { path: n.file_path, content, nodeId: n.id } : null;
676
+ })
677
+ .filter(Boolean);
678
+
679
+ if (files.length === 0) { done += chunk.length; continue; }
680
+
681
+ process.stdout.write(`\r[understand] Analyzing files ${done + 1}–${Math.min(done + files.length, batch.length)} / ${batch.length}...`);
682
+
683
+ try {
684
+ let text;
685
+ if (files.length === 1) {
686
+ text = await callClaude(system, buildFilePrompt(files[0].path, files[0].content, projectContext));
687
+ const parsed = parseJson(text);
688
+ if (parsed) analysisMap[files[0].path] = parsed;
689
+ } else {
690
+ text = await callClaude(system, buildBatchPrompt(files, projectContext), 2048);
691
+ const parsed = parseJson(text);
692
+ if (parsed && typeof parsed === 'object') {
693
+ for (const [fp, analysis] of Object.entries(parsed)) {
694
+ if (analysis && typeof analysis === 'object') analysisMap[fp] = analysis;
695
+ }
696
+ }
697
+ }
698
+ } catch (e) {
699
+ console.warn(`\n[understand] API error for batch at ${done}: ${e.message}`);
700
+ }
701
+
702
+ done += files.length;
703
+ // Polite rate limiting
704
+ if (chunks.indexOf(chunk) < chunks.length - 1) await new Promise(r => setTimeout(r, 300));
705
+ }
706
+ console.log(`\n[understand] LLM analysis complete: ${Object.keys(analysisMap).length} files analyzed`);
707
+ }
708
+
709
+ // ── Write analysis back to DB ─────────────────────────────────────────────
710
+ const updateNode = db.prepare(`UPDATE nodes SET properties = ? WHERE id = ?`);
711
+ let written = 0;
712
+
713
+ if (!dryRun) {
714
+ const tx = db.transaction(() => {
715
+ for (const node of batch) {
716
+ const existing = node.properties ? (() => { try { return JSON.parse(node.properties); } catch { return {}; } })() : {};
717
+ const llmData = analysisMap[node.file_path] || {};
718
+ const merged = {
719
+ ...existing,
720
+ ...(llmData.fileSummary ? { summary: llmData.fileSummary } : {}),
721
+ ...(llmData.tags ? { tags: llmData.tags } : {}),
722
+ ...(llmData.complexity ? { complexity: llmData.complexity } : {}),
723
+ ...(llmData.languageNotes ? { languageNotes: llmData.languageNotes } : {}),
724
+ ...(llmData.functionSummaries ? { functionSummaries: llmData.functionSummaries } : {}),
725
+ ...(llmData.classSummaries ? { classSummaries: llmData.classSummaries } : {}),
726
+ ua_analyzed_at: new Date().toISOString(),
727
+ };
728
+ updateNode.run(JSON.stringify(merged), node.id);
729
+ written++;
730
+ }
731
+ });
732
+ tx();
733
+ console.log(`[understand] Wrote enrichment data to ${written} nodes`);
734
+ } else {
735
+ console.log(`[understand] DRY RUN — would update ${batch.length} nodes`);
736
+ }
737
+
738
+ // ── Layer detection ──────────────────────────────────────────────────────
739
+ const layers = await detectAndWriteLayers(db, fileNodes, noLlm || !useLlm, dryRun, projectDir);
740
+
741
+ // ── Emit graph.json (for compatibility with ua-import.mjs) ──────────────
742
+ const graphJson = buildGraphJson(projectDir, fileNodes, analysisMap, layers);
743
+ const outputDir = dirname(outputArg);
744
+ if (!dryRun) {
745
+ mkdirSync(outputDir, { recursive: true });
746
+ writeFileSync(outputArg, JSON.stringify(graphJson, null, 2), 'utf-8');
747
+ console.log(`[understand] graph.json written to ${outputArg}`);
748
+ }
749
+
750
+ // ── Rebuild FTS ──────────────────────────────────────────────────────────
751
+ if (!dryRun) {
752
+ try {
753
+ db.prepare(`INSERT INTO nodes_fts(nodes_fts) VALUES('rebuild')`).run();
754
+ console.log('[understand] FTS index rebuilt');
755
+ } catch {}
756
+ }
757
+
758
+ // ── Update index_meta ────────────────────────────────────────────────────
759
+ if (!dryRun) {
760
+ const upsertMeta = db.prepare(
761
+ `INSERT INTO index_meta (key, value) VALUES (?, ?)
762
+ ON CONFLICT(key) DO UPDATE SET value=excluded.value`
763
+ );
764
+ upsertMeta.run('ua_analyzed_at', new Date().toISOString());
765
+ const currentHash = getCurrentCommitHash(projectDir);
766
+ if (currentHash) upsertMeta.run('ua_last_commit', currentHash);
767
+ }
768
+
769
+ mg.closeDb(db);
770
+
771
+ // ── Onboarding guide ─────────────────────────────────────────────────────
772
+ let onboardWritten = false;
773
+ if (onboard && !dryRun) {
774
+ const guide = buildOnboardingGuide(graphJson);
775
+ writeFileSync(onboardOut, guide, 'utf-8');
776
+ onboardWritten = true;
777
+ console.log(`[understand] Onboarding guide written to ${relative(CWD, onboardOut)}`);
778
+ }
779
+
780
+ // ── Final report ─────────────────────────────────────────────────────────
781
+ const title = dryRun
782
+ ? '║ /monomind:understand — DRY RUN (no writes) ║'
783
+ : '║ /monomind:understand — Enrichment Complete ║';
784
+ console.log('\n╔══════════════════════════════════════════════════╗');
785
+ console.log(title);
786
+ console.log('╠══════════════════════════════════════════════════╣');
787
+ console.log(`║ DB: ${relative(CWD, dbPathArg).padEnd(31)}║`);
788
+ console.log(`║ Nodes enriched: ${String(written).padEnd(31)}║`);
789
+ console.log(`║ Communities: ${String(layers.length).padEnd(31)}║`);
790
+ console.log(`║ graph.json: ${relative(CWD, outputArg).padEnd(31)}║`);
791
+ if (onboardWritten) {
792
+ console.log(`║ ONBOARDING.md: ${relative(CWD, onboardOut).padEnd(31)}║`);
793
+ }
794
+ console.log('╚══════════════════════════════════════════════════╝');
795
+ }
796
+
797
+ // ── Detect layers and write communities to DB ────────────────────────────────
798
+ async function detectAndWriteLayers(db, fileNodes, forceHeuristic, dryRun, dir) {
799
+ let layers;
800
+
801
+ if (!forceHeuristic && ANTHROPIC_API_KEY) {
802
+ console.log('[understand] Detecting architectural layers via LLM...');
803
+ const filePaths = fileNodes.map(n => n.file_path).filter(Boolean);
804
+ try {
805
+ const system = 'You are a software architecture expert. Respond with valid JSON only.';
806
+ const text = await callClaude(system, buildLayerPrompt(filePaths), 1024);
807
+ const parsed = parseJson(text);
808
+ if (Array.isArray(parsed) && parsed.length > 0) {
809
+ layers = applyLlmLayers(fileNodes, parsed);
810
+ console.log(`[understand] LLM detected ${layers.length} layers`);
811
+ }
812
+ } catch (e) {
813
+ console.warn('[understand] LLM layer detection failed, falling back to heuristic:', e.message);
814
+ }
815
+ }
816
+
817
+ if (!layers) {
818
+ layers = detectLayersHeuristic(fileNodes);
819
+ console.log(`[understand] Heuristic layer detection: ${layers.length} layers`);
820
+ }
821
+
822
+ if (!dryRun) {
823
+ let communityIdx = 1000;
824
+ const upsertCommunity = db.prepare(
825
+ `INSERT INTO communities (id, label, size, cohesion_score)
826
+ VALUES (?, ?, ?, 0.8)
827
+ ON CONFLICT(id) DO UPDATE SET label=excluded.label, size=excluded.size`
828
+ );
829
+ const updateNodeCommunity = db.prepare(`UPDATE nodes SET community_id = ? WHERE id = ?`);
830
+
831
+ const tx = db.transaction(() => {
832
+ for (const layer of layers) {
833
+ upsertCommunity.run(communityIdx, layer.name, layer.nodeIds.length);
834
+ for (const nodeId of layer.nodeIds) {
835
+ updateNodeCommunity.run(communityIdx, nodeId);
836
+ }
837
+ communityIdx++;
838
+ }
839
+ });
840
+ tx();
841
+ console.log(`[understand] Wrote ${layers.length} communities to DB`);
842
+ }
843
+
844
+ return layers;
845
+ }
846
+
847
+ // Apply LLM-suggested layer patterns to file nodes (ported from applyLLMLayers)
848
+ function applyLlmLayers(fileNodes, llmLayers) {
849
+ const map = new Map();
850
+ for (const l of llmLayers) map.set(l.name, []);
851
+
852
+ for (const node of fileNodes) {
853
+ if (!node.file_path) {
854
+ const other = map.get('Other') ?? [];
855
+ other.push(node.id);
856
+ map.set('Other', other);
857
+ continue;
858
+ }
859
+ const norm = node.file_path.replace(/\\/g, '/');
860
+ let assigned = false;
861
+ for (const l of llmLayers) {
862
+ for (const pattern of (l.filePatterns || [])) {
863
+ if (norm.startsWith(pattern) || norm.includes('/' + pattern)) {
864
+ map.get(l.name).push(node.id);
865
+ assigned = true;
866
+ break;
867
+ }
868
+ }
869
+ if (assigned) break;
870
+ }
871
+ if (!assigned) {
872
+ const other = map.get('Other') ?? [];
873
+ other.push(node.id);
874
+ map.set('Other', other);
875
+ }
876
+ }
877
+
878
+ const layers = [];
879
+ for (const [name, nodeIds] of map) {
880
+ if (nodeIds.length === 0) continue;
881
+ const l = llmLayers.find(x => x.name === name);
882
+ layers.push({ id: toLayerId(name), name, description: l?.description ?? 'Uncategorized', nodeIds });
883
+ }
884
+ return layers;
885
+ }
886
+
887
+ // Build a KnowledgeGraph-compatible graph.json for ua-import compatibility
888
+ function buildGraphJson(dir, fileNodes, analysisMap, layers) {
889
+ const projectName = basename(dir);
890
+ const nodes = fileNodes.map(n => {
891
+ const a = analysisMap[n.file_path] || {};
892
+ return {
893
+ id: 'file:' + (n.file_path || n.name),
894
+ type: 'file',
895
+ name: n.name,
896
+ filePath: n.file_path,
897
+ summary: a.fileSummary || '',
898
+ tags: a.tags || [],
899
+ complexity: a.complexity || 'moderate',
900
+ };
901
+ });
902
+
903
+ // Project metadata
904
+ let description = '';
905
+ try {
906
+ const pkgPath = join(dir, 'package.json');
907
+ if (existsSync(pkgPath)) {
908
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
909
+ description = pkg.description || '';
910
+ }
911
+ } catch {}
912
+
913
+ return {
914
+ version: '2.7.0',
915
+ kind: 'codebase',
916
+ project: {
917
+ name: projectName,
918
+ languages: detectLanguages(fileNodes),
919
+ frameworks: detectFrameworks(dir),
920
+ description,
921
+ analyzedAt: new Date().toISOString(),
922
+ gitCommitHash: getCurrentCommitHash(dir),
923
+ },
924
+ nodes,
925
+ edges: [],
926
+ layers: layers.map(l => ({ id: l.id, name: l.name, description: l.description, nodeIds: l.nodeIds })),
927
+ tour: [],
928
+ };
929
+ }
930
+
931
+ main().catch(e => {
932
+ console.error('[understand] Fatal error:', e.message);
933
+ process.exit(1);
934
+ });