@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/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
- // Deprecated setup command (redirects to init) — hidden from root help by custom formatHelp
562
+ // Logout command
539
563
  commander_1.program
540
- .command('setup')
541
- .description('[DEPRECATED] Use "ekkos init" instead')
542
- .option('-i, --ide <ide>', 'IDE to setup')
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
- console.log('');
546
- console.log(chalk_1.default.yellow('⚠️ The "setup" command is deprecated.'));
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(() => runRemoteCommand('synk', 'codex', ...process.argv.slice(3)));
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.map(c => c.name());
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.