@ekkos/cli 1.4.1 → 1.5.0
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 +309 -93
- package/dist/commands/init.js +59 -4
- package/dist/commands/living-docs.d.ts +1 -0
- package/dist/commands/living-docs.js +5 -2
- package/dist/commands/logout.d.ts +9 -0
- package/dist/commands/logout.js +104 -0
- package/dist/commands/run.js +56 -23
- package/dist/commands/workspaces.d.ts +4 -0
- package/dist/commands/workspaces.js +153 -0
- package/dist/index.js +82 -83
- package/dist/local/diff-engine.d.ts +19 -0
- package/dist/local/diff-engine.js +81 -0
- package/dist/local/entity-extractor.d.ts +18 -0
- package/dist/local/entity-extractor.js +67 -0
- package/dist/local/git-utils.d.ts +37 -0
- package/dist/local/git-utils.js +169 -0
- package/dist/local/living-docs-manager.d.ts +6 -0
- package/dist/local/living-docs-manager.js +180 -133
- package/dist/utils/notifier.d.ts +15 -0
- package/dist/utils/notifier.js +40 -0
- package/dist/utils/paths.d.ts +4 -0
- package/dist/utils/paths.js +7 -0
- package/dist/utils/state.d.ts +3 -0
- package/dist/utils/stdin-relay.d.ts +37 -0
- package/dist/utils/stdin-relay.js +155 -0
- package/package.json +4 -1
- package/templates/CLAUDE.md +3 -1
- package/dist/commands/setup.d.ts +0 -6
- package/dist/commands/setup.js +0 -389
package/dist/index.js
CHANGED
|
@@ -67,6 +67,7 @@ commander_1.program
|
|
|
67
67
|
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos codex')} ${chalk_1.default.gray('Start Codex (OpenAI) mode')}`,
|
|
68
68
|
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos daemon start')} ${chalk_1.default.gray('Start the background mobile sync service')}`,
|
|
69
69
|
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos docs watch')} ${chalk_1.default.gray('Keep local ekkOS_CONTEXT.md files updated on disk')}`,
|
|
70
|
+
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos cortex workspaces')} ${chalk_1.default.gray('Manage projects watched by the background guardian')}`,
|
|
70
71
|
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos connect gemini')} ${chalk_1.default.gray('Securely store your Gemini API key in the cloud')}`,
|
|
71
72
|
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos agent create --path ./repo')} ${chalk_1.default.gray('Spawn a headless remote agent in a directory')}`,
|
|
72
73
|
` ${chalk_1.default.gray('$')} ${chalk_1.default.white('ekkos run --dashboard')} ${chalk_1.default.gray('Launch Claude with live usage dashboard')}`,
|
|
@@ -169,7 +170,6 @@ commander_1.program
|
|
|
169
170
|
icon: '▸',
|
|
170
171
|
commands: [
|
|
171
172
|
{ name: 'agent', desc: 'Manage remote headless agents (create, list, stop)' },
|
|
172
|
-
{ name: 'swarm', desc: 'Parallel workers, Q-learning routing, and swarm dashboard' },
|
|
173
173
|
],
|
|
174
174
|
},
|
|
175
175
|
{
|
|
@@ -264,6 +264,8 @@ docsCmd
|
|
|
264
264
|
.option('--poll-interval-ms <ms>', 'Polling interval fallback in milliseconds', (value) => parseInt(value, 10))
|
|
265
265
|
.option('--debounce-ms <ms>', 'Debounce window before recompiling in milliseconds', (value) => parseInt(value, 10))
|
|
266
266
|
.option('--no-seed', 'Do not seed the platform registry while watching')
|
|
267
|
+
.option('--rich', 'Perform server-side semantic analysis for rich insights (Directive Audit, Cascade Analysis, Entity Extraction)', true)
|
|
268
|
+
.option('--no-rich', 'Disable server-side semantic analysis')
|
|
267
269
|
.action(async (options) => {
|
|
268
270
|
const { watchLivingDocs } = await Promise.resolve().then(() => __importStar(require('./commands/living-docs')));
|
|
269
271
|
watchLivingDocs({
|
|
@@ -272,8 +274,30 @@ docsCmd
|
|
|
272
274
|
pollIntervalMs: options.pollIntervalMs,
|
|
273
275
|
debounceMs: options.debounceMs,
|
|
274
276
|
noSeed: options.seed === false,
|
|
277
|
+
rich: options.rich !== false,
|
|
275
278
|
});
|
|
276
279
|
});
|
|
280
|
+
docsCmd
|
|
281
|
+
.command('workspaces')
|
|
282
|
+
.description('Manage projects watched by the Cortex background guardian')
|
|
283
|
+
.option('-l, --list', 'List all currently watched workspaces')
|
|
284
|
+
.option('-a, --add <path>', 'Add a new workspace to watch')
|
|
285
|
+
.option('-r, --remove <path>', 'Stop watching a workspace')
|
|
286
|
+
.action(async (options) => {
|
|
287
|
+
const { listWorkspaces, addWorkspace, removeWorkspace, manageWorkspaces } = await Promise.resolve().then(() => __importStar(require('./commands/workspaces')));
|
|
288
|
+
if (options.list) {
|
|
289
|
+
await listWorkspaces();
|
|
290
|
+
}
|
|
291
|
+
else if (options.add) {
|
|
292
|
+
await addWorkspace(path.resolve(options.add));
|
|
293
|
+
}
|
|
294
|
+
else if (options.remove) {
|
|
295
|
+
await removeWorkspace(path.resolve(options.remove));
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
await manageWorkspaces();
|
|
299
|
+
}
|
|
300
|
+
});
|
|
277
301
|
docsCmd
|
|
278
302
|
.command('setup-ci')
|
|
279
303
|
.description('Generate a GitHub Actions workflow to automatically validate Cortex docs on PRs')
|
|
@@ -535,88 +559,14 @@ commander_1.program
|
|
|
535
559
|
}
|
|
536
560
|
}
|
|
537
561
|
});
|
|
538
|
-
//
|
|
562
|
+
// Logout command
|
|
539
563
|
commander_1.program
|
|
540
|
-
.command('
|
|
541
|
-
.description('
|
|
542
|
-
.option('-
|
|
543
|
-
.option('-k, --key <key>', 'ekkOS API key')
|
|
564
|
+
.command('logout')
|
|
565
|
+
.description('Log out of ekkOS (clear API credentials)')
|
|
566
|
+
.option('-a, --all', 'Also clear Synk mobile sync credentials')
|
|
544
567
|
.action(async (options) => {
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
console.log(chalk_1.default.yellow(' Please use "ekkos init" instead.'));
|
|
548
|
-
console.log('');
|
|
549
|
-
console.log(chalk_1.default.gray('Running init with your options...'));
|
|
550
|
-
console.log('');
|
|
551
|
-
// Forward to init
|
|
552
|
-
const { init } = await Promise.resolve().then(() => __importStar(require('./commands/init')));
|
|
553
|
-
await init({
|
|
554
|
-
ide: options.ide,
|
|
555
|
-
key: options.key
|
|
556
|
-
});
|
|
557
|
-
});
|
|
558
|
-
// Swarm command - manage Q-learning routing
|
|
559
|
-
const swarmCmd = commander_1.program
|
|
560
|
-
.command('swarm')
|
|
561
|
-
.description('Manage Swarm Q-learning model routing');
|
|
562
|
-
swarmCmd
|
|
563
|
-
.command('status')
|
|
564
|
-
.description('Show Q-table stats (states, visits, epsilon, top actions)')
|
|
565
|
-
.action(async () => { const { swarmStatus } = await Promise.resolve().then(() => __importStar(require('./commands/swarm'))); swarmStatus(); });
|
|
566
|
-
swarmCmd
|
|
567
|
-
.command('reset')
|
|
568
|
-
.description('Clear Q-table from Redis (routing reverts to static rules)')
|
|
569
|
-
.action(async () => { const { swarmReset } = await Promise.resolve().then(() => __importStar(require('./commands/swarm'))); swarmReset(); });
|
|
570
|
-
swarmCmd
|
|
571
|
-
.command('export')
|
|
572
|
-
.description('Export Q-table to .swarm/q-learning-model.json')
|
|
573
|
-
.action(async () => { const { swarmExport } = await Promise.resolve().then(() => __importStar(require('./commands/swarm'))); swarmExport(); });
|
|
574
|
-
swarmCmd
|
|
575
|
-
.command('import')
|
|
576
|
-
.description('Import Q-table from .swarm/q-learning-model.json into Redis')
|
|
577
|
-
.action(async () => { const { swarmImport } = await Promise.resolve().then(() => __importStar(require('./commands/swarm'))); swarmImport(); });
|
|
578
|
-
swarmCmd
|
|
579
|
-
.command('launch')
|
|
580
|
-
.description('Launch parallel workers on a decomposed task (opens wizard if --task is omitted)')
|
|
581
|
-
.option('-w, --workers <count>', 'Number of parallel workers (2-8)', parseInt)
|
|
582
|
-
.option('-t, --task <task>', 'Task description to decompose and execute')
|
|
583
|
-
.option('--no-bypass', 'Disable bypass permissions mode')
|
|
584
|
-
.option('--no-decompose', 'Skip AI decomposition (send same task to all workers)')
|
|
585
|
-
.option('--no-queen', 'Skip launching the Python Queen coordinator')
|
|
586
|
-
.option('--queen-strategy <strategy>', 'Queen strategy (adaptive-default, hierarchical-cascade, mesh-consensus)')
|
|
587
|
-
.option('-v, --verbose', 'Show debug output')
|
|
588
|
-
.action(async (options) => {
|
|
589
|
-
// Auto-open wizard when --task is missing
|
|
590
|
-
if (!options.task) {
|
|
591
|
-
const { swarmSetup } = await Promise.resolve().then(() => __importStar(require('./commands/swarm-setup')));
|
|
592
|
-
swarmSetup();
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
const { swarmLaunch } = await Promise.resolve().then(() => __importStar(require('./commands/swarm')));
|
|
596
|
-
swarmLaunch({
|
|
597
|
-
workers: options.workers || 4,
|
|
598
|
-
task: options.task,
|
|
599
|
-
bypass: options.bypass !== false,
|
|
600
|
-
noDecompose: options.decompose === false,
|
|
601
|
-
noQueen: options.queen === false,
|
|
602
|
-
queenStrategy: options.queenStrategy,
|
|
603
|
-
verbose: options.verbose,
|
|
604
|
-
});
|
|
605
|
-
});
|
|
606
|
-
swarmCmd
|
|
607
|
-
.command('setup')
|
|
608
|
-
.description('Interactive TUI wizard for configuring and launching a swarm')
|
|
609
|
-
.action(async () => {
|
|
610
|
-
const { swarmSetup } = await Promise.resolve().then(() => __importStar(require('./commands/swarm-setup')));
|
|
611
|
-
swarmSetup();
|
|
612
|
-
});
|
|
613
|
-
swarmCmd
|
|
614
|
-
.command('swarm-dashboard')
|
|
615
|
-
.description('Live swarm dashboard')
|
|
616
|
-
.allowUnknownOption()
|
|
617
|
-
.action(async () => {
|
|
618
|
-
const { swarmDashboardCommand } = await Promise.resolve().then(() => __importStar(require('./commands/swarm-dashboard')));
|
|
619
|
-
swarmDashboardCommand.parse(process.argv.slice(3));
|
|
568
|
+
const { logout } = await Promise.resolve().then(() => __importStar(require('./commands/logout')));
|
|
569
|
+
await logout({ all: options.all });
|
|
620
570
|
});
|
|
621
571
|
// --- Remote & Agent Wrapper Helpers ---
|
|
622
572
|
function runRemoteCommand(command, ...args) {
|
|
@@ -654,7 +604,20 @@ commander_1.program
|
|
|
654
604
|
.command('codex')
|
|
655
605
|
.description('Launch Codex (OpenAI) with mobile control')
|
|
656
606
|
.allowUnknownOption()
|
|
657
|
-
.action(() =>
|
|
607
|
+
.action(async () => {
|
|
608
|
+
const crypto = await Promise.resolve().then(() => __importStar(require('crypto')));
|
|
609
|
+
const { getConfig, uuidToWords } = await Promise.resolve().then(() => __importStar(require('./utils/state')));
|
|
610
|
+
const { buildProxyUrl } = await Promise.resolve().then(() => __importStar(require('./utils/proxy-url')));
|
|
611
|
+
const sessionId = process.env.CLAUDE_SESSION_ID || crypto.randomBytes(8).toString('hex');
|
|
612
|
+
const sessionName = process.env.CLAUDE_SESSION_NAME || uuidToWords(sessionId);
|
|
613
|
+
const config = await getConfig();
|
|
614
|
+
if (config?.userId) {
|
|
615
|
+
process.env.OPENAI_BASE_URL = buildProxyUrl(config.userId, sessionName, process.cwd(), sessionId);
|
|
616
|
+
process.env.CLAUDE_SESSION_ID = sessionId;
|
|
617
|
+
process.env.CLAUDE_SESSION_NAME = sessionName;
|
|
618
|
+
}
|
|
619
|
+
return runRemoteCommand('synk', 'codex', ...process.argv.slice(3));
|
|
620
|
+
});
|
|
658
621
|
commander_1.program
|
|
659
622
|
.command('acp')
|
|
660
623
|
.description('Launch a generic ACP-compatible agent')
|
|
@@ -692,10 +655,46 @@ if (helpIdx !== -1) {
|
|
|
692
655
|
process.argv[helpIdx] = '--help';
|
|
693
656
|
}
|
|
694
657
|
// Default to `run` if no command specified (e.g. `ekkos`, `ekkos -b --dashboard`)
|
|
695
|
-
const knownCommands = commander_1.program.commands.
|
|
658
|
+
const knownCommands = commander_1.program.commands.flatMap(c => [c.name(), ...c.aliases()]);
|
|
696
659
|
const userArgs = process.argv.slice(2);
|
|
697
660
|
const hasCommand = userArgs.some(a => knownCommands.includes(a) || a === 'help' || a === '--help' || a === '-h' || a === '--version' || a === '-V');
|
|
698
661
|
if (!hasCommand) {
|
|
699
662
|
process.argv.splice(2, 0, 'run');
|
|
700
663
|
}
|
|
701
664
|
commander_1.program.parse();
|
|
665
|
+
// ── Auto-register workspace with local daemon ──────────────────────────────
|
|
666
|
+
// This ensures that simply running `ekkos` once in a directory is enough to
|
|
667
|
+
// activate the universal background watcher for that project.
|
|
668
|
+
void (async () => {
|
|
669
|
+
try {
|
|
670
|
+
const { getDaemonPortPath } = await Promise.resolve().then(() => __importStar(require('./utils/paths')));
|
|
671
|
+
const portPath = getDaemonPortPath();
|
|
672
|
+
if (!fs.existsSync(portPath))
|
|
673
|
+
return;
|
|
674
|
+
const port = fs.readFileSync(portPath, 'utf-8').trim();
|
|
675
|
+
if (!port || isNaN(Number(port)))
|
|
676
|
+
return;
|
|
677
|
+
// Resolve the workspace path - prefer git root, fallback to cwd
|
|
678
|
+
let workspacePath = process.cwd();
|
|
679
|
+
try {
|
|
680
|
+
const { getRepoRoot } = await Promise.resolve().then(() => __importStar(require('./local/git-utils')));
|
|
681
|
+
const gitRoot = getRepoRoot(workspacePath);
|
|
682
|
+
if (gitRoot)
|
|
683
|
+
workspacePath = gitRoot;
|
|
684
|
+
}
|
|
685
|
+
catch {
|
|
686
|
+
// ignore git errors
|
|
687
|
+
}
|
|
688
|
+
// Fire-and-forget signal to local daemon
|
|
689
|
+
void fetch(`http://127.0.0.1:${port}/register-workspace`, {
|
|
690
|
+
method: 'POST',
|
|
691
|
+
headers: { 'Content-Type': 'application/json' },
|
|
692
|
+
body: JSON.stringify({ path: workspacePath }),
|
|
693
|
+
}).catch(() => {
|
|
694
|
+
// Daemon might be starting up or port is stale - ignore
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
catch {
|
|
698
|
+
// Non-fatal
|
|
699
|
+
}
|
|
700
|
+
})();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cortex Diff Engine
|
|
3
|
+
* Responsible for extracting and compressing code changes for semantic analysis.
|
|
4
|
+
*/
|
|
5
|
+
export interface SemanticDiffOptions {
|
|
6
|
+
repoRoot: string;
|
|
7
|
+
directoryPath: string;
|
|
8
|
+
maxTokens?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface SemanticDiffResult {
|
|
11
|
+
rawDiff: string;
|
|
12
|
+
isCompressed: boolean;
|
|
13
|
+
summary?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Extracts a diff that is safe for LLM consumption.
|
|
17
|
+
* Respects .ekkosignore and performs compression if needed.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getSemanticDiff(options: SemanticDiffOptions): Promise<SemanticDiffResult>;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getSemanticDiff = getSemanticDiff;
|
|
4
|
+
const git_utils_js_1 = require("./git-utils.js");
|
|
5
|
+
/**
|
|
6
|
+
* Extracts a diff that is safe for LLM consumption.
|
|
7
|
+
* Respects .ekkosignore and performs compression if needed.
|
|
8
|
+
*/
|
|
9
|
+
async function getSemanticDiff(options) {
|
|
10
|
+
const { repoRoot, directoryPath } = options;
|
|
11
|
+
// 1. Get the uncommitted diff first
|
|
12
|
+
let rawDiff = (0, git_utils_js_1.getUncommittedDiff)(repoRoot, directoryPath);
|
|
13
|
+
// 2. If no uncommitted changes, check the most recent commit
|
|
14
|
+
if (!rawDiff) {
|
|
15
|
+
// This is useful if the user just committed but hasn't pushed yet
|
|
16
|
+
rawDiff = (0, git_utils_js_1.getRecentCommitDiff)(repoRoot, directoryPath);
|
|
17
|
+
}
|
|
18
|
+
if (!rawDiff) {
|
|
19
|
+
console.log(`[DiffEngine] No changes found in ${directoryPath} (checked uncommitted and last commit)`);
|
|
20
|
+
return { rawDiff: '', isCompressed: false };
|
|
21
|
+
}
|
|
22
|
+
// 3. Check if we need to compress (simple heuristic: > 20,000 chars)
|
|
23
|
+
if (rawDiff.length > 20000) {
|
|
24
|
+
return {
|
|
25
|
+
rawDiff: compressDiff(rawDiff),
|
|
26
|
+
isCompressed: true,
|
|
27
|
+
summary: generateDiffSummary(rawDiff)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
return { rawDiff, isCompressed: false };
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Compresses a large diff by extracting only the changed lines and minimal context.
|
|
34
|
+
* This saves tokens while preserving semantic intent.
|
|
35
|
+
*/
|
|
36
|
+
function compressDiff(diff) {
|
|
37
|
+
const lines = diff.split('\n');
|
|
38
|
+
const compressed = [];
|
|
39
|
+
let currentFile = '';
|
|
40
|
+
let inHunk = false;
|
|
41
|
+
for (const line of lines) {
|
|
42
|
+
if (line.startsWith('diff --git')) {
|
|
43
|
+
currentFile = line.split(' ').pop() || '';
|
|
44
|
+
compressed.push(line);
|
|
45
|
+
inHunk = false;
|
|
46
|
+
}
|
|
47
|
+
else if (line.startsWith('@@')) {
|
|
48
|
+
compressed.push(line);
|
|
49
|
+
inHunk = true;
|
|
50
|
+
}
|
|
51
|
+
else if (line.startsWith('+') || line.startsWith('-')) {
|
|
52
|
+
compressed.push(line);
|
|
53
|
+
}
|
|
54
|
+
else if (inHunk && compressed.length < 1000) {
|
|
55
|
+
// Include limited context lines if we have space
|
|
56
|
+
// For now, let's keep it very aggressive and skip context
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return compressed.join('\n');
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Generates a high-level summary of a massive diff (Phase 1: Basic AST-ish summary).
|
|
63
|
+
*/
|
|
64
|
+
function generateDiffSummary(diff) {
|
|
65
|
+
const lines = diff.split('\n');
|
|
66
|
+
const filesChanged = new Set();
|
|
67
|
+
let additions = 0;
|
|
68
|
+
let deletions = 0;
|
|
69
|
+
for (const line of lines) {
|
|
70
|
+
if (line.startsWith('+++ b/')) {
|
|
71
|
+
filesChanged.add(line.replace('+++ b/', ''));
|
|
72
|
+
}
|
|
73
|
+
else if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
74
|
+
additions++;
|
|
75
|
+
}
|
|
76
|
+
else if (line.startsWith('-') && !line.startsWith('---')) {
|
|
77
|
+
deletions++;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return `Massive change: ${filesChanged.size} files, +${additions} / -${deletions} lines. Diff was compressed for analysis.`;
|
|
81
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cortex Entity Extractor
|
|
3
|
+
* Scans code changes for key Technologies, Concepts, and Architectural Patterns.
|
|
4
|
+
* Seeds the ekkOS Knowledge Graph with high-fidelity local data.
|
|
5
|
+
*/
|
|
6
|
+
export interface ExtractedEntities {
|
|
7
|
+
technologies: string[];
|
|
8
|
+
concepts: string[];
|
|
9
|
+
patterns: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Extracts entities from a code diff.
|
|
13
|
+
*/
|
|
14
|
+
export declare function extractEntitiesFromDiff(diff: string): ExtractedEntities;
|
|
15
|
+
/**
|
|
16
|
+
* Higher-fidelity extraction: Scan specific file extensions for stack-specific entities.
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractStackEntities(files: string[]): string[];
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Cortex Entity Extractor
|
|
4
|
+
* Scans code changes for key Technologies, Concepts, and Architectural Patterns.
|
|
5
|
+
* Seeds the ekkOS Knowledge Graph with high-fidelity local data.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.extractEntitiesFromDiff = extractEntitiesFromDiff;
|
|
9
|
+
exports.extractStackEntities = extractStackEntities;
|
|
10
|
+
const TECH_KEYWORDS = [
|
|
11
|
+
'Zustand', 'React', 'Redis', 'Supabase', 'Postgres', 'Next.js', 'Vercel',
|
|
12
|
+
'Tailwind', 'Zod', 'Prisma', 'BullMQ', 'QStash', 'Upstash', 'OpenAI', 'Gemini',
|
|
13
|
+
'Claude', 'Neo4j', 'Fastify', 'Express', 'Tauri', 'Electron', 'SQLite',
|
|
14
|
+
'TypeScript', 'Rust', 'Python', 'Go', 'Docker', 'Kubernetes', 'AWS', 'GCP',
|
|
15
|
+
'Azure', 'GitHub Actions', 'Prometheus', 'Grafana', 'Sentry', 'LogSnag'
|
|
16
|
+
];
|
|
17
|
+
const CONCEPT_KEYWORDS = [
|
|
18
|
+
'Reactivity', 'State Management', 'Polling', 'Caching', 'Authentication',
|
|
19
|
+
'Authorization', 'Rate Limiting', 'Streaming', 'Batching', 'Concurrency',
|
|
20
|
+
'Event Driven', 'Serverless', 'Edge Computing', 'Memory Layer', 'Context Injection',
|
|
21
|
+
'Semantic Search', 'Vector Embedding', 'LLM Inference', 'Agent Orchestration'
|
|
22
|
+
];
|
|
23
|
+
const PATTERN_KEYWORDS = [
|
|
24
|
+
'Golden Loop', 'Directive Pre-Flight', 'Cascade Analysis', 'Cortex Watch',
|
|
25
|
+
'Living Docs', 'Auto-Forge', 'Knowledge Graph', 'GraphRAG', 'Agentic Workflow',
|
|
26
|
+
'Model Context Protocol', 'MCP', 'Tool Execution', 'System Instruction'
|
|
27
|
+
];
|
|
28
|
+
/**
|
|
29
|
+
* Extracts entities from a code diff.
|
|
30
|
+
*/
|
|
31
|
+
function extractEntitiesFromDiff(diff) {
|
|
32
|
+
const result = {
|
|
33
|
+
technologies: [],
|
|
34
|
+
concepts: [],
|
|
35
|
+
patterns: []
|
|
36
|
+
};
|
|
37
|
+
// Case-insensitive regex scan
|
|
38
|
+
const scan = (keywords, target) => {
|
|
39
|
+
for (const keyword of keywords) {
|
|
40
|
+
const regex = new RegExp(`\\b${keyword}\\b`, 'gi');
|
|
41
|
+
if (regex.test(diff)) {
|
|
42
|
+
target.push(keyword);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
scan(TECH_KEYWORDS, result.technologies);
|
|
47
|
+
scan(CONCEPT_KEYWORDS, result.concepts);
|
|
48
|
+
scan(PATTERN_KEYWORDS, result.patterns);
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Higher-fidelity extraction: Scan specific file extensions for stack-specific entities.
|
|
53
|
+
*/
|
|
54
|
+
function extractStackEntities(files) {
|
|
55
|
+
const stacks = [];
|
|
56
|
+
if (files.some(f => f.endsWith('.ts') || f.endsWith('.tsx')))
|
|
57
|
+
stacks.push('TypeScript');
|
|
58
|
+
if (files.some(f => f.endsWith('.py')))
|
|
59
|
+
stacks.push('Python');
|
|
60
|
+
if (files.some(f => f.endsWith('.rs')))
|
|
61
|
+
stacks.push('Rust');
|
|
62
|
+
if (files.some(f => f.endsWith('.go')))
|
|
63
|
+
stacks.push('Go');
|
|
64
|
+
if (files.some(f => f.endsWith('.sql')))
|
|
65
|
+
stacks.push('SQL');
|
|
66
|
+
return stacks;
|
|
67
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Common Git Utilities for ekkOS Cortex
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Formats a timestamp into a specific timezone string.
|
|
6
|
+
* Lifted from LivingDocsManager for central usage.
|
|
7
|
+
*/
|
|
8
|
+
export declare function formatZonedTimestamp(value: Date | string | number, timeZone: string): string;
|
|
9
|
+
/**
|
|
10
|
+
* Reads recent git changes for a specific directory.
|
|
11
|
+
*/
|
|
12
|
+
export declare function readRecentGitChanges(repoRoot: string, directoryPath: string, timeZone: string, maxDays?: number): Array<{
|
|
13
|
+
date: string;
|
|
14
|
+
message: string;
|
|
15
|
+
timestamp: string;
|
|
16
|
+
}>;
|
|
17
|
+
/**
|
|
18
|
+
* Gets the raw diff for uncommitted changes in a specific directory.
|
|
19
|
+
* Filters out ekkOS context files and non-code files (if possible via git).
|
|
20
|
+
*/
|
|
21
|
+
export declare function getUncommittedDiff(repoRoot: string, directoryPath: string): string;
|
|
22
|
+
/**
|
|
23
|
+
* Gets the diff for the most recent commit.
|
|
24
|
+
*/
|
|
25
|
+
export declare function getRecentCommitDiff(repoRoot: string, directoryPath: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Checks if the current directory is a git repository.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isGitRepo(dir: string): boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Gets the timestamp of the last commit for a specific directory.
|
|
32
|
+
*/
|
|
33
|
+
export declare function getLastCommitTimestamp(repoRoot: string, directoryPath: string): number | null;
|
|
34
|
+
/**
|
|
35
|
+
* Gets the absolute path to the git repository root.
|
|
36
|
+
*/
|
|
37
|
+
export declare function getRepoRoot(dir: string): string | null;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.formatZonedTimestamp = formatZonedTimestamp;
|
|
4
|
+
exports.readRecentGitChanges = readRecentGitChanges;
|
|
5
|
+
exports.getUncommittedDiff = getUncommittedDiff;
|
|
6
|
+
exports.getRecentCommitDiff = getRecentCommitDiff;
|
|
7
|
+
exports.isGitRepo = isGitRepo;
|
|
8
|
+
exports.getLastCommitTimestamp = getLastCommitTimestamp;
|
|
9
|
+
exports.getRepoRoot = getRepoRoot;
|
|
10
|
+
const child_process_1 = require("child_process");
|
|
11
|
+
/**
|
|
12
|
+
* Common Git Utilities for ekkOS Cortex
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* Formats a timestamp into a specific timezone string.
|
|
16
|
+
* Lifted from LivingDocsManager for central usage.
|
|
17
|
+
*/
|
|
18
|
+
function formatZonedTimestamp(value, timeZone) {
|
|
19
|
+
const date = value instanceof Date ? value : new Date(value);
|
|
20
|
+
if (Number.isNaN(date.getTime()))
|
|
21
|
+
return new Date().toISOString();
|
|
22
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
23
|
+
timeZone,
|
|
24
|
+
year: 'numeric',
|
|
25
|
+
month: '2-digit',
|
|
26
|
+
day: '2-digit',
|
|
27
|
+
hour: '2-digit',
|
|
28
|
+
minute: '2-digit',
|
|
29
|
+
second: '2-digit',
|
|
30
|
+
hourCycle: 'h23',
|
|
31
|
+
}).formatToParts(date);
|
|
32
|
+
const map = Object.fromEntries(parts
|
|
33
|
+
.filter(part => part.type !== 'literal')
|
|
34
|
+
.map(part => [part.type, part.value]));
|
|
35
|
+
const asUtc = Date.UTC(Number(map.year), Number(map.month) - 1, Number(map.day), Number(map.hour), Number(map.minute), Number(map.second), date.getUTCMilliseconds());
|
|
36
|
+
const offsetMinutes = Math.round((asUtc - date.getTime()) / 60000);
|
|
37
|
+
const zoned = new Date(date.getTime() + offsetMinutes * 60000);
|
|
38
|
+
const sign = offsetMinutes >= 0 ? '+' : '-';
|
|
39
|
+
const absOffset = Math.abs(offsetMinutes);
|
|
40
|
+
const pad = (num, width = 2) => num.toString().padStart(width, '0');
|
|
41
|
+
return [
|
|
42
|
+
`${pad(zoned.getUTCFullYear(), 4)}-${pad(zoned.getUTCMonth() + 1)}-${pad(zoned.getUTCDate())}`,
|
|
43
|
+
`T${pad(zoned.getUTCHours())}:${pad(zoned.getUTCMinutes())}:${pad(zoned.getUTCSeconds())}.${pad(zoned.getUTCMilliseconds(), 3)}`,
|
|
44
|
+
`${sign}${pad(Math.floor(absOffset / 60))}:${pad(absOffset % 60)}`,
|
|
45
|
+
].join('');
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Reads recent git changes for a specific directory.
|
|
49
|
+
*/
|
|
50
|
+
function readRecentGitChanges(repoRoot, directoryPath, timeZone, maxDays = 7) {
|
|
51
|
+
try {
|
|
52
|
+
const scope = directoryPath === '.' ? '.' : directoryPath;
|
|
53
|
+
const output = (0, child_process_1.execSync)(`git -C ${JSON.stringify(repoRoot)} log --max-count=15 --since="${maxDays} days ago" --pretty=format:"%ct%x09%s%x09%b" --stat -- ${JSON.stringify(scope)} ':!**/ekkOS_CONTEXT.md'`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1024 * 64 }).trim();
|
|
54
|
+
if (!output)
|
|
55
|
+
return [];
|
|
56
|
+
const entries = [];
|
|
57
|
+
const blocks = output.split(/\n(?="\d+\t)/);
|
|
58
|
+
for (const block of blocks) {
|
|
59
|
+
const lines = block.split('\n');
|
|
60
|
+
const header = (lines[0] || '').replace(/^"|"$/g, '');
|
|
61
|
+
const parts = header.split('\t');
|
|
62
|
+
const epochSeconds = parts[0];
|
|
63
|
+
const subject = parts[1] || '';
|
|
64
|
+
const body = (parts[2] || '').trim();
|
|
65
|
+
const epochMs = Number(epochSeconds || '0') * 1000;
|
|
66
|
+
if (!epochMs)
|
|
67
|
+
continue;
|
|
68
|
+
const changedFiles = [];
|
|
69
|
+
for (let i = 1; i < lines.length; i++) {
|
|
70
|
+
const statMatch = lines[i].match(/^\s*(.+?)\s*\|\s*\d+/);
|
|
71
|
+
if (statMatch) {
|
|
72
|
+
const filePath = statMatch[1].trim();
|
|
73
|
+
if (scope === '.' || filePath.startsWith(scope + '/') || filePath.startsWith(scope.replace(/^\.\//, '') + '/')) {
|
|
74
|
+
const shortName = filePath.split('/').pop() || filePath;
|
|
75
|
+
changedFiles.push(shortName);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const timestamp = formatZonedTimestamp(new Date(epochMs), timeZone);
|
|
80
|
+
let message = subject || 'Updated local code';
|
|
81
|
+
if (changedFiles.length > 0) {
|
|
82
|
+
message += ` — files: ${changedFiles.slice(0, 5).join(', ')}${changedFiles.length > 5 ? ` +${changedFiles.length - 5} more` : ''}`;
|
|
83
|
+
}
|
|
84
|
+
if (body && !body.toLowerCase().startsWith(subject.toLowerCase().slice(0, 20))) {
|
|
85
|
+
const bodyFirstLine = body.split('\n')[0].slice(0, 120);
|
|
86
|
+
if (bodyFirstLine.length > 10) {
|
|
87
|
+
message += ` | ${bodyFirstLine}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
entries.push({
|
|
91
|
+
date: timestamp,
|
|
92
|
+
timestamp,
|
|
93
|
+
message,
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
return entries;
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return [];
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Gets the raw diff for uncommitted changes in a specific directory.
|
|
104
|
+
* Filters out ekkOS context files and non-code files (if possible via git).
|
|
105
|
+
*/
|
|
106
|
+
function getUncommittedDiff(repoRoot, directoryPath) {
|
|
107
|
+
try {
|
|
108
|
+
const scope = directoryPath === '.' ? '.' : directoryPath;
|
|
109
|
+
// We filter out ekkOS_CONTEXT.md and lockfiles locally to save tokens
|
|
110
|
+
const command = `git -C ${JSON.stringify(repoRoot)} diff HEAD -- ${JSON.stringify(scope)} ':!**/ekkOS_CONTEXT.md' ':!**/package-lock.json' ':!**/pnpm-lock.yaml' ':!**/yarn.lock'`;
|
|
111
|
+
const diff = (0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1024 * 1024 * 2 }).trim();
|
|
112
|
+
return diff;
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
console.error(`[GitUtils] Failed to get uncommitted diff in ${directoryPath}:`, err.message);
|
|
116
|
+
return '';
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Gets the diff for the most recent commit.
|
|
121
|
+
*/
|
|
122
|
+
function getRecentCommitDiff(repoRoot, directoryPath) {
|
|
123
|
+
try {
|
|
124
|
+
const scope = directoryPath === '.' ? '.' : directoryPath;
|
|
125
|
+
const command = `git -C ${JSON.stringify(repoRoot)} diff @~1..@ -- ${JSON.stringify(scope)} ':!**/ekkOS_CONTEXT.md' ':!**/package-lock.json' ':!**/pnpm-lock.yaml' ':!**/yarn.lock'`;
|
|
126
|
+
const diff = (0, child_process_1.execSync)(command, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 1024 * 1024 * 2 }).trim();
|
|
127
|
+
return diff;
|
|
128
|
+
}
|
|
129
|
+
catch (err) {
|
|
130
|
+
console.error(`[GitUtils] Failed to get recent commit diff in ${directoryPath}:`, err.message);
|
|
131
|
+
return '';
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Checks if the current directory is a git repository.
|
|
136
|
+
*/
|
|
137
|
+
function isGitRepo(dir) {
|
|
138
|
+
try {
|
|
139
|
+
(0, child_process_1.execSync)(`git -C ${JSON.stringify(dir)} rev-parse --is-inside-work-tree`, { stdio: 'ignore' });
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Gets the timestamp of the last commit for a specific directory.
|
|
148
|
+
*/
|
|
149
|
+
function getLastCommitTimestamp(repoRoot, directoryPath) {
|
|
150
|
+
try {
|
|
151
|
+
const scope = directoryPath === '.' ? '.' : directoryPath;
|
|
152
|
+
const raw = (0, child_process_1.execSync)(`git -C ${JSON.stringify(repoRoot)} log --max-count=1 --pretty=format:%ct -- ${JSON.stringify(scope)} ':!**/ekkOS_CONTEXT.md'`, { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
153
|
+
return raw ? Number(raw) * 1000 : null;
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Gets the absolute path to the git repository root.
|
|
161
|
+
*/
|
|
162
|
+
function getRepoRoot(dir) {
|
|
163
|
+
try {
|
|
164
|
+
return (0, child_process_1.execSync)(`git -C ${JSON.stringify(dir)} rev-parse --show-toplevel`, { encoding: 'utf-8' }).trim();
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -5,6 +5,7 @@ export interface LocalLivingDocsManagerOptions {
|
|
|
5
5
|
timeZone?: string;
|
|
6
6
|
pollIntervalMs?: number;
|
|
7
7
|
flushDebounceMs?: number;
|
|
8
|
+
richAnalysis?: boolean;
|
|
8
9
|
onLog?: (message: string) => void;
|
|
9
10
|
}
|
|
10
11
|
export declare class LocalLivingDocsManager {
|
|
@@ -14,6 +15,7 @@ export declare class LocalLivingDocsManager {
|
|
|
14
15
|
private readonly timeZone;
|
|
15
16
|
private readonly pollIntervalMs;
|
|
16
17
|
private readonly flushDebounceMs;
|
|
18
|
+
private readonly richAnalysis;
|
|
17
19
|
private readonly onLog?;
|
|
18
20
|
private repoRoot;
|
|
19
21
|
private watchRoot;
|
|
@@ -50,6 +52,10 @@ export declare class LocalLivingDocsManager {
|
|
|
50
52
|
private compileStaleSystems;
|
|
51
53
|
private collectSystemFiles;
|
|
52
54
|
private compileSystem;
|
|
55
|
+
/**
|
|
56
|
+
* Calls the server-side semantic analyzer and injects rich insights into the context file.
|
|
57
|
+
*/
|
|
58
|
+
private injectSemanticAnalysis;
|
|
53
59
|
/**
|
|
54
60
|
* Metadata-only patch for server-compiled docs.
|
|
55
61
|
* Updates ONLY the approved local-mutable frontmatter fields without touching the body.
|