@ghl-ai/aw 0.1.58-beta.0 → 0.1.59

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/integrations.mjs CHANGED
@@ -19,10 +19,18 @@ import { join, basename } from 'node:path';
19
19
  import { homedir } from 'node:os';
20
20
  import * as fmt from './fmt.mjs';
21
21
  import { chalk } from './fmt.mjs';
22
+ import {
23
+ ensureContextModeIntegration,
24
+ getContextModeIntegrationSummary,
25
+ removeContextModeIntegration,
26
+ } from './integrations/context-mode.mjs';
22
27
 
23
28
  const execAsync = promisify(exec);
24
29
  const HOME = homedir();
25
- const MANIFEST_PATH = join(HOME, '.aw_registry', '.integration-manifest.json');
30
+
31
+ function manifestPath(home = HOME) {
32
+ return join(home, '.aw_registry', '.integration-manifest.json');
33
+ }
26
34
 
27
35
  function getErrorMessage(error) {
28
36
  return error instanceof Error ? error.message : String(error);
@@ -136,6 +144,16 @@ export const INTEGRATIONS = {
136
144
  requiresAuth: false,
137
145
  authNote: 'run /graphify . inside your IDE to build the graph whenever you need it',
138
146
  },
147
+
148
+ 'context-mode': {
149
+ type: 'context-mode',
150
+ label: 'Context Mode',
151
+ description: 'Local context-mode MCP server and hook-based context preservation',
152
+ autoInstall: false,
153
+ teams: [],
154
+ requiresAuth: false,
155
+ authNote: 'Run aw doctor to verify context-mode health.',
156
+ },
139
157
  };
140
158
 
141
159
  // ────────────────────────────────────────────────────────────────────────────────
@@ -148,25 +166,27 @@ export const BUNDLES = {};
148
166
  // MANIFEST MANAGEMENT
149
167
  // ────────────────────────────────────────────────────────────────────────────────
150
168
 
151
- export function readManifest() {
152
- if (!existsSync(MANIFEST_PATH)) {
169
+ export function readManifest(home = HOME) {
170
+ const filePath = manifestPath(home);
171
+ if (!existsSync(filePath)) {
153
172
  return { version: 1, installed: {} };
154
173
  }
155
174
 
156
175
  try {
157
- return JSON.parse(readFileSync(MANIFEST_PATH, 'utf8'));
176
+ return JSON.parse(readFileSync(filePath, 'utf8'));
158
177
  } catch {
159
178
  return { version: 1, installed: {} };
160
179
  }
161
180
  }
162
181
 
163
- export function writeManifest(manifest) {
164
- mkdirSync(join(HOME, '.aw_registry'), { recursive: true });
165
- writeFileSync(MANIFEST_PATH, JSON.stringify(manifest, null, 2) + '\n');
182
+ export function writeManifest(manifest, home = HOME) {
183
+ const filePath = manifestPath(home);
184
+ mkdirSync(join(filePath, '..'), { recursive: true });
185
+ writeFileSync(filePath, JSON.stringify(manifest, null, 2) + '\n');
166
186
  }
167
187
 
168
- export function isInstalled(key) {
169
- const manifest = readManifest();
188
+ export function isInstalled(key, options = {}) {
189
+ const manifest = readManifest(options.home);
170
190
  const entry = manifest.installed[key];
171
191
  if (!entry) return false;
172
192
  // Backward compat: pre-existing entries have no `status` field — treat as installed.
@@ -175,25 +195,35 @@ export function isInstalled(key) {
175
195
  return entry.status !== 'skipped';
176
196
  }
177
197
 
178
- function recordInstalled(key, type) {
179
- const manifest = readManifest();
198
+ function recordInstalled(key, type, options = {}) {
199
+ if (options.dryRun) return;
200
+ const manifest = readManifest(options.home);
180
201
  manifest.installed[key] = {
181
202
  type,
182
203
  status: 'installed',
183
204
  installedAt: new Date().toISOString(),
205
+ ...(options.extra || {}),
184
206
  };
185
- writeManifest(manifest);
207
+ writeManifest(manifest, options.home);
186
208
  }
187
209
 
188
- function recordSkipped(key, type, reason) {
189
- const manifest = readManifest();
210
+ function recordSkipped(key, type, reason, options = {}) {
211
+ if (options.dryRun) return;
212
+ const manifest = readManifest(options.home);
190
213
  manifest.installed[key] = {
191
214
  type,
192
215
  status: 'skipped',
193
216
  reason,
194
217
  installedAt: new Date().toISOString(),
195
218
  };
196
- writeManifest(manifest);
219
+ writeManifest(manifest, options.home);
220
+ }
221
+
222
+ function removeInstalled(key, options = {}) {
223
+ if (options.dryRun) return;
224
+ const manifest = readManifest(options.home);
225
+ delete manifest.installed[key];
226
+ writeManifest(manifest, options.home);
197
227
  }
198
228
 
199
229
  // ────────────────────────────────────────────────────────────────────────────────
@@ -419,7 +449,7 @@ async function resolveCliInvocation(cliCommand, pythonCmd, installer) {
419
449
  }
420
450
  }
421
451
 
422
- async function runPythonCli(integration, key, { silent = false } = {}) {
452
+ async function runPythonCli(integration, key, { silent = false, home = HOME } = {}) {
423
453
  const spinner = silent ? null : fmt.spinner();
424
454
 
425
455
  // 1. Detect Python ≥ minPython.
@@ -427,7 +457,7 @@ async function runPythonCli(integration, key, { silent = false } = {}) {
427
457
  const py = await detectPython(integration.minPython);
428
458
  if (!py) {
429
459
  if (!silent) spinner.stop(chalk.yellow('Python not found'));
430
- recordSkipped(key, 'python-cli', 'python-not-found');
460
+ recordSkipped(key, 'python-cli', 'python-not-found', { home });
431
461
  if (!silent) {
432
462
  fmt.logWarn(
433
463
  `${integration.label} skipped — Python ${integration.minPython.major}.${integration.minPython.minor}+ required.`,
@@ -559,7 +589,7 @@ async function runClaudePlugin(installCmd, { silent = false, marketplaceSource =
559
589
  // UNIVERSAL INSTALLER (OS-detected master script — covers all IDEs in one shot)
560
590
  // ────────────────────────────────────────────────────────────────────────────────
561
591
 
562
- async function runUniversalInstaller(integration, key, { silent = false } = {}) {
592
+ async function runUniversalInstaller(integration, key, { silent = false, home = HOME } = {}) {
563
593
  const spinner = silent ? null : fmt.spinner();
564
594
 
565
595
  if (!silent) spinner.start(`Installing ${integration.label} for all detected IDEs...`);
@@ -606,7 +636,7 @@ async function runUniversalInstaller(integration, key, { silent = false } = {})
606
636
  'Skipped',
607
637
  );
608
638
  }
609
- recordSkipped(key, 'universal-installer', 'wsl-not-found');
639
+ recordSkipped(key, 'universal-installer', 'wsl-not-found', { home });
610
640
  return false;
611
641
  }
612
642
  }
@@ -649,13 +679,60 @@ async function runUniversalInstaller(integration, key, { silent = false } = {})
649
679
  // INTEGRATION INSTALLER
650
680
  // ────────────────────────────────────────────────────────────────────────────────
651
681
 
652
- export async function installIntegration(key, { silent = false } = {}) {
682
+ function successResult(key, integration, extra = {}) {
683
+ return { success: true, key, integration, ...extra };
684
+ }
685
+
686
+ function failureResult(key, integration, error, extra = {}) {
687
+ return { success: false, key, integration, error, ...extra };
688
+ }
689
+
690
+ export function integrationSucceeded(result) {
691
+ return result === true || result?.success === true;
692
+ }
693
+
694
+ export async function installIntegration(key, options = {}) {
695
+ const { silent = false } = options;
696
+ const home = options.home || HOME;
697
+ const dryRun = options.dryRun === true;
653
698
  const integration = INTEGRATIONS[key];
654
699
  if (!integration) {
655
700
  throw new Error(`Unknown integration: ${key}`);
656
701
  }
657
702
 
658
- if (isInstalled(key)) {
703
+ if (integration.type === 'context-mode') {
704
+ const result = ensureContextModeIntegration(home, {
705
+ ...options,
706
+ home,
707
+ env: options.env || process.env,
708
+ dryRun,
709
+ });
710
+ if (dryRun) {
711
+ return successResult(key, integration, result);
712
+ }
713
+ if (result.binary.present && result.complete) {
714
+ recordInstalled(key, integration.type, {
715
+ home,
716
+ extra: {
717
+ binaryPath: result.binary.path,
718
+ version: result.binary.version,
719
+ },
720
+ });
721
+ return successResult(key, integration, result);
722
+ }
723
+ return failureResult(
724
+ key,
725
+ integration,
726
+ result.binary.present ? 'partial-config' : (result.warnings?.[0] || 'binary-missing'),
727
+ result,
728
+ );
729
+ }
730
+
731
+ if (dryRun) {
732
+ return successResult(key, integration, { plannedInstall: true });
733
+ }
734
+
735
+ if (isInstalled(key, { home })) {
659
736
  if (!silent) fmt.logWarn(`${integration.label} is already installed`);
660
737
  return false;
661
738
  }
@@ -664,7 +741,7 @@ export async function installIntegration(key, { silent = false } = {}) {
664
741
  if (integration.type === 'plugin') {
665
742
  // PLUGIN: Run claude plugin install
666
743
  await runClaudePlugin(integration.installCmd, { silent, marketplaceSource: integration.marketplaceSource ?? null });
667
- recordInstalled(key, 'plugin');
744
+ recordInstalled(key, 'plugin', { home });
668
745
 
669
746
  if (!silent) {
670
747
  fmt.logSuccess(`${integration.label} plugin installed`);
@@ -699,7 +776,7 @@ export async function installIntegration(key, { silent = false } = {}) {
699
776
  integration.mcpUrl
700
777
  );
701
778
 
702
- recordInstalled(key, 'remote-mcp');
779
+ recordInstalled(key, 'remote-mcp', { home });
703
780
 
704
781
  if (!silent) {
705
782
  fmt.logSuccess(`${integration.label} added to MCP servers`);
@@ -709,10 +786,10 @@ export async function installIntegration(key, { silent = false } = {}) {
709
786
  }
710
787
  } else if (integration.type === 'python-cli') {
711
788
  // PYTHON CLI: pip-install + run post-install + per-project hooks
712
- const ok = await runPythonCli(integration, key, { silent });
789
+ const ok = await runPythonCli(integration, key, { silent, home });
713
790
  if (!ok) return false; // skipped (e.g. Python missing) — manifest already recorded
714
791
 
715
- recordInstalled(key, 'python-cli');
792
+ recordInstalled(key, 'python-cli', { home });
716
793
 
717
794
  if (!silent) {
718
795
  fmt.logSuccess(`${integration.label} installed`);
@@ -722,9 +799,9 @@ export async function installIntegration(key, { silent = false } = {}) {
722
799
  }
723
800
  } else if (integration.type === 'universal-installer') {
724
801
  // UNIVERSAL INSTALLER: OS-detected master script, covers all IDEs in one shot
725
- const ok = await runUniversalInstaller(integration, key, { silent });
802
+ const ok = await runUniversalInstaller(integration, key, { silent, home });
726
803
  if (!ok) return false; // skipped (e.g. WSL not found) — manifest already recorded
727
- recordInstalled(key, 'universal-installer');
804
+ recordInstalled(key, 'universal-installer', { home });
728
805
 
729
806
  if (!silent) {
730
807
  fmt.logSuccess(`${integration.label} installed across all detected IDEs`);
@@ -748,13 +825,22 @@ export async function installIntegration(key, { silent = false } = {}) {
748
825
  // INTEGRATION REMOVER
749
826
  // ────────────────────────────────────────────────────────────────────────────────
750
827
 
751
- export async function removeIntegration(key, { silent = false } = {}) {
828
+ export async function removeIntegration(key, options = {}) {
829
+ const { silent = false } = options;
830
+ const home = options.home || HOME;
831
+ const dryRun = options.dryRun === true;
752
832
  const integration = INTEGRATIONS[key];
753
833
  if (!integration) {
754
834
  throw new Error(`Unknown integration: ${key}`);
755
835
  }
756
836
 
757
- if (!isInstalled(key)) {
837
+ if (integration.type === 'context-mode') {
838
+ const result = removeContextModeIntegration(home, { dryRun });
839
+ removeInstalled(key, { home, dryRun });
840
+ return successResult(key, integration, result);
841
+ }
842
+
843
+ if (!isInstalled(key, { home })) {
758
844
  if (!silent) fmt.logWarn(`${integration.label} is not installed`);
759
845
  return false;
760
846
  }
@@ -848,9 +934,7 @@ export async function removeIntegration(key, { silent = false } = {}) {
848
934
  }
849
935
 
850
936
  // Remove from manifest
851
- const manifest = readManifest();
852
- delete manifest.installed[key];
853
- writeManifest(manifest);
937
+ removeInstalled(key, { home });
854
938
 
855
939
  return true;
856
940
  } catch (e) {
@@ -865,8 +949,8 @@ export async function removeIntegration(key, { silent = false } = {}) {
865
949
  // HELPERS
866
950
  // ────────────────────────────────────────────────────────────────────────────────
867
951
 
868
- export function getInstalledList() {
869
- const manifest = readManifest();
952
+ export function getInstalledList(options = {}) {
953
+ const manifest = readManifest(options.home);
870
954
  // Only return entries actually installed — skipped entries (e.g. Python missing)
871
955
  // shouldn't show up as "installed" to the rest of the system.
872
956
  return Object.entries(manifest.installed)
@@ -874,6 +958,34 @@ export function getInstalledList() {
874
958
  .map(([key]) => key);
875
959
  }
876
960
 
961
+ export function getIntegrationSummary(key, options = {}) {
962
+ const integration = INTEGRATIONS[key];
963
+ if (!integration) return null;
964
+ if (integration.type === 'context-mode') {
965
+ return {
966
+ ...getContextModeIntegrationSummary(options.home || HOME, options),
967
+ label: integration.label,
968
+ description: integration.description,
969
+ type: integration.type,
970
+ };
971
+ }
972
+ const installed = isInstalled(key, options);
973
+ return {
974
+ name: key,
975
+ label: integration.label,
976
+ description: integration.description,
977
+ type: integration.type,
978
+ installed,
979
+ configured: installed,
980
+ state: installed ? 'installed' : 'available',
981
+ summary: installed ? `${integration.label} installed` : `${integration.label} available`,
982
+ };
983
+ }
984
+
985
+ export function getIntegrationSummaries(options = {}) {
986
+ return Object.keys(INTEGRATIONS).map(key => getIntegrationSummary(key, options));
987
+ }
988
+
877
989
  export function suggestForTeam(namespace) {
878
990
  if (!namespace) return [];
879
991
 
@@ -895,7 +1007,8 @@ export function suggestForTeam(namespace) {
895
1007
  // AUTO-INSTALL (called from init.mjs - installs suggested integrations)
896
1008
  // ────────────────────────────────────────────────────────────────────────────────
897
1009
 
898
- export async function autoInstallIntegrations(team, { silent = false, installer = installIntegration } = {}) {
1010
+ export async function autoInstallIntegrations(team, options = {}) {
1011
+ const { silent = false, installer = installIntegration } = options;
899
1012
  // Get suggested integrations for this team
900
1013
  const suggested = suggestForTeam(team);
901
1014
  if (!suggested || suggested.length === 0) {
@@ -903,7 +1016,7 @@ export async function autoInstallIntegrations(team, { silent = false, installer
903
1016
  }
904
1017
 
905
1018
  // Only install if they're not already installed
906
- const toInstall = suggested.filter((key) => !isInstalled(key));
1019
+ const toInstall = suggested.filter((key) => !isInstalled(key, options));
907
1020
  if (toInstall.length === 0) {
908
1021
  return [];
909
1022
  }
@@ -914,8 +1027,8 @@ export async function autoInstallIntegrations(team, { silent = false, installer
914
1027
 
915
1028
  const installed = [];
916
1029
  for (const key of toInstall) {
917
- const success = await installer(key, { silent });
918
- if (success) {
1030
+ const result = await installer(key, { ...options, silent });
1031
+ if (integrationSucceeded(result)) {
919
1032
  installed.push(INTEGRATIONS[key].label);
920
1033
  }
921
1034
  }
@@ -1011,8 +1124,8 @@ export async function promptAndInstall(team, { silent = false } = {}) {
1011
1124
  // Install all
1012
1125
  const installed = [];
1013
1126
  for (const key of toInstall) {
1014
- const success = await installIntegration(key, { silent: false });
1015
- if (success) {
1127
+ const result = await installIntegration(key, { silent: false });
1128
+ if (integrationSucceeded(result)) {
1016
1129
  installed.push(INTEGRATIONS[key].label);
1017
1130
  }
1018
1131
  }
@@ -0,0 +1,72 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const SELF_PATH = fileURLToPath(import.meta.url);
7
+
8
+ export function detectPackageManager(selfPath = SELF_PATH) {
9
+ const normalizedPath = String(selfPath || '').replace(/\\/g, '/');
10
+ if (normalizedPath.includes('/.volta/')) return 'volta';
11
+ if (normalizedPath.includes('/pnpm/')) return 'pnpm';
12
+ return 'npm';
13
+ }
14
+
15
+ export function buildGlobalPackageInstall(packageSpec, options = {}) {
16
+ const packageManager = options.packageManager || detectPackageManager(options.selfPath);
17
+ if (packageManager === 'volta') {
18
+ return { packageManager, command: 'volta', args: ['install', packageSpec] };
19
+ }
20
+ if (packageManager === 'pnpm') {
21
+ return { packageManager, command: 'pnpm', args: ['add', '-g', packageSpec] };
22
+ }
23
+ return { packageManager: 'npm', command: 'npm', args: ['i', '-g', packageSpec] };
24
+ }
25
+
26
+ export function formatCommand({ command, args = [] }) {
27
+ return [command, ...args].join(' ');
28
+ }
29
+
30
+ export function installGlobalPackage(packageSpec, options = {}) {
31
+ const plan = buildGlobalPackageInstall(packageSpec, options);
32
+ const result = spawnSync(plan.command, plan.args, {
33
+ cwd: options.cwd,
34
+ env: options.env || process.env,
35
+ encoding: 'utf8',
36
+ stdio: options.stdio || 'pipe',
37
+ timeout: options.timeout || 5 * 60 * 1000,
38
+ });
39
+
40
+ return {
41
+ ...plan,
42
+ ok: result.status === 0,
43
+ status: result.status,
44
+ stdout: result.stdout || '',
45
+ stderr: result.stderr || '',
46
+ error: result.error || null,
47
+ commandLine: formatCommand(plan),
48
+ };
49
+ }
50
+
51
+ export function getNpmPrefixBin(options = {}) {
52
+ const runner = options.runner || spawnSync;
53
+ const result = runner('npm', ['prefix', '-g'], {
54
+ env: options.env || process.env,
55
+ encoding: 'utf8',
56
+ stdio: 'pipe',
57
+ timeout: options.timeout || 10_000,
58
+ });
59
+ if (result.status !== 0) return null;
60
+ const prefix = String(result.stdout || '').trim();
61
+ if (!prefix) return null;
62
+ return join(prefix, process.platform === 'win32' ? '' : 'bin');
63
+ }
64
+
65
+ export function getAwPackageRoot(selfPath = SELF_PATH) {
66
+ if (selfPath.endsWith('package-manager.mjs')) return dirname(selfPath);
67
+ return dirname(selfPath);
68
+ }
69
+
70
+ export function executableExists(path) {
71
+ return Boolean(path && existsSync(path));
72
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ghl-ai/aw",
3
- "version": "0.1.58-beta.0",
3
+ "version": "0.1.59",
4
4
  "description": "Agentic Workspace CLI — pull, push & manage agents, skills and commands from the registry",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,6 +22,7 @@
22
22
  "link.mjs",
23
23
  "manifest.mjs",
24
24
  "mcp.mjs",
25
+ "package-manager.mjs",
25
26
  "paths.mjs",
26
27
  "plan.mjs",
27
28
  "registry.mjs",
@@ -54,9 +55,9 @@
54
55
  "license": "MIT",
55
56
  "scripts": {
56
57
  "test": "yarn test:vitest && yarn test:node",
57
- "test:vitest": "vitest run --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4 tests/integrations-graphify.test.mjs",
58
+ "test:vitest": "vitest run --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4 tests/integrations-graphify.test.mjs tests/integrations tests/package-manager.test.mjs",
58
59
  "test:node": "node tests/run-node-tests.mjs",
59
- "test:watch": "vitest --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4 tests/integrations-graphify.test.mjs",
60
+ "test:watch": "vitest --reporter=verbose tests/commands tests/mcp.test.mjs tests/telemetry.test.mjs tests/c4 tests/integrations-graphify.test.mjs tests/integrations tests/package-manager.test.mjs",
60
61
  "preuninstall": "node bin.js nuke 2>/dev/null || true"
61
62
  },
62
63
  "publishConfig": {
package/registry.mjs CHANGED
@@ -28,10 +28,33 @@ export function getAllFiles(dirPath) {
28
28
  return results;
29
29
  }
30
30
 
31
+ function findSkillRootDirs(skillsDir) {
32
+ const results = [];
33
+
34
+ function walk(dir, segments) {
35
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
36
+ if (!entry.isDirectory() || entry.name.startsWith('.')) continue;
37
+
38
+ const childDir = join(dir, entry.name);
39
+ const childSegments = [...segments, entry.name];
40
+ if (existsSync(join(childDir, 'SKILL.md'))) {
41
+ results.push({ skillPath: childDir, slug: childSegments.join('/') });
42
+ continue;
43
+ }
44
+
45
+ walk(childDir, childSegments);
46
+ }
47
+ }
48
+
49
+ walk(skillsDir, []);
50
+ return results;
51
+ }
52
+
31
53
  /**
32
54
  * Walk a registry directory tree and collect file-level entries.
33
55
  * Every file gets its own entry with its full registry path.
34
- * Skills are NOT atomic — each file inside a skill is registered individually.
56
+ * Skills are NOT atomic — each file inside a skill root is registered individually.
57
+ * A skill root is the nearest directory below skills/ that contains SKILL.md.
35
58
  */
36
59
  export function walkRegistryTree(baseDir, baseName) {
37
60
  const entries = [];
@@ -47,13 +70,9 @@ export function walkRegistryTree(baseDir, baseName) {
47
70
  const namespace = pathSegments.join('/');
48
71
 
49
72
  if (typeDir === 'skills') {
50
- // Skills are directories register each file inside individually
51
- for (const skillEntry of readdirSync(fullPath, { withFileTypes: true })) {
52
- if (skillEntry.name.startsWith('.')) continue;
53
- const skillPath = join(fullPath, skillEntry.name);
54
- if (!statSync(skillPath).isDirectory()) continue;
55
- const slug = skillEntry.name;
56
-
73
+ // Skills can be grouped in nested folders. The actual skill root is
74
+ // the first nested directory that contains SKILL.md.
75
+ for (const { skillPath, slug } of findSkillRootDirs(fullPath)) {
57
76
  for (const file of getAllFiles(skillPath)) {
58
77
  const relPath = relative(skillPath, file);
59
78
  const relNoExt = relPath.replace(/\.md$/, '');
package/update.mjs CHANGED
@@ -9,7 +9,7 @@
9
9
  // .volta/ → volta install pnpm/ → pnpm add -g
10
10
  // else → npm i -g
11
11
 
12
- import { readFileSync, mkdirSync, writeFileSync, statSync, rmSync, appendFileSync } from 'node:fs';
12
+ import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync, rmSync, appendFileSync } from 'node:fs';
13
13
  import { exec as execCb, execSync } from 'node:child_process';
14
14
  import { promisify } from 'node:util';
15
15
  import { join, dirname } from 'node:path';
@@ -39,15 +39,16 @@ const LOG_MAX_ENTRIES = 50;
39
39
 
40
40
  const SELF_PATH = fileURLToPath(import.meta.url);
41
41
 
42
- function detectPM() {
43
- if (SELF_PATH.includes('/.volta/')) return 'volta';
44
- if (SELF_PATH.includes('/pnpm/')) return 'pnpm';
42
+ function detectPM(selfPath = SELF_PATH) {
43
+ const normalizedPath = String(selfPath || '').replace(/\\/g, '/');
44
+ if (normalizedPath.includes('/.volta/')) return 'volta';
45
+ if (normalizedPath.includes('/pnpm/')) return 'pnpm';
45
46
  return 'npm';
46
47
  }
47
48
 
48
49
  const PM = detectPM();
49
50
 
50
- function getInstallCmd(version = 'latest') {
51
+ export function getInstallCmd(version = 'latest') {
51
52
  const spec = `${PKG_NAME}@${version}`;
52
53
  if (PM === 'volta') return `volta install ${spec}`;
53
54
  if (PM === 'pnpm') return `pnpm add -g ${spec}`;
@@ -91,23 +92,36 @@ function compareVersions(a, b) {
91
92
  // After install, re-resolve where the package actually landed and read
92
93
  // its package.json. Falls back to getLocalVersion() (our own bundle).
93
94
 
94
- function getInstalledVersion() {
95
+ export function getInstalledPackageRoot(options = {}) {
96
+ const packageManager = options.packageManager || PM;
97
+ const runner = options.execSync || execSync;
95
98
  try {
96
- if (PM === 'volta') {
99
+ if (packageManager === 'volta') {
97
100
  // Volta resolves via its own image; ask it directly
98
- const resolved = execSync('volta which aw', {
101
+ const resolved = runner('volta which aw', {
99
102
  encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10_000,
100
103
  }).trim();
101
- const pkgRoot = join(resolved, '..', '..');
102
- return JSON.parse(readFileSync(join(pkgRoot, 'package.json'), 'utf8')).version;
104
+ const imageRoot = join(resolved, '..', '..');
105
+ const packageRoot = join(imageRoot, 'lib', 'node_modules', PKG_NAME);
106
+ return existsSync(join(packageRoot, 'package.json')) ? packageRoot : imageRoot;
103
107
  }
104
108
 
105
109
  // npm / pnpm: read from the global prefix
106
- const prefix = execSync('npm prefix -g', {
110
+ const prefix = runner('npm prefix -g', {
107
111
  encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'], timeout: 10_000,
108
112
  }).trim();
109
- const pkgPath = join(prefix, 'lib', 'node_modules', PKG_NAME, 'package.json');
110
- return JSON.parse(readFileSync(pkgPath, 'utf8')).version;
113
+ if (packageManager === 'pnpm') {
114
+ return join(prefix, 'node_modules', PKG_NAME);
115
+ }
116
+ return join(prefix, 'lib', 'node_modules', PKG_NAME);
117
+ } catch {
118
+ return dirname(SELF_PATH);
119
+ }
120
+ }
121
+
122
+ function getInstalledVersion() {
123
+ try {
124
+ return JSON.parse(readFileSync(join(getInstalledPackageRoot(), 'package.json'), 'utf8')).version;
111
125
  } catch {
112
126
  return getLocalVersion();
113
127
  }
@@ -200,7 +214,8 @@ export function autoUpdate(result) {
200
214
  execSync(cmd, { stdio: 'pipe', timeout: 60_000 });
201
215
  const installed = getInstalledVersion();
202
216
  const ok = compareVersions(installed, result.current) > 0;
203
- const entry = { ts: new Date().toISOString(), pm: PM, cmd, from: result.current, to: installed, ok };
217
+ const packageRoot = getInstalledPackageRoot();
218
+ const entry = { ts: new Date().toISOString(), pm: PM, cmd, from: result.current, to: installed, packageRoot, ok };
204
219
  appendLog(entry);
205
220
  return { status: ok ? 'upgraded' : 'failed', ...entry };
206
221
  } catch (e) {
@@ -1,31 +0,0 @@
1
- import {
2
- ensureContextModeIntegration,
3
- getContextModeIntegrationSummary,
4
- removeContextModeIntegration,
5
- } from './context-mode.mjs';
6
-
7
- export const KNOWN_INTEGRATIONS = [
8
- {
9
- name: 'context-mode',
10
- description: 'Local context-mode MCP server and hook integration.',
11
- add: ensureContextModeIntegration,
12
- remove: removeContextModeIntegration,
13
- status: getContextModeIntegrationSummary,
14
- },
15
- ];
16
-
17
- export function resolveIntegration(name) {
18
- const normalized = String(name || '').trim().toLowerCase();
19
- const integration = KNOWN_INTEGRATIONS.find(item => item.name === normalized);
20
- if (integration) return integration;
21
-
22
- const known = KNOWN_INTEGRATIONS.map(item => item.name).join(', ');
23
- throw new Error(`Unknown integration: ${name || '<missing>'}. Known integrations: ${known}`);
24
- }
25
-
26
- export function listIntegrations(home, options = {}) {
27
- return KNOWN_INTEGRATIONS.map(integration => ({
28
- ...integration.status(home, options),
29
- description: integration.description,
30
- }));
31
- }