@ekkos/cli 1.3.7 → 1.3.9

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.
@@ -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 {};
@@ -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 = findGitRoot(targetPath);
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
- const rawSystems = scanDirectory(repoRoot, repoRoot, 0, 4, null);
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 response = await fetch(`${apiUrl}/api/v1/living-docs/seed`, {
326
- method: 'POST',
327
- headers: {
328
- 'Authorization': `Bearer ${apiKey}`,
329
- 'Content-Type': 'application/json',
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
  }
@@ -359,12 +359,8 @@ Use \`ekkOS_Forge\` when you:
359
359
 
360
360
  ## Pattern Application
361
361
 
362
- When you use a pattern from memory:
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() {
@@ -2,4 +2,5 @@ export * from './mcp';
2
2
  export * from './settings';
3
3
  export * from './hooks';
4
4
  export * from './skills';
5
+ export * from './agents';
5
6
  export * from './instructions';
@@ -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 CLAUDE.md to ~/.claude/CLAUDE.md
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 is deployed
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 (for preview)
17
+ * Get the ekkOS CLAUDE.md template content
11
18
  */
12
19
  export declare function getInstructionsContent(): string;
@@ -6,25 +6,52 @@ exports.getInstructionsContent = getInstructionsContent;
6
6
  const fs_1 = require("fs");
7
7
  const platform_1 = require("../utils/platform");
8
8
  const templates_1 = require("../utils/templates");
9
+ const BEGIN_MARKER = '<!-- ekkOS:begin — managed by ekkos init, do not remove this marker -->';
10
+ const END_MARKER = '<!-- ekkOS:end -->';
9
11
  /**
10
- * Deploy CLAUDE.md to ~/.claude/CLAUDE.md
12
+ * Deploy ekkOS block into ~/.claude/CLAUDE.md.
13
+ *
14
+ * Strategy:
15
+ * - If file doesn't exist → create it with just the ekkOS block.
16
+ * - If file exists and already has markers → replace the block between markers.
17
+ * - If file exists with no markers → prepend ekkOS block above existing content.
18
+ *
19
+ * Never destroys user content outside the markers.
11
20
  */
12
21
  function deployInstructions() {
13
- // Ensure .claude directory exists
14
22
  if (!(0, fs_1.existsSync)(platform_1.CLAUDE_DIR)) {
15
23
  (0, fs_1.mkdirSync)(platform_1.CLAUDE_DIR, { recursive: true });
16
24
  }
17
- // Copy CLAUDE.md template
18
- (0, templates_1.copyTemplateFile)('CLAUDE.md', platform_1.CLAUDE_MD);
25
+ const ekkosBlock = (0, templates_1.readTemplate)('CLAUDE.md');
26
+ if (!(0, fs_1.existsSync)(platform_1.CLAUDE_MD)) {
27
+ // No file — create fresh
28
+ (0, fs_1.writeFileSync)(platform_1.CLAUDE_MD, ekkosBlock);
29
+ return;
30
+ }
31
+ const existing = (0, fs_1.readFileSync)(platform_1.CLAUDE_MD, 'utf-8');
32
+ const beginIdx = existing.indexOf(BEGIN_MARKER);
33
+ const endIdx = existing.indexOf(END_MARKER);
34
+ if (beginIdx !== -1 && endIdx !== -1 && endIdx > beginIdx) {
35
+ // Markers found — replace in-place
36
+ const before = existing.slice(0, beginIdx);
37
+ const after = existing.slice(endIdx + END_MARKER.length);
38
+ (0, fs_1.writeFileSync)(platform_1.CLAUDE_MD, before + ekkosBlock + after);
39
+ return;
40
+ }
41
+ // No markers — prepend ekkOS block, preserve everything below
42
+ (0, fs_1.writeFileSync)(platform_1.CLAUDE_MD, ekkosBlock + '\n\n' + existing);
19
43
  }
20
44
  /**
21
- * Check if CLAUDE.md is deployed
45
+ * Check if CLAUDE.md has the ekkOS block
22
46
  */
23
47
  function isInstructionsDeployed() {
24
- return (0, fs_1.existsSync)(platform_1.CLAUDE_MD);
48
+ if (!(0, fs_1.existsSync)(platform_1.CLAUDE_MD))
49
+ return false;
50
+ const content = (0, fs_1.readFileSync)(platform_1.CLAUDE_MD, 'utf-8');
51
+ return content.includes(BEGIN_MARKER) && content.includes(END_MARKER);
25
52
  }
26
53
  /**
27
- * Get the CLAUDE.md content (for preview)
54
+ * Get the ekkOS CLAUDE.md template content
28
55
  */
29
56
  function getInstructionsContent() {
30
57
  try {