nexus-prime 7.2.0 → 7.3.0

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.
@@ -10,6 +10,18 @@ import * as fs from 'fs';
10
10
  import * as path from 'path';
11
11
  import { getInstallManifestPath } from './state-locator.js';
12
12
  export const MANIFEST_VERSION = 1;
13
+ /**
14
+ * Architecture generation marker for the installed runtime. Bump this when
15
+ * the install layout changes (new dashboard, new MCP registrations, new
16
+ * hook surfaces, new state directories) so the install-wizard can detect
17
+ * older shapes and run migrations rather than silently leaving stale
18
+ * registrations alongside the new ones.
19
+ *
20
+ * Manifests written before this field was introduced have `undefined` here
21
+ * and are treated as the inferred legacy generation `v6` (matching the
22
+ * setup-marker-v6.json filename Nexus has used since the v6 release).
23
+ */
24
+ export const INSTALL_ARCH_GENERATION = 'v7';
13
25
  function emptyManifest() {
14
26
  const now = Date.now();
15
27
  return {
@@ -19,6 +31,7 @@ function emptyManifest() {
19
31
  paths: [],
20
32
  registrations: [],
21
33
  setupMarkers: [],
34
+ architectureGeneration: INSTALL_ARCH_GENERATION,
22
35
  };
23
36
  }
24
37
  export function loadManifest(filePath = getInstallManifestPath()) {
@@ -37,12 +50,37 @@ export function loadManifest(filePath = getInstallManifestPath()) {
37
50
  paths: Array.isArray(m.paths) ? m.paths.filter(isPathEntry) : [],
38
51
  registrations: Array.isArray(m.registrations) ? m.registrations.filter(isRegistrationEntry) : [],
39
52
  setupMarkers: Array.isArray(m.setupMarkers) ? m.setupMarkers.filter((v) => typeof v === 'string') : [],
53
+ architectureGeneration: typeof m.architectureGeneration === 'string'
54
+ ? m.architectureGeneration
55
+ : undefined,
40
56
  };
41
57
  }
42
58
  catch {
43
59
  return emptyManifest();
44
60
  }
45
61
  }
62
+ /** Stamp the manifest with the current architecture generation. Idempotent. */
63
+ export function recordArchitectureGeneration(manifest, generation = INSTALL_ARCH_GENERATION) {
64
+ if (manifest.architectureGeneration === generation)
65
+ return manifest;
66
+ return { ...manifest, architectureGeneration: generation };
67
+ }
68
+ /**
69
+ * Inspect the manifest's recorded generation against the current build.
70
+ * `previousGeneration` is undefined for fresh installs, 'v6' for legacy
71
+ * manifests written before the field existed. `requiresMigration` is true
72
+ * when the manifest is older than INSTALL_ARCH_GENERATION.
73
+ */
74
+ export function detectArchitectureUpgrade(manifest) {
75
+ const previousGeneration = manifest.architectureGeneration;
76
+ const requiresMigration = previousGeneration !== INSTALL_ARCH_GENERATION
77
+ && (manifest.paths.length > 0 || manifest.registrations.length > 0 || manifest.setupMarkers.length > 0);
78
+ return {
79
+ previousGeneration,
80
+ currentGeneration: INSTALL_ARCH_GENERATION,
81
+ requiresMigration,
82
+ };
83
+ }
46
84
  function isPathEntry(value) {
47
85
  if (!value || typeof value !== 'object')
48
86
  return false;
@@ -58,7 +96,12 @@ function isRegistrationEntry(value) {
58
96
  export function saveManifest(manifest, filePath = getInstallManifestPath()) {
59
97
  const dir = path.dirname(filePath);
60
98
  fs.mkdirSync(dir, { recursive: true });
61
- const next = { ...manifest, version: MANIFEST_VERSION, updatedAt: Date.now() };
99
+ const next = {
100
+ ...manifest,
101
+ version: MANIFEST_VERSION,
102
+ updatedAt: Date.now(),
103
+ architectureGeneration: manifest.architectureGeneration ?? INSTALL_ARCH_GENERATION,
104
+ };
62
105
  fs.writeFileSync(filePath, JSON.stringify(next, null, 2), 'utf8');
63
106
  }
64
107
  export function recordPath(manifest, entry) {
@@ -23,6 +23,17 @@ export declare function getRuntimeTmpRoots(): string[];
23
23
  * directory names that engines create lazily, so missing entries are normal.
24
24
  */
25
25
  export declare function enumerateStatePaths(stateDir?: string): NexusPathEntry[];
26
+ /**
27
+ * Enumerate ngram .oversize.* archive snapshots produced by the rotate path.
28
+ * Cleanup keeps at most one of these (the newest) so a runaway DB can't grow
29
+ * a multi-GB backlog of stale carcasses.
30
+ */
31
+ export interface NgramArchiveEntry {
32
+ path: string;
33
+ bytes: number;
34
+ modifiedAt: number;
35
+ }
36
+ export declare function enumerateNgramArchives(stateDir?: string): NgramArchiveEntry[];
26
37
  /** Known IDE registration targets discovered in a workspace + home dir. */
27
38
  export interface RegistrationTarget {
28
39
  id: 'aider' | 'antigravity' | 'claude-desktop' | 'claude-home' | 'claude-workspace' | 'cline' | 'codex-json' | 'codex-toml' | 'continue' | 'cursor-home' | 'cursor-workspace' | 'opencode' | 'openclaw' | 'vscode-workspace' | 'windsurf';
@@ -12,6 +12,7 @@
12
12
  * worktree — git worktrees used by phantom execution
13
13
  * db — sqlite databases (memory, synapse, architects)
14
14
  */
15
+ import * as fs from 'fs';
15
16
  import * as os from 'os';
16
17
  import * as path from 'path';
17
18
  import { resolveNexusStateDir } from '../engines/runtime-registry.js';
@@ -71,6 +72,9 @@ export function enumerateStatePaths(stateDir = getNexusStateDir()) {
71
72
  { path: path.join(stateDir, 'memory.db'), scope: 'db', optional: true },
72
73
  { path: path.join(stateDir, 'memory.db-wal'), scope: 'db', optional: true },
73
74
  { path: path.join(stateDir, 'memory.db-shm'), scope: 'db', optional: true },
75
+ { path: path.join(stateDir, 'ngram-index.db'), scope: 'db', optional: true },
76
+ { path: path.join(stateDir, 'ngram-index.db-wal'), scope: 'db', optional: true },
77
+ { path: path.join(stateDir, 'ngram-index.db-shm'), scope: 'db', optional: true },
74
78
  { path: path.join(stateDir, '.synapse'), scope: 'db', optional: true },
75
79
  { path: path.join(stateDir, '.architects'), scope: 'db', optional: true },
76
80
  { path: path.join(stateDir, 'runs'), scope: 'runtime', optional: true },
@@ -87,6 +91,30 @@ export function enumerateStatePaths(stateDir = getNexusStateDir()) {
87
91
  { path: path.join(stateDir, 'nexus-daemon.lock.json'), scope: 'runtime', optional: true },
88
92
  ];
89
93
  }
94
+ export function enumerateNgramArchives(stateDir = getNexusStateDir()) {
95
+ const out = [];
96
+ try {
97
+ if (!fs.existsSync(stateDir))
98
+ return out;
99
+ for (const entry of fs.readdirSync(stateDir)) {
100
+ if (!entry.startsWith('ngram-index.db.oversize.'))
101
+ continue;
102
+ const full = path.join(stateDir, entry);
103
+ try {
104
+ const st = fs.statSync(full);
105
+ if (st.isFile())
106
+ out.push({ path: full, bytes: st.size, modifiedAt: st.mtimeMs });
107
+ }
108
+ catch {
109
+ // unreadable — skip
110
+ }
111
+ }
112
+ }
113
+ catch {
114
+ // unreadable state dir — return what we have
115
+ }
116
+ return out;
117
+ }
90
118
  export function enumerateRegistrationTargets(workspaceRoot, home = os.homedir()) {
91
119
  const claudeDesktopConfig = process.platform === 'darwin'
92
120
  ? path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json')
@@ -11,7 +11,7 @@
11
11
  // ─────────────────────────────────────────────────────────────────────────────
12
12
  import { getSharedLicenseManager } from './license-manager.js';
13
13
  import { getToolTier, isToolAllowed } from './tool-tiers.js';
14
- import { toolGateMessage, capWarningMessage, capExceededMessage, noLicenseMessage } from './upgrade-prompts.js';
14
+ import { toolGateMessage, capWarningMessage, capExceededMessage, noLicenseMessage, trialActiveMessage, trialExpiredMessage, } from './upgrade-prompts.js';
15
15
  function getMode() {
16
16
  const val = (process.env.NEXUS_ENFORCEMENT_MODE ?? 'soft').toLowerCase();
17
17
  if (val === 'audit' || val === 'hard')
@@ -46,6 +46,18 @@ export const LicenseEnforcementMiddleware = {
46
46
  // In all modes: let the tool execute (soft gate during ramp)
47
47
  return;
48
48
  }
49
+ if (status.degradedReason === 'trial-expired') {
50
+ if (mode !== 'audit') {
51
+ ctx.meta.licenseUpgradeHint = trialExpiredMessage();
52
+ }
53
+ // Fall through to tier check — caps revert to free tier on trial expiry.
54
+ }
55
+ if (status.trial && mode !== 'audit') {
56
+ // Soft footer so users know the runtime is being kept alive by the
57
+ // auto-issued local trial. Doesn't gate execution — paid tiers behave
58
+ // exactly like a real license while the trial is active.
59
+ ctx.meta.licenseUpgradeHint = trialActiveMessage(status.expiresAt);
60
+ }
49
61
  // ── Tool tier check ───────────────────────────────────────────────
50
62
  if (!isToolAllowed(toolName, status.tier)) {
51
63
  const requiredTier = getToolTier(toolName);
@@ -3,7 +3,7 @@ export { getCapsForTier, FREE_CAPS, PRO_CAPS, TEAM_CAPS, ENTERPRISE_CAPS } from
3
3
  export { getToolTier, isToolAllowed, tierLabel, TIER_RANK } from './tool-tiers.js';
4
4
  export { LicenseEnforcementMiddleware } from './enforcement.js';
5
5
  export { snapshotPCU, formatPCUStatus, type PCUSnapshot } from './pcu-meter.js';
6
- export { capWarningMessage, capExceededMessage, toolGateMessage, noLicenseMessage, } from './upgrade-prompts.js';
6
+ export { capWarningMessage, capExceededMessage, toolGateMessage, noLicenseMessage, trialActiveMessage, trialExpiredMessage, } from './upgrade-prompts.js';
7
7
  export { syncLicense, requestUpgrade } from './license-sync.js';
8
8
  export { loginFromCLI, readAuthToken, readAuthInfo, isLoggedIn, logout } from './web-auth.js';
9
9
  export type { PlanTier, PlanCaps, LicenseClaims, LicenseStatus, CapType, CapCheckResult, SkillProfile, DarwinScope, } from './types.js';
@@ -3,6 +3,6 @@ export { getCapsForTier, FREE_CAPS, PRO_CAPS, TEAM_CAPS, ENTERPRISE_CAPS } from
3
3
  export { getToolTier, isToolAllowed, tierLabel, TIER_RANK } from './tool-tiers.js';
4
4
  export { LicenseEnforcementMiddleware } from './enforcement.js';
5
5
  export { snapshotPCU, formatPCUStatus } from './pcu-meter.js';
6
- export { capWarningMessage, capExceededMessage, toolGateMessage, noLicenseMessage, } from './upgrade-prompts.js';
6
+ export { capWarningMessage, capExceededMessage, toolGateMessage, noLicenseMessage, trialActiveMessage, trialExpiredMessage, } from './upgrade-prompts.js';
7
7
  export { syncLicense, requestUpgrade } from './license-sync.js';
8
8
  export { loginFromCLI, readAuthToken, readAuthInfo, isLoggedIn, logout } from './web-auth.js';
@@ -1,6 +1,8 @@
1
1
  import type { CapCheckResult, CapType, LicenseStatus } from './types.js';
2
2
  export declare class LicenseManager {
3
3
  private readonly keyPath;
4
+ private readonly trialPath;
5
+ private readonly stateDir;
4
6
  private status;
5
7
  constructor(stateDir?: string);
6
8
  getStatus(): LicenseStatus;
@@ -12,6 +14,16 @@ export declare class LicenseManager {
12
14
  */
13
15
  checkCap(type: CapType, current: number): CapCheckResult | null;
14
16
  private loadAndValidate;
17
+ /**
18
+ * Auto-issued local trial. Every install gets 30 days of Pro caps the first
19
+ * time it boots without an activated key — this keeps the runtime usable
20
+ * while signup/activation flows run, and avoids "catalog degraded" surfaces
21
+ * dropping users into a broken-looking shell when the website is briefly
22
+ * unreachable. Set NEXUS_DISABLE_TRIAL=1 to opt out.
23
+ */
24
+ private resolveTrialStatus;
25
+ /** Returns the path of the trial marker (for tests + status surfaces). */
26
+ getTrialMarkerPath(): string;
15
27
  private parseAndVerify;
16
28
  }
17
29
  export declare function getSharedLicenseManager(): LicenseManager;
@@ -30,6 +30,45 @@ const NEXUS_PUBLIC_KEY_B64 = 'MCowBQYDK2VwAyEAbrBiMBqzIyatM/Q/plA0Dn2Y/TAu2UVmWG
30
30
  const UPGRADE_URL = 'https://nexus-prime.cfd/pricing';
31
31
  // Warn at this fraction of the cap (e.g. 0.8 = 80%)
32
32
  const WARN_THRESHOLD = 0.8;
33
+ // Trial configuration. Every install gets a 30-day full-tier trial the first
34
+ // time LicenseManager loads with no activated key — Synapse, Architects, all
35
+ // paid tools available — so users can actually evaluate the product. The
36
+ // trial marker (stateDir/trial.json) records issue + expiry, so subsequent
37
+ // loads stay deterministic offline. NEXUS_DISABLE_TRIAL=1 opts out (tests).
38
+ const TRIAL_DURATION_MS = 30 * 24 * 60 * 60 * 1000;
39
+ const TRIAL_TIER = 'enterprise';
40
+ const TRIAL_MARKER_FILENAME = 'trial.json';
41
+ function readTrialMarker(markerPath) {
42
+ try {
43
+ if (!fs.existsSync(markerPath))
44
+ return null;
45
+ const raw = fs.readFileSync(markerPath, 'utf8');
46
+ const parsed = JSON.parse(raw);
47
+ if (typeof parsed.issuedAt !== 'number' || typeof parsed.expiresAt !== 'number')
48
+ return null;
49
+ const tier = parsed.tier === 'pro' || parsed.tier === 'team' || parsed.tier === 'enterprise'
50
+ ? parsed.tier
51
+ : TRIAL_TIER;
52
+ return {
53
+ issuedAt: parsed.issuedAt,
54
+ expiresAt: parsed.expiresAt,
55
+ tier,
56
+ orgId: typeof parsed.orgId === 'string' ? parsed.orgId : null,
57
+ };
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ function writeTrialMarker(markerPath, marker) {
64
+ try {
65
+ fs.mkdirSync(path.dirname(markerPath), { recursive: true });
66
+ fs.writeFileSync(markerPath, JSON.stringify(marker, null, 2), 'utf8');
67
+ }
68
+ catch {
69
+ // best-effort: trial still applies in memory even if we can't persist it.
70
+ }
71
+ }
33
72
  // ── Helpers ──────────────────────────────────────────────────────────────────
34
73
  function base64urlDecode(s) {
35
74
  return Buffer.from(s.replace(/-/g, '+').replace(/_/g, '/'), 'base64');
@@ -62,6 +101,16 @@ function buildFreeStatus(degradedReason) {
62
101
  degradedReason,
63
102
  };
64
103
  }
104
+ function buildTrialStatus(marker) {
105
+ return {
106
+ valid: true,
107
+ tier: marker.tier,
108
+ caps: getCapsForTier(marker.tier),
109
+ expiresAt: marker.expiresAt,
110
+ orgId: marker.orgId,
111
+ trial: true,
112
+ };
113
+ }
65
114
  function validateClaims(claims) {
66
115
  if (!claims || typeof claims !== 'object')
67
116
  return false;
@@ -75,10 +124,14 @@ function validateClaims(claims) {
75
124
  // ── LicenseManager ───────────────────────────────────────────────────────────
76
125
  export class LicenseManager {
77
126
  keyPath;
127
+ trialPath;
128
+ stateDir;
78
129
  status;
79
130
  constructor(stateDir) {
80
131
  const dir = stateDir ?? resolveNexusStateDir();
132
+ this.stateDir = dir;
81
133
  this.keyPath = path.join(dir, 'license.key');
134
+ this.trialPath = path.join(dir, TRIAL_MARKER_FILENAME);
82
135
  this.status = this.loadAndValidate();
83
136
  }
84
137
  getStatus() {
@@ -112,9 +165,12 @@ export class LicenseManager {
112
165
  catch {
113
166
  // Best-effort
114
167
  }
115
- this.status = buildFreeStatus();
116
- if (prevTier !== 'free') {
117
- void emitLicenseEvent('license.tierChanged', { from: prevTier, to: 'free', reason: 'deactivate' });
168
+ // Fall back to whichever local status applies — trial if still in window,
169
+ // free otherwise. Avoids dropping users into a broken-looking runtime
170
+ // simply because they removed an explicit license key mid-trial.
171
+ this.status = this.resolveTrialStatus();
172
+ if (prevTier !== this.status.tier) {
173
+ void emitLicenseEvent('license.tierChanged', { from: prevTier, to: this.status.tier, reason: 'deactivate' });
118
174
  }
119
175
  }
120
176
  /**
@@ -153,7 +209,7 @@ export class LicenseManager {
153
209
  loadAndValidate() {
154
210
  try {
155
211
  if (!fs.existsSync(this.keyPath)) {
156
- return buildFreeStatus('not-activated');
212
+ return this.resolveTrialStatus();
157
213
  }
158
214
  const raw = fs.readFileSync(this.keyPath, 'utf8').trim();
159
215
  return this.parseAndVerify(raw);
@@ -162,6 +218,37 @@ export class LicenseManager {
162
218
  return buildFreeStatus('malformed');
163
219
  }
164
220
  }
221
+ /**
222
+ * Auto-issued local trial. Every install gets 30 days of Pro caps the first
223
+ * time it boots without an activated key — this keeps the runtime usable
224
+ * while signup/activation flows run, and avoids "catalog degraded" surfaces
225
+ * dropping users into a broken-looking shell when the website is briefly
226
+ * unreachable. Set NEXUS_DISABLE_TRIAL=1 to opt out.
227
+ */
228
+ resolveTrialStatus() {
229
+ if (process.env.NEXUS_DISABLE_TRIAL === '1') {
230
+ return buildFreeStatus('not-activated');
231
+ }
232
+ const now = Date.now();
233
+ let marker = readTrialMarker(this.trialPath);
234
+ if (!marker) {
235
+ marker = {
236
+ issuedAt: now,
237
+ expiresAt: now + TRIAL_DURATION_MS,
238
+ tier: TRIAL_TIER,
239
+ orgId: null,
240
+ };
241
+ writeTrialMarker(this.trialPath, marker);
242
+ }
243
+ if (marker.expiresAt <= now) {
244
+ return buildFreeStatus('trial-expired');
245
+ }
246
+ return buildTrialStatus(marker);
247
+ }
248
+ /** Returns the path of the trial marker (for tests + status surfaces). */
249
+ getTrialMarkerPath() {
250
+ return this.trialPath;
251
+ }
165
252
  parseAndVerify(token) {
166
253
  const decoded = decodeJwtPayload(token);
167
254
  if (!decoded)
@@ -25,7 +25,11 @@ export interface LicenseStatus {
25
25
  caps: PlanCaps;
26
26
  expiresAt: number | null;
27
27
  orgId: string | null;
28
- degradedReason?: 'expired' | 'signature-invalid' | 'malformed' | 'not-activated';
28
+ /** True when the active status is the auto-issued local trial. */
29
+ trial?: boolean;
30
+ /** Reason a license is degraded; absent when status is valid. Trials report
31
+ * `trial-expired` after the auto-issued window has closed. */
32
+ degradedReason?: 'expired' | 'signature-invalid' | 'malformed' | 'not-activated' | 'trial-expired';
29
33
  }
30
34
  export type CapType = 'memory_entries' | 'projects' | 'operatives';
31
35
  export interface CapCheckResult {
@@ -15,3 +15,13 @@ export declare function toolGateMessage(toolName: string, requiredTier: PlanTier
15
15
  * No license activated — prompt to sign up.
16
16
  */
17
17
  export declare function noLicenseMessage(): string;
18
+ /**
19
+ * Trial is active. Soft hint that surfaces alongside paid tools so users
20
+ * know the auto-issued window is what's keeping their runtime alive.
21
+ */
22
+ export declare function trialActiveMessage(expiresAt: number | null): string;
23
+ /**
24
+ * Trial has expired — runtime drops to free caps. Tell the user clearly so
25
+ * the gap between trial-expired and a real license is obvious.
26
+ */
27
+ export declare function trialExpiredMessage(): string;
@@ -60,3 +60,26 @@ export function noLicenseMessage() {
60
60
  ` Then: nexus-prime license activate <your-token>`,
61
61
  ].join('\n');
62
62
  }
63
+ /**
64
+ * Trial is active. Soft hint that surfaces alongside paid tools so users
65
+ * know the auto-issued window is what's keeping their runtime alive.
66
+ */
67
+ export function trialActiveMessage(expiresAt) {
68
+ const daysLeft = expiresAt ? Math.max(0, Math.ceil((expiresAt - Date.now()) / (24 * 60 * 60 * 1000))) : 0;
69
+ return [
70
+ `[Nexus Prime] Trial active (${daysLeft} day${daysLeft === 1 ? '' : 's'} remaining).`,
71
+ ` Activate a license: nexus-prime license activate <token>`,
72
+ ` Sign up: ${SIGNUP_URL}`,
73
+ ].join('\n');
74
+ }
75
+ /**
76
+ * Trial has expired — runtime drops to free caps. Tell the user clearly so
77
+ * the gap between trial-expired and a real license is obvious.
78
+ */
79
+ export function trialExpiredMessage() {
80
+ return [
81
+ `[Nexus Prime] Trial expired — running on free caps.`,
82
+ ` Activate: nexus-prime license activate <token>`,
83
+ ` Pricing: ${UPGRADE_URL}`,
84
+ ].join('\n');
85
+ }
@@ -5,7 +5,10 @@ interface AuthTokens {
5
5
  updated_at: string;
6
6
  }
7
7
  /**
8
- * Login to nexus-prime.cfd and store auth tokens locally.
8
+ * Login to nexus-prime.cfd and store auth tokens locally. Errors are
9
+ * normalised so signup/signin failures don't surface raw HTTP codes —
10
+ * users get something actionable, plus a reminder that the local 30-day
11
+ * trial keeps the runtime working while they sort out account state.
9
12
  */
10
13
  export declare function loginFromCLI(email: string, password: string): Promise<{
11
14
  email: string;
@@ -10,22 +10,47 @@ import os from 'os';
10
10
  const AUTH_FILE = path.join(os.homedir(), '.nexus-prime', 'auth.json');
11
11
  const API_BASE = process.env.NEXUS_WEB_API_URL ?? 'https://nexus-prime.cfd';
12
12
  /**
13
- * Login to nexus-prime.cfd and store auth tokens locally.
13
+ * Login to nexus-prime.cfd and store auth tokens locally. Errors are
14
+ * normalised so signup/signin failures don't surface raw HTTP codes —
15
+ * users get something actionable, plus a reminder that the local 30-day
16
+ * trial keeps the runtime working while they sort out account state.
14
17
  */
15
18
  export async function loginFromCLI(email, password) {
16
- const res = await fetch(`${API_BASE}/api/auth/login`, {
17
- method: 'POST',
18
- headers: { 'Content-Type': 'application/json' },
19
- body: JSON.stringify({ email, password }),
20
- signal: AbortSignal.timeout(15_000),
21
- });
19
+ let res;
20
+ try {
21
+ res = await fetch(`${API_BASE}/api/auth/login`, {
22
+ method: 'POST',
23
+ headers: { 'Content-Type': 'application/json' },
24
+ body: JSON.stringify({ email, password }),
25
+ signal: AbortSignal.timeout(15_000),
26
+ });
27
+ }
28
+ catch (err) {
29
+ const reason = err instanceof Error ? err.message : String(err);
30
+ throw new Error(`Could not reach ${API_BASE}: ${reason}. `
31
+ + `Your 30-day local trial remains active — run \`nexus-prime status\` to verify, `
32
+ + `then retry login when the network is back.`);
33
+ }
22
34
  if (!res.ok) {
23
35
  const body = await res.json().catch(() => ({}));
24
- throw new Error(body.error ?? `Login failed (HTTP ${res.status})`);
36
+ const detail = body.error;
37
+ if (res.status === 401 || res.status === 403) {
38
+ throw new Error(detail
39
+ ? `Sign-in rejected: ${detail}`
40
+ : `Sign-in rejected: bad email or password. Reset at ${API_BASE}/account/reset.`);
41
+ }
42
+ if (res.status === 404) {
43
+ throw new Error(`No account for ${email}. Sign up at ${API_BASE}/signup, then retry. `
44
+ + `(Your 30-day local trial keeps the runtime active in the meantime.)`);
45
+ }
46
+ if (res.status >= 500) {
47
+ throw new Error(`Server error (${res.status}) at ${API_BASE}. Your 30-day local trial keeps the runtime active — try again shortly.`);
48
+ }
49
+ throw new Error(detail ?? `Login failed (HTTP ${res.status})`);
25
50
  }
26
51
  const data = await res.json();
27
52
  if (!data.session?.access_token) {
28
- throw new Error('Invalid response: no access token returned');
53
+ throw new Error(`Sign-in succeeded but ${API_BASE} returned no access token. Contact support.`);
29
54
  }
30
55
  writeAuthToken(data.session.access_token, data.session.refresh_token, email);
31
56
  return { email, plan: data.plan };
@@ -11,22 +11,26 @@ function parseFloatNumber(value, fallback) {
11
11
  const parsed = Number.parseFloat(value ?? '');
12
12
  return Number.isFinite(parsed) ? parsed : fallback;
13
13
  }
14
- // Opt-in by default: Synapse creates no DB and registers no persistent state
15
- // unless the operator explicitly enables it via `SYNAPSE_ENABLED=1`. This keeps
16
- // `npm install nexus-prime` idempotent from a disk-usage perspective and makes
17
- // uninstall truly reversible for users who never touched the workforce pillar.
14
+ function clampInteger(value, min, max) {
15
+ if (!Number.isFinite(value))
16
+ return min;
17
+ return Math.max(min, Math.min(max, Math.floor(value)));
18
+ }
19
+ // Investor-ready default: Synapse is available out of the box, but with small
20
+ // safe limits so it does not spawn an unbounded workforce or create runaway disk
21
+ // pressure. Operators can still disable it with SYNAPSE_ENABLED=0.
18
22
  export const SynapseConfig = {
19
- enabled: parseBoolean(process.env.SYNAPSE_ENABLED, false),
20
- maxOpsPerTeam: parseInteger(process.env.SYNAPSE_MAX_OPERATIVES_PER_TEAM, 5),
21
- defaultBudgetUsd: parseFloatNumber(process.env.SYNAPSE_DEFAULT_BUDGET_USD, 50),
22
- sortieIntervalMs: parseInteger(process.env.SYNAPSE_SORTIE_INTERVAL_MS, 30_000),
23
- compactionBudgetTokens: parseInteger(process.env.SYNAPSE_COMPACTION_BUDGET_TOKENS, 100_000),
23
+ enabled: parseBoolean(process.env.SYNAPSE_ENABLED, true),
24
+ maxOpsPerTeam: clampInteger(parseInteger(process.env.SYNAPSE_MAX_OPERATIVES_PER_TEAM, 3), 1, 5),
25
+ defaultBudgetUsd: parseFloatNumber(process.env.SYNAPSE_DEFAULT_BUDGET_USD, 10),
26
+ sortieIntervalMs: parseInteger(process.env.SYNAPSE_SORTIE_INTERVAL_MS, 60_000),
27
+ compactionBudgetTokens: parseInteger(process.env.SYNAPSE_COMPACTION_BUDGET_TOKENS, 25_000),
24
28
  echoEnabled: parseBoolean(process.env.SYNAPSE_ECHO_ENABLED, true),
25
29
  echoMinSimilarity: parseFloatNumber(process.env.SYNAPSE_ECHO_MIN_SIMILARITY, 0.7),
26
30
  ledgerEnabled: parseBoolean(process.env.SYNAPSE_LEDGER_ENABLED, true),
27
- ledgerCommitIntervalMs: parseInteger(process.env.SYNAPSE_LEDGER_COMMIT_INTERVAL_MS, 60_000),
31
+ ledgerCommitIntervalMs: parseInteger(process.env.SYNAPSE_LEDGER_COMMIT_INTERVAL_MS, 120_000),
28
32
  watchdogEnabled: parseBoolean(process.env.SYNAPSE_WATCHDOG_ENABLED, true),
29
- watchdogPatrolIntervalMs: parseInteger(process.env.SYNAPSE_WATCHDOG_PATROL_INTERVAL_MS, 120_000),
33
+ watchdogPatrolIntervalMs: parseInteger(process.env.SYNAPSE_WATCHDOG_PATROL_INTERVAL_MS, 180_000),
30
34
  watchdogStallMs: parseInteger(process.env.SYNAPSE_WATCHDOG_STALL_MS, 300_000),
31
35
  watchdogZombieMs: parseInteger(process.env.SYNAPSE_WATCHDOG_ZOMBIE_MS, 900_000),
32
36
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nexus-prime",
3
- "version": "7.2.0",
3
+ "version": "7.3.0",
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",
@@ -49,7 +49,7 @@
49
49
  "test:public": "tsx test/public-surface.test.ts",
50
50
  "test:synapse": "tsx src/synapse/__tests__/run.ts",
51
51
  "test:architects": "tsx src/architects/__tests__/run.ts",
52
- "test": "npm run build && tsx test/basic.test.ts && tsx test/memory.test.ts && tsx test/memory-regressions.test.ts && tsx test/memory-bridge.test.ts && tsx test/automation-runtime.test.ts && tsx test/session-dna-search.test.ts && tsx test/channel-gateway.test.ts && tsx test/semantic-ranking.test.ts && tsx test/storage-maintenance.test.ts && tsx test/security-shield.test.ts && tsx test/compaction-sentinel.test.ts && tsx test/context-compressor.test.ts && tsx test/embedder.test.ts && tsx test/skill-learner.test.ts && tsx test/skill-distribution.test.ts && tsx test/darwin-integration.test.ts && tsx test/github-bridge.test.ts && tsx test/telemetry-remote.test.ts && tsx test/orchestrator-engine.test.ts && tsx test/phase9.test.ts && tsx test/work-ledger.test.ts && tsx src/verify-token-scoring.ts && tsx test/phantom.test.ts && tsx test/rag-collections.test.ts && tsx test/dashboard.test.ts && tsx test/docs.test.ts && tsx test/competitive-landscape.test.ts && tsx test/runtime-upgrade-path.test.ts && tsx test/runtime-setup.test.ts && tsx test/control-plane-integration.test.ts && tsx test/runtime-timeout.test.ts && tsx test/mcp-dashboard-contract.test.ts && tsx test/dashboard-surfaces.test.ts && tsx test/startup-and-runtime-regressions.test.ts && tsx test/mcp-readiness-truth.test.ts && tsx test/mcp-stdio-session.test.ts && tsx test/kernel-context.test.ts && tsx test/kernel-execution.test.ts && tsx test/kernel-runtime.test.ts && tsx test/adapter-boundary.test.ts && tsx test/mcp-deprecation.test.ts && tsx test/dashboard-mutations.test.ts && tsx test/hooks-adapter.test.ts && tsx test/admin-adapter.test.ts && tsx test/pkg-subexport.test.ts && tsx test/dashboard-sse-only.test.ts && tsx test/license-sync-offline.test.ts && tsx test/mcp-killlist-v2.test.ts && tsx test/uninstall.test.ts && tsx test/uninstall-lifecycle.test.ts && tsx test/unregister-configs.test.ts && tsx test/cleanup-storage.test.ts && tsx test/dashboard-memory-truthfulness.test.ts && npm run test:synapse && npm run test:architects && npm run test:public",
52
+ "test": "npm run build && tsx test/basic.test.ts && tsx test/memory.test.ts && tsx test/memory-regressions.test.ts && tsx test/memory-bridge.test.ts && tsx test/automation-runtime.test.ts && tsx test/session-dna-search.test.ts && tsx test/channel-gateway.test.ts && tsx test/semantic-ranking.test.ts && tsx test/storage-maintenance.test.ts && tsx test/ngram-index.test.ts && tsx test/security-shield.test.ts && tsx test/compaction-sentinel.test.ts && tsx test/context-compressor.test.ts && tsx test/embedder.test.ts && tsx test/skill-learner.test.ts && tsx test/skill-distribution.test.ts && tsx test/darwin-integration.test.ts && tsx test/github-bridge.test.ts && tsx test/telemetry-remote.test.ts && tsx test/orchestrator-engine.test.ts && tsx test/phase9.test.ts && tsx test/work-ledger.test.ts && tsx src/verify-token-scoring.ts && tsx test/phantom.test.ts && tsx test/rag-collections.test.ts && tsx test/dashboard.test.ts && tsx test/docs.test.ts && tsx test/competitive-landscape.test.ts && tsx test/runtime-upgrade-path.test.ts && tsx test/runtime-setup.test.ts && tsx test/control-plane-integration.test.ts && tsx test/runtime-timeout.test.ts && tsx test/mcp-dashboard-contract.test.ts && tsx test/dashboard-surfaces.test.ts && tsx test/startup-and-runtime-regressions.test.ts && tsx test/mcp-readiness-truth.test.ts && tsx test/mcp-stdio-session.test.ts && tsx test/kernel-context.test.ts && tsx test/kernel-execution.test.ts && tsx test/kernel-runtime.test.ts && tsx test/adapter-boundary.test.ts && tsx test/mcp-deprecation.test.ts && tsx test/dashboard-mutations.test.ts && tsx test/hooks-adapter.test.ts && tsx test/admin-adapter.test.ts && tsx test/pkg-subexport.test.ts && tsx test/dashboard-sse-only.test.ts && tsx test/license-sync-offline.test.ts && tsx test/mcp-killlist-v2.test.ts && tsx test/uninstall.test.ts && tsx test/uninstall-lifecycle.test.ts && tsx test/install-arch-upgrade.test.ts && tsx test/unregister-configs.test.ts && tsx test/cleanup-storage.test.ts && tsx test/dashboard-memory-truthfulness.test.ts && npm run test:synapse && npm run test:architects && npm run test:public",
53
53
  "lint": "eslint src --ext .ts",
54
54
  "audit:prod": "npm audit --omit=dev",
55
55
  "smoke:release": "tsx scripts/release-smoke.ts",