@ornexus/neocortex 4.0.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.

Potentially problematic release.


This version of @ornexus/neocortex might be problematic. Click here for more details.

Files changed (121) hide show
  1. package/LICENSE +56 -0
  2. package/README.md +32 -0
  3. package/install.js +486 -0
  4. package/install.ps1 +1790 -0
  5. package/install.sh +1587 -0
  6. package/package.json +104 -0
  7. package/packages/client/dist/adapters/adapter-registry.d.ts +61 -0
  8. package/packages/client/dist/adapters/adapter-registry.js +106 -0
  9. package/packages/client/dist/adapters/antigravity-adapter.d.ts +18 -0
  10. package/packages/client/dist/adapters/antigravity-adapter.js +77 -0
  11. package/packages/client/dist/adapters/claude-code-adapter.d.ts +19 -0
  12. package/packages/client/dist/adapters/claude-code-adapter.js +79 -0
  13. package/packages/client/dist/adapters/codex-adapter.d.ts +19 -0
  14. package/packages/client/dist/adapters/codex-adapter.js +80 -0
  15. package/packages/client/dist/adapters/cursor-adapter.d.ts +19 -0
  16. package/packages/client/dist/adapters/cursor-adapter.js +115 -0
  17. package/packages/client/dist/adapters/gemini-adapter.d.ts +18 -0
  18. package/packages/client/dist/adapters/gemini-adapter.js +71 -0
  19. package/packages/client/dist/adapters/index.d.ts +19 -0
  20. package/packages/client/dist/adapters/index.js +21 -0
  21. package/packages/client/dist/adapters/platform-detector.d.ts +46 -0
  22. package/packages/client/dist/adapters/platform-detector.js +106 -0
  23. package/packages/client/dist/adapters/target-adapter.d.ts +70 -0
  24. package/packages/client/dist/adapters/target-adapter.js +12 -0
  25. package/packages/client/dist/adapters/vscode-adapter.d.ts +19 -0
  26. package/packages/client/dist/adapters/vscode-adapter.js +72 -0
  27. package/packages/client/dist/agent/refresh-stubs.d.ts +65 -0
  28. package/packages/client/dist/agent/refresh-stubs.js +234 -0
  29. package/packages/client/dist/agent/update-agent-yaml.d.ts +26 -0
  30. package/packages/client/dist/agent/update-agent-yaml.js +102 -0
  31. package/packages/client/dist/agent/update-description.d.ts +45 -0
  32. package/packages/client/dist/agent/update-description.js +251 -0
  33. package/packages/client/dist/cache/crypto-utils.d.ts +30 -0
  34. package/packages/client/dist/cache/crypto-utils.js +76 -0
  35. package/packages/client/dist/cache/encrypted-cache.d.ts +30 -0
  36. package/packages/client/dist/cache/encrypted-cache.js +94 -0
  37. package/packages/client/dist/cache/in-memory-asset-cache.d.ts +59 -0
  38. package/packages/client/dist/cache/in-memory-asset-cache.js +70 -0
  39. package/packages/client/dist/cache/index.d.ts +13 -0
  40. package/packages/client/dist/cache/index.js +13 -0
  41. package/packages/client/dist/cli.d.ts +14 -0
  42. package/packages/client/dist/cli.js +194 -0
  43. package/packages/client/dist/commands/activate.d.ts +55 -0
  44. package/packages/client/dist/commands/activate.js +390 -0
  45. package/packages/client/dist/commands/cache-status.d.ts +39 -0
  46. package/packages/client/dist/commands/cache-status.js +112 -0
  47. package/packages/client/dist/commands/invoke.d.ts +70 -0
  48. package/packages/client/dist/commands/invoke.js +490 -0
  49. package/packages/client/dist/config/resolver-selection.d.ts +40 -0
  50. package/packages/client/dist/config/resolver-selection.js +278 -0
  51. package/packages/client/dist/config/secure-config.d.ts +78 -0
  52. package/packages/client/dist/config/secure-config.js +269 -0
  53. package/packages/client/dist/constants.d.ts +25 -0
  54. package/packages/client/dist/constants.js +25 -0
  55. package/packages/client/dist/context/context-collector.d.ts +28 -0
  56. package/packages/client/dist/context/context-collector.js +222 -0
  57. package/packages/client/dist/context/context-sanitizer.d.ts +28 -0
  58. package/packages/client/dist/context/context-sanitizer.js +145 -0
  59. package/packages/client/dist/index.d.ts +55 -0
  60. package/packages/client/dist/index.js +38 -0
  61. package/packages/client/dist/license/index.d.ts +5 -0
  62. package/packages/client/dist/license/index.js +5 -0
  63. package/packages/client/dist/license/license-client.d.ts +79 -0
  64. package/packages/client/dist/license/license-client.js +257 -0
  65. package/packages/client/dist/machine/fingerprint.d.ts +34 -0
  66. package/packages/client/dist/machine/fingerprint.js +160 -0
  67. package/packages/client/dist/machine/index.d.ts +5 -0
  68. package/packages/client/dist/machine/index.js +5 -0
  69. package/packages/client/dist/resilience/circuit-breaker.d.ts +70 -0
  70. package/packages/client/dist/resilience/circuit-breaker.js +170 -0
  71. package/packages/client/dist/resilience/degradation-manager.d.ts +67 -0
  72. package/packages/client/dist/resilience/degradation-manager.js +164 -0
  73. package/packages/client/dist/resilience/freshness-indicator.d.ts +59 -0
  74. package/packages/client/dist/resilience/freshness-indicator.js +100 -0
  75. package/packages/client/dist/resilience/index.d.ts +8 -0
  76. package/packages/client/dist/resilience/index.js +8 -0
  77. package/packages/client/dist/resilience/recovery-detector.d.ts +59 -0
  78. package/packages/client/dist/resilience/recovery-detector.js +74 -0
  79. package/packages/client/dist/resolvers/asset-resolver.d.ts +79 -0
  80. package/packages/client/dist/resolvers/asset-resolver.js +13 -0
  81. package/packages/client/dist/resolvers/local-resolver.d.ts +26 -0
  82. package/packages/client/dist/resolvers/local-resolver.js +218 -0
  83. package/packages/client/dist/resolvers/remote-resolver.d.ts +91 -0
  84. package/packages/client/dist/resolvers/remote-resolver.js +282 -0
  85. package/packages/client/dist/telemetry/index.d.ts +5 -0
  86. package/packages/client/dist/telemetry/index.js +5 -0
  87. package/packages/client/dist/telemetry/offline-queue.d.ts +57 -0
  88. package/packages/client/dist/telemetry/offline-queue.js +131 -0
  89. package/packages/client/dist/tier/index.d.ts +5 -0
  90. package/packages/client/dist/tier/index.js +5 -0
  91. package/packages/client/dist/tier/tier-aware-client.d.ts +97 -0
  92. package/packages/client/dist/tier/tier-aware-client.js +260 -0
  93. package/packages/client/dist/types/index.d.ts +140 -0
  94. package/packages/client/dist/types/index.js +38 -0
  95. package/postinstall.js +272 -0
  96. package/targets-stubs/antigravity/README.md +36 -0
  97. package/targets-stubs/antigravity/gemini.md +22 -0
  98. package/targets-stubs/antigravity/install-antigravity.sh +44 -0
  99. package/targets-stubs/antigravity/mcp-config.json +9 -0
  100. package/targets-stubs/antigravity/skill/SKILL.md +67 -0
  101. package/targets-stubs/claude-code/README.md +20 -0
  102. package/targets-stubs/claude-code/neocortex.agent.yaml +24 -0
  103. package/targets-stubs/claude-code/neocortex.md +125 -0
  104. package/targets-stubs/codex/README.md +32 -0
  105. package/targets-stubs/codex/agents.md +61 -0
  106. package/targets-stubs/codex/config-mcp.toml +6 -0
  107. package/targets-stubs/codex/install-codex.sh +61 -0
  108. package/targets-stubs/cursor/README.md +33 -0
  109. package/targets-stubs/cursor/agent.md +94 -0
  110. package/targets-stubs/cursor/install-cursor.sh +35 -0
  111. package/targets-stubs/cursor/mcp.json +11 -0
  112. package/targets-stubs/gemini-cli/README.md +34 -0
  113. package/targets-stubs/gemini-cli/agent.md +101 -0
  114. package/targets-stubs/gemini-cli/gemini.md +16 -0
  115. package/targets-stubs/gemini-cli/install-gemini.sh +56 -0
  116. package/targets-stubs/gemini-cli/settings-mcp.json +11 -0
  117. package/targets-stubs/vscode/README.md +34 -0
  118. package/targets-stubs/vscode/agent.md +102 -0
  119. package/targets-stubs/vscode/copilot-instructions.md +16 -0
  120. package/targets-stubs/vscode/install-vscode.sh +42 -0
  121. package/targets-stubs/vscode/mcp.json +13 -0
@@ -0,0 +1,257 @@
1
+ /**
2
+ * @license FSL-1.1
3
+ * Copyright (c) 2026 OrNexus AI
4
+ *
5
+ * This file is part of Neocortex CLI, licensed under the
6
+ * Functional Source License, Version 1.1 (FSL-1.1).
7
+ *
8
+ * Change Date: February 20, 2029
9
+ * Change License: MIT
10
+ *
11
+ * See the LICENSE file in the project root for full license text.
12
+ */
13
+ /**
14
+ * @neocortex/client - License Client
15
+ *
16
+ * Manages license activation, JWT token caching, and proactive refresh.
17
+ * Integrates with the IP Protection Server's license endpoints and
18
+ * uses EncryptedCache for persistent token storage.
19
+ *
20
+ * Story 31.01: REFRESH_THRESHOLD_S fixed from 3600 to 300 (5 min)
21
+ * Story 31.02: Refresh token support (store, use, rotate)
22
+ * Story 31.03: Refresh token persistence in EncryptedCache
23
+ *
24
+ * NEVER throws exceptions - returns null on failure (graceful degradation).
25
+ */
26
+ import { decodeJwt } from 'jose';
27
+ import { NoOpCache } from '../types/index.js';
28
+ import { getMachineFingerprint } from '../machine/fingerprint.js';
29
+ // ── Constants ────────────────────────────────────────────────────────────
30
+ const CACHE_KEY = 'neocortex:jwt:token';
31
+ const REFRESH_CACHE_KEY = 'neocortex:jwt:refresh_token';
32
+ const REFRESH_THRESHOLD_S = 300; // 5 minutes before expiry
33
+ const REFRESH_TOKEN_TTL_MS = 7 * 24 * 60 * 60 * 1000; // 7 days (matches server REFRESH_TOKEN_TTL_DAYS)
34
+ const DEFAULT_CLIENT_VERSION = '0.1.0';
35
+ // ── LicenseClient ────────────────────────────────────────────────────────
36
+ export class LicenseClient {
37
+ serverUrl;
38
+ licenseKey;
39
+ cache;
40
+ clientVersion;
41
+ machineId;
42
+ token = null;
43
+ refreshToken = null;
44
+ constructor(options) {
45
+ this.serverUrl = options.serverUrl.replace(/\/+$/, '');
46
+ this.licenseKey = options.licenseKey;
47
+ this.cache = options.cacheProvider ?? new NoOpCache();
48
+ this.clientVersion = options.clientVersion ?? DEFAULT_CLIENT_VERSION;
49
+ this.machineId = getMachineFingerprint();
50
+ }
51
+ /**
52
+ * Get a valid JWT token. Checks memory, cache, refresh token, then activation.
53
+ * NEVER throws - returns null on failure.
54
+ *
55
+ * Flow:
56
+ * 1. in-memory JWT valid? -> return
57
+ * 2. in-memory JWT needs-refresh? -> refresh(refresh_token) -> return
58
+ * 3. cached JWT valid? -> return
59
+ * 4. cached JWT needs-refresh? -> refresh(refresh_token) -> return
60
+ * 5. cached refresh_token exists? -> refresh(refresh_token) -> return (Story 31.02)
61
+ * 6. activate() -> return
62
+ */
63
+ async getToken() {
64
+ try {
65
+ // 1. Check in-memory token
66
+ if (this.token) {
67
+ const validity = this.checkTokenValidity(this.token);
68
+ if (validity === 'valid')
69
+ return this.token;
70
+ if (validity === 'needs-refresh') {
71
+ const refreshed = await this.refresh();
72
+ return refreshed?.token ?? this.token; // fallback to current if refresh fails
73
+ }
74
+ // expired or invalid - clear memory
75
+ this.token = null;
76
+ }
77
+ // 2. Check JWT cache
78
+ const cached = await this.loadFromCache();
79
+ if (cached) {
80
+ this.token = cached;
81
+ const validity = this.checkTokenValidity(cached);
82
+ if (validity === 'valid')
83
+ return cached;
84
+ if (validity === 'needs-refresh') {
85
+ const refreshed = await this.refresh();
86
+ return refreshed?.token ?? cached; // fallback to current if refresh fails
87
+ }
88
+ // expired - clear
89
+ this.token = null;
90
+ }
91
+ // 3. Try refresh with cached refresh_token (Story 31.02/31.03)
92
+ // Lazy load refresh_token from cache if not in memory
93
+ if (!this.refreshToken) {
94
+ this.refreshToken = await this.loadRefreshTokenFromCache();
95
+ }
96
+ if (this.refreshToken) {
97
+ const refreshed = await this.refresh();
98
+ if (refreshed?.token)
99
+ return refreshed.token;
100
+ }
101
+ // 4. Activate (last resort)
102
+ const result = await this.activate();
103
+ return result?.token ?? null;
104
+ }
105
+ catch {
106
+ return null;
107
+ }
108
+ }
109
+ /**
110
+ * Activate the license by calling POST /api/v1/license/activate.
111
+ * Stores token and refresh_token in memory and cache on success.
112
+ * NEVER throws - returns null on failure.
113
+ */
114
+ async activate() {
115
+ try {
116
+ const response = await fetch(`${this.serverUrl}/api/v1/license/activate`, {
117
+ method: 'POST',
118
+ headers: { 'Content-Type': 'application/json' },
119
+ body: JSON.stringify({
120
+ license_key: this.licenseKey,
121
+ machine_id: this.machineId,
122
+ client_version: this.clientVersion,
123
+ }),
124
+ });
125
+ if (!response.ok)
126
+ return null;
127
+ const data = (await response.json());
128
+ this.token = data.token;
129
+ this.refreshToken = data.refresh_token ?? null;
130
+ // Persist JWT to cache (fire-and-forget)
131
+ this.cache.set(CACHE_KEY, data.token, data.expires_in * 1000).catch(() => { });
132
+ // Persist refresh token with 7-day TTL (fire-and-forget)
133
+ if (data.refresh_token) {
134
+ this.cache.set(REFRESH_CACHE_KEY, data.refresh_token, REFRESH_TOKEN_TTL_MS).catch(() => { });
135
+ }
136
+ return {
137
+ token: data.token,
138
+ expiresIn: data.expires_in,
139
+ refreshToken: data.refresh_token,
140
+ };
141
+ }
142
+ catch {
143
+ return null;
144
+ }
145
+ }
146
+ /**
147
+ * Refresh using refresh_token by calling POST /api/v1/license/refresh.
148
+ * Sends refresh_token in body (no Authorization header needed).
149
+ * Handles token rotation: stores the new refresh_token from response.
150
+ * NEVER throws - returns null on failure.
151
+ *
152
+ * Story 31.02: Signature changed from refresh(currentToken) to refresh().
153
+ * The refresh_token in the body is the credential, not the JWT.
154
+ *
155
+ * @param _currentToken - Deprecated. Kept for backward compat but ignored.
156
+ */
157
+ async refresh(_currentToken) {
158
+ try {
159
+ // Load refresh_token: in-memory first, then cache
160
+ const rt = this.refreshToken ?? await this.loadRefreshTokenFromCache();
161
+ if (!rt) {
162
+ // No refresh token available -- cannot use new flow
163
+ return null;
164
+ }
165
+ const response = await fetch(`${this.serverUrl}/api/v1/license/refresh`, {
166
+ method: 'POST',
167
+ headers: { 'Content-Type': 'application/json' },
168
+ body: JSON.stringify({ refresh_token: rt }),
169
+ });
170
+ if (!response.ok) {
171
+ // Refresh token invalid/expired -- clear it
172
+ this.refreshToken = null;
173
+ this.cache.set(REFRESH_CACHE_KEY, '', 1).catch(() => { }); // expire immediately
174
+ return null;
175
+ }
176
+ const data = (await response.json());
177
+ this.token = data.token;
178
+ // Token rotation: server issues new refresh token
179
+ this.refreshToken = data.refresh_token ?? null;
180
+ // Persist JWT to cache (fire-and-forget)
181
+ this.cache.set(CACHE_KEY, data.token, data.expires_in * 1000).catch(() => { });
182
+ // Persist rotated refresh token (fire-and-forget)
183
+ if (data.refresh_token) {
184
+ this.cache.set(REFRESH_CACHE_KEY, data.refresh_token, REFRESH_TOKEN_TTL_MS).catch(() => { });
185
+ }
186
+ return {
187
+ token: data.token,
188
+ expiresIn: data.expires_in,
189
+ refreshToken: data.refresh_token,
190
+ };
191
+ }
192
+ catch {
193
+ return null;
194
+ }
195
+ }
196
+ /**
197
+ * Force-refresh the token.
198
+ * Used when tier_changed is detected or a 401 response indicates the token is expired.
199
+ * Falls back to re-activation if refresh fails.
200
+ * NEVER throws - returns new token or null on failure.
201
+ *
202
+ * Story 18.8: Updated to prefer refresh over re-activate (preserves updated tier).
203
+ * Story 31.02: Uses refresh_token flow instead of JWT-based refresh.
204
+ */
205
+ async forceRefresh() {
206
+ try {
207
+ // 1. Try refresh with refresh_token first
208
+ const refreshResult = await this.refresh();
209
+ if (refreshResult?.token)
210
+ return refreshResult.token;
211
+ // 2. Refresh failed -- clear everything and re-activate
212
+ this.token = null;
213
+ this.refreshToken = null;
214
+ await this.cache.clear().catch(() => { });
215
+ const result = await this.activate();
216
+ return result?.token ?? null;
217
+ }
218
+ catch {
219
+ return null;
220
+ }
221
+ }
222
+ // ── Private Methods ─────────────────────────────────────────────────
223
+ checkTokenValidity(token) {
224
+ try {
225
+ const payload = decodeJwt(token);
226
+ if (!payload.exp)
227
+ return 'expired';
228
+ const now = Math.floor(Date.now() / 1000);
229
+ if (payload.exp <= now)
230
+ return 'expired';
231
+ if (payload.exp - now <= REFRESH_THRESHOLD_S)
232
+ return 'needs-refresh';
233
+ return 'valid';
234
+ }
235
+ catch {
236
+ return 'expired';
237
+ }
238
+ }
239
+ async loadFromCache() {
240
+ try {
241
+ return await this.cache.get(CACHE_KEY);
242
+ }
243
+ catch {
244
+ return null;
245
+ }
246
+ }
247
+ async loadRefreshTokenFromCache() {
248
+ try {
249
+ const rt = await this.cache.get(REFRESH_CACHE_KEY);
250
+ // Guard against empty/expired entries
251
+ return rt && rt.length > 0 ? rt : null;
252
+ }
253
+ catch {
254
+ return null;
255
+ }
256
+ }
257
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * @license FSL-1.1
3
+ * Copyright (c) 2026 OrNexus AI
4
+ *
5
+ * This file is part of Neocortex CLI, licensed under the
6
+ * Functional Source License, Version 1.1 (FSL-1.1).
7
+ *
8
+ * Change Date: February 20, 2029
9
+ * Change License: MIT
10
+ *
11
+ * See the LICENSE file in the project root for full license text.
12
+ */
13
+ /**
14
+ * Generate a hardware-based fingerprint (not persisted).
15
+ * This is the original computation used as fallback and for seeding.
16
+ *
17
+ * @returns 64-character hex string (SHA-256 digest)
18
+ */
19
+ export declare function computeHardwareFingerprint(): string;
20
+ /**
21
+ * Generate a stable machine fingerprint as a SHA-256 hex digest.
22
+ *
23
+ * Story P26.01: On first call, generates from hardware and persists to
24
+ * ~/.neocortex/.machine-id. On subsequent calls, reads from disk.
25
+ * This ensures the fingerprint remains stable even if hardware attributes
26
+ * change (e.g., USB NIC added/removed, hostname change).
27
+ *
28
+ * Backward compat: if config.json has machineId, uses that to seed .machine-id.
29
+ *
30
+ * Fail-open: if disk operations fail, falls back to hardware-generated fingerprint.
31
+ *
32
+ * @returns 64-character hex string (SHA-256 digest)
33
+ */
34
+ export declare function getMachineFingerprint(): string;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * @license FSL-1.1
3
+ * Copyright (c) 2026 OrNexus AI
4
+ *
5
+ * This file is part of Neocortex CLI, licensed under the
6
+ * Functional Source License, Version 1.1 (FSL-1.1).
7
+ *
8
+ * Change Date: February 20, 2029
9
+ * Change License: MIT
10
+ *
11
+ * See the LICENSE file in the project root for full license text.
12
+ */
13
+ /**
14
+ * @neocortex/client - Machine Fingerprint
15
+ *
16
+ * Generates a stable, deterministic machine identifier from hardware/OS
17
+ * attributes using SHA-256. Used for license activation machine tracking.
18
+ */
19
+ import { createHash } from 'node:crypto';
20
+ import { existsSync, readFileSync, writeFileSync, mkdirSync, chmodSync } from 'node:fs';
21
+ import { join } from 'node:path';
22
+ import { arch, cpus, homedir, hostname, networkInterfaces, platform } from 'node:os';
23
+ const ALL_ZEROS_MAC = '00:00:00:00:00:00';
24
+ /** Path to the persisted machine-id file */
25
+ const NEOCORTEX_DIR = join(homedir(), '.neocortex');
26
+ const MACHINE_ID_FILE = join(NEOCORTEX_DIR, '.machine-id');
27
+ /**
28
+ * Collect sorted, non-internal MAC addresses from network interfaces.
29
+ * Filters out internal/loopback interfaces and all-zeros MACs.
30
+ */
31
+ function collectMacAddresses() {
32
+ const interfaces = networkInterfaces();
33
+ const macs = [];
34
+ for (const entries of Object.values(interfaces)) {
35
+ if (!entries)
36
+ continue;
37
+ for (const entry of entries) {
38
+ if (!entry.internal && entry.mac !== ALL_ZEROS_MAC) {
39
+ macs.push(entry.mac);
40
+ }
41
+ }
42
+ }
43
+ // Sort for determinism regardless of NIC enumeration order
44
+ return [...new Set(macs)].sort();
45
+ }
46
+ /**
47
+ * Generate a hardware-based fingerprint (not persisted).
48
+ * This is the original computation used as fallback and for seeding.
49
+ *
50
+ * @returns 64-character hex string (SHA-256 digest)
51
+ */
52
+ export function computeHardwareFingerprint() {
53
+ const host = hostname();
54
+ const plat = platform();
55
+ const architecture = arch();
56
+ const cpuList = cpus();
57
+ const cpuModel = cpuList.length > 0 ? cpuList[0].model : '';
58
+ const macs = collectMacAddresses();
59
+ const input = `${host}|${plat}|${architecture}|${cpuModel}|${macs.join(',')}`;
60
+ return createHash('sha256').update(input).digest('hex');
61
+ }
62
+ /**
63
+ * Set restrictive file permissions (chmod 600) on a file.
64
+ * Fail-open: never throws.
65
+ */
66
+ function setFilePermissions600(filePath) {
67
+ try {
68
+ if (process.platform !== 'win32') {
69
+ chmodSync(filePath, 0o600);
70
+ }
71
+ // Windows ACL handled by secure-config.ts setSecureFilePermissions() if needed
72
+ }
73
+ catch {
74
+ // Fail-open
75
+ }
76
+ }
77
+ /**
78
+ * Read persisted machine ID from disk.
79
+ * Returns null if file doesn't exist or can't be read.
80
+ */
81
+ function readPersistedMachineId() {
82
+ try {
83
+ if (!existsSync(MACHINE_ID_FILE))
84
+ return null;
85
+ const raw = readFileSync(MACHINE_ID_FILE, 'utf-8').trim();
86
+ // Validate: must be a 64-char hex string
87
+ if (/^[a-f0-9]{64}$/.test(raw))
88
+ return raw;
89
+ return null;
90
+ }
91
+ catch {
92
+ return null;
93
+ }
94
+ }
95
+ /**
96
+ * Persist machine ID to disk.
97
+ * Creates ~/.neocortex/ if needed. Sets chmod 600 on the file.
98
+ * Fail-open: never throws.
99
+ */
100
+ function persistMachineId(machineId) {
101
+ try {
102
+ mkdirSync(NEOCORTEX_DIR, { recursive: true });
103
+ writeFileSync(MACHINE_ID_FILE, machineId + '\n', 'utf-8');
104
+ setFilePermissions600(MACHINE_ID_FILE);
105
+ }
106
+ catch {
107
+ // Fail-open: if disk fails, fingerprint still works from hardware
108
+ }
109
+ }
110
+ /**
111
+ * Check if config.json has a machineId field that can seed .machine-id.
112
+ * This provides backward compatibility for existing installations.
113
+ */
114
+ function readMachineIdFromConfig() {
115
+ try {
116
+ const configPath = join(NEOCORTEX_DIR, 'config.json');
117
+ if (!existsSync(configPath))
118
+ return null;
119
+ const raw = readFileSync(configPath, 'utf-8');
120
+ const config = JSON.parse(raw.replace(/^\uFEFF/, ''));
121
+ const machineId = config.machineId;
122
+ if (typeof machineId === 'string' && /^[a-f0-9]{64}$/.test(machineId)) {
123
+ return machineId;
124
+ }
125
+ return null;
126
+ }
127
+ catch {
128
+ return null;
129
+ }
130
+ }
131
+ /**
132
+ * Generate a stable machine fingerprint as a SHA-256 hex digest.
133
+ *
134
+ * Story P26.01: On first call, generates from hardware and persists to
135
+ * ~/.neocortex/.machine-id. On subsequent calls, reads from disk.
136
+ * This ensures the fingerprint remains stable even if hardware attributes
137
+ * change (e.g., USB NIC added/removed, hostname change).
138
+ *
139
+ * Backward compat: if config.json has machineId, uses that to seed .machine-id.
140
+ *
141
+ * Fail-open: if disk operations fail, falls back to hardware-generated fingerprint.
142
+ *
143
+ * @returns 64-character hex string (SHA-256 digest)
144
+ */
145
+ export function getMachineFingerprint() {
146
+ // 1. Try reading persisted machine ID from disk
147
+ const persisted = readPersistedMachineId();
148
+ if (persisted)
149
+ return persisted;
150
+ // 2. Check config.json for backward-compatible machineId
151
+ const fromConfig = readMachineIdFromConfig();
152
+ if (fromConfig) {
153
+ persistMachineId(fromConfig);
154
+ return fromConfig;
155
+ }
156
+ // 3. Generate from hardware and persist
157
+ const fingerprint = computeHardwareFingerprint();
158
+ persistMachineId(fingerprint);
159
+ return fingerprint;
160
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @license FSL-1.1
3
+ * Copyright (c) 2026 OrNexus AI
4
+ */
5
+ export { getMachineFingerprint } from './fingerprint.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * @license FSL-1.1
3
+ * Copyright (c) 2026 OrNexus AI
4
+ */
5
+ export { getMachineFingerprint } from './fingerprint.js';
@@ -0,0 +1,70 @@
1
+ /**
2
+ * @license FSL-1.1
3
+ * Copyright (c) 2026 OrNexus AI
4
+ *
5
+ * This file is part of Neocortex CLI, licensed under the
6
+ * Functional Source License, Version 1.1 (FSL-1.1).
7
+ *
8
+ * Change Date: February 20, 2029
9
+ * Change License: MIT
10
+ *
11
+ * See the LICENSE file in the project root for full license text.
12
+ */
13
+ export type CircuitState = 'CLOSED' | 'OPEN' | 'HALF_OPEN';
14
+ export interface CircuitBreakerConfig {
15
+ /** Number of failures within window to trip circuit. Default: 3 */
16
+ failureThreshold: number;
17
+ /** Window in ms for counting failures. Default: 60_000 (60s) */
18
+ failureWindowMs: number;
19
+ /** Time in ms before OPEN transitions to HALF_OPEN. Default: 60_000 (60s) */
20
+ halfOpenAfterMs: number;
21
+ /** Path to persist circuit state. Default: ~/.neocortex/.circuit-state */
22
+ stateFilePath: string;
23
+ }
24
+ export interface FailureRecord {
25
+ timestamp: number;
26
+ }
27
+ export interface CircuitBreakerState {
28
+ state: CircuitState;
29
+ failures: FailureRecord[];
30
+ openedAt: number | null;
31
+ lastProbeAt: number | null;
32
+ }
33
+ export declare class ClientCircuitBreaker {
34
+ private readonly config;
35
+ private internalState;
36
+ constructor(config?: Partial<CircuitBreakerConfig>, initialState?: CircuitBreakerState);
37
+ /**
38
+ * Check if a request can be made to the server.
39
+ * Returns false if circuit is OPEN (fail-fast to cache).
40
+ */
41
+ canCall(): boolean;
42
+ /**
43
+ * Record a successful server response. Closes the circuit.
44
+ */
45
+ recordSuccess(): Promise<void>;
46
+ /**
47
+ * Record a failed server response. Opens circuit after threshold failures in window.
48
+ */
49
+ recordFailure(): Promise<void>;
50
+ /**
51
+ * Get current circuit breaker state (copy).
52
+ */
53
+ getState(): CircuitBreakerState;
54
+ /**
55
+ * Reset circuit breaker to initial CLOSED state.
56
+ */
57
+ reset(): Promise<void>;
58
+ /**
59
+ * Load circuit breaker state from disk.
60
+ */
61
+ static loadFromDisk(path?: string): Promise<ClientCircuitBreaker>;
62
+ /**
63
+ * Remove failures outside the counting window.
64
+ */
65
+ private pruneExpiredFailures;
66
+ /**
67
+ * Persist state to disk with atomic write.
68
+ */
69
+ private saveToDisk;
70
+ }