@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.
- package/LICENSE +56 -0
- package/README.md +32 -0
- package/install.js +486 -0
- package/install.ps1 +1790 -0
- package/install.sh +1587 -0
- package/package.json +104 -0
- package/packages/client/dist/adapters/adapter-registry.d.ts +61 -0
- package/packages/client/dist/adapters/adapter-registry.js +106 -0
- package/packages/client/dist/adapters/antigravity-adapter.d.ts +18 -0
- package/packages/client/dist/adapters/antigravity-adapter.js +77 -0
- package/packages/client/dist/adapters/claude-code-adapter.d.ts +19 -0
- package/packages/client/dist/adapters/claude-code-adapter.js +79 -0
- package/packages/client/dist/adapters/codex-adapter.d.ts +19 -0
- package/packages/client/dist/adapters/codex-adapter.js +80 -0
- package/packages/client/dist/adapters/cursor-adapter.d.ts +19 -0
- package/packages/client/dist/adapters/cursor-adapter.js +115 -0
- package/packages/client/dist/adapters/gemini-adapter.d.ts +18 -0
- package/packages/client/dist/adapters/gemini-adapter.js +71 -0
- package/packages/client/dist/adapters/index.d.ts +19 -0
- package/packages/client/dist/adapters/index.js +21 -0
- package/packages/client/dist/adapters/platform-detector.d.ts +46 -0
- package/packages/client/dist/adapters/platform-detector.js +106 -0
- package/packages/client/dist/adapters/target-adapter.d.ts +70 -0
- package/packages/client/dist/adapters/target-adapter.js +12 -0
- package/packages/client/dist/adapters/vscode-adapter.d.ts +19 -0
- package/packages/client/dist/adapters/vscode-adapter.js +72 -0
- package/packages/client/dist/agent/refresh-stubs.d.ts +65 -0
- package/packages/client/dist/agent/refresh-stubs.js +234 -0
- package/packages/client/dist/agent/update-agent-yaml.d.ts +26 -0
- package/packages/client/dist/agent/update-agent-yaml.js +102 -0
- package/packages/client/dist/agent/update-description.d.ts +45 -0
- package/packages/client/dist/agent/update-description.js +251 -0
- package/packages/client/dist/cache/crypto-utils.d.ts +30 -0
- package/packages/client/dist/cache/crypto-utils.js +76 -0
- package/packages/client/dist/cache/encrypted-cache.d.ts +30 -0
- package/packages/client/dist/cache/encrypted-cache.js +94 -0
- package/packages/client/dist/cache/in-memory-asset-cache.d.ts +59 -0
- package/packages/client/dist/cache/in-memory-asset-cache.js +70 -0
- package/packages/client/dist/cache/index.d.ts +13 -0
- package/packages/client/dist/cache/index.js +13 -0
- package/packages/client/dist/cli.d.ts +14 -0
- package/packages/client/dist/cli.js +194 -0
- package/packages/client/dist/commands/activate.d.ts +55 -0
- package/packages/client/dist/commands/activate.js +390 -0
- package/packages/client/dist/commands/cache-status.d.ts +39 -0
- package/packages/client/dist/commands/cache-status.js +112 -0
- package/packages/client/dist/commands/invoke.d.ts +70 -0
- package/packages/client/dist/commands/invoke.js +490 -0
- package/packages/client/dist/config/resolver-selection.d.ts +40 -0
- package/packages/client/dist/config/resolver-selection.js +278 -0
- package/packages/client/dist/config/secure-config.d.ts +78 -0
- package/packages/client/dist/config/secure-config.js +269 -0
- package/packages/client/dist/constants.d.ts +25 -0
- package/packages/client/dist/constants.js +25 -0
- package/packages/client/dist/context/context-collector.d.ts +28 -0
- package/packages/client/dist/context/context-collector.js +222 -0
- package/packages/client/dist/context/context-sanitizer.d.ts +28 -0
- package/packages/client/dist/context/context-sanitizer.js +145 -0
- package/packages/client/dist/index.d.ts +55 -0
- package/packages/client/dist/index.js +38 -0
- package/packages/client/dist/license/index.d.ts +5 -0
- package/packages/client/dist/license/index.js +5 -0
- package/packages/client/dist/license/license-client.d.ts +79 -0
- package/packages/client/dist/license/license-client.js +257 -0
- package/packages/client/dist/machine/fingerprint.d.ts +34 -0
- package/packages/client/dist/machine/fingerprint.js +160 -0
- package/packages/client/dist/machine/index.d.ts +5 -0
- package/packages/client/dist/machine/index.js +5 -0
- package/packages/client/dist/resilience/circuit-breaker.d.ts +70 -0
- package/packages/client/dist/resilience/circuit-breaker.js +170 -0
- package/packages/client/dist/resilience/degradation-manager.d.ts +67 -0
- package/packages/client/dist/resilience/degradation-manager.js +164 -0
- package/packages/client/dist/resilience/freshness-indicator.d.ts +59 -0
- package/packages/client/dist/resilience/freshness-indicator.js +100 -0
- package/packages/client/dist/resilience/index.d.ts +8 -0
- package/packages/client/dist/resilience/index.js +8 -0
- package/packages/client/dist/resilience/recovery-detector.d.ts +59 -0
- package/packages/client/dist/resilience/recovery-detector.js +74 -0
- package/packages/client/dist/resolvers/asset-resolver.d.ts +79 -0
- package/packages/client/dist/resolvers/asset-resolver.js +13 -0
- package/packages/client/dist/resolvers/local-resolver.d.ts +26 -0
- package/packages/client/dist/resolvers/local-resolver.js +218 -0
- package/packages/client/dist/resolvers/remote-resolver.d.ts +91 -0
- package/packages/client/dist/resolvers/remote-resolver.js +282 -0
- package/packages/client/dist/telemetry/index.d.ts +5 -0
- package/packages/client/dist/telemetry/index.js +5 -0
- package/packages/client/dist/telemetry/offline-queue.d.ts +57 -0
- package/packages/client/dist/telemetry/offline-queue.js +131 -0
- package/packages/client/dist/tier/index.d.ts +5 -0
- package/packages/client/dist/tier/index.js +5 -0
- package/packages/client/dist/tier/tier-aware-client.d.ts +97 -0
- package/packages/client/dist/tier/tier-aware-client.js +260 -0
- package/packages/client/dist/types/index.d.ts +140 -0
- package/packages/client/dist/types/index.js +38 -0
- package/postinstall.js +272 -0
- package/targets-stubs/antigravity/README.md +36 -0
- package/targets-stubs/antigravity/gemini.md +22 -0
- package/targets-stubs/antigravity/install-antigravity.sh +44 -0
- package/targets-stubs/antigravity/mcp-config.json +9 -0
- package/targets-stubs/antigravity/skill/SKILL.md +67 -0
- package/targets-stubs/claude-code/README.md +20 -0
- package/targets-stubs/claude-code/neocortex.agent.yaml +24 -0
- package/targets-stubs/claude-code/neocortex.md +125 -0
- package/targets-stubs/codex/README.md +32 -0
- package/targets-stubs/codex/agents.md +61 -0
- package/targets-stubs/codex/config-mcp.toml +6 -0
- package/targets-stubs/codex/install-codex.sh +61 -0
- package/targets-stubs/cursor/README.md +33 -0
- package/targets-stubs/cursor/agent.md +94 -0
- package/targets-stubs/cursor/install-cursor.sh +35 -0
- package/targets-stubs/cursor/mcp.json +11 -0
- package/targets-stubs/gemini-cli/README.md +34 -0
- package/targets-stubs/gemini-cli/agent.md +101 -0
- package/targets-stubs/gemini-cli/gemini.md +16 -0
- package/targets-stubs/gemini-cli/install-gemini.sh +56 -0
- package/targets-stubs/gemini-cli/settings-mcp.json +11 -0
- package/targets-stubs/vscode/README.md +34 -0
- package/targets-stubs/vscode/agent.md +102 -0
- package/targets-stubs/vscode/copilot-instructions.md +16 -0
- package/targets-stubs/vscode/install-vscode.sh +42 -0
- package/targets-stubs/vscode/mcp.json +13 -0
|
@@ -0,0 +1,490 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
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
|
+
import type { AssetResolver } from '../resolvers/asset-resolver.js';
|
|
14
|
+
import type { CreateResolverOptions } from '../types/index.js';
|
|
15
|
+
/** Result of resolver selection including the reason for the choice */
|
|
16
|
+
export interface ResolverSelectionResult {
|
|
17
|
+
readonly resolver: AssetResolver;
|
|
18
|
+
readonly reason: string;
|
|
19
|
+
readonly mode: 'local' | 'remote';
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create the appropriate AssetResolver based on configuration.
|
|
23
|
+
*
|
|
24
|
+
* Selection follows a strict priority chain:
|
|
25
|
+
* 1. forceLocal option -> LocalResolver
|
|
26
|
+
* 2. NEOCORTEX_MODE=local -> LocalResolver
|
|
27
|
+
* 3. NEOCORTEX_MODE=remote -> RemoteResolver
|
|
28
|
+
* 4. core/ exists locally -> LocalResolver
|
|
29
|
+
* 5. License key + no core/ -> RemoteResolver
|
|
30
|
+
* 6. Default -> LocalResolver
|
|
31
|
+
*
|
|
32
|
+
* @param options - Optional configuration overrides
|
|
33
|
+
* @returns Configured AssetResolver ready for use
|
|
34
|
+
*/
|
|
35
|
+
export declare function createResolver(options?: CreateResolverOptions): Promise<AssetResolver>;
|
|
36
|
+
/**
|
|
37
|
+
* Select resolver with full result including reason.
|
|
38
|
+
* Useful for logging/debugging why a specific resolver was chosen.
|
|
39
|
+
*/
|
|
40
|
+
export declare function selectResolver(options?: CreateResolverOptions): Promise<ResolverSelectionResult>;
|