@ekkos/cli 1.3.6 → 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 +96 -21
- package/dist/commands/gemini.js +46 -9
- 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 +192 -79
- 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 +1 -1
- package/templates/CLAUDE.md +89 -99
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
|
package/dist/commands/scan.d.ts
CHANGED
|
@@ -12,10 +12,78 @@
|
|
|
12
12
|
*
|
|
13
13
|
* Reuses discovery logic from apps/memory/workers/context-compiler/registry-seed.ts
|
|
14
14
|
*/
|
|
15
|
+
export interface SystemEntry {
|
|
16
|
+
system_id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description: string;
|
|
19
|
+
directory_path: string;
|
|
20
|
+
domain: string;
|
|
21
|
+
status: 'active';
|
|
22
|
+
parent_system_id: string | null;
|
|
23
|
+
metadata: Record<string, unknown>;
|
|
24
|
+
tags: string[];
|
|
25
|
+
aliases: string[];
|
|
26
|
+
}
|
|
15
27
|
interface ScanOptions {
|
|
16
28
|
compile?: boolean;
|
|
17
29
|
dryRun?: boolean;
|
|
18
30
|
path?: string;
|
|
19
31
|
}
|
|
32
|
+
export interface SeedResponse {
|
|
33
|
+
ok: boolean;
|
|
34
|
+
inserted: number;
|
|
35
|
+
updated: number;
|
|
36
|
+
errors: string[];
|
|
37
|
+
total: number;
|
|
38
|
+
duration_ms: number;
|
|
39
|
+
compile?: {
|
|
40
|
+
triggered: boolean;
|
|
41
|
+
error?: string;
|
|
42
|
+
reason?: string;
|
|
43
|
+
} | null;
|
|
44
|
+
error?: string;
|
|
45
|
+
}
|
|
46
|
+
export declare function loadApiKey(): string | null;
|
|
47
|
+
export declare function findGitRoot(startPath: string): string | null;
|
|
48
|
+
export declare function discoverSystems(targetPath: string, options?: {
|
|
49
|
+
scopeToTarget?: boolean;
|
|
50
|
+
}): {
|
|
51
|
+
gitRoot: string | null;
|
|
52
|
+
repoRoot: string;
|
|
53
|
+
targetRoot: string;
|
|
54
|
+
scopePath: string;
|
|
55
|
+
systems: SystemEntry[];
|
|
56
|
+
ekkosYml: EkkosYmlConfig | null;
|
|
57
|
+
};
|
|
58
|
+
export interface EkkosYmlSystem {
|
|
59
|
+
path: string;
|
|
60
|
+
name?: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
}
|
|
63
|
+
export interface EkkosYmlConfig {
|
|
64
|
+
version?: number;
|
|
65
|
+
project?: string;
|
|
66
|
+
description?: string;
|
|
67
|
+
stack?: {
|
|
68
|
+
language?: string;
|
|
69
|
+
framework?: string;
|
|
70
|
+
version?: string;
|
|
71
|
+
package_manager?: string;
|
|
72
|
+
};
|
|
73
|
+
systems?: EkkosYmlSystem[];
|
|
74
|
+
key_files?: string[];
|
|
75
|
+
ans?: {
|
|
76
|
+
tier?: string;
|
|
77
|
+
build?: string;
|
|
78
|
+
test?: string;
|
|
79
|
+
lint?: string;
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export declare function seedSystems(options: {
|
|
83
|
+
systems: SystemEntry[];
|
|
84
|
+
apiUrl: string;
|
|
85
|
+
apiKey: string;
|
|
86
|
+
compile?: boolean;
|
|
87
|
+
}): Promise<SeedResponse>;
|
|
20
88
|
export declare function scan(options: ScanOptions): Promise<void>;
|
|
21
89
|
export {};
|
package/dist/commands/scan.js
CHANGED
|
@@ -13,13 +13,51 @@
|
|
|
13
13
|
*
|
|
14
14
|
* Reuses discovery logic from apps/memory/workers/context-compiler/registry-seed.ts
|
|
15
15
|
*/
|
|
16
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
19
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
20
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
21
|
+
}
|
|
22
|
+
Object.defineProperty(o, k2, desc);
|
|
23
|
+
}) : (function(o, m, k, k2) {
|
|
24
|
+
if (k2 === undefined) k2 = k;
|
|
25
|
+
o[k2] = m[k];
|
|
26
|
+
}));
|
|
27
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
28
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
29
|
+
}) : function(o, v) {
|
|
30
|
+
o["default"] = v;
|
|
31
|
+
});
|
|
32
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
33
|
+
var ownKeys = function(o) {
|
|
34
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
35
|
+
var ar = [];
|
|
36
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
37
|
+
return ar;
|
|
38
|
+
};
|
|
39
|
+
return ownKeys(o);
|
|
40
|
+
};
|
|
41
|
+
return function (mod) {
|
|
42
|
+
if (mod && mod.__esModule) return mod;
|
|
43
|
+
var result = {};
|
|
44
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
45
|
+
__setModuleDefault(result, mod);
|
|
46
|
+
return result;
|
|
47
|
+
};
|
|
48
|
+
})();
|
|
16
49
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
50
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
51
|
};
|
|
19
52
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
53
|
+
exports.loadApiKey = loadApiKey;
|
|
54
|
+
exports.findGitRoot = findGitRoot;
|
|
55
|
+
exports.discoverSystems = discoverSystems;
|
|
56
|
+
exports.seedSystems = seedSystems;
|
|
20
57
|
exports.scan = scan;
|
|
21
58
|
const fs_1 = require("fs");
|
|
22
59
|
const path_1 = require("path");
|
|
60
|
+
const os_1 = require("os");
|
|
23
61
|
const chalk_1 = __importDefault(require("chalk"));
|
|
24
62
|
const ora_1 = __importDefault(require("ora"));
|
|
25
63
|
const platform_js_1 = require("../utils/platform.js");
|
|
@@ -58,6 +96,27 @@ function detectDomain(dirPath) {
|
|
|
58
96
|
const topLevel = dirPath.split('/')[0];
|
|
59
97
|
return DOMAIN_MAP[topLevel] || 'other';
|
|
60
98
|
}
|
|
99
|
+
function normalizeSystemPath(dirPath) {
|
|
100
|
+
return dirPath.replace(/\\/g, '/').replace(/^\.\//, '').replace(/\/+$/, '');
|
|
101
|
+
}
|
|
102
|
+
function resolveTargetDirectory(targetPath) {
|
|
103
|
+
const resolved = (0, path_1.resolve)(targetPath);
|
|
104
|
+
try {
|
|
105
|
+
return (0, fs_1.statSync)(resolved).isDirectory() ? resolved : (0, path_1.dirname)(resolved);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return resolved;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function intersectsScope(systemPath, scopePath) {
|
|
112
|
+
const normalizedSystem = normalizeSystemPath(systemPath);
|
|
113
|
+
const normalizedScope = normalizeSystemPath(scopePath);
|
|
114
|
+
if (!normalizedScope || normalizedScope === '.')
|
|
115
|
+
return true;
|
|
116
|
+
return normalizedSystem === normalizedScope ||
|
|
117
|
+
normalizedSystem.startsWith(`${normalizedScope}/`) ||
|
|
118
|
+
normalizedScope.startsWith(`${normalizedSystem}/`);
|
|
119
|
+
}
|
|
61
120
|
// ── System ID generation ─────────────────────────────────────────────────
|
|
62
121
|
function toSystemId(dirPath) {
|
|
63
122
|
return dirPath
|
|
@@ -249,6 +308,208 @@ function findGitRoot(startPath) {
|
|
|
249
308
|
}
|
|
250
309
|
return null;
|
|
251
310
|
}
|
|
311
|
+
function discoverSystems(targetPath, options) {
|
|
312
|
+
const targetRoot = resolveTargetDirectory(targetPath);
|
|
313
|
+
const gitRoot = findGitRoot(targetRoot);
|
|
314
|
+
const repoRoot = gitRoot || targetRoot;
|
|
315
|
+
const scopePath = normalizeSystemPath((0, path_1.relative)(repoRoot, targetRoot) || '.');
|
|
316
|
+
// Read ekkos.yml for system overrides and stack config
|
|
317
|
+
const ekkosYml = readEkkosYml(repoRoot);
|
|
318
|
+
let systems;
|
|
319
|
+
// If ekkos.yml has explicit systems[], use those as overrides
|
|
320
|
+
if (ekkosYml?.systems && ekkosYml.systems.length > 0) {
|
|
321
|
+
const overrideSystems = ekkosYml.systems.map(s => ({
|
|
322
|
+
system_id: toSystemId(s.path),
|
|
323
|
+
name: s.name || (0, path_1.basename)(s.path),
|
|
324
|
+
description: s.description || '',
|
|
325
|
+
directory_path: normalizeSystemPath(s.path),
|
|
326
|
+
domain: detectDomain(s.path),
|
|
327
|
+
status: 'active',
|
|
328
|
+
parent_system_id: null,
|
|
329
|
+
metadata: {},
|
|
330
|
+
tags: [],
|
|
331
|
+
aliases: [],
|
|
332
|
+
}));
|
|
333
|
+
// Also discover auto-detected systems and merge (overrides take precedence)
|
|
334
|
+
const autoSystems = assignParentSystems(scanDirectory(repoRoot, repoRoot, 0, 4, null));
|
|
335
|
+
const overridePaths = new Set(overrideSystems.map(s => s.directory_path));
|
|
336
|
+
systems = [
|
|
337
|
+
...overrideSystems,
|
|
338
|
+
...autoSystems.filter(s => !overridePaths.has(s.directory_path)),
|
|
339
|
+
];
|
|
340
|
+
systems = assignParentSystems(systems);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
const rawSystems = scanDirectory(repoRoot, repoRoot, 0, 4, null);
|
|
344
|
+
systems = assignParentSystems(rawSystems);
|
|
345
|
+
}
|
|
346
|
+
if (options?.scopeToTarget && scopePath !== '.') {
|
|
347
|
+
systems = assignParentSystems(systems
|
|
348
|
+
.filter(system => intersectsScope(system.directory_path, scopePath))
|
|
349
|
+
.map(system => ({
|
|
350
|
+
...system,
|
|
351
|
+
parent_system_id: null,
|
|
352
|
+
})));
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
gitRoot,
|
|
356
|
+
repoRoot,
|
|
357
|
+
targetRoot,
|
|
358
|
+
scopePath,
|
|
359
|
+
systems,
|
|
360
|
+
ekkosYml,
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function readEkkosYml(repoRoot) {
|
|
364
|
+
const names = ['ekkos.yml', 'ekkos.yaml', '.ekkos.yml', '.ekkos.yaml'];
|
|
365
|
+
for (const name of names) {
|
|
366
|
+
const filePath = (0, path_1.join)(repoRoot, name);
|
|
367
|
+
if ((0, fs_1.existsSync)(filePath)) {
|
|
368
|
+
try {
|
|
369
|
+
const raw = (0, fs_1.readFileSync)(filePath, 'utf-8');
|
|
370
|
+
// Simple YAML parser for the flat structure we need (avoid adding js-yaml dep)
|
|
371
|
+
return parseSimpleYaml(raw);
|
|
372
|
+
}
|
|
373
|
+
catch { /* malformed — skip */ }
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
/**
|
|
379
|
+
* Minimal YAML parser for ekkos.yml — handles the flat/nested structure we need.
|
|
380
|
+
* Does NOT support full YAML spec. For production, consider js-yaml.
|
|
381
|
+
*/
|
|
382
|
+
function parseSimpleYaml(raw) {
|
|
383
|
+
const result = {};
|
|
384
|
+
const lines = raw.split('\n');
|
|
385
|
+
let currentSection = null;
|
|
386
|
+
let currentItem = null;
|
|
387
|
+
const systems = [];
|
|
388
|
+
for (const line of lines) {
|
|
389
|
+
const stripped = line.replace(/#.*$/, '').trimEnd();
|
|
390
|
+
if (!stripped || stripped.trim() === '')
|
|
391
|
+
continue;
|
|
392
|
+
// Top-level key
|
|
393
|
+
const topMatch = stripped.match(/^(\w+):\s*"?([^"]*)"?\s*$/);
|
|
394
|
+
if (topMatch && !stripped.startsWith(' ') && !stripped.startsWith('\t')) {
|
|
395
|
+
const [, key, value] = topMatch;
|
|
396
|
+
if (key === 'version')
|
|
397
|
+
result.version = parseInt(value, 10);
|
|
398
|
+
else if (key === 'project')
|
|
399
|
+
result.project = value;
|
|
400
|
+
else if (key === 'description')
|
|
401
|
+
result.description = value;
|
|
402
|
+
currentSection = key;
|
|
403
|
+
continue;
|
|
404
|
+
}
|
|
405
|
+
// Section header (no value)
|
|
406
|
+
const sectionMatch = stripped.match(/^(\w+):\s*$/);
|
|
407
|
+
if (sectionMatch && !stripped.startsWith(' ')) {
|
|
408
|
+
currentSection = sectionMatch[1];
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
// Nested key under a section
|
|
412
|
+
const nestedMatch = stripped.match(/^\s{2}(\w+):\s*"?([^"]*)"?\s*$/);
|
|
413
|
+
if (nestedMatch && currentSection) {
|
|
414
|
+
const [, key, value] = nestedMatch;
|
|
415
|
+
if (currentSection === 'stack') {
|
|
416
|
+
if (!result.stack)
|
|
417
|
+
result.stack = {};
|
|
418
|
+
result.stack[key] = value;
|
|
419
|
+
}
|
|
420
|
+
else if (currentSection === 'ans') {
|
|
421
|
+
if (!result.ans)
|
|
422
|
+
result.ans = {};
|
|
423
|
+
result.ans[key] = value;
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
// Array item (systems or key_files)
|
|
428
|
+
const arrayItemMatch = stripped.match(/^\s{2}-\s+(?:path:\s*)?"?([^"]+)"?\s*$/);
|
|
429
|
+
if (arrayItemMatch && currentSection === 'systems') {
|
|
430
|
+
currentItem = { path: arrayItemMatch[1].trim() };
|
|
431
|
+
systems.push(currentItem);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
if (arrayItemMatch && currentSection === 'key_files') {
|
|
435
|
+
if (!result.key_files)
|
|
436
|
+
result.key_files = [];
|
|
437
|
+
result.key_files.push(arrayItemMatch[1].trim());
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
// Nested under array item
|
|
441
|
+
const itemFieldMatch = stripped.match(/^\s{4,}(\w+):\s*"?([^"]*)"?\s*$/);
|
|
442
|
+
if (itemFieldMatch && currentItem) {
|
|
443
|
+
currentItem[itemFieldMatch[1]] = itemFieldMatch[2];
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (systems.length > 0)
|
|
447
|
+
result.systems = systems;
|
|
448
|
+
return result;
|
|
449
|
+
}
|
|
450
|
+
async function seedSystems(options) {
|
|
451
|
+
const response = await fetch(`${options.apiUrl}/api/v1/living-docs/seed`, {
|
|
452
|
+
method: 'POST',
|
|
453
|
+
headers: {
|
|
454
|
+
'Authorization': `Bearer ${options.apiKey}`,
|
|
455
|
+
'Content-Type': 'application/json',
|
|
456
|
+
},
|
|
457
|
+
body: JSON.stringify({
|
|
458
|
+
systems: options.systems,
|
|
459
|
+
compile: !!options.compile,
|
|
460
|
+
}),
|
|
461
|
+
});
|
|
462
|
+
if (!response.ok) {
|
|
463
|
+
const errBody = await response.text();
|
|
464
|
+
throw new Error(`API returned ${response.status}: ${errBody}`);
|
|
465
|
+
}
|
|
466
|
+
return response.json();
|
|
467
|
+
}
|
|
468
|
+
// ── ekkos.yml scaffolding ────────────────────────────────────────────────
|
|
469
|
+
function scaffoldEkkosYml(project, hasPackageJson, hasCargo) {
|
|
470
|
+
const lines = [
|
|
471
|
+
'# ekkos.yml — Project contract for the ekkOS Autonomic Nervous System',
|
|
472
|
+
'# This file activates self-healing, evolution, and living documentation.',
|
|
473
|
+
'#',
|
|
474
|
+
'# Docs: https://docs.ekkos.dev/ekkos-yml',
|
|
475
|
+
'',
|
|
476
|
+
'version: 1',
|
|
477
|
+
`project: "${project}"`,
|
|
478
|
+
'',
|
|
479
|
+
];
|
|
480
|
+
// Stack detection for scaffold
|
|
481
|
+
if (hasPackageJson) {
|
|
482
|
+
lines.push('# Stack — tells ekkOS what language/framework you use (auto-detected if omitted)');
|
|
483
|
+
lines.push('# stack:');
|
|
484
|
+
lines.push('# language: typescript');
|
|
485
|
+
lines.push('# framework: next # or express, react, etc.');
|
|
486
|
+
lines.push('');
|
|
487
|
+
}
|
|
488
|
+
else if (hasCargo) {
|
|
489
|
+
lines.push('stack:');
|
|
490
|
+
lines.push(' language: rust');
|
|
491
|
+
lines.push('');
|
|
492
|
+
}
|
|
493
|
+
else {
|
|
494
|
+
lines.push('# Stack — uncomment and set your language for better docs');
|
|
495
|
+
lines.push('# stack:');
|
|
496
|
+
lines.push('# language: python # or rust, go, ruby, java, etc.');
|
|
497
|
+
lines.push('# framework: django # optional');
|
|
498
|
+
lines.push('');
|
|
499
|
+
}
|
|
500
|
+
lines.push('# Systems — custom system boundaries (auto-detected if omitted)', '# systems:', '# - path: "apps/api"', '# name: "API Service"', '# description: "REST API for the platform"', '# - path: "apps/web"', '# name: "Web Frontend"', '', '# Key files — files the compiler should always read', '# key_files:', '# - "settings/production.py"', '# - "config/routes.rb"', '', '# Vitals — connect observability providers to feel pain', '# vitals:', '# - type: sentry', '# project_id: "your-sentry-project-id"', '', '# ANS — autonomic nervous system configuration', 'ans:', ' tier: "R1" # R0=observe, R1=memory mutation, R2=source mutation (PRs), R3=reflex rollbacks');
|
|
501
|
+
if (hasPackageJson) {
|
|
502
|
+
lines.push(' build: "npm run build"', ' test: "npm test"', ' lint: "npm run lint"');
|
|
503
|
+
}
|
|
504
|
+
else if (hasCargo) {
|
|
505
|
+
lines.push(' build: "cargo build"', ' test: "cargo test"', ' lint: "cargo clippy"');
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
lines.push(' # build: "your build command"', ' # test: "your test command"');
|
|
509
|
+
}
|
|
510
|
+
lines.push('', '# Invariants — architectural rules the ANS must never break', '# invariants:', '# - "no-circular-dependencies"', '# - "auth-is-isolated"', '');
|
|
511
|
+
return lines.join('\n');
|
|
512
|
+
}
|
|
252
513
|
// ── Main scan command ────────────────────────────────────────────────────
|
|
253
514
|
async function scan(options) {
|
|
254
515
|
const startTime = Date.now();
|
|
@@ -260,21 +521,42 @@ async function scan(options) {
|
|
|
260
521
|
console.log(chalk_1.default.gray(' ─'.repeat(25)));
|
|
261
522
|
console.log('');
|
|
262
523
|
// Check if in a git repo
|
|
263
|
-
const gitRoot =
|
|
264
|
-
const repoRoot = gitRoot || targetPath;
|
|
524
|
+
const { gitRoot, targetRoot, scopePath } = discoverSystems(targetPath, { scopeToTarget: true });
|
|
265
525
|
if (gitRoot) {
|
|
266
526
|
console.log(chalk_1.default.gray(` Git root: ${gitRoot}`));
|
|
267
527
|
}
|
|
268
528
|
else {
|
|
269
529
|
console.log(chalk_1.default.yellow(` No git repo found — scanning ${targetPath}`));
|
|
270
530
|
}
|
|
531
|
+
if (scopePath !== '.') {
|
|
532
|
+
console.log(chalk_1.default.gray(` Workspace scope: ${targetRoot}`));
|
|
533
|
+
}
|
|
271
534
|
console.log('');
|
|
535
|
+
// Phase 0: Check for ekkos.yml — scaffold if missing
|
|
536
|
+
const repoRoot = gitRoot || targetRoot;
|
|
537
|
+
const ekkosYmlPath = (0, path_1.join)(repoRoot, 'ekkos.yml');
|
|
538
|
+
if (!(0, fs_1.existsSync)(ekkosYmlPath) && !isDryRun) {
|
|
539
|
+
const projectName = (0, path_1.basename)(repoRoot);
|
|
540
|
+
const hasPackageJson = (0, fs_1.existsSync)((0, path_1.join)(repoRoot, 'package.json'));
|
|
541
|
+
const hasCargo = (0, fs_1.existsSync)((0, path_1.join)(repoRoot, 'Cargo.toml'));
|
|
542
|
+
const content = scaffoldEkkosYml(projectName, hasPackageJson, hasCargo);
|
|
543
|
+
(0, fs_1.writeFileSync)(ekkosYmlPath, content, 'utf-8');
|
|
544
|
+
console.log(chalk_1.default.green(` Created ${chalk_1.default.bold('ekkos.yml')} — the seed of your project's nervous system`));
|
|
545
|
+
console.log('');
|
|
546
|
+
console.log(chalk_1.default.white(` Two ways to configure it:`));
|
|
547
|
+
console.log(chalk_1.default.gray(` 1. Edit ${chalk_1.default.white('ekkos.yml')} directly (build commands, vitals, tier)`));
|
|
548
|
+
console.log(chalk_1.default.gray(` 2. Use the guided wizard: ${chalk_1.default.cyan('https://platform.ekkos.dev/dashboard/settings/project')}`));
|
|
549
|
+
console.log('');
|
|
550
|
+
}
|
|
551
|
+
else if ((0, fs_1.existsSync)(ekkosYmlPath)) {
|
|
552
|
+
console.log(chalk_1.default.gray(` ekkos.yml: ${ekkosYmlPath}`));
|
|
553
|
+
console.log('');
|
|
554
|
+
}
|
|
272
555
|
// Phase 1: Scan repo structure
|
|
273
556
|
const scanSpinner = (0, ora_1.default)('Scanning repo structure...').start();
|
|
274
557
|
let systems;
|
|
275
558
|
try {
|
|
276
|
-
|
|
277
|
-
systems = assignParentSystems(rawSystems);
|
|
559
|
+
systems = discoverSystems(targetRoot, { scopeToTarget: true }).systems;
|
|
278
560
|
scanSpinner.succeed(`Found ${chalk_1.default.bold(systems.length.toString())} systems`);
|
|
279
561
|
}
|
|
280
562
|
catch (err) {
|
|
@@ -322,24 +604,12 @@ async function scan(options) {
|
|
|
322
604
|
const seedSpinner = (0, ora_1.default)('Seeding registry...').start();
|
|
323
605
|
try {
|
|
324
606
|
const apiUrl = process.env.EKKOS_API_URL || platform_js_1.MCP_API_URL;
|
|
325
|
-
const
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
},
|
|
331
|
-
body: JSON.stringify({
|
|
332
|
-
systems,
|
|
333
|
-
compile: shouldCompile,
|
|
334
|
-
}),
|
|
607
|
+
const result = await seedSystems({
|
|
608
|
+
systems,
|
|
609
|
+
apiUrl,
|
|
610
|
+
apiKey,
|
|
611
|
+
compile: shouldCompile,
|
|
335
612
|
});
|
|
336
|
-
if (!response.ok) {
|
|
337
|
-
const errBody = await response.text();
|
|
338
|
-
seedSpinner.fail('Seed failed');
|
|
339
|
-
console.error(chalk_1.default.red(` API returned ${response.status}: ${errBody}`));
|
|
340
|
-
process.exit(1);
|
|
341
|
-
}
|
|
342
|
-
const result = await response.json();
|
|
343
613
|
if (!result.ok) {
|
|
344
614
|
seedSpinner.fail('Seed failed');
|
|
345
615
|
console.error(chalk_1.default.red(` ${result.error || 'Unknown error'}`));
|
|
@@ -378,9 +648,35 @@ async function scan(options) {
|
|
|
378
648
|
}
|
|
379
649
|
process.exit(1);
|
|
380
650
|
}
|
|
651
|
+
// Phase 4: Register workspace for daemon living-docs watcher
|
|
652
|
+
try {
|
|
653
|
+
const workspacePath = gitRoot || targetRoot;
|
|
654
|
+
// Write workspace path to synk settings so the daemon can pick it up
|
|
655
|
+
const synkSettingsPath = (0, path_1.join)((0, os_1.homedir)(), '.ekkos', 'synk', 'settings.json');
|
|
656
|
+
if ((0, fs_1.existsSync)(synkSettingsPath)) {
|
|
657
|
+
const settings = JSON.parse((0, fs_1.readFileSync)(synkSettingsPath, 'utf-8'));
|
|
658
|
+
const existing = settings.watchedWorkspaces || [];
|
|
659
|
+
if (!existing.some((w) => w.path === workspacePath)) {
|
|
660
|
+
settings.watchedWorkspaces = [...existing, { path: workspacePath, addedAt: new Date().toISOString() }];
|
|
661
|
+
const { writeFileSync: writeSync } = await Promise.resolve().then(() => __importStar(require('fs')));
|
|
662
|
+
writeSync(synkSettingsPath, JSON.stringify(settings, null, 2));
|
|
663
|
+
console.log(chalk_1.default.gray(` Workspace registered for daemon watcher: ${workspacePath}`));
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
// Daemon settings may not exist yet — that's fine
|
|
669
|
+
}
|
|
381
670
|
// Summary
|
|
382
671
|
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|
|
383
672
|
console.log('');
|
|
384
|
-
console.log(chalk_1.default.green(` Done in ${duration}s`));
|
|
673
|
+
console.log(chalk_1.default.green(` ✓ Done in ${duration}s`));
|
|
674
|
+
console.log('');
|
|
675
|
+
console.log(chalk_1.default.white.bold(' What happens now:'));
|
|
676
|
+
console.log(chalk_1.default.gray(` • Living docs watcher keeps your ${chalk_1.default.white('ekkOS_CONTEXT.md')} files current`));
|
|
677
|
+
console.log(chalk_1.default.gray(` • ANS monitors your systems for anomalies`));
|
|
678
|
+
console.log(chalk_1.default.gray(` • Dream Forge runs nightly to evolve patterns`));
|
|
679
|
+
console.log('');
|
|
680
|
+
console.log(chalk_1.default.gray(` Configure vitals & autonomy: ${chalk_1.default.cyan('https://platform.ekkos.dev/dashboard/settings/project')}`));
|
|
385
681
|
console.log('');
|
|
386
682
|
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -359,12 +359,8 @@ Use \`ekkOS_Forge\` when you:
|
|
|
359
359
|
|
|
360
360
|
## Pattern Application
|
|
361
361
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
[ekkOS_APPLY] Using: "Pattern Name"
|
|
365
|
-
\`\`\`
|
|
366
|
-
|
|
367
|
-
This is how ekkOS learns and improves over time.
|
|
362
|
+
Apply relevant patterns silently in the answer.
|
|
363
|
+
Do not emit control markers such as \`[ekkOS_APPLY]\` or \`[ekkOS_SKIP]\` unless the runtime explicitly asks for them.
|
|
368
364
|
`;
|
|
369
365
|
}
|
|
370
366
|
function generateCascadeRules() {
|
package/dist/deploy/index.d.ts
CHANGED
package/dist/deploy/index.js
CHANGED
|
@@ -19,4 +19,5 @@ __exportStar(require("./mcp"), exports);
|
|
|
19
19
|
__exportStar(require("./settings"), exports);
|
|
20
20
|
__exportStar(require("./hooks"), exports);
|
|
21
21
|
__exportStar(require("./skills"), exports);
|
|
22
|
+
__exportStar(require("./agents"), exports);
|
|
22
23
|
__exportStar(require("./instructions"), exports);
|
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Deploy
|
|
2
|
+
* Deploy ekkOS block into ~/.claude/CLAUDE.md.
|
|
3
|
+
*
|
|
4
|
+
* Strategy:
|
|
5
|
+
* - If file doesn't exist → create it with just the ekkOS block.
|
|
6
|
+
* - If file exists and already has markers → replace the block between markers.
|
|
7
|
+
* - If file exists with no markers → prepend ekkOS block above existing content.
|
|
8
|
+
*
|
|
9
|
+
* Never destroys user content outside the markers.
|
|
3
10
|
*/
|
|
4
11
|
export declare function deployInstructions(): void;
|
|
5
12
|
/**
|
|
6
|
-
* Check if CLAUDE.md
|
|
13
|
+
* Check if CLAUDE.md has the ekkOS block
|
|
7
14
|
*/
|
|
8
15
|
export declare function isInstructionsDeployed(): boolean;
|
|
9
16
|
/**
|
|
10
|
-
* Get the CLAUDE.md content
|
|
17
|
+
* Get the ekkOS CLAUDE.md template content
|
|
11
18
|
*/
|
|
12
19
|
export declare function getInstructionsContent(): string;
|