@ghl-ai/aw 0.1.47-beta.5 → 0.1.47-beta.6

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 (2) hide show
  1. package/integrations.mjs +277 -42
  2. package/package.json +1 -1
package/integrations.mjs CHANGED
@@ -107,7 +107,10 @@ export const INTEGRATIONS = {
107
107
  type: 'python-cli',
108
108
  label: 'Graphify (Knowledge Graph)',
109
109
  description: 'Builds a queryable knowledge graph of your codebase + docs',
110
- pipPackage: 'graphifyy',
110
+ // mcp,pdf,office,svg,sql covers all practical codebase-analysis needs.
111
+ // Excluded: leiden (Python <3.13 only — breaks on newer Python), video (heavy whisper/yt-dlp),
112
+ // openai/gemini/ollama/bedrock (AI inference — users have their own setup), neo4j (specialized).
113
+ pipPackage: 'graphifyy[mcp,pdf,office,svg,sql]',
111
114
  cliCommand: 'graphify',
112
115
  minPython: { major: 3, minor: 10 },
113
116
  postInstall: [
@@ -116,10 +119,10 @@ export const INTEGRATIONS = {
116
119
  perProjectInstall: [
117
120
  // IDE wiring — only runs if that IDE's config dir exists on this machine.
118
121
  // Each command writes the IDE-specific CLAUDE.md/AGENTS.md section + hook.
119
- { args: ['claude', 'install'], requiresGit: false, requiresIde: '.claude' },
120
- { args: ['codex', 'install'], requiresGit: false, requiresIde: '.codex' },
121
- { args: ['cursor', 'install'], requiresGit: false, requiresIde: '.cursor' },
122
- { args: ['gemini', 'install'], requiresGit: false, requiresIde: '.gemini' },
122
+ { args: ['claude', 'install'], requiresGit: false, requiresIde: '.claude', requiresFile: 'graphify-out/graph.json' },
123
+ { args: ['codex', 'install'], requiresGit: false, requiresIde: '.codex', requiresFile: 'graphify-out/graph.json' },
124
+ { args: ['cursor', 'install'], requiresGit: false, requiresIde: '.cursor', requiresFile: 'graphify-out/graph.json' },
125
+ { args: ['gemini', 'install'], requiresGit: false, requiresIde: '.gemini', requiresFile: 'graphify-out/graph.json' },
123
126
  // Git hooks — post-commit AST rebuild + post-checkout sync + merge driver
124
127
  { args: ['hook', 'install'], requiresGit: true },
125
128
  // If a graph already exists, register it into the global graph immediately.
@@ -302,12 +305,16 @@ export function installGraphifyGlobalAddHook(homeDir) {
302
305
  e => e?.description !== MARKER,
303
306
  );
304
307
 
305
- // Always use `python -m graphify` (never the bare binary) so the hook is safe
306
- // across future shell sessions where an old graphify binary might shadow the
307
- // currently-installed version via PATH. Derived fresh here rather than baking
308
- // in the cli token resolved at install time.
309
- const pythonExe = IS_WINDOWS ? 'python' : 'python3';
310
- const cmd = `[ -f graphify-out/graph.json ] && ${pythonExe} -m graphify global add graphify-out/graph.json --as "$(basename "$PWD")" > /dev/null 2>&1 || true`;
308
+ // On Windows: bash syntax ([ -f ], $(basename), || true) does not work in cmd.exe/PS.
309
+ // graphify.exe is on PATH via ~/.local/bin (placed there by uv tool install).
310
+ // On POSIX: use python3 -m graphify so the module is always resolved from the
311
+ // correct interpreter regardless of which graphify binary is first on PATH.
312
+ let cmd;
313
+ if (IS_WINDOWS) {
314
+ cmd = `powershell -NoProfile -NonInteractive -Command "if (Test-Path 'graphify-out/graph.json') { graphify global add 'graphify-out/graph.json' --as (Split-Path -Leaf (Get-Location)); graphify claude install }"`;
315
+ } else {
316
+ cmd = `[ -f graphify-out/graph.json ] && { python3 -m graphify global add graphify-out/graph.json --as "$(basename "$PWD")" > /dev/null 2>&1; graphify claude install > /dev/null 2>&1; } || true`;
317
+ }
311
318
 
312
319
  settings.hooks.SessionStart.push({
313
320
  description: MARKER,
@@ -318,38 +325,133 @@ export function installGraphifyGlobalAddHook(homeDir) {
318
325
  writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
319
326
  }
320
327
 
321
- // Write the graphify-global MCP server entry to ~/.claude/settings.json so Claude Code
322
- // auto-starts it on launch. Uses `python -m graphify.serve` to avoid PATH issues with
323
- // stale graphify.exe binaries that may shadow the currently installed version.
324
- //
325
- // The command is a single stable token — `python` on Windows, `python3` on POSIX —
326
- // so the JSON args array is always parseable (py.cmd can be `py -3` which has a space
327
- // and would break if used directly as the command field).
328
- export function installGraphifyMcpServer(homeDir) {
329
- const claudeDir = join(homeDir, '.claude');
330
- if (!existsSync(claudeDir)) return;
328
+ // Write a global sessionStart hook to ~/.cursor/hooks.json so Cursor auto-wires
329
+ // graphify IDE context on session open whenever a built graph exists in the project.
330
+ export function installGraphifyCursorGlobalHook(homeDir) {
331
+ const cursorDir = join(homeDir, '.cursor');
332
+ if (!existsSync(cursorDir)) return;
331
333
 
332
- const settingsPath = join(claudeDir, 'settings.json');
333
- let settings = {};
334
- if (existsSync(settingsPath)) {
335
- try { settings = JSON.parse(readFileSync(settingsPath, 'utf8')); } catch { /* corrupt — start fresh */ }
334
+ const hooksPath = join(cursorDir, 'hooks.json');
335
+ let hooks = { version: 1, hooks: {} };
336
+ if (existsSync(hooksPath)) {
337
+ try { hooks = JSON.parse(readFileSync(hooksPath, 'utf8')); } catch { /* corrupt — start fresh */ }
338
+ }
339
+
340
+ if (!hooks.hooks) hooks.hooks = {};
341
+ if (!Array.isArray(hooks.hooks.sessionStart)) hooks.hooks.sessionStart = [];
342
+
343
+ const MARKER = 'graphify-cursor-install';
344
+ hooks.hooks.sessionStart = hooks.hooks.sessionStart.filter(
345
+ e => e?.description !== MARKER,
346
+ );
347
+
348
+ let cmd;
349
+ if (IS_WINDOWS) {
350
+ cmd = `powershell -NoProfile -NonInteractive -Command "if (Test-Path 'graphify-out/graph.json') { graphify global add 'graphify-out/graph.json' --as (Split-Path -Leaf (Get-Location)); graphify cursor install }"`;
351
+ } else {
352
+ cmd = `[ -f graphify-out/graph.json ] && { graphify global add graphify-out/graph.json --as "$(basename "$PWD")" > /dev/null 2>&1; graphify cursor install > /dev/null 2>&1; } || true`;
353
+ }
354
+
355
+ hooks.hooks.sessionStart.push({ description: MARKER, command: cmd });
356
+ writeFileSync(hooksPath, JSON.stringify(hooks, null, 2) + '\n');
357
+ }
358
+
359
+ // Write a global SessionStart hook to ~/.codex/hooks.json so Codex auto-wires
360
+ // graphify IDE context on session open whenever a built graph exists in the project.
361
+ export function installGraphifyCodexGlobalHook(homeDir) {
362
+ const codexDir = join(homeDir, '.codex');
363
+ if (!existsSync(codexDir)) return;
364
+
365
+ const hooksPath = join(codexDir, 'hooks.json');
366
+ let hooks = { hooks: {} };
367
+ if (existsSync(hooksPath)) {
368
+ try { hooks = JSON.parse(readFileSync(hooksPath, 'utf8')); } catch { /* corrupt — start fresh */ }
336
369
  }
337
370
 
338
- if (!settings.mcpServers) settings.mcpServers = {};
371
+ if (!hooks.hooks) hooks.hooks = {};
372
+ if (!Array.isArray(hooks.hooks.SessionStart)) hooks.hooks.SessionStart = [];
339
373
 
340
- // Use join() so the path uses OS-correct separators on all platforms.
374
+ const MARKER = 'graphify-codex-install';
375
+ hooks.hooks.SessionStart = hooks.hooks.SessionStart.filter(
376
+ e => e?.description !== MARKER,
377
+ );
378
+
379
+ let cmd;
380
+ if (IS_WINDOWS) {
381
+ cmd = `powershell -NoProfile -NonInteractive -Command "if (Test-Path 'graphify-out/graph.json') { graphify global add 'graphify-out/graph.json' --as (Split-Path -Leaf (Get-Location)); graphify codex install }"`;
382
+ } else {
383
+ cmd = `[ -f graphify-out/graph.json ] && { graphify global add graphify-out/graph.json --as "$(basename "$PWD")" > /dev/null 2>&1; graphify codex install > /dev/null 2>&1; } || true`;
384
+ }
385
+
386
+ hooks.hooks.SessionStart.push({
387
+ description: MARKER,
388
+ hooks: [{ type: 'command', command: cmd }],
389
+ });
390
+ writeFileSync(hooksPath, JSON.stringify(hooks, null, 2) + '\n');
391
+ }
392
+
393
+ // Build the platform-correct MCP server config object for graphify-global.
394
+ function buildGraphifyMcpConfig(homeDir) {
341
395
  const globalGraphPath = join(homeDir, '.graphify', 'global-graph.json');
342
- // `python` on Windows (Microsoft Store + standard installer both put it on PATH),
343
- // `python3` on macOS/Linux (standard convention; `python` may not exist).
344
- const pythonExe = IS_WINDOWS ? 'python' : 'python3';
396
+ if (IS_WINDOWS) {
397
+ const uvToolPython = join(homeDir, 'AppData', 'Roaming', 'uv', 'tools', 'graphifyy', 'Scripts', 'python.exe');
398
+ const command = existsSync(uvToolPython) ? uvToolPython : 'graphify';
399
+ const args = existsSync(uvToolPython)
400
+ ? ['-m', 'graphify.serve', globalGraphPath]
401
+ : ['serve', globalGraphPath];
402
+ return { command, args };
403
+ }
404
+ return { command: 'python3', args: ['-m', 'graphify.serve', globalGraphPath] };
405
+ }
345
406
 
346
- settings.mcpServers['graphify-global'] = {
347
- command: pythonExe,
348
- args: ['-m', 'graphify.serve', globalGraphPath],
349
- };
407
+ // Add a stdio MCP server block to a Codex config.toml (idempotent).
408
+ function addToTomlStdioMcp(filePath, serverName, command, args) {
409
+ mkdirSync(join(filePath, '..'), { recursive: true });
410
+ let content = existsSync(filePath) ? readFileSync(filePath, 'utf8') : '';
411
+ // Remove existing block — regex consumes lines that don't START with '[' (handles
412
+ // args = [...] which contains '[' mid-line but not at line start).
413
+ const escapedName = serverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
414
+ const blockRegex = new RegExp(`\\[mcp_servers\\.${escapedName}\\](?:\\n(?!\\[)[^\\n]*)*\\n?`, 'g');
415
+ content = content.replace(blockRegex, '');
416
+ // TOML: single-quote command path to avoid backslash escaping issues on Windows.
417
+ // JSON.stringify for args — JSON array syntax is valid TOML inline array.
418
+ const argsToml = JSON.stringify(args);
419
+ content += `[mcp_servers.${serverName}]\ncommand = '${command}'\nargs = ${argsToml}\n\n`;
420
+ writeFileSync(filePath, content);
421
+ }
350
422
 
351
- mkdirSync(claudeDir, { recursive: true });
352
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
423
+ // Write the graphify-global MCP server entry to ~/.claude.json, ~/.cursor/mcp.json,
424
+ // and ~/.codex/config.toml so the global graph is accessible in all three IDEs.
425
+ export function installGraphifyMcpServer(homeDir) {
426
+ const mcpConfig = buildGraphifyMcpConfig(homeDir);
427
+
428
+ // Claude Code: ~/.claude.json (user-level MCP config surfaced in /mcp UI)
429
+ const claudeJsonPath = join(homeDir, '.claude.json');
430
+ try {
431
+ let settings = {};
432
+ if (existsSync(claudeJsonPath)) {
433
+ try { settings = JSON.parse(readFileSync(claudeJsonPath, 'utf8')); } catch { /* corrupt */ }
434
+ }
435
+ if (!settings.mcpServers) settings.mcpServers = {};
436
+ settings.mcpServers['graphify-global'] = mcpConfig;
437
+ writeFileSync(claudeJsonPath, JSON.stringify(settings, null, 2) + '\n');
438
+ } catch { /* best effort */ }
439
+
440
+ // Cursor: ~/.cursor/mcp.json (same JSON mcpServers format, supports stdio)
441
+ const cursorMcpPath = join(homeDir, '.cursor', 'mcp.json');
442
+ if (existsSync(join(homeDir, '.cursor'))) {
443
+ try {
444
+ addToJsonMcp(cursorMcpPath, 'graphify-global', mcpConfig);
445
+ } catch { /* best effort */ }
446
+ }
447
+
448
+ // Codex: ~/.codex/config.toml (TOML, supports stdio via command + args)
449
+ const codexConfigPath = join(homeDir, '.codex', 'config.toml');
450
+ if (existsSync(join(homeDir, '.codex'))) {
451
+ try {
452
+ addToTomlStdioMcp(codexConfigPath, 'graphify-global', mcpConfig.command, mcpConfig.args);
453
+ } catch { /* best effort */ }
454
+ }
353
455
  }
354
456
 
355
457
  // After pip install, the CLI binary may not be on PATH (pip --user puts it in a Scripts
@@ -434,8 +536,21 @@ async function runPythonCli(integration, key, { silent = false } = {}) {
434
536
  // The graph itself is built manually via `/graphify .` — graphify's own per-IDE
435
537
  // install (step 6 below) writes the CLAUDE.md / AGENTS.md sections for that.
436
538
  if (integration.cliCommand === 'graphify') {
539
+ // Bootstrap global-graph.json now if a graph exists in cwd — graphify.serve requires
540
+ // the file to exist on startup, but only creates it on first `global add` call.
541
+ const cwdGraph = join(process.cwd(), 'graphify-out', 'graph.json');
542
+ if (existsSync(cwdGraph) && process.cwd() !== HOME) {
543
+ try {
544
+ await execAsync(
545
+ `${cli} global add graphify-out/graph.json --as ${basename(process.cwd())}`,
546
+ { cwd: process.cwd(), timeout: 30 * 1000 },
547
+ );
548
+ } catch { /* best effort — non-fatal if already registered */ }
549
+ }
437
550
  installGraphifyMcpServer(HOME);
438
551
  installGraphifyGlobalAddHook(HOME);
552
+ installGraphifyCursorGlobalHook(HOME);
553
+ installGraphifyCodexGlobalHook(HOME);
439
554
  }
440
555
 
441
556
  // 6. Run per-project hooks if cwd looks like a real project (not HOME).
@@ -762,14 +877,134 @@ export async function removeIntegration(key, { silent = false } = {}) {
762
877
 
763
878
  if (!silent) fmt.logSuccess(`${integration.label} removed from MCP servers`);
764
879
  } else if (integration.type === 'python-cli') {
765
- // PYTHON CLI: only remove the manifest entry leave the pip package installed
766
- // because the user may use the CLI outside of `aw`. Print manual cleanup hints.
880
+ // PYTHON CLI: remove global hooks + MCP wired by aw, leave pip package installed.
881
+ if (integration.cliCommand === 'graphify') {
882
+ // Remove SessionStart hook from ~/.claude/settings.json
883
+ const settingsPath = join(HOME, '.claude', 'settings.json');
884
+ try {
885
+ if (existsSync(settingsPath)) {
886
+ const s = JSON.parse(readFileSync(settingsPath, 'utf8'));
887
+ if (Array.isArray(s.hooks?.SessionStart)) {
888
+ s.hooks.SessionStart = s.hooks.SessionStart.filter(
889
+ e => e?.description !== 'graphify-global-add',
890
+ );
891
+ }
892
+ writeFileSync(settingsPath, JSON.stringify(s, null, 2) + '\n');
893
+ if (!silent) fmt.logStep('Removed graphify SessionStart hook from ~/.claude/settings.json');
894
+ }
895
+ } catch { /* best effort */ }
896
+
897
+ // Remove graphify-global MCP from ~/.claude.json
898
+ const claudeJsonPath = join(HOME, '.claude.json');
899
+ try {
900
+ if (existsSync(claudeJsonPath)) {
901
+ const c = JSON.parse(readFileSync(claudeJsonPath, 'utf8'));
902
+ if (c.mcpServers?.['graphify-global']) {
903
+ delete c.mcpServers['graphify-global'];
904
+ writeFileSync(claudeJsonPath, JSON.stringify(c, null, 2) + '\n');
905
+ if (!silent) fmt.logStep('Removed graphify-global MCP from ~/.claude.json');
906
+ }
907
+ }
908
+ } catch { /* best effort */ }
909
+
910
+ // Remove graphify-global MCP from ~/.cursor/mcp.json
911
+ const cursorMcpPath = join(HOME, '.cursor', 'mcp.json');
912
+ try {
913
+ if (existsSync(cursorMcpPath)) {
914
+ const c = JSON.parse(readFileSync(cursorMcpPath, 'utf8'));
915
+ if (c.mcpServers?.['graphify-global']) {
916
+ delete c.mcpServers['graphify-global'];
917
+ writeFileSync(cursorMcpPath, JSON.stringify(c, null, 2) + '\n');
918
+ if (!silent) fmt.logStep('Removed graphify-global MCP from ~/.cursor/mcp.json');
919
+ }
920
+ }
921
+ } catch { /* best effort */ }
922
+
923
+ // Remove graphify-global block from ~/.codex/config.toml
924
+ const codexConfigPath = join(HOME, '.codex', 'config.toml');
925
+ try {
926
+ if (existsSync(codexConfigPath)) {
927
+ let content = readFileSync(codexConfigPath, 'utf8');
928
+ // Regex consumes lines that don't START with '[' — handles args = [...]
929
+ // which contains '[' mid-line but not at line start.
930
+ const blockRegex = /\[mcp_servers\.graphify-global\](?:\n(?!\[)[^\n]*)*\n?/g;
931
+ content = content.replace(blockRegex, '');
932
+ writeFileSync(codexConfigPath, content);
933
+ if (!silent) fmt.logStep('Removed graphify-global MCP from ~/.codex/config.toml');
934
+ }
935
+ } catch { /* best effort */ }
936
+
937
+ // Remove sessionStart hook from ~/.cursor/hooks.json
938
+ const cursorHooksPath = join(HOME, '.cursor', 'hooks.json');
939
+ try {
940
+ if (existsSync(cursorHooksPath)) {
941
+ const h = JSON.parse(readFileSync(cursorHooksPath, 'utf8'));
942
+ if (Array.isArray(h.hooks?.sessionStart)) {
943
+ h.hooks.sessionStart = h.hooks.sessionStart.filter(
944
+ e => e?.description !== 'graphify-cursor-install',
945
+ );
946
+ writeFileSync(cursorHooksPath, JSON.stringify(h, null, 2) + '\n');
947
+ if (!silent) fmt.logStep('Removed graphify sessionStart hook from ~/.cursor/hooks.json');
948
+ }
949
+ }
950
+ } catch { /* best effort */ }
951
+
952
+ // Remove SessionStart hook from ~/.codex/hooks.json
953
+ const codexHooksPath = join(HOME, '.codex', 'hooks.json');
954
+ try {
955
+ if (existsSync(codexHooksPath)) {
956
+ const h = JSON.parse(readFileSync(codexHooksPath, 'utf8'));
957
+ if (Array.isArray(h.hooks?.SessionStart)) {
958
+ h.hooks.SessionStart = h.hooks.SessionStart.filter(
959
+ e => e?.description !== 'graphify-codex-install',
960
+ );
961
+ writeFileSync(codexHooksPath, JSON.stringify(h, null, 2) + '\n');
962
+ if (!silent) fmt.logStep('Removed graphify SessionStart hook from ~/.codex/hooks.json');
963
+ }
964
+ }
965
+ } catch { /* best effort */ }
966
+
967
+ // Per-project cleanup: uninstall IDE wiring + git hooks + deregister from global graph.
968
+ // Mirrors perProjectInstall in reverse. Runs best-effort — non-fatal if graphify not on PATH.
969
+ const cwd = process.cwd();
970
+ const isHome = cwd === HOME;
971
+ const hasGit = existsSync(join(cwd, '.git'));
972
+ const hasGraph = existsSync(join(cwd, 'graphify-out', 'graph.json'));
973
+ if (!isHome) {
974
+ const ideUninstalls = [
975
+ { cmd: 'graphify claude uninstall', requiresIde: '.claude' },
976
+ { cmd: 'graphify codex uninstall', requiresIde: '.codex' },
977
+ { cmd: 'graphify cursor uninstall', requiresIde: '.cursor' },
978
+ { cmd: 'graphify gemini uninstall', requiresIde: '.gemini' },
979
+ ];
980
+ for (const step of ideUninstalls) {
981
+ if (!existsSync(join(HOME, step.requiresIde))) continue;
982
+ try {
983
+ await execAsync(step.cmd, { cwd, timeout: 30 * 1000 });
984
+ if (!silent) fmt.logStep(`Ran: ${step.cmd}`);
985
+ } catch { /* best effort */ }
986
+ }
987
+ if (hasGit) {
988
+ try {
989
+ await execAsync('graphify hook uninstall', { cwd, timeout: 30 * 1000 });
990
+ if (!silent) fmt.logStep('Ran: graphify hook uninstall');
991
+ } catch { /* best effort */ }
992
+ }
993
+ if (hasGraph) {
994
+ try {
995
+ await execAsync(`graphify global remove --as ${basename(cwd)}`, { cwd, timeout: 30 * 1000 });
996
+ if (!silent) fmt.logStep(`Deregistered ${basename(cwd)} from global graph`);
997
+ } catch { /* best effort */ }
998
+ }
999
+ }
1000
+ }
1001
+
767
1002
  if (!silent) {
768
1003
  fmt.logWarn(
769
- `Removed ${integration.label} from the aw manifest. The Python package was left installed.\n` +
770
- ` To fully remove, run in each project: \`${integration.cliCommand} claude uninstall\` and \`${integration.cliCommand} hook uninstall\`\n` +
771
- ` Then uninstall the package: \`pip uninstall ${integration.pipPackage.split(/[<>=]/)[0]}\` (or \`uv tool uninstall\` / \`pipx uninstall\`)`,
772
- 'Manual Cleanup',
1004
+ `The Python package was left installed (use outside of aw is possible).\n` +
1005
+ ` To fully remove: \`uv tool uninstall graphifyy\` (or \`pipx uninstall\` / \`pip uninstall graphifyy\`)\n` +
1006
+ ` To remove per-project wiring: \`${integration.cliCommand} claude uninstall\` inside each project`,
1007
+ 'Note',
773
1008
  );
774
1009
  }
775
1010
  } else if (integration.type === 'universal-installer') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.47-beta.5",
3
+ "version": "0.1.47-beta.6",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {