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.
- package/dist/architects/config.js +6 -1
- package/dist/cli/doctor-storage.d.ts +19 -0
- package/dist/cli/doctor-storage.js +72 -3
- package/dist/cli/install-wizard.d.ts +15 -0
- package/dist/cli/install-wizard.js +46 -2
- package/dist/dashboard/routes/health.js +20 -0
- package/dist/engines/ngram-index.d.ts +37 -3
- package/dist/engines/ngram-index.js +209 -23
- package/dist/engines/orchestrator/types.d.ts +5 -0
- package/dist/engines/orchestrator/types.js +64 -2
- package/dist/engines/orchestrator.js +29 -4
- package/dist/engines/runtime-hygiene.d.ts +4 -0
- package/dist/engines/runtime-hygiene.js +45 -1
- package/dist/install/manifest.d.ts +29 -0
- package/dist/install/manifest.js +44 -1
- package/dist/install/state-locator.d.ts +11 -0
- package/dist/install/state-locator.js +28 -0
- package/dist/licensing/enforcement.js +13 -1
- package/dist/licensing/index.d.ts +1 -1
- package/dist/licensing/index.js +1 -1
- package/dist/licensing/license-manager.d.ts +12 -0
- package/dist/licensing/license-manager.js +91 -4
- package/dist/licensing/types.d.ts +5 -1
- package/dist/licensing/upgrade-prompts.d.ts +10 -0
- package/dist/licensing/upgrade-prompts.js +23 -0
- package/dist/licensing/web-auth.d.ts +4 -1
- package/dist/licensing/web-auth.js +34 -9
- package/dist/synapse/config.js +15 -11
- package/package.json +2 -2
package/dist/install/manifest.js
CHANGED
|
@@ -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 = {
|
|
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';
|
package/dist/licensing/index.js
CHANGED
|
@@ -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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
|
|
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(
|
|
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 };
|
package/dist/synapse/config.js
CHANGED
|
@@ -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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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,
|
|
20
|
-
maxOpsPerTeam: parseInteger(process.env.SYNAPSE_MAX_OPERATIVES_PER_TEAM, 5),
|
|
21
|
-
defaultBudgetUsd: parseFloatNumber(process.env.SYNAPSE_DEFAULT_BUDGET_USD,
|
|
22
|
-
sortieIntervalMs: parseInteger(process.env.SYNAPSE_SORTIE_INTERVAL_MS,
|
|
23
|
-
compactionBudgetTokens: parseInteger(process.env.SYNAPSE_COMPACTION_BUDGET_TOKENS,
|
|
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,
|
|
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,
|
|
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.
|
|
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",
|