@soleri/cli 9.14.3 → 9.16.7

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.
Files changed (87) hide show
  1. package/dist/commands/add-domain.js +65 -0
  2. package/dist/commands/add-domain.js.map +1 -1
  3. package/dist/commands/agent.js +51 -20
  4. package/dist/commands/agent.js.map +1 -1
  5. package/dist/commands/brain.d.ts +8 -0
  6. package/dist/commands/brain.js +83 -0
  7. package/dist/commands/brain.js.map +1 -0
  8. package/dist/commands/chat.d.ts +11 -0
  9. package/dist/commands/chat.js +295 -0
  10. package/dist/commands/chat.js.map +1 -0
  11. package/dist/commands/create.js +11 -9
  12. package/dist/commands/create.js.map +1 -1
  13. package/dist/commands/dev.js +19 -0
  14. package/dist/commands/dev.js.map +1 -1
  15. package/dist/commands/dream.js +1 -12
  16. package/dist/commands/dream.js.map +1 -1
  17. package/dist/commands/hooks.js +29 -0
  18. package/dist/commands/hooks.js.map +1 -1
  19. package/dist/commands/install.js +3 -9
  20. package/dist/commands/install.js.map +1 -1
  21. package/dist/commands/knowledge.d.ts +9 -0
  22. package/dist/commands/knowledge.js +99 -0
  23. package/dist/commands/knowledge.js.map +1 -0
  24. package/dist/commands/pack.js +164 -3
  25. package/dist/commands/pack.js.map +1 -1
  26. package/dist/commands/schedule.d.ts +11 -0
  27. package/dist/commands/schedule.js +130 -0
  28. package/dist/commands/schedule.js.map +1 -0
  29. package/dist/commands/staging.d.ts +2 -17
  30. package/dist/commands/staging.js +4 -4
  31. package/dist/commands/staging.js.map +1 -1
  32. package/dist/commands/validate-skills.d.ts +10 -0
  33. package/dist/commands/validate-skills.js +47 -0
  34. package/dist/commands/validate-skills.js.map +1 -0
  35. package/dist/commands/vault.js +2 -11
  36. package/dist/commands/vault.js.map +1 -1
  37. package/dist/main.js +10 -0
  38. package/dist/main.js.map +1 -1
  39. package/dist/utils/checks.js +17 -32
  40. package/dist/utils/checks.js.map +1 -1
  41. package/dist/utils/core-resolver.d.ts +3 -0
  42. package/dist/utils/core-resolver.js +38 -0
  43. package/dist/utils/core-resolver.js.map +1 -0
  44. package/dist/utils/vault-db.d.ts +5 -0
  45. package/dist/utils/vault-db.js +17 -0
  46. package/dist/utils/vault-db.js.map +1 -0
  47. package/package.json +1 -1
  48. package/src/__tests__/doctor.test.ts +46 -1
  49. package/src/__tests__/hook-packs.test.ts +0 -18
  50. package/src/__tests__/hooks-convert.test.ts +0 -28
  51. package/src/__tests__/hooks-sync.test.ts +109 -0
  52. package/src/__tests__/hooks.test.ts +0 -20
  53. package/src/__tests__/install-verify.test.ts +1 -1
  54. package/src/__tests__/install.test.ts +7 -10
  55. package/src/__tests__/update.test.ts +0 -19
  56. package/src/__tests__/validator.test.ts +0 -16
  57. package/src/commands/add-domain.ts +89 -1
  58. package/src/commands/agent.ts +53 -17
  59. package/src/commands/brain.ts +93 -0
  60. package/src/commands/chat.ts +373 -0
  61. package/src/commands/create.ts +11 -8
  62. package/src/commands/dev.ts +21 -0
  63. package/src/commands/dream.ts +1 -11
  64. package/src/commands/hooks.ts +32 -0
  65. package/src/commands/install.ts +3 -8
  66. package/src/commands/knowledge.ts +124 -0
  67. package/src/commands/pack.ts +219 -1
  68. package/src/commands/schedule.ts +150 -0
  69. package/src/commands/staging.ts +5 -5
  70. package/src/commands/validate-skills.ts +58 -0
  71. package/src/commands/vault.ts +2 -11
  72. package/src/main.ts +10 -0
  73. package/src/utils/checks.ts +18 -30
  74. package/src/utils/core-resolver.ts +39 -0
  75. package/src/utils/vault-db.ts +15 -0
  76. package/dist/hook-packs/converter/template.test.ts +0 -133
  77. package/dist/hook-packs/yolo-safety/scripts/anti-deletion.sh +0 -274
  78. package/dist/prompts/archetypes.d.ts +0 -22
  79. package/dist/prompts/archetypes.js +0 -298
  80. package/dist/prompts/archetypes.js.map +0 -1
  81. package/dist/prompts/playbook.d.ts +0 -64
  82. package/dist/prompts/playbook.js +0 -436
  83. package/dist/prompts/playbook.js.map +0 -1
  84. package/dist/utils/format-paths.d.ts +0 -14
  85. package/dist/utils/format-paths.js +0 -27
  86. package/dist/utils/format-paths.js.map +0 -1
  87. package/src/__tests__/dream.test.ts +0 -119
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Knowledge CLI — export vault entries as portable knowledge bundle JSON files.
3
+ *
4
+ * `soleri knowledge export --domain <name>` — export single domain to knowledge/<name>/patterns.json
5
+ * `soleri knowledge export --all` — export all domains
6
+ * `soleri knowledge export --min-score 0.5` — filter low-quality entries
7
+ */
8
+
9
+ import { mkdirSync, writeFileSync } from 'node:fs';
10
+ import { join, resolve } from 'node:path';
11
+ import type { Command } from 'commander';
12
+ import * as log from '../utils/logger.js';
13
+ import { detectAgent } from '../utils/agent-context.js';
14
+ import { resolveVaultDbPath } from '../utils/vault-db.js';
15
+
16
+ interface ExportOpts {
17
+ domain?: string;
18
+ all?: boolean;
19
+ minScore?: string;
20
+ output?: string;
21
+ }
22
+
23
+ export function registerKnowledge(program: Command): void {
24
+ const knowledge = program.command('knowledge').description('Knowledge bundle management');
25
+
26
+ knowledge
27
+ .command('export')
28
+ .description('Export vault entries to portable knowledge bundle JSON files')
29
+ .option('--domain <name>', 'Export a specific domain')
30
+ .option('--all', 'Export all domains')
31
+ .option('--min-score <number>', 'Minimum quality score threshold (0-1)', '0')
32
+ .option('--output <dir>', 'Output directory (default: ./knowledge/)')
33
+ .action(async (opts: ExportOpts) => {
34
+ const agent = detectAgent();
35
+ if (!agent) {
36
+ log.fail('Not in a Soleri agent project', 'Run from an agent directory');
37
+ process.exit(1);
38
+ }
39
+
40
+ if (!opts.domain && !opts.all) {
41
+ log.fail('Specify --domain <name> or --all');
42
+ process.exit(1);
43
+ }
44
+
45
+ const vaultDbPath = resolveVaultDbPath(agent.agentId);
46
+ if (!vaultDbPath) {
47
+ log.fail('Vault DB not found', 'Run the agent once to initialize its vault database.');
48
+ process.exit(1);
49
+ }
50
+
51
+ const minScore = parseFloat(opts.minScore ?? '0');
52
+ const outputDir = opts.output ? resolve(opts.output) : resolve('knowledge');
53
+
54
+ const { Vault } = await import('@soleri/core');
55
+ const vault = new Vault(vaultDbPath);
56
+
57
+ try {
58
+ log.heading('Knowledge Export');
59
+
60
+ const domainsToExport: string[] = [];
61
+
62
+ if (opts.all) {
63
+ const all = vault.list({ limit: 50_000 });
64
+ const domainSet = new Set<string>();
65
+ for (const e of all) {
66
+ if (e.domain) domainSet.add(e.domain);
67
+ }
68
+ domainsToExport.push(...domainSet);
69
+ } else if (opts.domain) {
70
+ domainsToExport.push(opts.domain);
71
+ }
72
+
73
+ if (domainsToExport.length === 0) {
74
+ log.warn('No domains found in vault');
75
+ return;
76
+ }
77
+
78
+ let totalExported = 0;
79
+
80
+ for (const domain of domainsToExport) {
81
+ const entries = vault.list({ domain, limit: 10_000 });
82
+
83
+ // Filter by score if provided (vault entries may have a score/severity field)
84
+ const filtered = entries.filter((e) => {
85
+ if (minScore <= 0) return true;
86
+ // Use tags count as a rough quality proxy when no explicit score exists
87
+ const tagScore = (e.tags?.length ?? 0) / 10 + (e.description ? 0.5 : 0);
88
+ return tagScore >= minScore;
89
+ });
90
+
91
+ if (filtered.length === 0) {
92
+ log.warn(`Domain "${domain}": no entries passed min-score filter`);
93
+ continue;
94
+ }
95
+
96
+ // Format entries into knowledge bundle schema
97
+ const bundleEntries = filtered.map((e) => ({
98
+ id: e.id,
99
+ type: e.type,
100
+ domain: e.domain,
101
+ title: e.title,
102
+ description: e.description,
103
+ ...(e.severity ? { severity: e.severity } : {}),
104
+ ...(e.tags?.length ? { tags: e.tags } : {}),
105
+ }));
106
+
107
+ const domainDir = join(outputDir, domain);
108
+ mkdirSync(domainDir, { recursive: true });
109
+ const outPath = join(domainDir, 'patterns.json');
110
+ writeFileSync(outPath, JSON.stringify(bundleEntries, null, 2) + '\n', 'utf-8');
111
+
112
+ log.pass(`${domain}: exported ${filtered.length} entries`, outPath);
113
+ totalExported += filtered.length;
114
+ }
115
+
116
+ log.pass(
117
+ `Total: ${totalExported} entries across ${domainsToExport.length} domain(s)`,
118
+ outputDir,
119
+ );
120
+ } finally {
121
+ vault.close();
122
+ }
123
+ });
124
+ }
@@ -8,7 +8,7 @@
8
8
  */
9
9
 
10
10
  import { join, resolve as pathResolve } from 'node:path';
11
- import { existsSync, readFileSync, readdirSync } from 'node:fs';
11
+ import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from 'node:fs';
12
12
  import type { Command } from 'commander';
13
13
  import * as p from '@clack/prompts';
14
14
  import {
@@ -17,8 +17,14 @@ import {
17
17
  resolvePack,
18
18
  checkNpmVersion,
19
19
  getBuiltinKnowledgePacksDirs,
20
+ LLMClient,
21
+ KeyPool,
22
+ loadKeyPoolConfig,
23
+ Vault,
24
+ SOLERI_HOME,
20
25
  } from '@soleri/core';
21
26
  import type { LockEntry, PackSource } from '@soleri/core';
27
+ import { resolveVaultDbPath } from '../utils/vault-db.js';
22
28
 
23
29
  // ─── Tier display helpers ────────────────────────────────────────────
24
30
 
@@ -774,10 +780,222 @@ export function registerPack(program: Command): void {
774
780
  process.exit(1);
775
781
  }
776
782
  });
783
+
784
+ // ─── Seed ─────────────────────────────────────────────────────────────
785
+ pack
786
+ .command('seed')
787
+ .argument('<domain>', 'Domain to generate knowledge entries for (e.g. typescript, react)')
788
+ .option('--entries <count>', 'Number of entries to generate', '15')
789
+ .option('--dry-run', 'Preview generated entries without seeding vault')
790
+ .option('--output <path>', 'Save entries as pack files instead of seeding vault')
791
+ .option('--yes', 'Skip confirmation prompt')
792
+ .description('Generate domain knowledge entries using LLM and seed them into the vault')
793
+ .action(
794
+ async (
795
+ domain: string,
796
+ opts: { entries?: string; dryRun?: boolean; output?: string; yes?: boolean },
797
+ ) => {
798
+ const ctx = detectAgent();
799
+ if (!ctx) {
800
+ p.log.error('No agent project detected in current directory.');
801
+ process.exit(1);
802
+ }
803
+
804
+ const entryCount = Math.min(Math.max(parseInt(opts.entries ?? '15', 10) || 15, 5), 30);
805
+
806
+ // ─── LLM client setup ──────────────────────────────────────
807
+ const keyConfig = loadKeyPoolConfig(ctx.agentId);
808
+ const openaiPool = new KeyPool(keyConfig.openai);
809
+ const anthropicPool = new KeyPool(keyConfig.anthropic);
810
+ const llm = new LLMClient(openaiPool, anthropicPool, ctx.agentId);
811
+ const available = llm.isAvailable();
812
+
813
+ if (!available.openai && !available.anthropic) {
814
+ p.log.error(
815
+ 'No LLM API key configured. Set OPENAI_API_KEY or ANTHROPIC_API_KEY, ' +
816
+ `or add keys to ${join(SOLERI_HOME, ctx.agentId, 'keys.json')}.`,
817
+ );
818
+ process.exit(1);
819
+ }
820
+
821
+ // ─── Generate entries ──────────────────────────────────────
822
+ const s = p.spinner();
823
+ s.start(`Generating ${entryCount} knowledge entries for domain "${domain}"...`);
824
+
825
+ const provider = available.anthropic ? 'anthropic' : 'openai';
826
+ const model = available.anthropic ? 'claude-haiku-4-5-20251001' : 'gpt-4o-mini';
827
+
828
+ const systemPrompt = `You are a knowledge engineering expert. Generate structured knowledge entries for developer vaults.
829
+ Each entry must be valid JSON matching this schema:
830
+ {
831
+ "id": "string (kebab-case, domain-prefixed, unique)",
832
+ "type": "pattern" | "anti-pattern" | "rule" | "playbook",
833
+ "domain": "string",
834
+ "title": "string (concise, searchable)",
835
+ "severity": "critical" | "warning" | "suggestion",
836
+ "description": "string (2-3 sentences, actionable)",
837
+ "why": "string (1-2 sentences explaining the reasoning)",
838
+ "tags": ["array", "of", "keywords"],
839
+ "context": "string (optional — when this applies)"
840
+ }
841
+
842
+ Return ONLY a JSON array of entries, no prose, no markdown fences.`;
843
+
844
+ const userPrompt = `Generate exactly ${entryCount} high-quality knowledge vault entries for the "${domain}" domain.
845
+ Focus on: patterns that prevent bugs, common anti-patterns, best practices, and rules experienced developers follow.
846
+ Make entries concrete and actionable — not generic platitudes.
847
+ Return a JSON array of ${entryCount} entries.`;
848
+
849
+ let generatedEntries: SeedEntry[] = [];
850
+
851
+ try {
852
+ const result = await llm.complete({
853
+ caller: 'pack-seed',
854
+ systemPrompt,
855
+ userPrompt,
856
+ provider,
857
+ model,
858
+ maxTokens: 8000,
859
+ temperature: 0.4,
860
+ });
861
+
862
+ s.stop('Generation complete');
863
+
864
+ // Parse JSON from response
865
+ const text = result.text.trim();
866
+ const jsonStart = text.indexOf('[');
867
+ const jsonEnd = text.lastIndexOf(']');
868
+ if (jsonStart === -1 || jsonEnd === -1) {
869
+ p.log.error('LLM did not return a valid JSON array');
870
+ process.exit(1);
871
+ }
872
+
873
+ const parsed = JSON.parse(text.slice(jsonStart, jsonEnd + 1)) as unknown[];
874
+ generatedEntries = parsed
875
+ .filter(
876
+ (e): e is SeedEntry =>
877
+ typeof e === 'object' &&
878
+ e !== null &&
879
+ 'id' in e &&
880
+ 'title' in e &&
881
+ 'description' in e,
882
+ )
883
+ .map((e) => ({
884
+ ...e,
885
+ domain: domain,
886
+ tags: Array.isArray(e.tags) ? e.tags : [],
887
+ }));
888
+ } catch (err) {
889
+ s.stop('Generation failed');
890
+ p.log.error(err instanceof Error ? err.message : String(err));
891
+ process.exit(1);
892
+ }
893
+
894
+ if (generatedEntries.length === 0) {
895
+ p.log.error('No valid entries generated.');
896
+ process.exit(1);
897
+ }
898
+
899
+ // ─── Dedup check ───────────────────────────────────────────
900
+ const vaultDbPath = resolveVaultDbPath(ctx.agentId);
901
+ let dedupedEntries = generatedEntries;
902
+
903
+ if (vaultDbPath) {
904
+ const vault = new Vault(vaultDbPath);
905
+ const existing = new Set(vault.list({ domain, limit: 1000 }).map((e) => e.id));
906
+ vault.close();
907
+ const before = dedupedEntries.length;
908
+ dedupedEntries = dedupedEntries.filter((e) => !existing.has(e.id));
909
+ const skipped = before - dedupedEntries.length;
910
+ if (skipped > 0) {
911
+ p.log.info(`Skipped ${skipped} duplicate entries (already in vault)`);
912
+ }
913
+ }
914
+
915
+ if (dedupedEntries.length === 0) {
916
+ p.log.success(`All generated entries already exist in vault for domain "${domain}".`);
917
+ return;
918
+ }
919
+
920
+ // ─── Preview table ─────────────────────────────────────────
921
+ console.log('');
922
+ console.log(` Generated ${dedupedEntries.length} entries for "${domain}":`);
923
+ console.log('');
924
+ const idWidth = Math.max(2, ...dedupedEntries.map((e) => e.id.length));
925
+ const titleWidth = Math.max(5, ...dedupedEntries.map((e) => e.title.length));
926
+ console.log(
927
+ ` ${'ID'.padEnd(idWidth)} ${'Title'.padEnd(titleWidth)} Type Severity`,
928
+ );
929
+ console.log(` ${'-'.repeat(idWidth)} ${'-'.repeat(titleWidth)} ----------- --------`);
930
+ for (const entry of dedupedEntries) {
931
+ console.log(
932
+ ` ${entry.id.padEnd(idWidth)} ${entry.title.padEnd(titleWidth)} ${(entry.type ?? 'pattern').padEnd(11)} ${entry.severity ?? 'suggestion'}`,
933
+ );
934
+ }
935
+ console.log('');
936
+
937
+ if (opts.dryRun) {
938
+ p.log.info(`Dry run — ${dedupedEntries.length} entries generated, vault not modified.`);
939
+ return;
940
+ }
941
+
942
+ // ─── Output to files ───────────────────────────────────────
943
+ if (opts.output) {
944
+ const outDir = pathResolve(opts.output);
945
+ mkdirSync(outDir, { recursive: true });
946
+ const outPath = join(outDir, `${domain}.json`);
947
+ writeFileSync(outPath, JSON.stringify({ domain, entries: dedupedEntries }, null, 2));
948
+ p.log.success(`Saved ${dedupedEntries.length} entries to ${outPath}`);
949
+ return;
950
+ }
951
+
952
+ // ─── Confirm + seed vault ──────────────────────────────────
953
+ if (!opts.yes) {
954
+ const confirm = await p.confirm({
955
+ message: `Seed ${dedupedEntries.length} entries into vault for domain "${domain}"?`,
956
+ });
957
+ if (p.isCancel(confirm) || !confirm) {
958
+ p.log.info('Cancelled — vault not modified.');
959
+ return;
960
+ }
961
+ }
962
+
963
+ if (!vaultDbPath) {
964
+ p.log.error('Vault not initialized. Run `soleri install` first.');
965
+ process.exit(1);
966
+ }
967
+
968
+ const vault = new Vault(vaultDbPath);
969
+ const seeded = vault.seed(
970
+ dedupedEntries.map((e) => ({
971
+ ...e,
972
+ id: e.id,
973
+ type: (e.type ?? 'pattern') as 'pattern' | 'anti-pattern' | 'rule' | 'playbook',
974
+ severity: (e.severity ?? 'suggestion') as 'critical' | 'warning' | 'suggestion',
975
+ tags: e.tags ?? [],
976
+ })),
977
+ );
978
+ vault.close();
979
+
980
+ p.log.success(`Seeded ${seeded} knowledge entries for domain "${domain}" into vault.`);
981
+ },
982
+ );
777
983
  }
778
984
 
779
985
  // ─── Helpers ──────────────────────────────────────────────────────────
780
986
 
987
+ interface SeedEntry {
988
+ id: string;
989
+ type?: string;
990
+ domain: string;
991
+ title: string;
992
+ severity?: string;
993
+ description: string;
994
+ why?: string;
995
+ tags: string[];
996
+ context?: string;
997
+ }
998
+
781
999
  function listMdFiles(dir: string): string[] {
782
1000
  try {
783
1001
  const { basename } = require('node:path');
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Schedule CLI — manage autonomous scheduled agent tasks.
3
+ *
4
+ * `soleri schedule create --name X --cron "0 2 * * *" --prompt "run dream"`
5
+ * `soleri schedule list`
6
+ * `soleri schedule delete --id <id>`
7
+ * `soleri schedule pause --id <id>`
8
+ * `soleri schedule resume --id <id>`
9
+ */
10
+
11
+ import type { Command } from 'commander';
12
+ import * as p from '@clack/prompts';
13
+ import { detectAgent } from '../utils/agent-context.js';
14
+ import * as log from '../utils/logger.js';
15
+
16
+ export function registerSchedule(program: Command): void {
17
+ const schedule = program
18
+ .command('schedule')
19
+ .description('Manage autonomous scheduled agent tasks');
20
+
21
+ // ─── create ────────────────────────────────────────────────────────
22
+ schedule
23
+ .command('create')
24
+ .description('Create a new scheduled task')
25
+ .requiredOption('--name <name>', 'Task name (unique per agent)')
26
+ .requiredOption('--cron <expr>', 'Cron expression (5-field, min 1-hour interval)')
27
+ .requiredOption('--prompt <text>', 'Prompt passed to claude -p when task fires')
28
+ .option('--project-dir <path>', 'Agent project directory (default: current directory)')
29
+ .action(async (opts: { name: string; cron: string; prompt: string; projectDir?: string }) => {
30
+ const agent = detectAgent();
31
+ if (!agent) {
32
+ log.fail('Not in a Soleri agent project', 'Run from an agent directory');
33
+ process.exit(1);
34
+ }
35
+
36
+ const { Scheduler, InMemorySchedulerStore, validateCron } = await import('@soleri/core');
37
+
38
+ const cronError = validateCron(opts.cron);
39
+ if (cronError) {
40
+ log.fail('Invalid cron expression', cronError);
41
+ process.exit(1);
42
+ }
43
+
44
+ const s = p.spinner();
45
+ s.start('Creating scheduled task...');
46
+
47
+ try {
48
+ const scheduler = new Scheduler(undefined, new InMemorySchedulerStore());
49
+ const result = await scheduler.create({
50
+ name: opts.name,
51
+ cronExpression: opts.cron,
52
+ prompt: opts.prompt,
53
+ projectPath: opts.projectDir ?? agent.agentPath,
54
+ });
55
+
56
+ if ('error' in result) {
57
+ s.stop('Failed');
58
+ log.fail(result.error);
59
+ process.exit(1);
60
+ }
61
+
62
+ s.stop('Task created');
63
+ log.pass(`Task "${opts.name}" scheduled`, `ID: ${result.id}`);
64
+ p.log.info(`Cron: ${opts.cron}`);
65
+ p.log.info(`Platform ID: ${result.platformId ?? 'pending'}`);
66
+ } catch (err) {
67
+ s.stop('Failed');
68
+ log.fail(err instanceof Error ? err.message : String(err));
69
+ process.exit(1);
70
+ }
71
+ });
72
+
73
+ // ─── list ──────────────────────────────────────────────────────────
74
+ schedule
75
+ .command('list')
76
+ .description('List all scheduled tasks')
77
+ .action(async () => {
78
+ const { Scheduler, InMemorySchedulerStore } = await import('@soleri/core');
79
+ const scheduler = new Scheduler(undefined, new InMemorySchedulerStore());
80
+ const tasks = await scheduler.list();
81
+
82
+ if (tasks.length === 0) {
83
+ p.log.info('No scheduled tasks. Use: soleri schedule create');
84
+ return;
85
+ }
86
+
87
+ log.heading(`Scheduled Tasks (${tasks.length})`);
88
+ for (const t of tasks) {
89
+ const status = t.enabled ? 'enabled' : 'paused';
90
+ const sync = t.platformSynced ? 'synced' : 'not synced';
91
+ p.log.info(`${t.name} [${status}] [${sync}]`);
92
+ p.log.info(` ID: ${t.id} Cron: ${t.cronExpression}`);
93
+ p.log.info(` Prompt: ${t.prompt.slice(0, 60)}${t.prompt.length > 60 ? '...' : ''}`);
94
+ }
95
+ });
96
+
97
+ // ─── delete ────────────────────────────────────────────────────────
98
+ schedule
99
+ .command('delete')
100
+ .description('Delete a scheduled task')
101
+ .requiredOption('--id <id>', 'Task ID to delete')
102
+ .action(async (opts: { id: string }) => {
103
+ const { Scheduler, InMemorySchedulerStore } = await import('@soleri/core');
104
+ const scheduler = new Scheduler(undefined, new InMemorySchedulerStore());
105
+ const result = await scheduler.delete(opts.id);
106
+
107
+ if (!result.deleted) {
108
+ log.fail(result.error ?? 'Delete failed');
109
+ process.exit(1);
110
+ }
111
+
112
+ log.pass(`Task ${opts.id} deleted`);
113
+ });
114
+
115
+ // ─── pause ─────────────────────────────────────────────────────────
116
+ schedule
117
+ .command('pause')
118
+ .description('Pause a scheduled task without deleting it')
119
+ .requiredOption('--id <id>', 'Task ID to pause')
120
+ .action(async (opts: { id: string }) => {
121
+ const { Scheduler, InMemorySchedulerStore } = await import('@soleri/core');
122
+ const scheduler = new Scheduler(undefined, new InMemorySchedulerStore());
123
+ const result = await scheduler.pause(opts.id);
124
+
125
+ if (!result.paused) {
126
+ log.fail(result.error ?? 'Pause failed');
127
+ process.exit(1);
128
+ }
129
+
130
+ log.pass(`Task ${opts.id} paused`);
131
+ });
132
+
133
+ // ─── resume ────────────────────────────────────────────────────────
134
+ schedule
135
+ .command('resume')
136
+ .description('Resume a paused scheduled task')
137
+ .requiredOption('--id <id>', 'Task ID to resume')
138
+ .action(async (opts: { id: string }) => {
139
+ const { Scheduler, InMemorySchedulerStore } = await import('@soleri/core');
140
+ const scheduler = new Scheduler(undefined, new InMemorySchedulerStore());
141
+ const result = await scheduler.resume(opts.id);
142
+
143
+ if (!result.resumed) {
144
+ log.fail(result.error ?? 'Resume failed');
145
+ process.exit(1);
146
+ }
147
+
148
+ log.pass(`Task ${opts.id} resumed`);
149
+ });
150
+ }
@@ -4,12 +4,12 @@ import { join, relative } from 'node:path';
4
4
  import { homedir } from 'node:os';
5
5
  import * as log from '../utils/logger.js';
6
6
 
7
- export const STAGING_ROOT = join(homedir(), '.soleri', 'staging');
7
+ const STAGING_ROOT = join(homedir(), '.soleri', 'staging');
8
8
 
9
9
  /** Default max age for stale staging entries (7 days). */
10
10
  const DEFAULT_MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
11
11
 
12
- export interface StagedEntry {
12
+ interface StagedEntry {
13
13
  id: string;
14
14
  timestamp: string;
15
15
  path: string;
@@ -20,7 +20,7 @@ export interface StagedEntry {
20
20
  /**
21
21
  * Walk a directory tree and collect all items with their relative paths.
22
22
  */
23
- export function walkDir(dir: string, base: string): { relPath: string; size: number }[] {
23
+ function walkDir(dir: string, base: string): { relPath: string; size: number }[] {
24
24
  const results: { relPath: string; size: number }[] = [];
25
25
  if (!existsSync(dir)) return results;
26
26
 
@@ -41,7 +41,7 @@ export function walkDir(dir: string, base: string): { relPath: string; size: num
41
41
  /**
42
42
  * List all staged entries.
43
43
  */
44
- export function listStaged(): StagedEntry[] {
44
+ function listStaged(): StagedEntry[] {
45
45
  if (!existsSync(STAGING_ROOT)) return [];
46
46
 
47
47
  const entries: StagedEntry[] = [];
@@ -69,7 +69,7 @@ export function listStaged(): StagedEntry[] {
69
69
  /**
70
70
  * Parse a duration string like "7d", "24h", "30m" into milliseconds.
71
71
  */
72
- export function parseDuration(duration: string): number | null {
72
+ function parseDuration(duration: string): number | null {
73
73
  const match = duration.match(/^(\d+)(d|h|m)$/);
74
74
  if (!match) return null;
75
75
 
@@ -0,0 +1,58 @@
1
+ /**
2
+ * `soleri validate-skills` — validate user-installed SKILL.md op-call examples
3
+ * against the engine's actual Zod schemas.
4
+ *
5
+ * Scans ~/.claude/skills/ for SKILL.md files, extracts inline op-call examples,
6
+ * and checks each example's params against the corresponding facade schema.
7
+ * Exits with code 1 if any mismatches are found.
8
+ */
9
+
10
+ import { homedir } from 'node:os';
11
+ import { join } from 'node:path';
12
+ import type { Command } from 'commander';
13
+ import { validateSkillDocs } from '@soleri/core/skills/validate-skills';
14
+ import * as log from '../utils/logger.js';
15
+
16
+ const DEFAULT_SKILLS_DIR = join(homedir(), '.claude', 'skills');
17
+
18
+ export function registerValidateSkills(program: Command): void {
19
+ program
20
+ .command('validate-skills')
21
+ .description('Validate SKILL.md op-call examples against engine Zod schemas')
22
+ .option('--skills-dir <path>', 'Path to skills directory', DEFAULT_SKILLS_DIR)
23
+ .action((opts: { skillsDir: string }) => {
24
+ const skillsDir = opts.skillsDir;
25
+
26
+ log.heading('Soleri Validate Skills');
27
+ log.dim(`Scanning: ${skillsDir}`);
28
+ console.log();
29
+
30
+ const result = validateSkillDocs(skillsDir);
31
+
32
+ log.dim(`Schema registry: ${result.registrySize} ops`);
33
+ log.dim(`Skill files: ${result.totalFiles}`);
34
+ log.dim(`Op examples: ${result.totalExamples}`);
35
+ console.log();
36
+
37
+ if (result.totalFiles === 0) {
38
+ log.warn('No SKILL.md files found', skillsDir);
39
+ return;
40
+ }
41
+
42
+ if (result.valid) {
43
+ log.pass('All examples validate against their schemas.');
44
+ return;
45
+ }
46
+
47
+ log.fail(`Found ${result.errors.length} validation error(s):`);
48
+ console.log();
49
+
50
+ for (const err of result.errors) {
51
+ const location = err.line ? `:${err.line}` : '';
52
+ console.log(` ERROR ${err.file}${location} — op:${err.op}: ${err.message}`);
53
+ }
54
+
55
+ console.log();
56
+ process.exit(1);
57
+ });
58
+ }
@@ -7,12 +7,10 @@
7
7
  */
8
8
 
9
9
  import { resolve } from 'node:path';
10
- import { existsSync } from 'node:fs';
11
- import { join } from 'node:path';
12
10
  import type { Command } from 'commander';
13
11
  import { detectAgent } from '../utils/agent-context.js';
14
12
  import * as log from '../utils/logger.js';
15
- import { SOLERI_HOME } from '@soleri/core';
13
+ import { resolveVaultDbPath } from '../utils/vault-db.js';
16
14
 
17
15
  export function registerVault(program: Command): void {
18
16
  const vault = program.command('vault').description('Vault knowledge management');
@@ -31,14 +29,7 @@ export function registerVault(program: Command): void {
31
29
 
32
30
  const outputDir = opts.path ? resolve(opts.path) : resolve('knowledge');
33
31
 
34
- // Find vault DB — check new path first, then legacy
35
- const newDbPath = join(SOLERI_HOME, agent.agentId, 'vault.db');
36
- const legacyDbPath = join(SOLERI_HOME, '..', `.${agent.agentId}`, 'vault.db');
37
- const vaultDbPath = existsSync(newDbPath)
38
- ? newDbPath
39
- : existsSync(legacyDbPath)
40
- ? legacyDbPath
41
- : null;
32
+ const vaultDbPath = resolveVaultDbPath(agent.agentId);
42
33
 
43
34
  if (!vaultDbPath) {
44
35
  log.fail('Vault DB not found', 'Run the agent once to initialize its vault database.');
package/src/main.ts CHANGED
@@ -33,6 +33,11 @@ import { registerVault } from './commands/vault.js';
33
33
  import { registerYolo } from './commands/yolo.js';
34
34
  import { registerDream } from './commands/dream.js';
35
35
  import { registerUpdate } from './commands/update.js';
36
+ import { registerBrain } from './commands/brain.js';
37
+ import { registerValidateSkills } from './commands/validate-skills.js';
38
+ import { registerKnowledge } from './commands/knowledge.js';
39
+ import { registerSchedule } from './commands/schedule.js';
40
+ import { registerChat } from './commands/chat.js';
36
41
 
37
42
  const require = createRequire(import.meta.url);
38
43
  const { version } = require('../package.json');
@@ -98,4 +103,9 @@ registerVault(program);
98
103
  registerYolo(program);
99
104
  registerDream(program);
100
105
  registerUpdate(program);
106
+ registerBrain(program);
107
+ registerValidateSkills(program);
108
+ registerKnowledge(program);
109
+ registerSchedule(program);
110
+ registerChat(program);
101
111
  program.parse();