homegraph 1.6.0 → 1.6.2

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.
Files changed (59) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +64 -14
  3. package/dist/bin/homegraph.js +2 -2
  4. package/dist/db/index.js +1 -0
  5. package/dist/db/migrations.d.ts +6 -1
  6. package/dist/db/migrations.js +26 -1
  7. package/dist/db/queries.d.ts +9 -0
  8. package/dist/db/queries.js +32 -7
  9. package/dist/db/schema.sql +4 -0
  10. package/dist/extraction/grammars.js +2 -1
  11. package/dist/extraction/languages/c-cpp.d.ts +8 -0
  12. package/dist/extraction/languages/c-cpp.js +44 -0
  13. package/dist/extraction/wasm-runtime-flags.d.ts +4 -0
  14. package/dist/extraction/wasm-runtime-flags.js +9 -0
  15. package/dist/graph/evidence-paths.d.ts +2 -0
  16. package/dist/graph/evidence-paths.js +20 -3
  17. package/dist/index.js +7 -1
  18. package/dist/installer/targets/icode.d.ts +43 -0
  19. package/dist/installer/targets/icode.js +631 -0
  20. package/dist/installer/targets/registry.js +2 -0
  21. package/dist/installer/targets/types.d.ts +1 -1
  22. package/dist/mcp/arkts-evidence-packs.d.ts +17 -0
  23. package/dist/mcp/arkts-evidence-packs.js +218 -10
  24. package/dist/mcp/evidence-audit.d.ts +3 -0
  25. package/dist/mcp/evidence-audit.js +40 -0
  26. package/dist/mcp/evidence-rendering.js +1 -1
  27. package/dist/mcp/explore-repeat-guard.d.ts +19 -5
  28. package/dist/mcp/explore-repeat-guard.js +35 -14
  29. package/dist/mcp/explore-session-state.d.ts +5 -0
  30. package/dist/mcp/explore-session-state.js +1 -0
  31. package/dist/mcp/implementation-context.d.ts +35 -0
  32. package/dist/mcp/implementation-context.js +232 -0
  33. package/dist/mcp/locate-contract.js +1 -1
  34. package/dist/mcp/query-cache.d.ts +1 -15
  35. package/dist/mcp/query-cache.js +20 -11
  36. package/dist/mcp/request-evidence.d.ts +69 -0
  37. package/dist/mcp/request-evidence.js +297 -0
  38. package/dist/mcp/server-instructions.d.ts +2 -2
  39. package/dist/mcp/server-instructions.js +15 -15
  40. package/dist/mcp/session.js +3 -2
  41. package/dist/mcp/tools.d.ts +39 -12
  42. package/dist/mcp/tools.js +237 -121
  43. package/dist/resolution/c-feature-variants.d.ts +40 -0
  44. package/dist/resolution/c-feature-variants.js +272 -0
  45. package/dist/resolution/cpp-macro-visibility.d.ts +21 -0
  46. package/dist/resolution/cpp-macro-visibility.js +393 -0
  47. package/dist/resolution/index.js +11 -0
  48. package/dist/resolution/name-matcher.js +49 -2
  49. package/dist/resolution/types.d.ts +8 -0
  50. package/dist/resolution/types.js +8 -0
  51. package/dist/search/literal-evidence.js +11 -3
  52. package/dist/search/query-plan-provider.js +13 -5
  53. package/dist/search/query-plan.d.ts +3 -1
  54. package/dist/search/query-plan.js +11 -4
  55. package/dist/search/request-contract.d.ts +29 -0
  56. package/dist/search/request-contract.js +76 -0
  57. package/dist/sync/watcher.d.ts +30 -0
  58. package/dist/sync/watcher.js +45 -0
  59. package/package.json +1 -1
@@ -0,0 +1,43 @@
1
+ /**
2
+ * iCode target.
3
+ *
4
+ * iCode (product name) ships as the Python package `chrys`; agent profiles
5
+ * are individual YAML files, one per profile, under `~/.chrys/agents/`
6
+ * (macOS/Linux) or `%APPDATA%\chrys\agents\` (Windows). Each profile is a
7
+ * complete agent definition carrying a `tools.mcp` list of MCP server
8
+ * configs.
9
+ *
10
+ * iCode has NO project-local config concept — this target is global-only.
11
+ *
12
+ * Install (synchronous, non-interactive — no per-profile prompt):
13
+ * 1. Every EXISTING user profile under `~/.chrys/agents/*.yaml|yml` gets
14
+ * the homegraph MCP entry injected in place — a surgical line edit
15
+ * that preserves sibling MCP servers and every other section.
16
+ * 2. Built-in agents (Code / QA / Explore / General) are discovered from
17
+ * the installed iCode runtime
18
+ * (`.../site-packages/chrys/service/profiles/agents/builtins/`). A
19
+ * built-in with no user profile of the same name gets a "shadow"
20
+ * profile written to `~/.chrys/agents/<name>.yaml`: the built-in's
21
+ * FULL content plus the homegraph entry. (iCode's own override rule:
22
+ * a user profile with a built-in's name REPLACES the built-in
23
+ * entirely, so the copy must be complete.)
24
+ * If the runtime can't be located, install degrades to editing
25
+ * existing profiles only and returns manual guidance in `notes` — it
26
+ * never throws.
27
+ *
28
+ * Uninstall strips the homegraph entry from every user profile that
29
+ * carries it (shadow files are left as plain built-in copies), matching
30
+ * the other targets' "remove only what install wrote" contract.
31
+ *
32
+ * No instructions file is written (#529/#704) — guidance ships in the MCP
33
+ * `initialize` response, which iCode injects into system reminders by
34
+ * default (`expose_instructions: true`).
35
+ *
36
+ * NOTE: unlike the codegraph fork, this target must NOT suppress the
37
+ * "install the CLI on PATH" step in `src/installer/index.ts` — the MCP
38
+ * command is the `homegraph` Node CLI, which iCode (a Python app) does not
39
+ * bundle.
40
+ */
41
+ import { AgentTarget } from './types';
42
+ export declare const icodeTarget: AgentTarget;
43
+ //# sourceMappingURL=icode.d.ts.map
@@ -0,0 +1,631 @@
1
+ "use strict";
2
+ /**
3
+ * iCode target.
4
+ *
5
+ * iCode (product name) ships as the Python package `chrys`; agent profiles
6
+ * are individual YAML files, one per profile, under `~/.chrys/agents/`
7
+ * (macOS/Linux) or `%APPDATA%\chrys\agents\` (Windows). Each profile is a
8
+ * complete agent definition carrying a `tools.mcp` list of MCP server
9
+ * configs.
10
+ *
11
+ * iCode has NO project-local config concept — this target is global-only.
12
+ *
13
+ * Install (synchronous, non-interactive — no per-profile prompt):
14
+ * 1. Every EXISTING user profile under `~/.chrys/agents/*.yaml|yml` gets
15
+ * the homegraph MCP entry injected in place — a surgical line edit
16
+ * that preserves sibling MCP servers and every other section.
17
+ * 2. Built-in agents (Code / QA / Explore / General) are discovered from
18
+ * the installed iCode runtime
19
+ * (`.../site-packages/chrys/service/profiles/agents/builtins/`). A
20
+ * built-in with no user profile of the same name gets a "shadow"
21
+ * profile written to `~/.chrys/agents/<name>.yaml`: the built-in's
22
+ * FULL content plus the homegraph entry. (iCode's own override rule:
23
+ * a user profile with a built-in's name REPLACES the built-in
24
+ * entirely, so the copy must be complete.)
25
+ * If the runtime can't be located, install degrades to editing
26
+ * existing profiles only and returns manual guidance in `notes` — it
27
+ * never throws.
28
+ *
29
+ * Uninstall strips the homegraph entry from every user profile that
30
+ * carries it (shadow files are left as plain built-in copies), matching
31
+ * the other targets' "remove only what install wrote" contract.
32
+ *
33
+ * No instructions file is written (#529/#704) — guidance ships in the MCP
34
+ * `initialize` response, which iCode injects into system reminders by
35
+ * default (`expose_instructions: true`).
36
+ *
37
+ * NOTE: unlike the codegraph fork, this target must NOT suppress the
38
+ * "install the CLI on PATH" step in `src/installer/index.ts` — the MCP
39
+ * command is the `homegraph` Node CLI, which iCode (a Python app) does not
40
+ * bundle.
41
+ */
42
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
43
+ if (k2 === undefined) k2 = k;
44
+ var desc = Object.getOwnPropertyDescriptor(m, k);
45
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
46
+ desc = { enumerable: true, get: function() { return m[k]; } };
47
+ }
48
+ Object.defineProperty(o, k2, desc);
49
+ }) : (function(o, m, k, k2) {
50
+ if (k2 === undefined) k2 = k;
51
+ o[k2] = m[k];
52
+ }));
53
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
54
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
55
+ }) : function(o, v) {
56
+ o["default"] = v;
57
+ });
58
+ var __importStar = (this && this.__importStar) || (function () {
59
+ var ownKeys = function(o) {
60
+ ownKeys = Object.getOwnPropertyNames || function (o) {
61
+ var ar = [];
62
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
63
+ return ar;
64
+ };
65
+ return ownKeys(o);
66
+ };
67
+ return function (mod) {
68
+ if (mod && mod.__esModule) return mod;
69
+ var result = {};
70
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
71
+ __setModuleDefault(result, mod);
72
+ return result;
73
+ };
74
+ })();
75
+ Object.defineProperty(exports, "__esModule", { value: true });
76
+ exports.icodeTarget = void 0;
77
+ const fs = __importStar(require("fs"));
78
+ const path = __importStar(require("path"));
79
+ const os = __importStar(require("os"));
80
+ const shared_1 = require("./shared");
81
+ // ---------------------------------------------------------------------------
82
+ // Paths
83
+ // ---------------------------------------------------------------------------
84
+ function configDir() {
85
+ if (process.platform === 'win32') {
86
+ const appData = process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming');
87
+ return path.join(appData, 'chrys');
88
+ }
89
+ return path.join(os.homedir(), '.chrys');
90
+ }
91
+ function agentsDir() {
92
+ return path.join(configDir(), 'agents');
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // iCode runtime roots — where the PyApp installation (and its builtins) live
96
+ // ---------------------------------------------------------------------------
97
+ function icodeRuntimeRoots() {
98
+ const roots = [];
99
+ // Custom override (mirrors the PyApp install dir for the `chrys` project).
100
+ const envOverride = process.env.PYAPP_INSTALL_DIR_ICODE;
101
+ if (envOverride) {
102
+ roots.push(path.resolve(envOverride.replace(/^~/, os.homedir())));
103
+ }
104
+ const home = os.homedir();
105
+ if (process.platform === 'win32') {
106
+ const localAppData = process.env.LOCALAPPDATA ?? path.join(home, 'AppData', 'Local');
107
+ roots.push(path.join(localAppData, 'pyapp', 'data', 'chrys'));
108
+ }
109
+ else if (process.platform === 'darwin') {
110
+ roots.push(path.join(home, 'Library', 'Application Support', 'pyapp', 'chrys'));
111
+ }
112
+ else {
113
+ const xdgData = process.env.XDG_DATA_HOME ?? path.join(home, '.local', 'share');
114
+ roots.push(path.join(xdgData, 'pyapp', 'chrys'));
115
+ }
116
+ return roots;
117
+ }
118
+ /** Walk a directory tree looking for a chain of path segments. */
119
+ function findSubdirs(root, segments) {
120
+ if (segments.length === 0)
121
+ return fs.existsSync(root) ? [root] : [];
122
+ const head = segments[0];
123
+ const tail = segments.slice(1);
124
+ const current = path.join(root, head);
125
+ if (tail.length === 0)
126
+ return fs.existsSync(current) ? [current] : [];
127
+ // 'site-packages' can sit at an arbitrary depth — search recursively.
128
+ if (head === 'site-packages') {
129
+ const results = [];
130
+ const stack = [root];
131
+ while (stack.length > 0) {
132
+ const dir = stack.pop();
133
+ let entries;
134
+ try {
135
+ entries = fs.readdirSync(dir, { withFileTypes: true });
136
+ }
137
+ catch {
138
+ continue;
139
+ }
140
+ for (const entry of entries) {
141
+ if (!entry.isDirectory())
142
+ continue;
143
+ const full = path.join(dir, entry.name);
144
+ if (entry.name === 'site-packages')
145
+ results.push(...findSubdirs(full, tail));
146
+ else
147
+ stack.push(full);
148
+ }
149
+ }
150
+ return results;
151
+ }
152
+ return findSubdirs(current, tail);
153
+ }
154
+ function parseVersion(segment) {
155
+ const nums = [];
156
+ for (const p of segment.split('.')) {
157
+ if (!/^\d+$/.test(p))
158
+ return null;
159
+ nums.push(Number(p));
160
+ }
161
+ return nums.length > 0 ? nums : null;
162
+ }
163
+ function versionFromPath(p) {
164
+ let current = path.dirname(p);
165
+ while (current !== path.dirname(current)) {
166
+ const v = parseVersion(path.basename(current));
167
+ if (v)
168
+ return v;
169
+ current = path.dirname(current);
170
+ }
171
+ return [];
172
+ }
173
+ function mtimeOf(p) {
174
+ try {
175
+ return fs.statSync(p).mtimeMs;
176
+ }
177
+ catch {
178
+ return 0;
179
+ }
180
+ }
181
+ // ---------------------------------------------------------------------------
182
+ // Built-in agent discovery (from the installed runtime)
183
+ // ---------------------------------------------------------------------------
184
+ const BUILTIN_SEARCH = ['site-packages', 'chrys', 'service', 'profiles', 'agents', 'builtins'];
185
+ function discoverBuiltins() {
186
+ const seen = new Set();
187
+ const results = [];
188
+ const userAgentDir = agentsDir();
189
+ for (const root of icodeRuntimeRoots()) {
190
+ const builtinDirs = findSubdirs(root, BUILTIN_SEARCH);
191
+ if (builtinDirs.length === 0)
192
+ continue;
193
+ // Newest version + highest mtime first.
194
+ builtinDirs.sort((a, b) => {
195
+ const va = versionFromPath(a);
196
+ const vb = versionFromPath(b);
197
+ for (let i = 0; i < Math.max(va.length, vb.length); i++) {
198
+ const d = (vb[i] ?? 0) - (va[i] ?? 0);
199
+ if (d !== 0)
200
+ return d;
201
+ }
202
+ return mtimeOf(b) - mtimeOf(a);
203
+ });
204
+ for (const dir of builtinDirs) {
205
+ let entries;
206
+ try {
207
+ entries = fs.readdirSync(dir, { withFileTypes: true });
208
+ }
209
+ catch {
210
+ continue;
211
+ }
212
+ for (const entry of entries) {
213
+ if (!entry.isFile())
214
+ continue;
215
+ const ext = path.extname(entry.name).toLowerCase();
216
+ if (ext !== '.yaml' && ext !== '.yml')
217
+ continue;
218
+ const name = path.basename(entry.name, ext);
219
+ if (seen.has(name))
220
+ continue;
221
+ seen.add(name);
222
+ const sourcePath = path.join(dir, entry.name);
223
+ const content = readText(sourcePath);
224
+ if (!content)
225
+ continue;
226
+ results.push({
227
+ name,
228
+ sourcePath,
229
+ userPath: path.join(userAgentDir, `${name}.yaml`),
230
+ content,
231
+ });
232
+ }
233
+ // Only the highest-priority runtime that has YAML files wins.
234
+ if (results.length > 0)
235
+ break;
236
+ }
237
+ if (results.length > 0)
238
+ break;
239
+ }
240
+ return results;
241
+ }
242
+ function discoverProfiles() {
243
+ const result = [];
244
+ const seen = new Set();
245
+ // 1. User-created profiles.
246
+ const dir = agentsDir();
247
+ if (fs.existsSync(dir)) {
248
+ try {
249
+ for (const f of fs.readdirSync(dir)) {
250
+ const ext = path.extname(f).toLowerCase();
251
+ if (ext !== '.yaml' && ext !== '.yml')
252
+ continue;
253
+ const name = path.basename(f, ext);
254
+ seen.add(name);
255
+ const userPath = path.join(dir, f);
256
+ result.push({ name, source: 'user', userPath, content: readText(userPath) });
257
+ }
258
+ }
259
+ catch {
260
+ /* ignore */
261
+ }
262
+ }
263
+ // 2. Built-ins not yet shadowed by a user profile.
264
+ for (const b of discoverBuiltins()) {
265
+ if (seen.has(b.name))
266
+ continue;
267
+ seen.add(b.name);
268
+ result.push({ name: b.name, source: 'builtin', userPath: b.userPath, content: b.content });
269
+ }
270
+ return result.sort((a, b) => a.name.localeCompare(b.name));
271
+ }
272
+ // ---------------------------------------------------------------------------
273
+ // Small helpers
274
+ // ---------------------------------------------------------------------------
275
+ function readText(file) {
276
+ try {
277
+ return fs.readFileSync(file, 'utf-8');
278
+ }
279
+ catch {
280
+ return '';
281
+ }
282
+ }
283
+ function splitLines(content) {
284
+ return content.replace(/\r\n/g, '\n').replace(/\r/g, '\n').split('\n');
285
+ }
286
+ function joinLines(lines) {
287
+ while (lines.length > 0 && lines[lines.length - 1] === '')
288
+ lines.pop();
289
+ return lines.join('\n') + '\n';
290
+ }
291
+ function ensureTrailingNewline(text) {
292
+ return text.endsWith('\n') ? text : text + '\n';
293
+ }
294
+ function escapeRegExp(value) {
295
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
296
+ }
297
+ /** Find a top-level YAML key (zero indent). */
298
+ function topLevelRange(lines, key) {
299
+ const start = lines.findIndex((line) => line.trim() === `${key}:`);
300
+ if (start === -1)
301
+ return null;
302
+ let end = lines.length;
303
+ for (let i = start + 1; i < lines.length; i++) {
304
+ if ((lines[i] ?? '').trim() === '')
305
+ continue;
306
+ if (/^[A-Za-z_][A-Za-z0-9_-]*:\s*(?:#.*)?$/.test(lines[i] ?? '')) {
307
+ end = i;
308
+ break;
309
+ }
310
+ }
311
+ return { start, end };
312
+ }
313
+ /** Find a 2-space-indented child key within a parent range. */
314
+ function childRange(lines, parent, child) {
315
+ const startPattern = new RegExp(`^ ${escapeRegExp(child)}:\\s*(?:#.*)?$`);
316
+ let start = -1;
317
+ for (let i = parent.start + 1; i < parent.end; i++) {
318
+ if (startPattern.test(lines[i] ?? '')) {
319
+ start = i;
320
+ break;
321
+ }
322
+ }
323
+ if (start === -1)
324
+ return null;
325
+ let end = parent.end;
326
+ for (let i = start + 1; i < parent.end; i++) {
327
+ const line = lines[i] ?? '';
328
+ if (line.trim() === '')
329
+ continue;
330
+ // List items (" - name: ...") belong to the child; only a same-indent
331
+ // mapping key terminates the range.
332
+ if (/^ [A-Za-z_]/.test(line)) {
333
+ end = i;
334
+ break;
335
+ }
336
+ }
337
+ while (end > start + 1 && (lines[end - 1] ?? '').trim() === '')
338
+ end--;
339
+ return { start, end };
340
+ }
341
+ // ---------------------------------------------------------------------------
342
+ // HomeGraph MCP entry block
343
+ // ---------------------------------------------------------------------------
344
+ function renderHomeGraphMcpEntry() {
345
+ const mcp = (0, shared_1.getMcpServerConfig)();
346
+ const lines = [
347
+ ' - name: homegraph',
348
+ ' transport: stdio',
349
+ ` command: ${mcp.command}`,
350
+ ' args:',
351
+ ];
352
+ for (const arg of mcp.args)
353
+ lines.push(` - ${arg}`);
354
+ lines.push(' enabled: true');
355
+ return lines;
356
+ }
357
+ function hasHomeGraphMcp(content) {
358
+ const lines = splitLines(content);
359
+ const tools = topLevelRange(lines, 'tools');
360
+ if (!tools)
361
+ return false;
362
+ const mcp = childRange(lines, tools, 'mcp');
363
+ if (!mcp)
364
+ return false;
365
+ for (let i = mcp.start + 1; i < mcp.end; i++) {
366
+ if (/^ {2,4}- name:\s*homegraph\b/.test(lines[i] ?? ''))
367
+ return true;
368
+ }
369
+ return false;
370
+ }
371
+ function findMcpInsertPoint(lines, mcpRange) {
372
+ for (let i = mcpRange.end - 1; i > mcpRange.start; i--) {
373
+ const line = lines[i] ?? '';
374
+ if (line.trim() === '')
375
+ continue;
376
+ if ((line.match(/^( *)/)?.[1] ?? '').length >= 4)
377
+ return i;
378
+ }
379
+ return mcpRange.start;
380
+ }
381
+ function lastIndentedLine(lines, parent) {
382
+ for (let i = parent.end - 1; i > parent.start; i--) {
383
+ const line = lines[i] ?? '';
384
+ if (line.trim() === '')
385
+ continue;
386
+ if (/^ /.test(line))
387
+ return i;
388
+ }
389
+ return parent.start;
390
+ }
391
+ function insertAt(lines, afterIdx, newLines) {
392
+ const result = [...lines];
393
+ result.splice(afterIdx + 1, 0, ...newLines);
394
+ return joinLines(result);
395
+ }
396
+ function addHomeGraphMcp(content) {
397
+ const lines = splitLines(content);
398
+ const tools = topLevelRange(lines, 'tools');
399
+ // Already present → no-op.
400
+ if (tools) {
401
+ const mcp = childRange(lines, tools, 'mcp');
402
+ if (mcp && hasHomeGraphMcp(content))
403
+ return content;
404
+ }
405
+ const entry = renderHomeGraphMcpEntry();
406
+ // No `tools:` section at all → append one.
407
+ if (!tools) {
408
+ return joinLines([...lines, '', 'tools:', ' mcp:', ...entry, '']);
409
+ }
410
+ const mcp = childRange(lines, tools, 'mcp');
411
+ // `tools:` exists but no `mcp:` list → add `mcp:` after the last child.
412
+ if (!mcp) {
413
+ const insertAfter = lastIndentedLine(lines, tools);
414
+ return insertAt(lines, insertAfter, [' mcp:', ...entry, '']);
415
+ }
416
+ // `mcp:` exists → append the entry after the last mcp item.
417
+ const insertAfter = findMcpInsertPoint(lines, mcp);
418
+ return insertAt(lines, insertAfter, entry);
419
+ }
420
+ // ---------------------------------------------------------------------------
421
+ // Uninstall helpers
422
+ // ---------------------------------------------------------------------------
423
+ function findHomeGraphEntryRange(lines, mcpRange) {
424
+ let entryStart = -1;
425
+ for (let i = mcpRange.start + 1; i < mcpRange.end; i++) {
426
+ if (/^ {2,4}- name:\s*homegraph\b/.test(lines[i] ?? '')) {
427
+ entryStart = i;
428
+ break;
429
+ }
430
+ }
431
+ if (entryStart === -1)
432
+ return null;
433
+ let entryEnd = mcpRange.end;
434
+ for (let i = entryStart + 1; i < mcpRange.end; i++) {
435
+ if (/^ {2,4}- name:/.test(lines[i] ?? '')) {
436
+ entryEnd = i;
437
+ break;
438
+ }
439
+ }
440
+ while (entryEnd > entryStart + 1 && (lines[entryEnd - 1] ?? '').trim() === '')
441
+ entryEnd--;
442
+ return { start: entryStart, end: entryEnd };
443
+ }
444
+ function stripLines(lines, start, end) {
445
+ const result = [...lines];
446
+ result.splice(start, end - start);
447
+ const cleaned = [];
448
+ let prevBlank = false;
449
+ for (const line of result) {
450
+ const isBlank = line.trim() === '';
451
+ if (isBlank && prevBlank)
452
+ continue;
453
+ cleaned.push(line);
454
+ prevBlank = isBlank;
455
+ }
456
+ return joinLines(cleaned);
457
+ }
458
+ function removeHomeGraphMcp(content) {
459
+ const lines = splitLines(content);
460
+ const tools = topLevelRange(lines, 'tools');
461
+ if (!tools)
462
+ return content;
463
+ const mcp = childRange(lines, tools, 'mcp');
464
+ if (!mcp)
465
+ return content;
466
+ const entryRange = findHomeGraphEntryRange(lines, mcp);
467
+ if (!entryRange)
468
+ return content;
469
+ const remaining = lines.slice(mcp.start + 1, mcp.end).some((l, i) => {
470
+ const idx = mcp.start + 1 + i;
471
+ if (idx >= entryRange.start && idx < entryRange.end)
472
+ return false;
473
+ return l.trim() !== '';
474
+ });
475
+ // Last entry in the mcp list → drop the whole `mcp:` block; else drop just
476
+ // the entry, leaving siblings untouched.
477
+ return remaining
478
+ ? stripLines(lines, entryRange.start, entryRange.end)
479
+ : stripLines(lines, mcp.start, mcp.end);
480
+ }
481
+ // ---------------------------------------------------------------------------
482
+ // Target implementation
483
+ // ---------------------------------------------------------------------------
484
+ class ICodeTarget {
485
+ id = 'icode';
486
+ displayName = 'iCode';
487
+ // No public docs URL known; left undefined rather than pointing somewhere wrong.
488
+ /** iCode has no project-local config concept. */
489
+ supportsLocation(loc) {
490
+ return loc === 'global';
491
+ }
492
+ detect(loc) {
493
+ if (loc !== 'global') {
494
+ return { installed: false, alreadyConfigured: false };
495
+ }
496
+ let installed = fs.existsSync(configDir());
497
+ if (!installed) {
498
+ for (const root of icodeRuntimeRoots()) {
499
+ if (fs.existsSync(root)) {
500
+ installed = true;
501
+ break;
502
+ }
503
+ }
504
+ }
505
+ if (!installed)
506
+ return { installed: false, alreadyConfigured: false };
507
+ let alreadyConfigured = false;
508
+ let firstPath;
509
+ for (const p of discoverProfiles()) {
510
+ if (hasHomeGraphMcp(p.content)) {
511
+ alreadyConfigured = true;
512
+ firstPath = p.userPath;
513
+ break;
514
+ }
515
+ }
516
+ return { installed, alreadyConfigured, configPath: firstPath ?? agentsDir() };
517
+ }
518
+ install(loc, _opts) {
519
+ if (loc !== 'global') {
520
+ return {
521
+ files: [],
522
+ notes: ['iCode has no project-local config — re-run with --location=global.'],
523
+ };
524
+ }
525
+ // Escape hatch: never create new shadow profiles for built-ins (still
526
+ // edits existing user profiles).
527
+ const skipBuiltins = process.env.HOMEGRAPH_ICODE_NO_BUILTINS === '1';
528
+ const all = discoverProfiles();
529
+ const profiles = skipBuiltins ? all.filter((p) => p.source === 'user') : all;
530
+ const builtinsFound = discoverBuiltins().length > 0;
531
+ if (profiles.length === 0) {
532
+ return {
533
+ files: [],
534
+ notes: [
535
+ fs.existsSync(configDir())
536
+ ? 'No iCode agent profiles found. Run `icode` once to initialize the built-ins, then re-run `homegraph install --target icode`.'
537
+ : 'iCode does not appear to be installed. Install iCode first, then re-run `homegraph install --target icode`.',
538
+ ],
539
+ };
540
+ }
541
+ const files = [];
542
+ for (const prof of profiles) {
543
+ if (prof.content.trim().length === 0)
544
+ continue; // unreadable / empty
545
+ if (hasHomeGraphMcp(prof.content)) {
546
+ files.push({ path: prof.userPath, action: 'unchanged' });
547
+ continue;
548
+ }
549
+ const after = addHomeGraphMcp(prof.content);
550
+ const existed = fs.existsSync(prof.userPath);
551
+ (0, shared_1.atomicWriteFileSync)(prof.userPath, ensureTrailingNewline(after));
552
+ files.push({ path: prof.userPath, action: existed ? 'updated' : 'created' });
553
+ }
554
+ const created = files.filter((f) => f.action === 'created').length;
555
+ const updated = files.filter((f) => f.action === 'updated').length;
556
+ const unchanged = files.filter((f) => f.action === 'unchanged').length;
557
+ const notes = [];
558
+ if (created > 0) {
559
+ notes.push(`Created ${created} shadow profile(s) for built-in agents with homegraph MCP.`);
560
+ }
561
+ if (updated > 0) {
562
+ notes.push(`Injected homegraph MCP server into ${updated} existing profile(s).`);
563
+ }
564
+ if (unchanged > 0)
565
+ notes.push(`${unchanged} profile(s) already had homegraph configured.`);
566
+ if (!builtinsFound) {
567
+ // Degrade to manual guidance — never error.
568
+ notes.push('Could not locate the installed iCode runtime, so built-in agents (Code/QA/Explore/General) were not auto-configured. ' +
569
+ 'Add the block below to their YAML under ~/.chrys/agents/ manually (or set PYAPP_INSTALL_DIR_ICODE and re-run):\n' +
570
+ renderHomeGraphMcpEntry().join('\n'));
571
+ }
572
+ if (notes.length === 0)
573
+ notes.push('Nothing was changed.');
574
+ return { files, notes };
575
+ }
576
+ uninstall(loc) {
577
+ if (loc !== 'global')
578
+ return { files: [] };
579
+ const dir = agentsDir();
580
+ if (!fs.existsSync(dir)) {
581
+ return { files: [], notes: ['No iCode agent profiles found — nothing to uninstall.'] };
582
+ }
583
+ let userFiles;
584
+ try {
585
+ userFiles = fs
586
+ .readdirSync(dir)
587
+ .filter((f) => /\.ya?ml$/i.test(f))
588
+ .map((f) => path.join(dir, f))
589
+ .sort();
590
+ }
591
+ catch {
592
+ return { files: [], notes: ['No iCode agent profiles found — nothing to uninstall.'] };
593
+ }
594
+ const files = [];
595
+ for (const profilePath of userFiles) {
596
+ const before = readText(profilePath);
597
+ if (!hasHomeGraphMcp(before)) {
598
+ files.push({ path: profilePath, action: 'kept' });
599
+ continue;
600
+ }
601
+ const after = removeHomeGraphMcp(before);
602
+ (0, shared_1.atomicWriteFileSync)(profilePath, ensureTrailingNewline(after));
603
+ files.push({ path: profilePath, action: 'removed' });
604
+ }
605
+ const removed = files.filter((f) => f.action === 'removed').length;
606
+ return {
607
+ files,
608
+ notes: [
609
+ removed > 0
610
+ ? `Removed homegraph MCP from ${removed} iCode profile(s).`
611
+ : 'homegraph MCP was not configured in any iCode profile.',
612
+ ],
613
+ };
614
+ }
615
+ printConfig(_loc) {
616
+ return [
617
+ '# Add to each profile YAML under ~/.chrys/agents/',
618
+ '# Find the "tools:" section, then add under a "mcp:" list:',
619
+ '',
620
+ ...renderHomeGraphMcpEntry(),
621
+ '',
622
+ '# Or install with: homegraph install --target icode',
623
+ '',
624
+ ].join('\n');
625
+ }
626
+ describePaths(_loc) {
627
+ return discoverProfiles().map((p) => p.userPath);
628
+ }
629
+ }
630
+ exports.icodeTarget = new ICodeTarget();
631
+ //# sourceMappingURL=icode.js.map
@@ -23,6 +23,7 @@ const hermes_1 = require("./hermes");
23
23
  const gemini_1 = require("./gemini");
24
24
  const antigravity_1 = require("./antigravity");
25
25
  const kiro_1 = require("./kiro");
26
+ const icode_1 = require("./icode");
26
27
  exports.ALL_TARGETS = Object.freeze([
27
28
  claude_1.claudeTarget,
28
29
  cursor_1.cursorTarget,
@@ -34,6 +35,7 @@ exports.ALL_TARGETS = Object.freeze([
34
35
  gemini_1.geminiTarget,
35
36
  antigravity_1.antigravityTarget,
36
37
  kiro_1.kiroTarget,
38
+ icode_1.icodeTarget,
37
39
  ]);
38
40
  function getTarget(id) {
39
41
  return exports.ALL_TARGETS.find((t) => t.id === id);