@ornexus/neocortex 4.0.1 → 4.0.2

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.
Files changed (62) hide show
  1. package/install.ps1 +92 -33
  2. package/install.sh +15 -1
  3. package/package.json +3 -3
  4. package/packages/client/dist/adapters/adapter-registry.js +1 -106
  5. package/packages/client/dist/adapters/antigravity-adapter.js +2 -77
  6. package/packages/client/dist/adapters/claude-code-adapter.js +3 -79
  7. package/packages/client/dist/adapters/codex-adapter.js +2 -80
  8. package/packages/client/dist/adapters/cursor-adapter.js +4 -115
  9. package/packages/client/dist/adapters/gemini-adapter.js +2 -71
  10. package/packages/client/dist/adapters/index.js +1 -21
  11. package/packages/client/dist/adapters/platform-detector.js +1 -106
  12. package/packages/client/dist/adapters/target-adapter.js +0 -12
  13. package/packages/client/dist/adapters/vscode-adapter.js +2 -72
  14. package/packages/client/dist/agent/refresh-stubs.js +2 -234
  15. package/packages/client/dist/agent/update-agent-yaml.js +1 -102
  16. package/packages/client/dist/agent/update-description.js +1 -251
  17. package/packages/client/dist/cache/crypto-utils.js +1 -76
  18. package/packages/client/dist/cache/encrypted-cache.js +1 -94
  19. package/packages/client/dist/cache/in-memory-asset-cache.js +1 -70
  20. package/packages/client/dist/cache/index.js +1 -13
  21. package/packages/client/dist/cli.js +2 -163
  22. package/packages/client/dist/commands/activate.js +8 -390
  23. package/packages/client/dist/commands/cache-status.js +2 -112
  24. package/packages/client/dist/commands/invoke.js +28 -490
  25. package/packages/client/dist/config/resolver-selection.js +1 -278
  26. package/packages/client/dist/config/secure-config.js +12 -269
  27. package/packages/client/dist/constants.js +1 -25
  28. package/packages/client/dist/context/context-collector.js +2 -222
  29. package/packages/client/dist/context/context-sanitizer.js +1 -145
  30. package/packages/client/dist/index.js +1 -38
  31. package/packages/client/dist/license/index.js +1 -5
  32. package/packages/client/dist/license/license-client.js +1 -257
  33. package/packages/client/dist/machine/fingerprint.js +2 -160
  34. package/packages/client/dist/machine/index.js +1 -5
  35. package/packages/client/dist/resilience/circuit-breaker.js +1 -170
  36. package/packages/client/dist/resilience/degradation-manager.js +1 -164
  37. package/packages/client/dist/resilience/freshness-indicator.js +1 -100
  38. package/packages/client/dist/resilience/index.js +1 -8
  39. package/packages/client/dist/resilience/recovery-detector.js +1 -74
  40. package/packages/client/dist/resolvers/asset-resolver.js +0 -13
  41. package/packages/client/dist/resolvers/local-resolver.js +8 -218
  42. package/packages/client/dist/resolvers/remote-resolver.js +1 -282
  43. package/packages/client/dist/telemetry/index.js +1 -5
  44. package/packages/client/dist/telemetry/offline-queue.js +1 -131
  45. package/packages/client/dist/tier/index.js +1 -5
  46. package/packages/client/dist/tier/tier-aware-client.js +1 -260
  47. package/packages/client/dist/types/index.js +1 -38
  48. package/targets-stubs/antigravity/gemini.md +1 -1
  49. package/targets-stubs/antigravity/install-antigravity.sh +49 -3
  50. package/targets-stubs/antigravity/skill/SKILL.md +23 -4
  51. package/targets-stubs/claude-code/neocortex.agent.yaml +19 -1
  52. package/targets-stubs/claude-code/neocortex.md +64 -29
  53. package/targets-stubs/codex/agents.md +20 -3
  54. package/targets-stubs/codex/config-mcp.toml +5 -0
  55. package/targets-stubs/cursor/agent.md +23 -5
  56. package/targets-stubs/cursor/install-cursor.sh +51 -3
  57. package/targets-stubs/cursor/mcp.json +7 -0
  58. package/targets-stubs/gemini-cli/agent.md +37 -6
  59. package/targets-stubs/gemini-cli/install-gemini.sh +50 -17
  60. package/targets-stubs/vscode/agent.md +47 -10
  61. package/targets-stubs/vscode/install-vscode.sh +50 -3
  62. package/targets-stubs/vscode/mcp.json +8 -0
@@ -1,112 +1,2 @@
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 - Cache Status Command
15
- *
16
- * Displays comprehensive cache and circuit breaker status information.
17
- * Shows entry count, total size, age range, stale entries, circuit state,
18
- * and telemetry queue stats.
19
- *
20
- * Story 42.9 - AC7
21
- */
22
- import { readdir, stat } from 'node:fs/promises';
23
- import { join } from 'node:path';
24
- // ── Implementation ──────────────────────────────────────────────────────
25
- /**
26
- * Gather comprehensive cache status information.
27
- */
28
- export async function getCacheStatus(options) {
29
- const staleThreshold = options.staleThresholdMs ?? 86_400_000; // 24h default
30
- let totalEntries = 0;
31
- let totalSizeBytes = 0;
32
- let staleEntries = 0;
33
- let oldestEntry = null;
34
- let newestEntry = null;
35
- try {
36
- const entries = await readdir(options.cacheDir);
37
- const encFiles = entries.filter((e) => e.endsWith('.enc'));
38
- totalEntries = encFiles.length;
39
- const now = Date.now();
40
- for (const file of encFiles) {
41
- try {
42
- const filePath = join(options.cacheDir, file);
43
- const fileStat = await stat(filePath);
44
- totalSizeBytes += fileStat.size;
45
- const mtime = fileStat.mtime;
46
- if (!oldestEntry || mtime < oldestEntry)
47
- oldestEntry = mtime;
48
- if (!newestEntry || mtime > newestEntry)
49
- newestEntry = mtime;
50
- if (now - mtime.getTime() > staleThreshold) {
51
- staleEntries++;
52
- }
53
- }
54
- catch {
55
- // Skip files that can't be stat'd
56
- }
57
- }
58
- }
59
- catch {
60
- // Cache directory doesn't exist or can't be read
61
- }
62
- const circuitState = options.circuitBreaker.getState().state;
63
- let telemetryQueueSize = 0;
64
- try {
65
- const queueStats = await options.telemetryQueue.getStats();
66
- telemetryQueueSize = queueStats.count;
67
- }
68
- catch {
69
- // Queue unavailable
70
- }
71
- return {
72
- totalEntries,
73
- totalSizeBytes,
74
- totalSizeFormatted: formatBytes(totalSizeBytes),
75
- oldestEntry,
76
- newestEntry,
77
- staleEntries,
78
- circuitState,
79
- lastSync: newestEntry, // Most recent cache write approximates last sync
80
- telemetryQueueSize,
81
- };
82
- }
83
- /**
84
- * Format cache status as a human-readable string for terminal output.
85
- */
86
- export function formatCacheStatus(info) {
87
- const lines = [
88
- '+-------------------------------------------------+',
89
- '| NEOCORTEX CACHE STATUS |',
90
- '+-------------------------------------------------+',
91
- `| Total entries: ${String(info.totalEntries).padEnd(29)}|`,
92
- `| Total size: ${info.totalSizeFormatted.padEnd(29)}|`,
93
- `| Oldest entry: ${(info.oldestEntry?.toISOString() ?? 'N/A').padEnd(29)}|`,
94
- `| Newest entry: ${(info.newestEntry?.toISOString() ?? 'N/A').padEnd(29)}|`,
95
- `| Stale entries: ${String(info.staleEntries).padEnd(29)}|`,
96
- '|-------------------------------------------------|',
97
- `| Circuit breaker: ${info.circuitState.padEnd(29)}|`,
98
- `| Last sync: ${(info.lastSync?.toISOString() ?? 'N/A').padEnd(29)}|`,
99
- `| Telemetry queue: ${(info.telemetryQueueSize + ' events').padEnd(29)}|`,
100
- '+-------------------------------------------------+',
101
- ];
102
- return lines.join('\n');
103
- }
104
- // ── Helpers ─────────────────────────────────────────────────────────────
105
- function formatBytes(bytes) {
106
- if (bytes === 0)
107
- return '0 B';
108
- const units = ['B', 'KB', 'MB', 'GB'];
109
- const i = Math.floor(Math.log(bytes) / Math.log(1024));
110
- const value = bytes / Math.pow(1024, i);
111
- return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
112
- }
1
+ import{readdir as y,stat as g}from"node:fs/promises";import{join as f}from"node:path";async function T(t){const r=t.staleThresholdMs??864e5;let e=0,s=0,c=0,i=null,a=null;try{const u=(await y(t.cacheDir)).filter(l=>l.endsWith(".enc"));e=u.length;const h=Date.now();for(const l of u)try{const m=f(t.cacheDir,l),d=await g(m);s+=d.size;const n=d.mtime;(!i||n<i)&&(i=n),(!a||n>a)&&(a=n),h-n.getTime()>r&&c++}catch{}}catch{}const S=t.circuitBreaker.getState().state;let o=0;try{o=(await t.telemetryQueue.getStats()).count}catch{}return{totalEntries:e,totalSizeBytes:s,totalSizeFormatted:$(s),oldestEntry:i,newestEntry:a,staleEntries:c,circuitState:S,lastSync:a,telemetryQueueSize:o}}function B(t){return["+-------------------------------------------------+","| NEOCORTEX CACHE STATUS |","+-------------------------------------------------+",`| Total entries: ${String(t.totalEntries).padEnd(29)}|`,`| Total size: ${t.totalSizeFormatted.padEnd(29)}|`,`| Oldest entry: ${(t.oldestEntry?.toISOString()??"N/A").padEnd(29)}|`,`| Newest entry: ${(t.newestEntry?.toISOString()??"N/A").padEnd(29)}|`,`| Stale entries: ${String(t.staleEntries).padEnd(29)}|`,"|-------------------------------------------------|",`| Circuit breaker: ${t.circuitState.padEnd(29)}|`,`| Last sync: ${(t.lastSync?.toISOString()??"N/A").padEnd(29)}|`,`| Telemetry queue: ${(t.telemetryQueueSize+" events").padEnd(29)}|`,"+-------------------------------------------------+"].join(`
2
+ `)}function $(t){if(t===0)return"0 B";const r=["B","KB","MB","GB"],e=Math.floor(Math.log(t)/Math.log(1024));return`${(t/Math.pow(1024,e)).toFixed(e===0?0:1)} ${r[e]}`}export{B as formatCacheStatus,T as getCacheStatus};
@@ -1,490 +1,28 @@
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 - Invoke Command
15
- *
16
- * Primary entry point for server-side orchestration.
17
- * Sends raw user args to POST /api/v1/invoke and returns
18
- * complete orchestration instructions from the server.
19
- *
20
- * Story 45.2 - AC1-AC6
21
- */
22
- import { existsSync, readFileSync, unlinkSync } from 'node:fs';
23
- import { join } from 'node:path';
24
- import { homedir } from 'node:os';
25
- import { LicenseClient } from '../license/license-client.js';
26
- import { EncryptedCache } from '../cache/encrypted-cache.js';
27
- import { NoOpCache } from '../types/index.js';
28
- import { TierAwareClient } from '../tier/tier-aware-client.js';
29
- import { loadSecureConfig } from '../config/secure-config.js';
30
- import { DEFAULT_SERVER_URL } from '../constants.js';
31
- // ── Constants ─────────────────────────────────────────────────────────────
32
- const CONFIG_DIR = join(homedir(), '.neocortex');
33
- const CACHE_DIR = join(CONFIG_DIR, 'cache');
34
- const MENU_CACHE_FILE = join(CACHE_DIR, 'menu-cache.json');
35
- const MENU_CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
36
- const DEFAULT_TIMEOUT_MS = 30_000;
37
- const CLIENT_VERSION = '3.9.62';
38
- // ── State Snapshot Collection ──────────────────────────────────────────────
39
- /**
40
- * Read state.json and construct a sanitized snapshot for the server.
41
- * Relative paths only - no absolute paths sent to server.
42
- */
43
- export function collectStateSnapshot(projectRoot) {
44
- const stateJsonPath = join(projectRoot, '.neocortex', 'state.json');
45
- if (!existsSync(stateJsonPath)) {
46
- // Return minimal snapshot if state.json doesn't exist
47
- return {
48
- config: {
49
- project_name: 'unknown',
50
- default_branch: 'main',
51
- language: 'pt-BR',
52
- },
53
- stories: {},
54
- epics: {},
55
- };
56
- }
57
- let stateData;
58
- try {
59
- const raw = readFileSync(stateJsonPath, 'utf-8');
60
- stateData = JSON.parse(raw);
61
- }
62
- catch {
63
- return {
64
- config: {
65
- project_name: 'unknown',
66
- default_branch: 'main',
67
- language: 'pt-BR',
68
- },
69
- stories: {},
70
- epics: {},
71
- };
72
- }
73
- // Extract config - check both locations for compatibility
74
- const config = (stateData.config ?? stateData.project ?? {});
75
- // Extract stories - sanitize sensitive fields
76
- const rawStories = (stateData.stories ?? {});
77
- const stories = {};
78
- for (const [id, story] of Object.entries(rawStories)) {
79
- stories[id] = {
80
- id: story.id ?? id,
81
- title: story.title,
82
- epic_id: story.epic_id,
83
- status: story.status ?? 'backlog',
84
- steps_completed: story.steps_completed ?? [],
85
- last_step: story.last_step ?? null,
86
- branch_name: story.branch_name ?? null,
87
- pr_number: story.pr_number,
88
- workflow_issue: story.workflow_issue,
89
- };
90
- }
91
- // Extract epics
92
- const rawEpics = (stateData.epics ?? {});
93
- const epics = {};
94
- for (const [id, epic] of Object.entries(rawEpics)) {
95
- epics[id] = {
96
- id: epic.id ?? id,
97
- title: epic.title,
98
- status: epic.status,
99
- stories: epic.stories,
100
- total_stories: epic.total_stories,
101
- completed_stories: epic.completed_stories,
102
- };
103
- }
104
- return {
105
- config: {
106
- project_name: (config.project_name ?? config.name ?? 'unknown'),
107
- default_branch: (config.default_branch ?? 'main'),
108
- language: (config.language ?? 'pt-BR'),
109
- yolo_mode: config.yolo_mode,
110
- user_name: config.user_name,
111
- worktree_base: config.worktree_base,
112
- max_parallel_stories: config.max_parallel_stories,
113
- },
114
- stories,
115
- epics,
116
- };
117
- }
118
- // ── Menu Cache (Encrypted - Story 61.1) ──────────────────────────────────
119
- const MENU_CACHE_KEY = 'neocortex:menu:cache';
120
- /**
121
- * Read menu cache from EncryptedCache.
122
- * Falls back gracefully: if decryption fails or data is stale, returns null.
123
- * Also cleans up legacy plaintext menu-cache.json if it exists.
124
- */
125
- async function getMenuCache(encryptedCache) {
126
- try {
127
- const raw = await encryptedCache.get(MENU_CACHE_KEY);
128
- if (!raw)
129
- return null;
130
- const cache = JSON.parse(raw);
131
- // Invalidate on version mismatch (stale cache from previous install)
132
- if (cache.version !== CLIENT_VERSION) {
133
- return null;
134
- }
135
- // Check TTL (EncryptedCache also has TTL, but we double-check for version-based invalidation)
136
- if (Date.now() - cache.cachedAt > MENU_CACHE_TTL_MS) {
137
- return null; // Expired
138
- }
139
- return cache;
140
- }
141
- catch {
142
- return null;
143
- }
144
- }
145
- /**
146
- * Write menu cache to EncryptedCache.
147
- * Deletes legacy plaintext menu-cache.json on first encrypted write.
148
- */
149
- async function setMenuCache(encryptedCache, instructions, metadata) {
150
- try {
151
- const cache = {
152
- instructions,
153
- metadata,
154
- cachedAt: Date.now(),
155
- version: CLIENT_VERSION,
156
- };
157
- await encryptedCache.set(MENU_CACHE_KEY, JSON.stringify(cache), MENU_CACHE_TTL_MS);
158
- // Delete legacy plaintext menu-cache.json if it exists (F1 remediation)
159
- deleteLegacyMenuCache();
160
- }
161
- catch {
162
- // Cache write failure is non-critical
163
- }
164
- }
165
- /**
166
- * Remove legacy plaintext menu-cache.json file.
167
- * Called after successful encrypted write to prevent IP leakage.
168
- */
169
- function deleteLegacyMenuCache() {
170
- try {
171
- if (existsSync(MENU_CACHE_FILE)) {
172
- unlinkSync(MENU_CACHE_FILE);
173
- }
174
- }
175
- catch {
176
- // Non-critical: best-effort cleanup
177
- }
178
- }
179
- // ── Config Loading (Story 61.2 - Secure) ─────────────────────────────────
180
- /**
181
- * Load config with automatic decryption of license key.
182
- * Handles migration from plaintext licenseKey to encryptedLicenseKey.
183
- */
184
- function loadConfig() {
185
- return loadSecureConfig();
186
- }
187
- async function getAuthTokenAndClient(serverUrl, licenseKey) {
188
- try {
189
- let cacheProvider;
190
- if (licenseKey) {
191
- const cacheDir = join(CONFIG_DIR, 'cache');
192
- cacheProvider = new EncryptedCache({ cacheDir, passphrase: licenseKey });
193
- }
194
- else {
195
- // Legacy config without licenseKey - use NoOpCache with warning
196
- process.stderr.write('[neocortex] Warning: No license key in config. Run "neocortex activate" to re-authenticate.\n');
197
- cacheProvider = new NoOpCache();
198
- }
199
- const client = new LicenseClient({
200
- serverUrl,
201
- licenseKey: licenseKey ?? '',
202
- cacheProvider,
203
- });
204
- const token = await client.getToken();
205
- if (!token)
206
- return null;
207
- const tierClient = new TierAwareClient({
208
- cacheProvider,
209
- licenseClient: client,
210
- });
211
- return { token, client, tierClient };
212
- }
213
- catch {
214
- return null;
215
- }
216
- }
217
- // ── HTTP Request ──────────────────────────────────────────────────────────
218
- /** P50.05: One-time warning flag per process */
219
- let versionWarningShown = false;
220
- async function sendInvokeRequest(serverUrl, body, authToken) {
221
- const url = `${serverUrl}/api/v1/invoke`;
222
- const controller = new AbortController();
223
- const timeoutId = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
224
- try {
225
- const response = await fetch(url, {
226
- method: 'POST',
227
- headers: {
228
- 'Content-Type': 'application/json',
229
- 'Authorization': `Bearer ${authToken}`,
230
- 'X-Client-Version': CLIENT_VERSION,
231
- },
232
- body: JSON.stringify(body),
233
- signal: controller.signal,
234
- });
235
- clearTimeout(timeoutId);
236
- if (!response.ok) {
237
- const errorText = await response.text().catch(() => 'Unknown error');
238
- let errorJson;
239
- try {
240
- errorJson = JSON.parse(errorText);
241
- }
242
- catch {
243
- // Not JSON
244
- }
245
- return {
246
- ok: false,
247
- status: response.status,
248
- error: errorJson
249
- ? `${errorJson.error_code ?? 'ERROR'}: ${errorJson.message ?? errorText}`
250
- : `HTTP ${response.status}: ${errorText}`,
251
- errorBody: errorJson,
252
- };
253
- }
254
- const data = (await response.json());
255
- // P50.05: Detect version warning header (non-blocking, once per process)
256
- const versionWarning = response.headers.get('X-Client-Version-Warning');
257
- if (versionWarning && !versionWarningShown) {
258
- versionWarningShown = true;
259
- process.stderr.write(`\n[Neocortex] ${versionWarning}\n\n`);
260
- }
261
- return { ok: true, status: response.status, data };
262
- }
263
- catch (error) {
264
- clearTimeout(timeoutId);
265
- const message = error instanceof Error ? error.message : String(error);
266
- return {
267
- ok: false,
268
- status: 0,
269
- error: message.includes('abort')
270
- ? `Request timeout after ${DEFAULT_TIMEOUT_MS / 1000}s`
271
- : `Network error: ${message}`,
272
- };
273
- }
274
- }
275
- // ── Main Invoke Function ──────────────────────────────────────────────────
276
- /**
277
- * Execute the invoke command.
278
- *
279
- * Flow:
280
- * 1. Load config to get server URL
281
- * 2. Collect state snapshot from project root
282
- * 3. Check menu cache for empty invocations
283
- * 4. Send POST /api/v1/invoke
284
- * 5. Format and return result
285
- */
286
- export async function invoke(options) {
287
- const projectRoot = options.projectRoot ?? process.cwd();
288
- const platformTarget = options.platformTarget ?? 'claude-code';
289
- // 1. Determine server URL
290
- const config = loadConfig();
291
- const serverUrl = (options.serverUrl ?? config?.serverUrl ?? DEFAULT_SERVER_URL).replace(/\/+$/, '');
292
- // 2. Collect state snapshot
293
- const stateSnapshot = collectStateSnapshot(projectRoot);
294
- // 2a. Create encrypted cache for menu (uses licenseKey as passphrase)
295
- const menuCache = config?.licenseKey
296
- ? new EncryptedCache({ cacheDir: CACHE_DIR, passphrase: config.licenseKey })
297
- : null;
298
- // 3. Check menu cache for empty invocations (AC6)
299
- const trimmedArgs = options.args.trim();
300
- if (!trimmedArgs && menuCache) {
301
- const cachedMenu = await getMenuCache(menuCache);
302
- if (cachedMenu) {
303
- return {
304
- success: true,
305
- instructions: cachedMenu.instructions,
306
- metadata: cachedMenu.metadata,
307
- exitCode: 0,
308
- };
309
- }
310
- }
311
- // 4. Get auth token and license client from config
312
- const auth = await getAuthTokenAndClient(serverUrl, config?.licenseKey);
313
- if (!auth) {
314
- // Story P26.04: Include fingerprint change as possible cause
315
- const hint = config && !config.licenseKey
316
- ? ' This may be caused by a machine fingerprint change (e.g., hardware or hostname change).'
317
- : '';
318
- return {
319
- success: false,
320
- error: `Not authenticated.${hint} Visit https://neocortex.ornexus.com/login to get your license key, then run: neocortex activate YOUR-LICENSE-KEY`,
321
- exitCode: 2, // Not configured
322
- };
323
- }
324
- // 4a. Pre-flight tier check (optimistic -- if it fails, still proceed)
325
- const trigger = extractTrigger(trimmedArgs);
326
- if (trigger) {
327
- try {
328
- const preFlightResult = await auth.tierClient.preFlightCheck(trigger);
329
- if (!preFlightResult.allowed) {
330
- process.stderr.write(`[neocortex] ${preFlightResult.message}\n`);
331
- return {
332
- success: false,
333
- error: preFlightResult.message ?? 'Trigger not available on your plan',
334
- exitCode: 1,
335
- };
336
- }
337
- }
338
- catch {
339
- // Fail-open: pre-flight errors should not block invocation
340
- }
341
- }
342
- const requestBody = {
343
- args: trimmedArgs,
344
- projectRoot: projectRoot.replace(homedir(), '~'), // Sanitize absolute path
345
- stateSnapshot,
346
- platformTarget,
347
- };
348
- // 5. Send request
349
- let result = await sendInvokeRequest(serverUrl, requestBody, auth.token);
350
- // 5a. 401 retry: inspect fallback_action and retry once after forceRefresh
351
- if (!result.ok && result.status === 401) {
352
- const fallbackAction = result.errorBody?.fallback_action;
353
- if (fallbackAction === 'refresh_token' || fallbackAction === 're_authenticate') {
354
- const newToken = await auth.client.forceRefresh();
355
- if (newToken) {
356
- result = await sendInvokeRequest(serverUrl, requestBody, newToken);
357
- }
358
- }
359
- }
360
- // P50.04: 426 UPGRADE_REQUIRED -- forced update with clear instructions
361
- if (!result.ok && result.status === 426) {
362
- const body = result.errorBody;
363
- const upgradeCmd = body?.upgrade_command ?? 'npm install -g @ornexus/neocortex@latest';
364
- const minVersion = body?.min_version ?? 'unknown';
365
- process.stderr.write('\n');
366
- process.stderr.write('==================================================\n');
367
- process.stderr.write(' UPGRADE REQUIRED\n');
368
- process.stderr.write('==================================================\n');
369
- process.stderr.write(`\n Your Neocortex version (${CLIENT_VERSION}) is no longer supported.\n`);
370
- process.stderr.write(` Minimum required: ${minVersion}\n\n`);
371
- process.stderr.write(' Run this command to update:\n\n');
372
- process.stderr.write(` ${upgradeCmd}\n\n`);
373
- process.stderr.write(' After updating, re-run your command.\n');
374
- process.stderr.write('==================================================\n\n');
375
- return {
376
- success: false,
377
- error: `UPGRADE_REQUIRED: Client version ${CLIENT_VERSION} is below minimum ${minVersion}. Run: ${upgradeCmd}`,
378
- exitCode: 3,
379
- };
380
- }
381
- if (!result.ok || !result.data) {
382
- // AC5: Error exit code
383
- const exitCode = result.status === 401 ? 2 :
384
- result.status === 429 ? 1 :
385
- result.status >= 500 ? 1 : 1;
386
- return {
387
- success: false,
388
- error: result.error ?? 'Unknown error from server',
389
- exitCode,
390
- };
391
- }
392
- // 6. Cache menu responses (AC6) - encrypted (Story 61.1)
393
- if (!trimmedArgs && result.data.metadata?.mode === 'menu' && menuCache) {
394
- setMenuCache(menuCache, result.data.instructions, result.data.metadata).catch(() => { });
395
- }
396
- // 6a. Update cached quota from server response metadata (Epic 60)
397
- if (result.data.metadata) {
398
- auth.tierClient.updateQuotaFromResponse(result.data.metadata).catch(() => { });
399
- }
400
- // 6b. Story 18.7 + 18.8: Detect tier change and auto-refresh
401
- if (result.data.metadata?.tier_changed) {
402
- const newTier = result.data.metadata.current_tier;
403
- try {
404
- const newToken = await auth.client.forceRefresh();
405
- if (newToken) {
406
- await auth.tierClient.invalidateTierCache();
407
- process.stderr.write(`[Neocortex] Token atualizado automaticamente para tier ${newTier}\n`);
408
- }
409
- else {
410
- process.stderr.write(`[Neocortex] Seu tier foi atualizado para ${newTier}! Execute "neocortex activate" para obter um token atualizado.\n`);
411
- }
412
- }
413
- catch {
414
- // Fail-open: tier change notification is best-effort
415
- process.stderr.write(`[Neocortex] Seu tier foi atualizado para ${newTier}! Execute "neocortex activate" para obter um token atualizado.\n`);
416
- }
417
- }
418
- return {
419
- success: true,
420
- instructions: result.data.instructions,
421
- metadata: result.data.metadata,
422
- exitCode: 0,
423
- };
424
- }
425
- // ── Trigger Extraction ──────────────────────────────────────────────────
426
- /**
427
- * Extract trigger name from args string.
428
- * Triggers start with '*' (e.g., "*yolo", "*implement", "*status").
429
- * Returns the trigger name without the '*' prefix, or null if no trigger.
430
- */
431
- function extractTrigger(args) {
432
- const match = args.match(/^\*([a-zA-Z][\w-]*)/);
433
- return match ? match[1] : null;
434
- }
435
- // ── CLI Entry Point ───────────────────────────────────────────────────────
436
- /**
437
- * CLI handler for the invoke command.
438
- * Parses CLI args and delegates to invoke().
439
- *
440
- * Usage:
441
- * neocortex-client invoke --args "*yolo @story.md" --project-root /path
442
- * neocortex-client invoke --args "*status" --format json
443
- */
444
- export async function invokeCliHandler(argv) {
445
- let args = '';
446
- let projectRoot = process.cwd();
447
- let format = 'plain';
448
- let serverUrl;
449
- // Parse CLI arguments
450
- for (let i = 0; i < argv.length; i++) {
451
- switch (argv[i]) {
452
- case '--args':
453
- args = argv[++i] ?? '';
454
- break;
455
- case '--project-root':
456
- projectRoot = argv[++i] ?? process.cwd();
457
- break;
458
- case '--format':
459
- format = (argv[++i] ?? 'plain');
460
- break;
461
- case '--server-url':
462
- serverUrl = argv[++i];
463
- break;
464
- }
465
- }
466
- const result = await invoke({ args, projectRoot, format, serverUrl });
467
- if (!result.success) {
468
- // AC5: Error to stderr as JSON
469
- process.stderr.write(JSON.stringify({
470
- error_code: result.exitCode === 2 ? 'NOT_CONFIGURED' : 'INVOKE_ERROR',
471
- message: result.error,
472
- }) + '\n');
473
- return result.exitCode;
474
- }
475
- if (format === 'json') {
476
- // AC3: Full JSON to stdout
477
- process.stdout.write(JSON.stringify({
478
- instructions: result.instructions,
479
- metadata: result.metadata,
480
- }) + '\n');
481
- }
482
- else {
483
- // AC4: Instructions to stdout, metadata to stderr
484
- process.stdout.write((result.instructions ?? '') + '\n');
485
- if (result.metadata) {
486
- process.stderr.write(JSON.stringify(result.metadata) + '\n');
487
- }
488
- }
489
- return 0;
490
- }
1
+ import{existsSync as g,readFileSync as S,unlinkSync as O}from"node:fs";import{join as f}from"node:path";import{homedir as _}from"node:os";import{LicenseClient as $}from"../license/license-client.js";import{EncryptedCache as y}from"../cache/encrypted-cache.js";import{NoOpCache as v}from"../types/index.js";import{TierAwareClient as A}from"../tier/tier-aware-client.js";import{loadSecureConfig as U}from"../config/secure-config.js";import{DEFAULT_SERVER_URL as j}from"../constants.js";const C=f(_(),".neocortex"),k=f(C,"cache"),x=f(k,"menu-cache.json"),R=1440*60*1e3,E=3e4,h="3.9.62";function D(r){const s=f(r,".neocortex","state.json");if(!g(s))return{config:{project_name:"unknown",default_branch:"main",language:"pt-BR"},stories:{},epics:{}};let n;try{const u=S(s,"utf-8");n=JSON.parse(u)}catch{return{config:{project_name:"unknown",default_branch:"main",language:"pt-BR"},stories:{},epics:{}}}const t=n.config??n.project??{},l=n.stories??{},i={};for(const[u,o]of Object.entries(l))i[u]={id:o.id??u,title:o.title,epic_id:o.epic_id,status:o.status??"backlog",steps_completed:o.steps_completed??[],last_step:o.last_step??null,branch_name:o.branch_name??null,pr_number:o.pr_number,workflow_issue:o.workflow_issue};const e=n.epics??{},d={};for(const[u,o]of Object.entries(e))d[u]={id:o.id??u,title:o.title,status:o.status,stories:o.stories,total_stories:o.total_stories,completed_stories:o.completed_stories};return{config:{project_name:t.project_name??t.name??"unknown",default_branch:t.default_branch??"main",language:t.language??"pt-BR",yolo_mode:t.yolo_mode,user_name:t.user_name,worktree_base:t.worktree_base,max_parallel_stories:t.max_parallel_stories},stories:i,epics:d}}const b="neocortex:menu:cache";async function I(r){try{const s=await r.get(b);if(!s)return null;const n=JSON.parse(s);return n.version!==h||Date.now()-n.cachedAt>R?null:n}catch{return null}}async function M(r,s,n){try{const t={instructions:s,metadata:n,cachedAt:Date.now(),version:h};await r.set(b,JSON.stringify(t),R),J()}catch{}}function J(){try{g(x)&&O(x)}catch{}}function F(){return U()}async function L(r,s){try{let n;if(s){const e=f(C,"cache");n=new y({cacheDir:e,passphrase:s})}else process.stderr.write(`[neocortex] Warning: No license key in config. Run "neocortex activate" to re-authenticate.
2
+ `),n=new v;const t=new $({serverUrl:r,licenseKey:s??"",cacheProvider:n}),l=await t.getToken();if(!l)return null;const i=new A({cacheProvider:n,licenseClient:t});return{token:l,client:t,tierClient:i}}catch{return null}}let T=!1;async function N(r,s,n){const t=`${r}/api/v1/invoke`,l=new AbortController,i=setTimeout(()=>l.abort(),E);try{const e=await fetch(t,{method:"POST",headers:{"Content-Type":"application/json",Authorization:`Bearer ${n}`,"X-Client-Version":h},body:JSON.stringify(s),signal:l.signal});if(clearTimeout(i),!e.ok){const o=await e.text().catch(()=>"Unknown error");let m;try{m=JSON.parse(o)}catch{}return{ok:!1,status:e.status,error:m?`${m.error_code??"ERROR"}: ${m.message??o}`:`HTTP ${e.status}: ${o}`,errorBody:m}}const d=await e.json(),u=e.headers.get("X-Client-Version-Warning");return u&&!T&&(T=!0,process.stderr.write(`
3
+ [Neocortex] ${u}
4
+
5
+ `)),{ok:!0,status:e.status,data:d}}catch(e){clearTimeout(i);const d=e instanceof Error?e.message:String(e);return{ok:!1,status:0,error:d.includes("abort")?`Request timeout after ${E/1e3}s`:`Network error: ${d}`}}}async function B(r){const s=r.projectRoot??process.cwd(),n=r.platformTarget??"claude-code",t=F(),l=(r.serverUrl??t?.serverUrl??j).replace(/\/+$/,""),i=D(s),e=t?.licenseKey?new y({cacheDir:k,passphrase:t.licenseKey}):null,d=r.args.trim();if(!d&&e){const c=await I(e);if(c)return{success:!0,instructions:c.instructions,metadata:c.metadata,exitCode:0}}const u=await L(l,t?.licenseKey);if(!u)return{success:!1,error:`Not authenticated.${t&&!t.licenseKey?" This may be caused by a machine fingerprint change (e.g., hardware or hostname change).":""} Visit https://neocortex.ornexus.com/login to get your license key, then run: neocortex activate YOUR-LICENSE-KEY`,exitCode:2};const o=z(d);if(o)try{const c=await u.tierClient.preFlightCheck(o);if(!c.allowed)return process.stderr.write(`[neocortex] ${c.message}
6
+ `),{success:!1,error:c.message??"Trigger not available on your plan",exitCode:1}}catch{}const m={args:d,projectRoot:s.replace(_(),"~"),stateSnapshot:i,platformTarget:n};let a=await N(l,m,u.token);if(!a.ok&&a.status===401){const c=a.errorBody?.fallback_action;if(c==="refresh_token"||c==="re_authenticate"){const p=await u.client.forceRefresh();p&&(a=await N(l,m,p))}}if(!a.ok&&a.status===426){const c=a.errorBody,p=c?.upgrade_command??"npm install -g @ornexus/neocortex@latest",w=c?.min_version??"unknown";return process.stderr.write(`
7
+ `),process.stderr.write(`==================================================
8
+ `),process.stderr.write(` UPGRADE REQUIRED
9
+ `),process.stderr.write(`==================================================
10
+ `),process.stderr.write(`
11
+ Your Neocortex version (${h}) is no longer supported.
12
+ `),process.stderr.write(` Minimum required: ${w}
13
+
14
+ `),process.stderr.write(` Run this command to update:
15
+
16
+ `),process.stderr.write(` ${p}
17
+
18
+ `),process.stderr.write(` After updating, re-run your command.
19
+ `),process.stderr.write(`==================================================
20
+
21
+ `),{success:!1,error:`UPGRADE_REQUIRED: Client version ${h} is below minimum ${w}. Run: ${p}`,exitCode:3}}if(!a.ok||!a.data){const c=a.status===401?2:(a.status===429||a.status>=500,1);return{success:!1,error:a.error??"Unknown error from server",exitCode:c}}if(!d&&a.data.metadata?.mode==="menu"&&e&&M(e,a.data.instructions,a.data.metadata).catch(()=>{}),a.data.metadata&&u.tierClient.updateQuotaFromResponse(a.data.metadata).catch(()=>{}),a.data.metadata?.tier_changed){const c=a.data.metadata.current_tier;try{await u.client.forceRefresh()?(await u.tierClient.invalidateTierCache(),process.stderr.write(`[Neocortex] Token atualizado automaticamente para tier ${c}
22
+ `)):process.stderr.write(`[Neocortex] Seu tier foi atualizado para ${c}! Execute "neocortex activate" para obter um token atualizado.
23
+ `)}catch{process.stderr.write(`[Neocortex] Seu tier foi atualizado para ${c}! Execute "neocortex activate" para obter um token atualizado.
24
+ `)}}return{success:!0,instructions:a.data.instructions,metadata:a.data.metadata,exitCode:0}}function z(r){const s=r.match(/^\*([a-zA-Z][\w-]*)/);return s?s[1]:null}async function X(r){let s="",n=process.cwd(),t="plain",l;for(let e=0;e<r.length;e++)switch(r[e]){case"--args":s=r[++e]??"";break;case"--project-root":n=r[++e]??process.cwd();break;case"--format":t=r[++e]??"plain";break;case"--server-url":l=r[++e];break}const i=await B({args:s,projectRoot:n,format:t,serverUrl:l});return i.success?(t==="json"?process.stdout.write(JSON.stringify({instructions:i.instructions,metadata:i.metadata})+`
25
+ `):(process.stdout.write((i.instructions??"")+`
26
+ `),i.metadata&&process.stderr.write(JSON.stringify(i.metadata)+`
27
+ `)),0):(process.stderr.write(JSON.stringify({error_code:i.exitCode===2?"NOT_CONFIGURED":"INVOKE_ERROR",message:i.error})+`
28
+ `),i.exitCode)}export{D as collectStateSnapshot,B as invoke,X as invokeCliHandler};