@soleri/cli 9.14.2 → 9.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent.js +51 -20
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/brain.d.ts +8 -0
- package/dist/commands/brain.js +83 -0
- package/dist/commands/brain.js.map +1 -0
- package/dist/commands/dream.js +1 -12
- package/dist/commands/dream.js.map +1 -1
- package/dist/commands/install.js +3 -9
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/validate-skills.d.ts +10 -0
- package/dist/commands/validate-skills.js +47 -0
- package/dist/commands/validate-skills.js.map +1 -0
- package/dist/commands/vault.js +2 -11
- package/dist/commands/vault.js.map +1 -1
- package/dist/main.js +4 -0
- package/dist/main.js.map +1 -1
- package/dist/prompts/create-wizard.js +4 -1
- package/dist/prompts/create-wizard.js.map +1 -1
- package/dist/utils/checks.js +17 -32
- package/dist/utils/checks.js.map +1 -1
- package/dist/utils/core-resolver.d.ts +3 -0
- package/dist/utils/core-resolver.js +38 -0
- package/dist/utils/core-resolver.js.map +1 -0
- package/dist/utils/vault-db.d.ts +5 -0
- package/dist/utils/vault-db.js +17 -0
- package/dist/utils/vault-db.js.map +1 -0
- package/package.json +1 -1
- package/src/__tests__/create-wizard.test.ts +86 -0
- package/src/__tests__/doctor.test.ts +46 -1
- package/src/__tests__/install-verify.test.ts +1 -1
- package/src/__tests__/install.test.ts +7 -10
- package/src/commands/agent.ts +53 -17
- package/src/commands/brain.ts +93 -0
- package/src/commands/dream.ts +1 -11
- package/src/commands/install.ts +3 -8
- package/src/commands/validate-skills.ts +58 -0
- package/src/commands/vault.ts +2 -11
- package/src/main.ts +4 -0
- package/src/prompts/create-wizard.ts +5 -1
- package/src/utils/checks.ts +18 -30
- package/src/utils/core-resolver.ts +39 -0
- package/src/utils/vault-db.ts +15 -0
- package/dist/hook-packs/converter/template.test.ts +0 -133
- package/dist/hook-packs/yolo-safety/scripts/anti-deletion.sh +0 -274
- package/dist/prompts/archetypes.d.ts +0 -22
- package/dist/prompts/archetypes.js +0 -298
- package/dist/prompts/archetypes.js.map +0 -1
- package/dist/prompts/playbook.d.ts +0 -64
- package/dist/prompts/playbook.js +0 -436
- package/dist/prompts/playbook.js.map +0 -1
- package/dist/utils/format-paths.d.ts +0 -14
- package/dist/utils/format-paths.js +0 -27
- package/dist/utils/format-paths.js.map +0 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brain CLI — brain session management.
|
|
3
|
+
*
|
|
4
|
+
* `soleri brain close-orphans` — close orphaned sessions (default: --max-age 1h)
|
|
5
|
+
* `soleri brain close-orphans --max-age 2h` — close sessions older than 2h
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Command } from 'commander';
|
|
9
|
+
import { detectAgent } from '../utils/agent-context.js';
|
|
10
|
+
import { pass, fail, info, heading } from '../utils/logger.js';
|
|
11
|
+
import { resolveVaultDbPath } from '../utils/vault-db.js';
|
|
12
|
+
|
|
13
|
+
function parseMaxAge(value: string): number {
|
|
14
|
+
const match = value.match(/^(\d+)(h|m|s)$/);
|
|
15
|
+
if (!match) throw new Error(`Invalid --max-age format "${value}". Use e.g. 1h, 30m, 90s`);
|
|
16
|
+
const n = parseInt(match[1], 10);
|
|
17
|
+
const unit = match[2];
|
|
18
|
+
if (unit === 'h') return n * 60 * 60 * 1000;
|
|
19
|
+
if (unit === 'm') return n * 60 * 1000;
|
|
20
|
+
return n * 1000;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function registerBrain(program: Command): void {
|
|
24
|
+
const brain = program.command('brain').description('Brain session management');
|
|
25
|
+
|
|
26
|
+
brain
|
|
27
|
+
.command('close-orphans')
|
|
28
|
+
.description('Close orphaned brain sessions that were never completed')
|
|
29
|
+
.option('--max-age <duration>', 'Close sessions older than this age (e.g. 1h, 30m)', '1h')
|
|
30
|
+
.action(async (opts: { maxAge: string }) => {
|
|
31
|
+
const agent = detectAgent();
|
|
32
|
+
if (!agent) {
|
|
33
|
+
fail('Not in a Soleri agent project', 'Run from an agent directory');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const dbPath = resolveVaultDbPath(agent.agentId);
|
|
38
|
+
if (!dbPath) {
|
|
39
|
+
info('Vault DB not found — no sessions to close.');
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let maxAgeMs: number;
|
|
44
|
+
try {
|
|
45
|
+
maxAgeMs = parseMaxAge(opts.maxAge);
|
|
46
|
+
} catch (e: unknown) {
|
|
47
|
+
fail(
|
|
48
|
+
e instanceof Error ? e.message : String(e),
|
|
49
|
+
'Example: soleri brain close-orphans --max-age 1h',
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { Vault, Brain, BrainIntelligence } = await import('@soleri/core');
|
|
55
|
+
const vault = new Vault(dbPath);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const brainInstance = new Brain(vault);
|
|
59
|
+
const intelligence = new BrainIntelligence(vault, brainInstance);
|
|
60
|
+
|
|
61
|
+
const cutoff = new Date(Date.now() - maxAgeMs).toISOString().replace('T', ' ').slice(0, 19);
|
|
62
|
+
const activeSessions = intelligence.listSessions({ active: true, limit: 1000 });
|
|
63
|
+
const orphans = activeSessions.filter((s) => s.startedAt < cutoff);
|
|
64
|
+
|
|
65
|
+
if (orphans.length === 0) {
|
|
66
|
+
info(`No orphaned sessions older than ${opts.maxAge}.`);
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
heading('Brain — Close Orphans');
|
|
71
|
+
|
|
72
|
+
let closed = 0;
|
|
73
|
+
for (const s of orphans) {
|
|
74
|
+
try {
|
|
75
|
+
intelligence.lifecycle({
|
|
76
|
+
action: 'end',
|
|
77
|
+
sessionId: s.id,
|
|
78
|
+
planOutcome: 'abandoned',
|
|
79
|
+
context: `auto-closed via CLI: no completion after ${opts.maxAge}`,
|
|
80
|
+
});
|
|
81
|
+
closed++;
|
|
82
|
+
} catch {
|
|
83
|
+
// best-effort — never block on failures
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
pass(`Closed ${closed} orphaned session${closed === 1 ? '' : 's'}`);
|
|
88
|
+
process.exit(0);
|
|
89
|
+
} finally {
|
|
90
|
+
vault.close();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
}
|
package/src/commands/dream.ts
CHANGED
|
@@ -7,20 +7,10 @@
|
|
|
7
7
|
* `soleri dream status` — show dream status + cron info
|
|
8
8
|
*/
|
|
9
9
|
|
|
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 { pass, fail, info, heading, dim } from '../utils/logger.js';
|
|
15
|
-
import {
|
|
16
|
-
|
|
17
|
-
function resolveVaultDbPath(agentId: string): string | null {
|
|
18
|
-
const newDbPath = join(SOLERI_HOME, agentId, 'vault.db');
|
|
19
|
-
const legacyDbPath = join(SOLERI_HOME, '..', `.${agentId}`, 'vault.db');
|
|
20
|
-
if (existsSync(newDbPath)) return newDbPath;
|
|
21
|
-
if (existsSync(legacyDbPath)) return legacyDbPath;
|
|
22
|
-
return null;
|
|
23
|
-
}
|
|
13
|
+
import { resolveVaultDbPath } from '../utils/vault-db.js';
|
|
24
14
|
|
|
25
15
|
export function registerDream(program: Command): void {
|
|
26
16
|
const dream = program.command('dream').description('Vault memory consolidation');
|
package/src/commands/install.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
-
import { createRequire } from 'node:module';
|
|
3
2
|
import {
|
|
4
3
|
accessSync,
|
|
5
4
|
constants as fsConstants,
|
|
@@ -13,6 +12,7 @@ import { homedir } from 'node:os';
|
|
|
13
12
|
import * as p from '@clack/prompts';
|
|
14
13
|
import { detectAgent } from '../utils/agent-context.js';
|
|
15
14
|
import { detectArtifacts } from '../utils/agent-artifacts.js';
|
|
15
|
+
import { resolveInstalledEngineBin } from '../utils/core-resolver.js';
|
|
16
16
|
|
|
17
17
|
/** Default parent directory for agents: ~/.soleri/ */
|
|
18
18
|
const SOLERI_HOME = process.env.SOLERI_HOME ?? join(homedir(), '.soleri');
|
|
@@ -27,13 +27,8 @@ export const toPosix = (p: string): string => p.replace(/\\/g, '/');
|
|
|
27
27
|
* Falls back to `npx @soleri/engine` if resolution fails (e.g. not installed globally).
|
|
28
28
|
*/
|
|
29
29
|
export function resolveEngineBin(): { command: string; bin: string } {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const bin = require.resolve('@soleri/core/dist/engine/bin/soleri-engine.js');
|
|
33
|
-
return { command: 'node', bin };
|
|
34
|
-
} catch {
|
|
35
|
-
return { command: 'npx', bin: '@soleri/engine' };
|
|
36
|
-
}
|
|
30
|
+
const bin = resolveInstalledEngineBin();
|
|
31
|
+
return bin ? { command: 'node', bin } : { command: 'npx', bin: '@soleri/engine' };
|
|
37
32
|
}
|
|
38
33
|
|
|
39
34
|
/** MCP server entry for file-tree agents (resolved engine path, no npx) */
|
|
@@ -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
|
+
}
|
package/src/commands/vault.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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,8 @@ 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';
|
|
36
38
|
|
|
37
39
|
const require = createRequire(import.meta.url);
|
|
38
40
|
const { version } = require('../package.json');
|
|
@@ -98,4 +100,6 @@ registerVault(program);
|
|
|
98
100
|
registerYolo(program);
|
|
99
101
|
registerDream(program);
|
|
100
102
|
registerUpdate(program);
|
|
103
|
+
registerBrain(program);
|
|
104
|
+
registerValidateSkills(program);
|
|
101
105
|
program.parse();
|
|
@@ -42,12 +42,16 @@ export async function runCreateWizard(initialName?: string): Promise<CreateWizar
|
|
|
42
42
|
p.intro('Create a new Soleri agent');
|
|
43
43
|
|
|
44
44
|
// ─── Step 1: Name ───────────────────────────────────────────
|
|
45
|
+
const NAME_PLACEHOLDER = 'aria';
|
|
46
|
+
|
|
45
47
|
const name = (await p.text({
|
|
46
48
|
message: 'What should your agent be called?',
|
|
47
|
-
placeholder:
|
|
49
|
+
placeholder: NAME_PLACEHOLDER,
|
|
48
50
|
initialValue: initialName,
|
|
49
51
|
validate: (v) => {
|
|
50
52
|
if (!v || v.trim().length === 0) return 'Name is required';
|
|
53
|
+
if (v.trim().toLowerCase() === NAME_PLACEHOLDER)
|
|
54
|
+
return `"${NAME_PLACEHOLDER}" is just an example — type your own agent name`;
|
|
51
55
|
if (v.length > 50) return 'Max 50 characters';
|
|
52
56
|
},
|
|
53
57
|
})) as string;
|
package/src/utils/checks.ts
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
6
|
import { execFileSync } from 'node:child_process';
|
|
7
|
-
import { homedir } from 'node:os';
|
|
8
7
|
import { detectAgent, type AgentFormat } from './agent-context.js';
|
|
8
|
+
import { detectArtifacts } from './agent-artifacts.js';
|
|
9
|
+
import { resolveCorePackageJsonPath } from './core-resolver.js';
|
|
9
10
|
import { getInstalledPacks } from '../hook-packs/registry.js';
|
|
10
11
|
|
|
11
12
|
export interface CheckResult {
|
|
@@ -166,49 +167,36 @@ export function checkInstructionsDir(agentPath: string): CheckResult {
|
|
|
166
167
|
}
|
|
167
168
|
|
|
168
169
|
export function checkEngineReachable(): CheckResult {
|
|
169
|
-
|
|
170
|
-
require.resolve('@soleri/core/package.json');
|
|
170
|
+
if (resolveCorePackageJsonPath() !== null) {
|
|
171
171
|
return { status: 'pass', label: 'Engine', detail: '@soleri/core reachable' };
|
|
172
|
-
} catch {
|
|
173
|
-
return {
|
|
174
|
-
status: 'fail',
|
|
175
|
-
label: 'Engine',
|
|
176
|
-
detail: '@soleri/core not found — engine is required for file-tree agents',
|
|
177
|
-
};
|
|
178
172
|
}
|
|
173
|
+
|
|
174
|
+
return {
|
|
175
|
+
status: 'fail',
|
|
176
|
+
label: 'Engine',
|
|
177
|
+
detail: '@soleri/core not found — engine is required for file-tree agents',
|
|
178
|
+
};
|
|
179
179
|
}
|
|
180
180
|
|
|
181
181
|
function checkMcpRegistration(dir?: string): CheckResult {
|
|
182
182
|
const ctx = detectAgent(dir);
|
|
183
183
|
if (!ctx) return { status: 'warn', label: 'MCP registration', detail: 'no agent detected' };
|
|
184
184
|
|
|
185
|
-
const
|
|
186
|
-
if (
|
|
185
|
+
const artifacts = detectArtifacts(ctx.agentId, ctx.agentPath);
|
|
186
|
+
if (artifacts.mcpServerEntries.length === 0) {
|
|
187
187
|
return {
|
|
188
188
|
status: 'warn',
|
|
189
189
|
label: 'MCP registration',
|
|
190
|
-
detail:
|
|
190
|
+
detail: `not found in ~/.claude.json, ~/.codex/config.toml, or ~/.config/opencode/opencode.json`,
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
label: 'MCP registration',
|
|
201
|
-
detail: `registered as "${ctx.agentId}"`,
|
|
202
|
-
};
|
|
203
|
-
}
|
|
204
|
-
return {
|
|
205
|
-
status: 'warn',
|
|
206
|
-
label: 'MCP registration',
|
|
207
|
-
detail: `"${ctx.agentId}" not found in ~/.claude.json`,
|
|
208
|
-
};
|
|
209
|
-
} catch {
|
|
210
|
-
return { status: 'fail', label: 'MCP registration', detail: 'failed to parse ~/.claude.json' };
|
|
211
|
-
}
|
|
194
|
+
const targets = [...new Set(artifacts.mcpServerEntries.map((entry) => entry.target))];
|
|
195
|
+
return {
|
|
196
|
+
status: 'pass',
|
|
197
|
+
label: 'MCP registration',
|
|
198
|
+
detail: `registered in ${targets.join(', ')}`,
|
|
199
|
+
};
|
|
212
200
|
}
|
|
213
201
|
|
|
214
202
|
function checkCognee(): CheckResult {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
|
|
5
|
+
function resolveCoreEntryPath(): string | null {
|
|
6
|
+
try {
|
|
7
|
+
return fileURLToPath(import.meta.resolve('@soleri/core'));
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function resolveCorePackageJsonPath(): string | null {
|
|
14
|
+
const entryPath = resolveCoreEntryPath();
|
|
15
|
+
if (!entryPath) return null;
|
|
16
|
+
|
|
17
|
+
const packageJsonPath = join(dirname(entryPath), '..', 'package.json');
|
|
18
|
+
return existsSync(packageJsonPath) ? packageJsonPath : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function readInstalledCoreVersion(): string | null {
|
|
22
|
+
const packageJsonPath = resolveCorePackageJsonPath();
|
|
23
|
+
if (!packageJsonPath) return null;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as { version?: unknown };
|
|
27
|
+
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
28
|
+
} catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function resolveInstalledEngineBin(): string | null {
|
|
34
|
+
const entryPath = resolveCoreEntryPath();
|
|
35
|
+
if (!entryPath) return null;
|
|
36
|
+
|
|
37
|
+
const engineBinPath = join(dirname(entryPath), 'engine', 'bin', 'soleri-engine.js');
|
|
38
|
+
return existsSync(engineBinPath) ? engineBinPath : null;
|
|
39
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { SOLERI_HOME } from '@soleri/core';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Resolve the vault DB path for a given agent.
|
|
7
|
+
* Checks the current path first, then falls back to the legacy dot-prefixed path.
|
|
8
|
+
*/
|
|
9
|
+
export function resolveVaultDbPath(agentId: string): string | null {
|
|
10
|
+
const newDbPath = join(SOLERI_HOME, agentId, 'vault.db');
|
|
11
|
+
const legacyDbPath = join(SOLERI_HOME, '..', `.${agentId}`, 'vault.db');
|
|
12
|
+
if (existsSync(newDbPath)) return newDbPath;
|
|
13
|
+
if (existsSync(legacyDbPath)) return legacyDbPath;
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { generateHookScript, generateManifest, HOOK_EVENTS, ACTION_LEVELS } from './template.js';
|
|
3
|
-
import type { HookConversionConfig } from './template.js';
|
|
4
|
-
|
|
5
|
-
describe('generateHookScript', () => {
|
|
6
|
-
const baseConfig: HookConversionConfig = {
|
|
7
|
-
name: 'test-hook',
|
|
8
|
-
event: 'PreToolUse',
|
|
9
|
-
toolMatcher: 'Write|Edit',
|
|
10
|
-
filePatterns: ['**/marketing/**'],
|
|
11
|
-
action: 'remind',
|
|
12
|
-
message: 'Check brand guidelines before editing marketing files',
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
it('should generate a valid POSIX shell script', () => {
|
|
16
|
-
const script = generateHookScript(baseConfig);
|
|
17
|
-
expect(script).toContain('#!/bin/sh');
|
|
18
|
-
expect(script).toContain('set -eu');
|
|
19
|
-
expect(script).toContain('INPUT=$(cat)');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('should include tool matcher for PreToolUse', () => {
|
|
23
|
-
const script = generateHookScript(baseConfig);
|
|
24
|
-
expect(script).toContain('TOOL_NAME=');
|
|
25
|
-
expect(script).toContain('Write|Edit');
|
|
26
|
-
expect(script).toContain('case "$TOOL_NAME" in');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should include file pattern matching', () => {
|
|
30
|
-
const script = generateHookScript(baseConfig);
|
|
31
|
-
expect(script).toContain('FILE_PATH=');
|
|
32
|
-
expect(script).toContain('MATCHED=false');
|
|
33
|
-
expect(script).toContain('marketing');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('should output remind action by default', () => {
|
|
37
|
-
const script = generateHookScript(baseConfig);
|
|
38
|
-
expect(script).toContain('REMINDER:');
|
|
39
|
-
expect(script).toContain('continue: true');
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('should output warn action', () => {
|
|
43
|
-
const script = generateHookScript({ ...baseConfig, action: 'warn' });
|
|
44
|
-
expect(script).toContain('WARNING:');
|
|
45
|
-
expect(script).toContain('continue: true');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('should output block action', () => {
|
|
49
|
-
const script = generateHookScript({ ...baseConfig, action: 'block' });
|
|
50
|
-
expect(script).toContain('BLOCKED:');
|
|
51
|
-
expect(script).toContain('continue: false');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('should skip tool matcher for non-tool events', () => {
|
|
55
|
-
const script = generateHookScript({ ...baseConfig, event: 'PreCompact' });
|
|
56
|
-
expect(script).not.toContain('TOOL_NAME');
|
|
57
|
-
expect(script).not.toContain('case');
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('should skip file pattern matching when no patterns', () => {
|
|
61
|
-
const script = generateHookScript({ ...baseConfig, filePatterns: undefined });
|
|
62
|
-
expect(script).not.toContain('FILE_PATH');
|
|
63
|
-
expect(script).not.toContain('MATCHED');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('should generate scripts for all 5 hook events', () => {
|
|
67
|
-
for (const event of HOOK_EVENTS) {
|
|
68
|
-
const script = generateHookScript({ ...baseConfig, event });
|
|
69
|
-
expect(script).toContain(`# Event: ${event}`);
|
|
70
|
-
expect(script).toContain('#!/bin/sh');
|
|
71
|
-
}
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('should escape single quotes in messages', () => {
|
|
75
|
-
const script = generateHookScript({ ...baseConfig, message: "Don't forget the guidelines" });
|
|
76
|
-
// Should not have unbalanced quotes
|
|
77
|
-
expect(script).toContain('forget');
|
|
78
|
-
});
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
describe('generateManifest', () => {
|
|
82
|
-
const config: HookConversionConfig = {
|
|
83
|
-
name: 'my-hook',
|
|
84
|
-
event: 'PreToolUse',
|
|
85
|
-
toolMatcher: 'Write',
|
|
86
|
-
action: 'remind',
|
|
87
|
-
message: 'Test message',
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
it('should generate valid manifest with required fields', () => {
|
|
91
|
-
const manifest = generateManifest(config);
|
|
92
|
-
expect(manifest.name).toBe('my-hook');
|
|
93
|
-
expect(manifest.version).toBe('1.0.0');
|
|
94
|
-
expect(manifest.hooks).toEqual([]);
|
|
95
|
-
expect(manifest.scripts).toHaveLength(1);
|
|
96
|
-
expect(manifest.lifecycleHooks).toHaveLength(1);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('should set script name and file correctly', () => {
|
|
100
|
-
const manifest = generateManifest(config);
|
|
101
|
-
expect(manifest.scripts![0].name).toBe('my-hook');
|
|
102
|
-
expect(manifest.scripts![0].file).toBe('my-hook.sh');
|
|
103
|
-
expect(manifest.scripts![0].targetDir).toBe('hooks');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('should set lifecycle hook event and command', () => {
|
|
107
|
-
const manifest = generateManifest(config);
|
|
108
|
-
const lc = manifest.lifecycleHooks![0];
|
|
109
|
-
expect(lc.event).toBe('PreToolUse');
|
|
110
|
-
expect(lc.command).toBe('sh ~/.claude/hooks/my-hook.sh');
|
|
111
|
-
expect(lc.type).toBe('command');
|
|
112
|
-
expect(lc.timeout).toBe(10);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('should use description from config or fallback to message', () => {
|
|
116
|
-
expect(generateManifest(config).description).toBe('Test message');
|
|
117
|
-
expect(generateManifest({ ...config, description: 'Custom desc' }).description).toBe(
|
|
118
|
-
'Custom desc',
|
|
119
|
-
);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('should include actionLevel', () => {
|
|
123
|
-
expect(generateManifest(config).actionLevel).toBe('remind');
|
|
124
|
-
expect(generateManifest({ ...config, action: 'block' }).actionLevel).toBe('block');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('should generate manifests for all action levels', () => {
|
|
128
|
-
for (const action of ACTION_LEVELS) {
|
|
129
|
-
const manifest = generateManifest({ ...config, action });
|
|
130
|
-
expect(manifest.actionLevel).toBe(action);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
});
|