@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.
- package/install.ps1 +92 -33
- package/install.sh +15 -1
- package/package.json +3 -3
- package/packages/client/dist/adapters/adapter-registry.js +1 -106
- package/packages/client/dist/adapters/antigravity-adapter.js +2 -77
- package/packages/client/dist/adapters/claude-code-adapter.js +3 -79
- package/packages/client/dist/adapters/codex-adapter.js +2 -80
- package/packages/client/dist/adapters/cursor-adapter.js +4 -115
- package/packages/client/dist/adapters/gemini-adapter.js +2 -71
- package/packages/client/dist/adapters/index.js +1 -21
- package/packages/client/dist/adapters/platform-detector.js +1 -106
- package/packages/client/dist/adapters/target-adapter.js +0 -12
- package/packages/client/dist/adapters/vscode-adapter.js +2 -72
- package/packages/client/dist/agent/refresh-stubs.js +2 -234
- package/packages/client/dist/agent/update-agent-yaml.js +1 -102
- package/packages/client/dist/agent/update-description.js +1 -251
- package/packages/client/dist/cache/crypto-utils.js +1 -76
- package/packages/client/dist/cache/encrypted-cache.js +1 -94
- package/packages/client/dist/cache/in-memory-asset-cache.js +1 -70
- package/packages/client/dist/cache/index.js +1 -13
- package/packages/client/dist/cli.js +2 -163
- package/packages/client/dist/commands/activate.js +8 -390
- package/packages/client/dist/commands/cache-status.js +2 -112
- package/packages/client/dist/commands/invoke.js +28 -490
- package/packages/client/dist/config/resolver-selection.js +1 -278
- package/packages/client/dist/config/secure-config.js +12 -269
- package/packages/client/dist/constants.js +1 -25
- package/packages/client/dist/context/context-collector.js +2 -222
- package/packages/client/dist/context/context-sanitizer.js +1 -145
- package/packages/client/dist/index.js +1 -38
- package/packages/client/dist/license/index.js +1 -5
- package/packages/client/dist/license/license-client.js +1 -257
- package/packages/client/dist/machine/fingerprint.js +2 -160
- package/packages/client/dist/machine/index.js +1 -5
- package/packages/client/dist/resilience/circuit-breaker.js +1 -170
- package/packages/client/dist/resilience/degradation-manager.js +1 -164
- package/packages/client/dist/resilience/freshness-indicator.js +1 -100
- package/packages/client/dist/resilience/index.js +1 -8
- package/packages/client/dist/resilience/recovery-detector.js +1 -74
- package/packages/client/dist/resolvers/asset-resolver.js +0 -13
- package/packages/client/dist/resolvers/local-resolver.js +8 -218
- package/packages/client/dist/resolvers/remote-resolver.js +1 -282
- package/packages/client/dist/telemetry/index.js +1 -5
- package/packages/client/dist/telemetry/offline-queue.js +1 -131
- package/packages/client/dist/tier/index.js +1 -5
- package/packages/client/dist/tier/tier-aware-client.js +1 -260
- package/packages/client/dist/types/index.js +1 -38
- package/targets-stubs/antigravity/gemini.md +1 -1
- package/targets-stubs/antigravity/install-antigravity.sh +49 -3
- package/targets-stubs/antigravity/skill/SKILL.md +23 -4
- package/targets-stubs/claude-code/neocortex.agent.yaml +19 -1
- package/targets-stubs/claude-code/neocortex.md +64 -29
- package/targets-stubs/codex/agents.md +20 -3
- package/targets-stubs/codex/config-mcp.toml +5 -0
- package/targets-stubs/cursor/agent.md +23 -5
- package/targets-stubs/cursor/install-cursor.sh +51 -3
- package/targets-stubs/cursor/mcp.json +7 -0
- package/targets-stubs/gemini-cli/agent.md +37 -6
- package/targets-stubs/gemini-cli/install-gemini.sh +50 -17
- package/targets-stubs/vscode/agent.md +47 -10
- package/targets-stubs/vscode/install-vscode.sh +50 -3
- package/targets-stubs/vscode/mcp.json +8 -0
|
@@ -1,112 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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};
|