@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.
Files changed (53) hide show
  1. package/dist/commands/agent.js +51 -20
  2. package/dist/commands/agent.js.map +1 -1
  3. package/dist/commands/brain.d.ts +8 -0
  4. package/dist/commands/brain.js +83 -0
  5. package/dist/commands/brain.js.map +1 -0
  6. package/dist/commands/dream.js +1 -12
  7. package/dist/commands/dream.js.map +1 -1
  8. package/dist/commands/install.js +3 -9
  9. package/dist/commands/install.js.map +1 -1
  10. package/dist/commands/validate-skills.d.ts +10 -0
  11. package/dist/commands/validate-skills.js +47 -0
  12. package/dist/commands/validate-skills.js.map +1 -0
  13. package/dist/commands/vault.js +2 -11
  14. package/dist/commands/vault.js.map +1 -1
  15. package/dist/main.js +4 -0
  16. package/dist/main.js.map +1 -1
  17. package/dist/prompts/create-wizard.js +4 -1
  18. package/dist/prompts/create-wizard.js.map +1 -1
  19. package/dist/utils/checks.js +17 -32
  20. package/dist/utils/checks.js.map +1 -1
  21. package/dist/utils/core-resolver.d.ts +3 -0
  22. package/dist/utils/core-resolver.js +38 -0
  23. package/dist/utils/core-resolver.js.map +1 -0
  24. package/dist/utils/vault-db.d.ts +5 -0
  25. package/dist/utils/vault-db.js +17 -0
  26. package/dist/utils/vault-db.js.map +1 -0
  27. package/package.json +1 -1
  28. package/src/__tests__/create-wizard.test.ts +86 -0
  29. package/src/__tests__/doctor.test.ts +46 -1
  30. package/src/__tests__/install-verify.test.ts +1 -1
  31. package/src/__tests__/install.test.ts +7 -10
  32. package/src/commands/agent.ts +53 -17
  33. package/src/commands/brain.ts +93 -0
  34. package/src/commands/dream.ts +1 -11
  35. package/src/commands/install.ts +3 -8
  36. package/src/commands/validate-skills.ts +58 -0
  37. package/src/commands/vault.ts +2 -11
  38. package/src/main.ts +4 -0
  39. package/src/prompts/create-wizard.ts +5 -1
  40. package/src/utils/checks.ts +18 -30
  41. package/src/utils/core-resolver.ts +39 -0
  42. package/src/utils/vault-db.ts +15 -0
  43. package/dist/hook-packs/converter/template.test.ts +0 -133
  44. package/dist/hook-packs/yolo-safety/scripts/anti-deletion.sh +0 -274
  45. package/dist/prompts/archetypes.d.ts +0 -22
  46. package/dist/prompts/archetypes.js +0 -298
  47. package/dist/prompts/archetypes.js.map +0 -1
  48. package/dist/prompts/playbook.d.ts +0 -64
  49. package/dist/prompts/playbook.js +0 -436
  50. package/dist/prompts/playbook.js.map +0 -1
  51. package/dist/utils/format-paths.d.ts +0 -14
  52. package/dist/utils/format-paths.js +0 -27
  53. 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
+ }
@@ -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 { SOLERI_HOME } from '@soleri/core';
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');
@@ -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
- try {
31
- const require = createRequire(import.meta.url);
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
+ }
@@ -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,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: 'Ernesto',
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;
@@ -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
- try {
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 claudeJsonPath = join(homedir(), '.claude.json');
186
- if (!existsSync(claudeJsonPath)) {
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: '~/.claude.json not found',
190
+ detail: `not found in ~/.claude.json, ~/.codex/config.toml, or ~/.config/opencode/opencode.json`,
191
191
  };
192
192
  }
193
193
 
194
- try {
195
- const config = JSON.parse(readFileSync(claudeJsonPath, 'utf-8'));
196
- const servers = config.mcpServers ?? {};
197
- if (ctx.agentId in servers) {
198
- return {
199
- status: 'pass',
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
- });