nexus-prime 7.3.0 → 7.3.1

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.
@@ -17,6 +17,7 @@ import { runPostinstallCleanup } from '../postinstall/cleanup.js';
17
17
  import { openBrowser, printHandoffBanner, withSpinner } from './interactive-setup.js';
18
18
  import { appendToManifest, detectArchitectureUpgrade, loadManifest, recordArchitectureGeneration, recordPath, recordRegistration, recordSetupMarker, INSTALL_ARCH_GENERATION, } from '../install/manifest.js';
19
19
  import { enumerateNgramArchives } from '../install/state-locator.js';
20
+ import { NEXUS_HOOK_COMMAND_MARKER, writeNexusClaudeCodeHooks } from '../install/claude-code-hooks.js';
20
21
  /** Compute sha256 of a file's content. Returns null if the file cannot be read. */
21
22
  export function computeFileHash(filePath) {
22
23
  try {
@@ -148,35 +149,8 @@ function _nexusMcpEntry(workspaceRoot) {
148
149
  */
149
150
  function _writeClaudeCodeHooks(workspaceRoot) {
150
151
  const settingsPath = join(workspaceRoot, '.claude', 'settings.json');
151
- let existing = {};
152
- if (existsSync(settingsPath)) {
153
- try {
154
- existing = JSON.parse(readFileSync(settingsPath, 'utf8'));
155
- }
156
- catch { /* start fresh if unreadable */ }
157
- }
158
- const NEXUS_CMD_MARKER = 'nexus-prime hook';
159
- const nexusHooks = {
160
- UserPromptSubmit: [{ matcher: '', hooks: [{ type: 'command', command: 'nexus-prime hook bootstrap' }] }],
161
- PreToolUse: [{ matcher: 'Edit|Write|MultiEdit', hooks: [{ type: 'command', command: 'nexus-prime hook mindkit' }] }],
162
- PostToolUse: [{ matcher: 'Edit|Write|MultiEdit|Bash', hooks: [{ type: 'command', command: 'nexus-prime hook memory' }] }],
163
- Stop: [{ matcher: '', hooks: [{ type: 'command', command: 'nexus-prime hook session-dna' }] }],
164
- };
165
- const existingHooks = (existing.hooks && typeof existing.hooks === 'object' && !Array.isArray(existing.hooks))
166
- ? existing.hooks
167
- : {};
168
- for (const [event, entries] of Object.entries(nexusHooks)) {
169
- const current = Array.isArray(existingHooks[event]) ? existingHooks[event] : [];
170
- // Strip stale nexus-prime hook entries then append fresh ones
171
- const filtered = current.filter((e) => typeof e === 'object' && e !== null &&
172
- !(Array.isArray(e.hooks) &&
173
- e.hooks.some((h) => typeof h === 'object' && h !== null && typeof h.command === 'string' &&
174
- h.command.includes(NEXUS_CMD_MARKER))));
175
- existingHooks[event] = [...filtered, ...entries];
176
- }
177
- existing.hooks = existingHooks;
178
- mkdirSync(dirname(settingsPath), { recursive: true });
179
- writeFileSync(settingsPath, JSON.stringify(existing, null, 2), 'utf8');
152
+ // Single source of truth: hook spec + dedup logic live in install/claude-code-hooks.ts.
153
+ writeNexusClaudeCodeHooks(settingsPath);
180
154
  const isHome = workspaceRoot === homedir();
181
155
  appendToManifest((m) => {
182
156
  const withPath = recordPath(m, { path: settingsPath, kind: 'file', scope: 'state' });
@@ -184,7 +158,7 @@ function _writeClaudeCodeHooks(workspaceRoot) {
184
158
  target: isHome ? 'claude-home' : 'claude-workspace',
185
159
  filePath: settingsPath,
186
160
  jsonPath: ['hooks'],
187
- entryMarker: NEXUS_CMD_MARKER,
161
+ entryMarker: NEXUS_HOOK_COMMAND_MARKER,
188
162
  });
189
163
  });
190
164
  }
package/dist/cli.js CHANGED
@@ -38,67 +38,24 @@ import { runUninstall } from './cli/uninstall.js';
38
38
  import { runCleanup } from './cli/cleanup.js';
39
39
  import { runDoctorStorage } from './cli/doctor-storage.js';
40
40
  const tokenEngine = new TokenSupremacyEngine();
41
+ import { getNexusHookSpec, writeNexusClaudeCodeHooks, } from './install/claude-code-hooks.js';
41
42
  /**
42
43
  * Write (or merge) nexus-prime Claude Code hook entries into ~/.claude/settings.json.
43
- * Idempotent: removes any pre-existing `nexus-prime hook *` entries before adding fresh ones.
44
+ * Idempotent delegates to the shared writer in install/claude-code-hooks.ts
45
+ * so cli.ts and install-wizard.ts cannot drift on the hook spec or dedup logic.
44
46
  */
45
47
  function writeClaudeCodeHooks(settingsPath, dryRun) {
46
- let settings = {};
47
- if (existsSync(settingsPath)) {
48
- try {
49
- settings = JSON.parse(readFileSync(settingsPath, 'utf8'));
50
- }
51
- catch {
52
- settings = {};
53
- }
54
- }
55
- const hooks = (settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks))
56
- ? { ...settings.hooks }
57
- : {};
58
- const nexusHooks = {
59
- UserPromptSubmit: [
60
- { hooks: [{ type: 'command', command: 'nexus-prime hook bootstrap', timeout: 30 }] },
61
- ],
62
- PreToolUse: [
63
- {
64
- matcher: 'Edit|Write|MultiEdit',
65
- hooks: [{ type: 'command', command: 'nexus-prime hook mindkit', timeout: 10 }],
66
- },
67
- ],
68
- PostToolUse: [
69
- {
70
- matcher: 'Edit|Write|MultiEdit|Bash',
71
- hooks: [{ type: 'command', command: 'nexus-prime hook memory', timeout: 10 }],
72
- },
73
- ],
74
- Stop: [
75
- { hooks: [{ type: 'command', command: 'nexus-prime hook session-dna', timeout: 60 }] },
76
- ],
77
- };
78
- for (const [event, entries] of Object.entries(nexusHooks)) {
79
- const existing = Array.isArray(hooks[event]) ? hooks[event] : [];
80
- // Remove stale nexus-prime hook entries before re-adding
81
- const filtered = existing.filter((entry) => {
82
- if (!entry || typeof entry !== 'object')
83
- return true;
84
- const hooksList = entry.hooks;
85
- if (!Array.isArray(hooksList))
86
- return true;
87
- return !hooksList.some((h) => h && typeof h.command === 'string' &&
88
- h.command.startsWith('nexus-prime hook'));
89
- });
90
- hooks[event] = [...filtered, ...entries];
91
- }
92
- settings.hooks = hooks;
48
+ const result = writeNexusClaudeCodeHooks(settingsPath, { dryRun });
93
49
  if (dryRun) {
94
50
  console.log('Hooks preview (would write to ~/.claude/settings.json):');
95
- console.log(JSON.stringify(nexusHooks, null, 2));
51
+ console.log(JSON.stringify(getNexusHookSpec(), null, 2));
96
52
  return;
97
53
  }
98
- mkdirSync(dirname(settingsPath), { recursive: true });
99
- writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
100
- console.log(` Hooks: ${settingsPath}`);
101
- Object.keys(nexusHooks).forEach(ev => console.log(` ${ev} → nexus-prime hook ${ev.toLowerCase().replace('tooluse', '').trim() || ev}`));
54
+ console.log(` Hooks: ${settingsPath}${result.changed ? '' : ' (no change)'}`);
55
+ result.events.forEach((ev) => {
56
+ const tail = ev.toLowerCase().replace('tooluse', '').trim() || ev;
57
+ console.log(` ${ev} → nexus-prime hook ${tail}`);
58
+ });
102
59
  }
103
60
  const program = new Command();
104
61
  let nexus = null;
@@ -21,6 +21,7 @@ import { handleAuthoringRoutes } from './routes/authoring.js';
21
21
  import { handleArchitectsRoutes } from './routes/architects.js';
22
22
  import { handleWorkforceRoutes } from './routes/workforce.js';
23
23
  import { handleGraphRoutes } from './routes/graph.js';
24
+ import { DASHBOARD_API_VERSION } from './contract.js';
24
25
  import { SseBroker } from './stream/sse-broker.js';
25
26
  import { nexusEventBus } from '../engines/event-bus.js';
26
27
  const __filename = fileURLToPath(import.meta.url);
@@ -30,7 +31,6 @@ const DEFAULT_PORT = parseInt(process.env.NEXUS_DASHBOARD_PORT || '3377', 10);
30
31
  const MAX_PORT_SCAN = 24;
31
32
  const DASHBOARD_PROBE_TIMEOUT_MS = 3000;
32
33
  const DASHBOARD_PROBE_ATTEMPTS = 2;
33
- const DASHBOARD_API_VERSION = '4';
34
34
  const DASHBOARD_SCHEMA_VERSION = 1;
35
35
  const DASHBOARD_PRETTY_JSON = process.env.NEXUS_DASHBOARD_PRETTY_JSON === '1';
36
36
  const CORE_CAPABILITIES = {
@@ -0,0 +1,33 @@
1
+ /** Marker that identifies a hook entry as Nexus-owned. */
2
+ export declare const NEXUS_HOOK_COMMAND_MARKER = "nexus-prime hook";
3
+ interface HookCommand {
4
+ type: 'command';
5
+ command: string;
6
+ timeout?: number;
7
+ }
8
+ interface HookEntry {
9
+ matcher?: string;
10
+ hooks: HookCommand[];
11
+ }
12
+ /**
13
+ * Canonical Nexus hook spec. Edit here, not in callers.
14
+ * Timeouts protect Claude Code from a wedged Nexus process — leave them set.
15
+ */
16
+ export declare function getNexusHookSpec(): Record<string, HookEntry[]>;
17
+ export interface NexusHookWriteResult {
18
+ /** True when the file would be written (or was written). */
19
+ changed: boolean;
20
+ /** Hook event names that ended up with Nexus entries. */
21
+ events: string[];
22
+ /** Number of stale Nexus entries removed before re-adding. */
23
+ staleRemoved: number;
24
+ }
25
+ /**
26
+ * Idempotent writer for the Nexus hook block. Returns metadata so callers can
27
+ * report status (`installed | unchanged | dry-run`) without re-implementing
28
+ * the merge logic.
29
+ */
30
+ export declare function writeNexusClaudeCodeHooks(settingsPath: string, options?: {
31
+ dryRun?: boolean;
32
+ }): NexusHookWriteResult;
33
+ export {};
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Single source of truth for the Claude Code hook entries Nexus Prime owns.
3
+ * Both `nexus-prime install` (cli.ts) and the install-wizard call into here
4
+ * so we cannot drift on hook spec, dedup logic, or marker detection.
5
+ *
6
+ * Foreign user hooks are preserved — only entries whose command starts with
7
+ * `nexus-prime hook` are stripped before re-adding.
8
+ */
9
+ import * as fs from 'fs';
10
+ import * as path from 'path';
11
+ /** Marker that identifies a hook entry as Nexus-owned. */
12
+ export const NEXUS_HOOK_COMMAND_MARKER = 'nexus-prime hook';
13
+ /**
14
+ * Canonical Nexus hook spec. Edit here, not in callers.
15
+ * Timeouts protect Claude Code from a wedged Nexus process — leave them set.
16
+ */
17
+ export function getNexusHookSpec() {
18
+ return {
19
+ UserPromptSubmit: [
20
+ { hooks: [{ type: 'command', command: 'nexus-prime hook bootstrap', timeout: 30 }] },
21
+ ],
22
+ PreToolUse: [
23
+ {
24
+ matcher: 'Edit|Write|MultiEdit',
25
+ hooks: [{ type: 'command', command: 'nexus-prime hook mindkit', timeout: 10 }],
26
+ },
27
+ ],
28
+ PostToolUse: [
29
+ {
30
+ matcher: 'Edit|Write|MultiEdit|Bash',
31
+ hooks: [{ type: 'command', command: 'nexus-prime hook memory', timeout: 10 }],
32
+ },
33
+ ],
34
+ Stop: [
35
+ { hooks: [{ type: 'command', command: 'nexus-prime hook session-dna', timeout: 60 }] },
36
+ ],
37
+ };
38
+ }
39
+ function isNexusHookEntry(entry) {
40
+ if (!entry || typeof entry !== 'object')
41
+ return false;
42
+ const hooksList = entry.hooks;
43
+ if (!Array.isArray(hooksList))
44
+ return false;
45
+ return hooksList.some((h) => h && typeof h === 'object'
46
+ && typeof h.command === 'string'
47
+ && h.command.trimStart().startsWith(NEXUS_HOOK_COMMAND_MARKER));
48
+ }
49
+ /**
50
+ * Idempotent writer for the Nexus hook block. Returns metadata so callers can
51
+ * report status (`installed | unchanged | dry-run`) without re-implementing
52
+ * the merge logic.
53
+ */
54
+ export function writeNexusClaudeCodeHooks(settingsPath, options = {}) {
55
+ const dryRun = options.dryRun ?? false;
56
+ let settings = {};
57
+ if (fs.existsSync(settingsPath)) {
58
+ try {
59
+ const raw = fs.readFileSync(settingsPath, 'utf8');
60
+ const parsed = JSON.parse(raw);
61
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
62
+ settings = parsed;
63
+ }
64
+ }
65
+ catch {
66
+ settings = {};
67
+ }
68
+ }
69
+ const incomingHooks = getNexusHookSpec();
70
+ const events = Object.keys(incomingHooks);
71
+ const existing = (settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks))
72
+ ? { ...settings.hooks }
73
+ : {};
74
+ let staleRemoved = 0;
75
+ let changed = !fs.existsSync(settingsPath);
76
+ for (const event of events) {
77
+ const before = Array.isArray(existing[event]) ? existing[event] : [];
78
+ const filtered = before.filter((entry) => {
79
+ const isNexus = isNexusHookEntry(entry);
80
+ if (isNexus)
81
+ staleRemoved += 1;
82
+ return !isNexus;
83
+ });
84
+ const next = [...filtered, ...incomingHooks[event]];
85
+ if (filtered.length !== before.length || next.length !== before.length)
86
+ changed = true;
87
+ existing[event] = next;
88
+ }
89
+ settings.hooks = existing;
90
+ if (dryRun || !changed) {
91
+ return { changed, events, staleRemoved };
92
+ }
93
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
94
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf8');
95
+ return { changed, events, staleRemoved };
96
+ }
@@ -88,6 +88,8 @@ export function enumerateStatePaths(stateDir = getNexusStateDir()) {
88
88
  { path: path.join(stateDir, 'bootstrap-receipts'), scope: 'state', optional: true },
89
89
  { path: path.join(stateDir, 'setup-marker-v6.json'), scope: 'state', optional: true },
90
90
  { path: path.join(stateDir, 'install-manifest.json'), scope: 'state', optional: true },
91
+ { path: path.join(stateDir, 'license.key'), scope: 'state', optional: true },
92
+ { path: path.join(stateDir, 'trial.json'), scope: 'state', optional: true },
91
93
  { path: path.join(stateDir, 'nexus-daemon.lock.json'), scope: 'runtime', optional: true },
92
94
  ];
93
95
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-prime",
3
- "version": "7.3.0",
3
+ "version": "7.3.1",
4
4
  "description": "Local-first MCP control plane for coding agents with bootstrap-orchestrate execution, memory fabric, token budgeting, and worktree-backed swarms",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",