@soleri/cli 9.15.0 → 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.
- package/dist/commands/add-domain.js +65 -0
- package/dist/commands/add-domain.js.map +1 -1
- package/dist/commands/chat.d.ts +11 -0
- package/dist/commands/chat.js +295 -0
- package/dist/commands/chat.js.map +1 -0
- package/dist/commands/create.js +11 -9
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/dev.js +19 -0
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/hooks.js +29 -0
- package/dist/commands/hooks.js.map +1 -1
- package/dist/commands/knowledge.d.ts +9 -0
- package/dist/commands/knowledge.js +99 -0
- package/dist/commands/knowledge.js.map +1 -0
- package/dist/commands/pack.js +164 -3
- package/dist/commands/pack.js.map +1 -1
- package/dist/commands/schedule.d.ts +11 -0
- package/dist/commands/schedule.js +130 -0
- package/dist/commands/schedule.js.map +1 -0
- package/dist/commands/staging.d.ts +2 -17
- package/dist/commands/staging.js +4 -4
- package/dist/commands/staging.js.map +1 -1
- package/dist/main.js +6 -0
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/hook-packs.test.ts +0 -18
- package/src/__tests__/hooks-convert.test.ts +0 -28
- package/src/__tests__/hooks-sync.test.ts +109 -0
- package/src/__tests__/hooks.test.ts +0 -20
- package/src/__tests__/update.test.ts +0 -19
- package/src/__tests__/validator.test.ts +0 -16
- package/src/commands/add-domain.ts +89 -1
- package/src/commands/chat.ts +373 -0
- package/src/commands/create.ts +11 -8
- package/src/commands/dev.ts +21 -0
- package/src/commands/hooks.ts +32 -0
- package/src/commands/knowledge.ts +124 -0
- package/src/commands/pack.ts +219 -1
- package/src/commands/schedule.ts +150 -0
- package/src/commands/staging.ts +5 -5
- package/src/main.ts +6 -0
- 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
|
+
}
|
package/src/commands/pack.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/commands/staging.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/src/main.ts
CHANGED
|
@@ -35,6 +35,9 @@ import { registerDream } from './commands/dream.js';
|
|
|
35
35
|
import { registerUpdate } from './commands/update.js';
|
|
36
36
|
import { registerBrain } from './commands/brain.js';
|
|
37
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';
|
|
38
41
|
|
|
39
42
|
const require = createRequire(import.meta.url);
|
|
40
43
|
const { version } = require('../package.json');
|
|
@@ -102,4 +105,7 @@ registerDream(program);
|
|
|
102
105
|
registerUpdate(program);
|
|
103
106
|
registerBrain(program);
|
|
104
107
|
registerValidateSkills(program);
|
|
108
|
+
registerKnowledge(program);
|
|
109
|
+
registerSchedule(program);
|
|
110
|
+
registerChat(program);
|
|
105
111
|
program.parse();
|