@ekkos/cli 1.3.7 → 1.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/dashboard.js +67 -11
- package/dist/commands/gemini.js +34 -8
- package/dist/commands/init.js +92 -21
- package/dist/commands/living-docs.d.ts +8 -0
- package/dist/commands/living-docs.js +66 -0
- package/dist/commands/run.js +55 -6
- package/dist/commands/scan.d.ts +68 -0
- package/dist/commands/scan.js +318 -22
- package/dist/commands/setup.js +2 -6
- package/dist/deploy/index.d.ts +1 -0
- package/dist/deploy/index.js +1 -0
- package/dist/deploy/instructions.d.ts +10 -3
- package/dist/deploy/instructions.js +34 -7
- package/dist/index.js +91 -53
- package/dist/lib/usage-parser.js +18 -2
- package/dist/local/index.d.ts +4 -0
- package/dist/local/index.js +14 -1
- package/dist/local/language-config.d.ts +55 -0
- package/dist/local/language-config.js +729 -0
- package/dist/local/living-docs-manager.d.ts +59 -0
- package/dist/local/living-docs-manager.js +1084 -0
- package/dist/local/stack-detection.d.ts +21 -0
- package/dist/local/stack-detection.js +406 -0
- package/package.json +10 -7
- package/templates/CLAUDE.md +89 -99
- package/LICENSE +0 -21
|
@@ -69,16 +69,24 @@ const usage_parser_js_1 = require("../lib/usage-parser.js");
|
|
|
69
69
|
const state_js_1 = require("../utils/state.js");
|
|
70
70
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
71
71
|
// ── Pricing ──
|
|
72
|
-
// Pricing per MTok from https://
|
|
72
|
+
// Pricing per MTok from https://docs.anthropic.com/en/docs/about-claude/pricing
|
|
73
|
+
// Includes prompt caching rates (5m cache write=1.25x input, cache read=0.1x input).
|
|
73
74
|
const MODEL_PRICING = {
|
|
74
75
|
'claude-opus-4-6-20260514': { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.50 },
|
|
75
76
|
'claude-opus-4-6': { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.50 },
|
|
77
|
+
'claude-opus-4-5-20251101': { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.50 },
|
|
78
|
+
'claude-opus-4-5': { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.50 },
|
|
79
|
+
'claude-opus-4-20250514': { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.50 },
|
|
80
|
+
'claude-opus-4': { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.50 },
|
|
76
81
|
'claude-sonnet-4-6-20260514': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
|
|
77
|
-
'claude-opus-4-5-20250620': { input: 5, output: 25, cacheWrite: 6.25, cacheRead: 0.50 },
|
|
78
82
|
'claude-sonnet-4-6': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
|
|
79
83
|
'claude-sonnet-4-5-20250929': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
|
|
84
|
+
'claude-sonnet-4-5': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
|
|
80
85
|
'claude-sonnet-4-5-20250514': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
|
|
86
|
+
'claude-sonnet-4-20250514': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
|
|
87
|
+
'claude-sonnet-4': { input: 3, output: 15, cacheWrite: 3.75, cacheRead: 0.30 },
|
|
81
88
|
'claude-haiku-4-5-20251001': { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.10 },
|
|
89
|
+
'claude-haiku-4-5': { input: 1, output: 5, cacheWrite: 1.25, cacheRead: 0.10 },
|
|
82
90
|
};
|
|
83
91
|
function getModelPricing(modelId) {
|
|
84
92
|
if (MODEL_PRICING[modelId])
|
|
@@ -86,13 +94,33 @@ function getModelPricing(modelId) {
|
|
|
86
94
|
if (modelId.includes('opus'))
|
|
87
95
|
return MODEL_PRICING['claude-opus-4-6'];
|
|
88
96
|
if (modelId.includes('sonnet'))
|
|
89
|
-
return MODEL_PRICING['claude-sonnet-4-
|
|
97
|
+
return MODEL_PRICING['claude-sonnet-4-6'];
|
|
90
98
|
if (modelId.includes('haiku'))
|
|
91
99
|
return MODEL_PRICING['claude-haiku-4-5-20251001'];
|
|
92
|
-
return MODEL_PRICING['claude-sonnet-4-
|
|
100
|
+
return MODEL_PRICING['claude-sonnet-4-6'];
|
|
93
101
|
}
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
// Claude Sonnet 4/4.5 has legacy long-context premium rates when context-1m
|
|
103
|
+
// is enabled and input exceeds 200k. Sonnet/Opus 4.6 is GA 1M at standard rates.
|
|
104
|
+
function isLegacyLongContextPremiumModel(modelId) {
|
|
105
|
+
const normalized = (modelId || '').toLowerCase();
|
|
106
|
+
if (/^claude-sonnet-4-6(?:$|-)/.test(normalized))
|
|
107
|
+
return false;
|
|
108
|
+
return /^claude-sonnet-4(?:$|-)/.test(normalized);
|
|
109
|
+
}
|
|
110
|
+
function calculateTurnCost(model, usage, options) {
|
|
111
|
+
const base = getModelPricing(model);
|
|
112
|
+
const longContextPremium = options?.longContextPremium === true
|
|
113
|
+
&& isLegacyLongContextPremiumModel(model);
|
|
114
|
+
const p = longContextPremium
|
|
115
|
+
? {
|
|
116
|
+
// Long context pricing modifiers:
|
|
117
|
+
// Input/cache bucket = 2x, output = 1.5x.
|
|
118
|
+
input: base.input * 2,
|
|
119
|
+
output: base.output * 1.5,
|
|
120
|
+
cacheWrite: base.cacheWrite * 2,
|
|
121
|
+
cacheRead: base.cacheRead * 2,
|
|
122
|
+
}
|
|
123
|
+
: base;
|
|
96
124
|
return ((usage.input_tokens / 1000000) * p.input +
|
|
97
125
|
(usage.output_tokens / 1000000) * p.output +
|
|
98
126
|
(usage.cache_creation_tokens / 1000000) * p.cacheWrite +
|
|
@@ -199,6 +227,9 @@ function parseJsonlFile(jsonlPath, sessionName) {
|
|
|
199
227
|
const contextTierHint = typeof entry.message._ekkos_context_tier === 'string'
|
|
200
228
|
? entry.message._ekkos_context_tier.trim().toLowerCase()
|
|
201
229
|
: undefined;
|
|
230
|
+
const explicitExtraUsage = entry.message._ekkos_extra_usage === true
|
|
231
|
+
|| entry.message._ekkos_extra_usage === '1'
|
|
232
|
+
|| entry.message._ekkos_extra_usage === 'true';
|
|
202
233
|
const inputTokens = usage.input_tokens || 0;
|
|
203
234
|
const outputTokens = usage.output_tokens || 0;
|
|
204
235
|
const cacheReadTokens = usage.cache_read_input_tokens || 0;
|
|
@@ -215,11 +246,19 @@ function parseJsonlFile(jsonlPath, sessionName) {
|
|
|
215
246
|
cache_read_tokens: cacheReadTokens,
|
|
216
247
|
cache_creation_tokens: cacheCreationTokens,
|
|
217
248
|
};
|
|
249
|
+
const legacyLongContextPremium = isLegacyLongContextPremiumModel(routedModel) && (explicitExtraUsage
|
|
250
|
+
|| (contextTierHint === '1m' && contextTokens > 200000));
|
|
218
251
|
// Cost at actual model pricing (Haiku if routed)
|
|
219
|
-
const turnCost = calculateTurnCost(routedModel, usageData
|
|
252
|
+
const turnCost = calculateTurnCost(routedModel, usageData, {
|
|
253
|
+
longContextPremium: legacyLongContextPremium,
|
|
254
|
+
});
|
|
220
255
|
// Cost if it had been Opus (for savings calculation)
|
|
256
|
+
const requestedModelPremium = isLegacyLongContextPremiumModel(model) && (explicitExtraUsage
|
|
257
|
+
|| (contextTierHint === '1m' && contextTokens > 200000));
|
|
221
258
|
const opusCost = routedModel !== model
|
|
222
|
-
? calculateTurnCost(model, usageData
|
|
259
|
+
? calculateTurnCost(model, usageData, {
|
|
260
|
+
longContextPremium: requestedModelPremium,
|
|
261
|
+
})
|
|
223
262
|
: turnCost;
|
|
224
263
|
const savings = opusCost - turnCost;
|
|
225
264
|
const msgTools = toolsByMessage.get(msgId);
|
|
@@ -388,7 +427,20 @@ function resolveJsonlPath(sessionName, createdAfterMs) {
|
|
|
388
427
|
if (fs.existsSync(activeSessionsPath)) {
|
|
389
428
|
try {
|
|
390
429
|
const sessions = JSON.parse(fs.readFileSync(activeSessionsPath, 'utf-8'));
|
|
391
|
-
|
|
430
|
+
// Skip stale entries whose PID is dead (prevents cross-binding after restart)
|
|
431
|
+
const match = sessions.find((s) => {
|
|
432
|
+
if (s.sessionName !== sessionName)
|
|
433
|
+
return false;
|
|
434
|
+
if (s.pid && s.pid > 1) {
|
|
435
|
+
try {
|
|
436
|
+
process.kill(s.pid, 0);
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
return false;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
return true;
|
|
443
|
+
});
|
|
392
444
|
if (match?.projectPath) {
|
|
393
445
|
if (isStableSessionId(match.sessionId)) {
|
|
394
446
|
// Prefer exact sessionId lookup, but if that file does not exist yet
|
|
@@ -2165,10 +2217,14 @@ exports.dashboardCommand = new commander_1.Command('dashboard')
|
|
|
2165
2217
|
if (!sessionName)
|
|
2166
2218
|
process.exit(0);
|
|
2167
2219
|
}
|
|
2168
|
-
|
|
2220
|
+
// Use launch timestamp as lower bound so initial resolution never picks up
|
|
2221
|
+
// stale JSONL files from previous sessions (the lazy resolution already does
|
|
2222
|
+
// this correctly — this ensures the INITIAL resolve matches).
|
|
2223
|
+
const launchTs = Date.now();
|
|
2224
|
+
const jsonlPath = resolveJsonlPath(sessionName, launchTs);
|
|
2169
2225
|
if (!jsonlPath) {
|
|
2170
2226
|
// JSONL may not exist yet (session just started) — launch with lazy resolution
|
|
2171
2227
|
console.log(chalk_1.default.gray(`Waiting for JSONL for "${sessionName}"...`));
|
|
2172
2228
|
}
|
|
2173
|
-
await launchDashboard(sessionName, jsonlPath || null, refreshMs, null,
|
|
2229
|
+
await launchDashboard(sessionName, jsonlPath || null, refreshMs, null, launchTs);
|
|
2174
2230
|
});
|
package/dist/commands/gemini.js
CHANGED
|
@@ -59,6 +59,39 @@ const proxy_url_1 = require("../utils/proxy-url");
|
|
|
59
59
|
let cliSessionName = null;
|
|
60
60
|
let cliSessionId = null;
|
|
61
61
|
const isWindows = process.platform === 'win32';
|
|
62
|
+
const PULSE_LOADED_TEXT = ' 🧠 ekkOS_Pulse Loaded!';
|
|
63
|
+
const PULSE_SHINE_FRAME_MS = 50;
|
|
64
|
+
const PULSE_SHINE_SWEEPS = 2;
|
|
65
|
+
function sleep(ms) {
|
|
66
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
67
|
+
}
|
|
68
|
+
function renderPulseShineFrame(text, shineIndex) {
|
|
69
|
+
const chars = Array.from(text);
|
|
70
|
+
return chars.map((ch, i) => {
|
|
71
|
+
const dist = Math.abs(i - shineIndex);
|
|
72
|
+
if (dist === 0)
|
|
73
|
+
return chalk_1.default.whiteBright.bold(ch);
|
|
74
|
+
if (dist === 1)
|
|
75
|
+
return chalk_1.default.yellowBright.bold(ch);
|
|
76
|
+
if (dist === 2)
|
|
77
|
+
return chalk_1.default.yellowBright(ch);
|
|
78
|
+
return chalk_1.default.yellow(ch);
|
|
79
|
+
}).join('');
|
|
80
|
+
}
|
|
81
|
+
async function showPulseLoadedBanner() {
|
|
82
|
+
if (!process.stdout.isTTY) {
|
|
83
|
+
console.log(chalk_1.default.cyan(PULSE_LOADED_TEXT));
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
const charCount = Array.from(PULSE_LOADED_TEXT).length;
|
|
87
|
+
for (let sweep = 0; sweep < PULSE_SHINE_SWEEPS; sweep++) {
|
|
88
|
+
for (let shineIndex = -3; shineIndex < charCount + 3; shineIndex++) {
|
|
89
|
+
process.stdout.write(`\r${renderPulseShineFrame(PULSE_LOADED_TEXT, shineIndex)}`);
|
|
90
|
+
await sleep(PULSE_SHINE_FRAME_MS);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
process.stdout.write(`\r${chalk_1.default.yellow(PULSE_LOADED_TEXT)}\n`);
|
|
94
|
+
}
|
|
62
95
|
/**
|
|
63
96
|
* Resolve Gemini CLI binary path.
|
|
64
97
|
* Checks common locations then falls back to PATH lookup.
|
|
@@ -118,13 +151,6 @@ function buildGeminiEnv(options) {
|
|
|
118
151
|
const proxyUrl = (0, proxy_url_1.buildGeminiProxyUrl)(userId, cliSessionName, process.cwd(), cliSessionId);
|
|
119
152
|
env.GOOGLE_GEMINI_BASE_URL = proxyUrl;
|
|
120
153
|
env.GOOGLE_VERTEX_BASE_URL = proxyUrl;
|
|
121
|
-
// User must bring their own Gemini API key — ekkOS just proxies the traffic
|
|
122
|
-
// for IPC compression and pattern injection (same model as Claude).
|
|
123
|
-
if (!env.GEMINI_API_KEY && !env.GOOGLE_API_KEY) {
|
|
124
|
-
console.warn(chalk_1.default.yellow(' ⚠ GEMINI_API_KEY not set.'));
|
|
125
|
-
console.warn(chalk_1.default.gray(' Get one at: https://aistudio.google.com/apikey'));
|
|
126
|
-
console.warn(chalk_1.default.gray(' Then: export GEMINI_API_KEY=your_key'));
|
|
127
|
-
}
|
|
128
154
|
if (options.verbose) {
|
|
129
155
|
// Redact userId from log
|
|
130
156
|
const safeUrl = proxyUrl.replace(/\/proxy\/[^/]+\//, '/proxy/[user]/');
|
|
@@ -151,7 +177,7 @@ async function gemini(options = {}) {
|
|
|
151
177
|
const sessionName = cliSessionName || 'gemini-session';
|
|
152
178
|
(0, state_1.registerActiveSession)(sessionId, sessionName, process.cwd());
|
|
153
179
|
if (!options.noProxy) {
|
|
154
|
-
|
|
180
|
+
await showPulseLoadedBanner();
|
|
155
181
|
}
|
|
156
182
|
console.log('');
|
|
157
183
|
// Extract any trailing arguments meant for the inner CLI
|
package/dist/commands/init.js
CHANGED
|
@@ -5,6 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.init = init;
|
|
7
7
|
const fs_1 = require("fs");
|
|
8
|
+
const child_process_1 = require("child_process");
|
|
9
|
+
const path_1 = require("path");
|
|
8
10
|
const chalk_1 = __importDefault(require("chalk"));
|
|
9
11
|
const inquirer_1 = __importDefault(require("inquirer"));
|
|
10
12
|
const ora_1 = __importDefault(require("ora"));
|
|
@@ -15,6 +17,7 @@ const settings_1 = require("../deploy/settings");
|
|
|
15
17
|
// DEPRECATED: Hooks removed in hookless architecture migration
|
|
16
18
|
// import { deployHooks } from '../deploy/hooks';
|
|
17
19
|
const skills_1 = require("../deploy/skills");
|
|
20
|
+
const agents_1 = require("../deploy/agents");
|
|
18
21
|
const instructions_1 = require("../deploy/instructions");
|
|
19
22
|
const templates_1 = require("../utils/templates");
|
|
20
23
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -34,15 +37,20 @@ async function pollForApproval(code, expiresIn) {
|
|
|
34
37
|
const pollInterval = 3000; // 3 seconds
|
|
35
38
|
const maxAttempts = Math.floor((expiresIn * 1000) / pollInterval);
|
|
36
39
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (response.ok) {
|
|
42
|
-
const data = await response.json();
|
|
43
|
-
if (data.status !== 'pending') {
|
|
44
|
-
return data;
|
|
40
|
+
try {
|
|
41
|
+
const response = await fetch(`${platform_1.PLATFORM_URL}/api/device-auth/codes/${code}/status`, { method: 'GET' });
|
|
42
|
+
if (response.status === 410) {
|
|
43
|
+
return { status: 'expired' };
|
|
45
44
|
}
|
|
45
|
+
if (response.ok) {
|
|
46
|
+
const data = await response.json();
|
|
47
|
+
if (data.status !== 'pending') {
|
|
48
|
+
return data;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Network error — silently retry instead of killing the flow
|
|
46
54
|
}
|
|
47
55
|
// Wait before next poll
|
|
48
56
|
await new Promise(resolve => setTimeout(resolve, pollInterval));
|
|
@@ -77,17 +85,33 @@ async function deviceAuth() {
|
|
|
77
85
|
throw error;
|
|
78
86
|
}
|
|
79
87
|
console.log('');
|
|
80
|
-
console.log(chalk_1.default.white.bold(`Your code: ${chalk_1.default.cyan.bold(deviceCode.code)}`));
|
|
88
|
+
console.log(chalk_1.default.white.bold(` Your code: ${chalk_1.default.cyan.bold(deviceCode.code)}`));
|
|
89
|
+
// Auto-copy code to clipboard
|
|
90
|
+
try {
|
|
91
|
+
if (process.platform === 'darwin') {
|
|
92
|
+
(0, child_process_1.execSync)(`echo -n "${deviceCode.code}" | pbcopy`, { stdio: 'ignore' });
|
|
93
|
+
}
|
|
94
|
+
else if (process.platform === 'win32') {
|
|
95
|
+
(0, child_process_1.execSync)(`echo|set /p="${deviceCode.code}" | clip`, { stdio: 'ignore' });
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
(0, child_process_1.execSync)(`echo -n "${deviceCode.code}" | xclip -selection clipboard`, { stdio: 'ignore' });
|
|
99
|
+
}
|
|
100
|
+
console.log(chalk_1.default.green(' ✓ Copied to clipboard'));
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// Clipboard not available — no problem, code is displayed
|
|
104
|
+
}
|
|
81
105
|
console.log('');
|
|
82
106
|
// Open browser
|
|
83
107
|
const verificationUrl = deviceCode.verificationUrl || `${platform_1.PLATFORM_URL}/activate`;
|
|
84
|
-
console.log(chalk_1.default.gray(`Opening browser
|
|
108
|
+
console.log(chalk_1.default.gray(` Opening browser → ${verificationUrl}`));
|
|
85
109
|
try {
|
|
86
110
|
await (0, open_1.default)(verificationUrl);
|
|
87
111
|
}
|
|
88
112
|
catch {
|
|
89
|
-
console.log(chalk_1.default.yellow('Could not open browser automatically.'));
|
|
90
|
-
console.log(chalk_1.default.gray(`Please visit: ${verificationUrl}`));
|
|
113
|
+
console.log(chalk_1.default.yellow(' Could not open browser automatically.'));
|
|
114
|
+
console.log(chalk_1.default.gray(` Please visit: ${verificationUrl}`));
|
|
91
115
|
}
|
|
92
116
|
console.log('');
|
|
93
117
|
// Poll for approval
|
|
@@ -217,13 +241,13 @@ async function selectIDEs(autoSelect = false) {
|
|
|
217
241
|
}
|
|
218
242
|
console.log('');
|
|
219
243
|
}
|
|
220
|
-
// Auto-select
|
|
244
|
+
// Auto-select: configure ALL detected IDEs (no prompt)
|
|
221
245
|
if (autoSelect || detectedList.length === 1) {
|
|
222
|
-
const
|
|
223
|
-
const
|
|
224
|
-
console.log(chalk_1.default.green(`✓ Auto-detected: ${chalk_1.default.bold(
|
|
246
|
+
const toSetup = detectedList.length > 0 ? detectedList : [current ?? 'claude'];
|
|
247
|
+
const names = toSetup.map(id => id === 'claude' ? 'Claude Code' : id === 'cursor' ? 'Cursor' : 'Windsurf');
|
|
248
|
+
console.log(chalk_1.default.green(`✓ Auto-detected: ${chalk_1.default.bold(names.join(', '))}`));
|
|
225
249
|
console.log('');
|
|
226
|
-
return
|
|
250
|
+
return toSetup;
|
|
227
251
|
}
|
|
228
252
|
const ideChoices = [
|
|
229
253
|
{ name: 'Claude Code', value: 'claude', checked: detectedList.includes('claude') || current === 'claude' },
|
|
@@ -249,6 +273,7 @@ async function deployForClaude(apiKey, userId, options) {
|
|
|
249
273
|
mcp: false,
|
|
250
274
|
settings: false,
|
|
251
275
|
skills: { count: 0, skills: [] },
|
|
276
|
+
agents: { count: 0, agents: [] },
|
|
252
277
|
instructions: false
|
|
253
278
|
};
|
|
254
279
|
// MCP configuration
|
|
@@ -283,12 +308,21 @@ async function deployForClaude(apiKey, userId, options) {
|
|
|
283
308
|
spinner.fail('Skills deployment failed');
|
|
284
309
|
}
|
|
285
310
|
}
|
|
286
|
-
//
|
|
311
|
+
// Agents (prune, rewind, scout, trace)
|
|
312
|
+
spinner = (0, ora_1.default)('Deploying ekkOS agents...').start();
|
|
313
|
+
try {
|
|
314
|
+
result.agents = (0, agents_1.deployAgents)();
|
|
315
|
+
spinner.succeed(`Agents (${result.agents.agents.join(', ')})`);
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
spinner.fail('Agent deployment failed');
|
|
319
|
+
}
|
|
320
|
+
// CLAUDE.md (merge — never overwrites existing user content)
|
|
287
321
|
spinner = (0, ora_1.default)('Deploying global instructions...').start();
|
|
288
322
|
try {
|
|
289
323
|
(0, instructions_1.deployInstructions)();
|
|
290
324
|
result.instructions = true;
|
|
291
|
-
spinner.succeed('Global instructions (CLAUDE.md)');
|
|
325
|
+
spinner.succeed('Global instructions (CLAUDE.md — merged)');
|
|
292
326
|
}
|
|
293
327
|
catch (error) {
|
|
294
328
|
spinner.fail('Global instructions failed');
|
|
@@ -461,6 +495,40 @@ async function init(options) {
|
|
|
461
495
|
catch {
|
|
462
496
|
verifySpinner.warn('Could not reach ekkOS API — check your network');
|
|
463
497
|
}
|
|
498
|
+
// Phase: Scaffold ekkos.yml if not present in the current directory or git root
|
|
499
|
+
const cwd = process.cwd();
|
|
500
|
+
let projectRoot = cwd;
|
|
501
|
+
try {
|
|
502
|
+
projectRoot = (0, child_process_1.execSync)('git rev-parse --show-toplevel', { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
503
|
+
}
|
|
504
|
+
catch { /* not a git repo — use cwd */ }
|
|
505
|
+
const ekkosYmlPath = (0, path_1.join)(projectRoot, 'ekkos.yml');
|
|
506
|
+
if (!(0, fs_1.existsSync)(ekkosYmlPath)) {
|
|
507
|
+
const projectName = (0, path_1.basename)(projectRoot);
|
|
508
|
+
const hasPackageJson = (0, fs_1.existsSync)((0, path_1.join)(projectRoot, 'package.json'));
|
|
509
|
+
const hasCargo = (0, fs_1.existsSync)((0, path_1.join)(projectRoot, 'Cargo.toml'));
|
|
510
|
+
const buildCmd = hasPackageJson ? 'npm run build' : hasCargo ? 'cargo build' : '';
|
|
511
|
+
const testCmd = hasPackageJson ? 'npm test' : hasCargo ? 'cargo test' : '';
|
|
512
|
+
const lintCmd = hasPackageJson ? 'npm run lint' : hasCargo ? 'cargo clippy' : '';
|
|
513
|
+
const ymlLines = [
|
|
514
|
+
'version: 1',
|
|
515
|
+
`project: "${projectName}"`,
|
|
516
|
+
'',
|
|
517
|
+
'ans:',
|
|
518
|
+
' tier: "R1"',
|
|
519
|
+
];
|
|
520
|
+
if (buildCmd)
|
|
521
|
+
ymlLines.push(` build: "${buildCmd}"`);
|
|
522
|
+
if (testCmd)
|
|
523
|
+
ymlLines.push(` test: "${testCmd}"`);
|
|
524
|
+
if (lintCmd)
|
|
525
|
+
ymlLines.push(` lint: "${lintCmd}"`);
|
|
526
|
+
ymlLines.push('');
|
|
527
|
+
(0, fs_1.writeFileSync)(ekkosYmlPath, ymlLines.join('\n'), 'utf-8');
|
|
528
|
+
console.log('');
|
|
529
|
+
console.log(chalk_1.default.green(` Created ${chalk_1.default.bold('ekkos.yml')} in ${projectRoot}`));
|
|
530
|
+
console.log(chalk_1.default.gray(` Customize: ${chalk_1.default.cyan('https://platform.ekkos.dev/dashboard/settings/project')}`));
|
|
531
|
+
}
|
|
464
532
|
// Summary with prominent next step
|
|
465
533
|
const ideNames = installedIDEs.map(id => id === 'claude' ? 'Claude Code' : id === 'cursor' ? 'Cursor' : 'Windsurf');
|
|
466
534
|
const mcpPaths = {
|
|
@@ -488,8 +556,11 @@ async function init(options) {
|
|
|
488
556
|
console.log(chalk_1.default.yellow.bold(' NEXT STEPS:'));
|
|
489
557
|
console.log('');
|
|
490
558
|
console.log(chalk_1.default.white(` 1. Restart ${ideNames.join(' / ')}`));
|
|
491
|
-
console.log(chalk_1.default.white(' 2. Run ') + chalk_1.default.cyan.bold('ekkos') + chalk_1.default.white(' to
|
|
559
|
+
console.log(chalk_1.default.white(' 2. Run ') + chalk_1.default.cyan.bold('ekkos scan') + chalk_1.default.white(' to map your project systems'));
|
|
560
|
+
console.log(chalk_1.default.white(' 3. Run ') + chalk_1.default.cyan.bold('ekkos') + chalk_1.default.white(' to start coding with memory'));
|
|
492
561
|
console.log('');
|
|
493
|
-
console.log(chalk_1.default.gray(` Dashboard:
|
|
562
|
+
console.log(chalk_1.default.gray(` Dashboard: https://platform.ekkos.dev/dashboard`));
|
|
563
|
+
console.log(chalk_1.default.gray(` Configure ANS: https://platform.ekkos.dev/dashboard/settings/project`));
|
|
564
|
+
console.log(chalk_1.default.gray(` Connect vitals: https://platform.ekkos.dev/dashboard/settings/vitals`));
|
|
494
565
|
console.log('');
|
|
495
566
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.watchLivingDocs = watchLivingDocs;
|
|
7
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const living_docs_manager_js_1 = require("../local/living-docs-manager.js");
|
|
10
|
+
const state_1 = require("../utils/state");
|
|
11
|
+
const platform_js_1 = require("../utils/platform.js");
|
|
12
|
+
function printStartupSummary(options) {
|
|
13
|
+
console.log('');
|
|
14
|
+
console.log(chalk_1.default.cyan.bold(' ekkOS Living Docs Watch'));
|
|
15
|
+
console.log(chalk_1.default.gray(' ─────────────────────────'));
|
|
16
|
+
console.log(chalk_1.default.gray(` Path: ${options.targetPath}`));
|
|
17
|
+
console.log(chalk_1.default.gray(` Timezone: ${options.timeZone}`));
|
|
18
|
+
console.log(chalk_1.default.gray(` Registry seed: ${options.seedingEnabled ? 'enabled' : 'disabled'}`));
|
|
19
|
+
console.log('');
|
|
20
|
+
console.log(chalk_1.default.gray(' Watching local files and rewriting ekkOS_CONTEXT.md on change.'));
|
|
21
|
+
console.log(chalk_1.default.gray(' Press Ctrl+C to stop.'));
|
|
22
|
+
console.log('');
|
|
23
|
+
}
|
|
24
|
+
async function watchLivingDocs(options) {
|
|
25
|
+
const targetPath = (0, path_1.resolve)(options.path || process.cwd());
|
|
26
|
+
const timeZone = options.timeZone || process.env.EKKOS_USER_TIMEZONE || Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
27
|
+
const apiKey = options.noSeed ? null : (0, state_1.getAuthToken)();
|
|
28
|
+
const apiUrl = options.noSeed ? undefined : (process.env.EKKOS_API_URL || platform_js_1.MCP_API_URL);
|
|
29
|
+
const manager = new living_docs_manager_js_1.LocalLivingDocsManager({
|
|
30
|
+
targetPath,
|
|
31
|
+
apiUrl,
|
|
32
|
+
apiKey,
|
|
33
|
+
timeZone,
|
|
34
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
35
|
+
flushDebounceMs: options.debounceMs,
|
|
36
|
+
onLog: message => console.log(chalk_1.default.gray(` ${message}`)),
|
|
37
|
+
});
|
|
38
|
+
printStartupSummary({
|
|
39
|
+
targetPath,
|
|
40
|
+
timeZone,
|
|
41
|
+
seedingEnabled: !!(apiUrl && apiKey),
|
|
42
|
+
});
|
|
43
|
+
manager.start();
|
|
44
|
+
await new Promise((resolvePromise) => {
|
|
45
|
+
let stopped = false;
|
|
46
|
+
const stop = () => {
|
|
47
|
+
if (stopped)
|
|
48
|
+
return;
|
|
49
|
+
stopped = true;
|
|
50
|
+
manager.stop();
|
|
51
|
+
process.off('SIGINT', handleSigInt);
|
|
52
|
+
process.off('SIGTERM', handleSigTerm);
|
|
53
|
+
resolvePromise();
|
|
54
|
+
};
|
|
55
|
+
const handleSigInt = () => {
|
|
56
|
+
console.log('');
|
|
57
|
+
console.log(chalk_1.default.gray(' Stopping living docs watcher...'));
|
|
58
|
+
stop();
|
|
59
|
+
};
|
|
60
|
+
const handleSigTerm = () => {
|
|
61
|
+
stop();
|
|
62
|
+
};
|
|
63
|
+
process.on('SIGINT', handleSigInt);
|
|
64
|
+
process.on('SIGTERM', handleSigTerm);
|
|
65
|
+
});
|
|
66
|
+
}
|
package/dist/commands/run.js
CHANGED
|
@@ -49,6 +49,7 @@ const doctor_1 = require("./doctor");
|
|
|
49
49
|
const stream_tailer_1 = require("../capture/stream-tailer");
|
|
50
50
|
const jsonl_rewriter_1 = require("../capture/jsonl-rewriter");
|
|
51
51
|
const transcript_repair_1 = require("../capture/transcript-repair");
|
|
52
|
+
const living_docs_manager_js_1 = require("../local/living-docs-manager.js");
|
|
52
53
|
// Try to load node-pty (may fail on Node 24+)
|
|
53
54
|
// IMPORTANT: This must be awaited in run() to avoid racey false fallbacks.
|
|
54
55
|
let pty = null;
|
|
@@ -291,10 +292,12 @@ const isWindows = os.platform() === 'win32';
|
|
|
291
292
|
// Cosmetic patches may fail on newer versions but don't affect functionality
|
|
292
293
|
const PINNED_CLAUDE_VERSION = 'latest';
|
|
293
294
|
// Max output tokens for Claude responses
|
|
294
|
-
// Default:
|
|
295
|
-
//
|
|
296
|
-
//
|
|
297
|
-
|
|
295
|
+
// Default: 128000 (Opus 4.6 / Sonnet 4.6 max output)
|
|
296
|
+
// Override via EKKOS_MAX_OUTPUT_TOKENS env var if needed
|
|
297
|
+
// Note: Claude Code has a known bug where CLAUDE_CODE_MAX_OUTPUT_TOKENS is ignored on Opus 4.6
|
|
298
|
+
// (see github.com/anthropics/claude-code/issues/24159). We set it anyway + the proxy also
|
|
299
|
+
// enforces this via max_tokens override on the API request.
|
|
300
|
+
const EKKOS_MAX_OUTPUT_TOKENS = process.env.EKKOS_MAX_OUTPUT_TOKENS || '128000';
|
|
298
301
|
const proxy_url_1 = require("../utils/proxy-url");
|
|
299
302
|
// Track proxy mode for getEkkosEnv (set by run() based on options)
|
|
300
303
|
let proxyModeEnabled = true;
|
|
@@ -315,7 +318,7 @@ let cliSessionName = null;
|
|
|
315
318
|
let cliSessionId = null;
|
|
316
319
|
/**
|
|
317
320
|
* Get environment with ekkOS enhancements
|
|
318
|
-
* - Sets CLAUDE_CODE_MAX_OUTPUT_TOKENS to
|
|
321
|
+
* - Sets CLAUDE_CODE_MAX_OUTPUT_TOKENS to 128K (API max for Opus/Sonnet 4.6)
|
|
319
322
|
* - Routes API through ekkOS proxy for seamless context eviction (when enabled)
|
|
320
323
|
* - Sets EKKOS_PROXY_MODE to signal JSONL rewriter to disable eviction
|
|
321
324
|
* - Passes session headers for eviction/retrieval context tracking
|
|
@@ -332,6 +335,11 @@ function getEkkosEnv() {
|
|
|
332
335
|
// Native autocompact would compact without ekkOS saving state first, causing knowledge loss.
|
|
333
336
|
// Only ekkOS-wrapped sessions get this; vanilla `claude` keeps autocompact on.
|
|
334
337
|
DISABLE_AUTO_COMPACT: 'true',
|
|
338
|
+
// Set max output tokens to 128K (Opus 4.6 / Sonnet 4.6 API max).
|
|
339
|
+
// Note: Claude Code has a known bug where this env var is ignored on Opus 4.6
|
|
340
|
+
// (github.com/anthropics/claude-code/issues/24159). The proxy also enforces
|
|
341
|
+
// this by overriding max_tokens on the API request as a fallback.
|
|
342
|
+
CLAUDE_CODE_MAX_OUTPUT_TOKENS: EKKOS_MAX_OUTPUT_TOKENS,
|
|
335
343
|
};
|
|
336
344
|
/* eslint-enable no-restricted-syntax */
|
|
337
345
|
// Check if proxy is disabled via env var or options
|
|
@@ -504,7 +512,9 @@ function resolveClaudePath() {
|
|
|
504
512
|
// No system Claude found — fall through to ekkOS-managed install
|
|
505
513
|
}
|
|
506
514
|
// ekkOS-managed installation (for pinned versions or no system Claude)
|
|
507
|
-
|
|
515
|
+
// When 'latest', skip the slow `claude --version` subprocess — any version is fine.
|
|
516
|
+
const ekkosClaudeExists = fs.existsSync(EKKOS_CLAUDE_BIN);
|
|
517
|
+
if (ekkosClaudeExists && (PINNED_CLAUDE_VERSION === 'latest' || checkClaudeVersion(EKKOS_CLAUDE_BIN))) {
|
|
508
518
|
return EKKOS_CLAUDE_BIN;
|
|
509
519
|
}
|
|
510
520
|
// Auto-install to ekkOS-managed directory
|
|
@@ -811,6 +821,9 @@ function writeSessionFiles(sessionId, sessionName) {
|
|
|
811
821
|
* Launch ekkos run + dashboard in isolated tmux panes (60/40 split)
|
|
812
822
|
*/
|
|
813
823
|
function launchWithDashboard(options) {
|
|
824
|
+
// Prune dead-PID entries from active-sessions.json so the dashboard
|
|
825
|
+
// (and resolveSessionName) never cross-bind to stale sessions after restart.
|
|
826
|
+
(0, state_1.getActiveSessions)();
|
|
814
827
|
const tmuxSession = `ekkos-${Date.now().toString(36)}`;
|
|
815
828
|
const launchTime = Date.now();
|
|
816
829
|
// Pre-generate session name so dashboard can start immediately (no polling).
|
|
@@ -1043,6 +1056,24 @@ async function run(options) {
|
|
|
1043
1056
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1044
1057
|
(0, state_1.ensureEkkosDir)();
|
|
1045
1058
|
(0, state_1.clearAutoClearFlag)();
|
|
1059
|
+
// Construct manager early but defer .start() until Claude's first idle prompt
|
|
1060
|
+
// (filesystem scans run only after Claude is fully loaded and waiting for input)
|
|
1061
|
+
let localLivingDocsManager = null;
|
|
1062
|
+
if (process.env.EKKOS_LOCAL_LIVING_DOCS !== '0') {
|
|
1063
|
+
try {
|
|
1064
|
+
localLivingDocsManager = new living_docs_manager_js_1.LocalLivingDocsManager({
|
|
1065
|
+
targetPath: process.cwd(),
|
|
1066
|
+
apiUrl: process.env.EKKOS_API_URL || MEMORY_API_URL,
|
|
1067
|
+
apiKey: (0, state_1.getAuthToken)(),
|
|
1068
|
+
timeZone: process.env.EKKOS_USER_TIMEZONE || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
1069
|
+
onLog: message => dlog(message),
|
|
1070
|
+
});
|
|
1071
|
+
// .start() deferred to first idle prompt — see idle detection below (~line 2685)
|
|
1072
|
+
}
|
|
1073
|
+
catch (err) {
|
|
1074
|
+
dlog(`[LivingDocs:Local] Failed to start: ${err.message}`);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1046
1077
|
// Resolve Claude path
|
|
1047
1078
|
const rawClaudePath = resolveClaudePath();
|
|
1048
1079
|
const isNpxMode = rawClaudePath.startsWith('npx:');
|
|
@@ -1321,6 +1352,7 @@ async function run(options) {
|
|
|
1321
1352
|
// Clear terminal to prevent startup banner artifacts bleeding into Claude Code's Ink TUI
|
|
1322
1353
|
process.stdout.write('\x1B[2J\x1B[H');
|
|
1323
1354
|
dlog(`Animation complete. shellReady=${shellReady}, buffered=${bufferedOutput.length} chunks`);
|
|
1355
|
+
// Living docs .start() is deferred further — triggers on first idle prompt (Claude fully loaded)
|
|
1324
1356
|
// Show loading indicator if Claude is still initializing
|
|
1325
1357
|
if (shellReady && bufferedOutput.length === 0) {
|
|
1326
1358
|
process.stdout.write(chalk_1.default.gray(' Connecting to Claude...'));
|
|
@@ -1367,6 +1399,7 @@ async function run(options) {
|
|
|
1367
1399
|
// Tracks idle→active transitions to print the session banner once per turn
|
|
1368
1400
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1369
1401
|
let wasIdle = false; // Start as NOT idle — first idle→active fires after startup
|
|
1402
|
+
let livingDocsStarted = false; // Deferred until Claude's first idle prompt
|
|
1370
1403
|
let turnCount = 0; // Incremented each time a new turn banner prints
|
|
1371
1404
|
let lastBannerTime = Date.now(); // Grace period so startup output doesn't trigger banner
|
|
1372
1405
|
// Stream tailer for mid-turn context capture (must be declared before polling code)
|
|
@@ -2324,6 +2357,20 @@ async function run(options) {
|
|
|
2324
2357
|
// We check the raw outputBuffer (stripped) so the idle regex fires reliably.
|
|
2325
2358
|
if (IDLE_PROMPT_REGEX.test(stripAnsi(outputBuffer))) {
|
|
2326
2359
|
wasIdle = true;
|
|
2360
|
+
// Start living docs on first idle prompt — Claude is fully loaded, won't compete for I/O
|
|
2361
|
+
if (!livingDocsStarted && localLivingDocsManager) {
|
|
2362
|
+
livingDocsStarted = true;
|
|
2363
|
+
// Use setImmediate so the current data handler returns first
|
|
2364
|
+
setImmediate(() => {
|
|
2365
|
+
try {
|
|
2366
|
+
localLivingDocsManager.start();
|
|
2367
|
+
dlog('[LivingDocs:Local] Manager started (first idle prompt)');
|
|
2368
|
+
}
|
|
2369
|
+
catch (err) {
|
|
2370
|
+
dlog(`[LivingDocs:Local] Failed to start: ${err.message}`);
|
|
2371
|
+
}
|
|
2372
|
+
});
|
|
2373
|
+
}
|
|
2327
2374
|
}
|
|
2328
2375
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2329
2376
|
// ORPHAN TOOL_RESULT DETECTION (LOCAL MODE ONLY)
|
|
@@ -2711,6 +2758,7 @@ Use Perplexity for deep research. Be thorough but efficient. Start now.`;
|
|
|
2711
2758
|
// Handle PTY exit
|
|
2712
2759
|
shell.onExit(async ({ exitCode }) => {
|
|
2713
2760
|
(0, state_1.clearAutoClearFlag)();
|
|
2761
|
+
localLivingDocsManager?.stop();
|
|
2714
2762
|
stopStreamTailer(); // Stop stream capture
|
|
2715
2763
|
(0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
|
|
2716
2764
|
cleanupInstanceFile(instanceId); // Clean up instance file
|
|
@@ -2730,6 +2778,7 @@ Use Perplexity for deep research. Be thorough but efficient. Start now.`;
|
|
|
2730
2778
|
// Cleanup on exit signals
|
|
2731
2779
|
const cleanup = () => {
|
|
2732
2780
|
(0, state_1.clearAutoClearFlag)();
|
|
2781
|
+
localLivingDocsManager?.stop();
|
|
2733
2782
|
stopStreamTailer(); // Stop stream capture
|
|
2734
2783
|
(0, state_1.unregisterActiveSession)(); // Remove from active sessions registry
|
|
2735
2784
|
cleanupInstanceFile(instanceId); // Clean up instance file
|