@soleri/cli 9.13.1 → 9.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ import { dirname, join, resolve } from 'node:path';
12
12
  import { homedir } from 'node:os';
13
13
  import * as p from '@clack/prompts';
14
14
  import { detectAgent } from '../utils/agent-context.js';
15
+ import { detectArtifacts } from '../utils/agent-artifacts.js';
15
16
 
16
17
  /** Default parent directory for agents: ~/.soleri/ */
17
18
  const SOLERI_HOME = process.env.SOLERI_HOME ?? join(homedir(), '.soleri');
@@ -25,7 +26,7 @@ export const toPosix = (p: string): string => p.replace(/\\/g, '/');
25
26
  * Resolve the absolute path to the soleri-engine binary.
26
27
  * Falls back to `npx @soleri/engine` if resolution fails (e.g. not installed globally).
27
28
  */
28
- function resolveEngineBin(): { command: string; bin: string } {
29
+ export function resolveEngineBin(): { command: string; bin: string } {
29
30
  try {
30
31
  const require = createRequire(import.meta.url);
31
32
  const bin = require.resolve('@soleri/core/dist/engine/bin/soleri-engine.js');
@@ -80,6 +81,87 @@ function checkWritable(filePath: string): boolean {
80
81
  }
81
82
  }
82
83
 
84
+ /**
85
+ * Facade suffixes pre-approved for every Soleri agent.
86
+ * Each suffix becomes `mcp__<agentId>__<agentId>_<suffix>` in settings.local.json.
87
+ */
88
+ const PRE_APPROVED_FACADE_SUFFIXES = [
89
+ 'core',
90
+ 'vault',
91
+ 'plan',
92
+ 'brain',
93
+ 'memory',
94
+ 'admin',
95
+ 'curator',
96
+ 'orchestrate',
97
+ 'control',
98
+ 'context',
99
+ 'agency',
100
+ 'operator',
101
+ 'chat',
102
+ 'archive',
103
+ 'sync',
104
+ 'review',
105
+ 'intake',
106
+ 'links',
107
+ 'branching',
108
+ 'tier',
109
+ 'loop',
110
+ 'embedding',
111
+ 'dream',
112
+ 'testing',
113
+ 'typescript',
114
+ ] as const;
115
+
116
+ /**
117
+ * Write pre-approved facade permissions to ~/.claude/settings.local.json.
118
+ * Merges with existing permissions — never removes entries added by the user or other agents.
119
+ */
120
+ export function installClaudePermissions(agentId: string): void {
121
+ const claudeDir = join(homedir(), '.claude');
122
+ const settingsPath = join(claudeDir, 'settings.local.json');
123
+
124
+ // Build permission entries: mcp__<agentId>__<agentId>_<suffix>
125
+ const newEntries = PRE_APPROVED_FACADE_SUFFIXES.map(
126
+ (suffix) => `mcp__${agentId}__${agentId}_${suffix}`,
127
+ );
128
+
129
+ let config: Record<string, unknown> = {};
130
+ if (existsSync(settingsPath)) {
131
+ try {
132
+ config = JSON.parse(readFileSync(settingsPath, 'utf-8'));
133
+ } catch {
134
+ // Corrupted file — start fresh but warn
135
+ p.log.warn(`Could not parse ${settingsPath} — creating fresh permissions`);
136
+ config = {};
137
+ }
138
+ }
139
+
140
+ if (!config.permissions || typeof config.permissions !== 'object') {
141
+ config.permissions = {};
142
+ }
143
+ const permissions = config.permissions as Record<string, unknown>;
144
+
145
+ const existing = Array.isArray(permissions.allow) ? (permissions.allow as string[]) : [];
146
+ const merged = [...new Set([...existing, ...newEntries])];
147
+ permissions.allow = merged;
148
+
149
+ try {
150
+ mkdirSync(claudeDir, { recursive: true });
151
+ writeFileSync(settingsPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
152
+ } catch {
153
+ p.log.warn(`Could not write ${settingsPath} — MCP tools may require manual approval`);
154
+ return;
155
+ }
156
+
157
+ const added = merged.length - existing.length;
158
+ if (added > 0) {
159
+ p.log.success(`Pre-approved ${merged.length} facade permissions in settings.local.json`);
160
+ } else {
161
+ p.log.info('Facade permissions already configured in settings.local.json');
162
+ }
163
+ }
164
+
83
165
  export function installClaude(agentId: string, agentDir: string, isFileTree: boolean): void {
84
166
  const configPath = join(homedir(), '.claude.json');
85
167
 
@@ -113,7 +195,10 @@ export function installClaude(agentId: string, agentDir: string, isFileTree: boo
113
195
  p.log.error(`Cannot write to ${configPath}. Check file permissions.`);
114
196
  process.exit(1);
115
197
  }
116
- p.log.success(`Registered ${agentId} in ~/.claude.json`);
198
+ p.log.success(`Registered ${agentId} in ~/.claude.json (restart your session to load)`);
199
+
200
+ // Pre-approve facade permissions so users don't hit approval prompts
201
+ installClaudePermissions(agentId);
117
202
  }
118
203
 
119
204
  function installCodex(agentId: string, agentDir: string, isFileTree: boolean): void {
@@ -162,7 +247,7 @@ function installCodex(agentId: string, agentDir: string, isFileTree: boolean): v
162
247
  p.log.error(`Cannot write to ${configPath}. Check file permissions.`);
163
248
  process.exit(1);
164
249
  }
165
- p.log.success(`Registered ${agentId} in ~/.codex/config.toml`);
250
+ p.log.success(`Registered ${agentId} in ~/.codex/config.toml (restart your session to load)`);
166
251
  }
167
252
 
168
253
  function installOpencode(agentId: string, agentDir: string, isFileTree: boolean): void {
@@ -222,7 +307,29 @@ function installOpencode(agentId: string, agentDir: string, isFileTree: boolean)
222
307
  p.log.error(`Cannot write to ${configPath}. Check file permissions.`);
223
308
  process.exit(1);
224
309
  }
225
- p.log.success(`Registered ${agentId} in ~/.config/opencode/opencode.json`);
310
+ p.log.success(
311
+ `Registered ${agentId} in ~/.config/opencode/opencode.json (restart your session to load)`,
312
+ );
313
+ }
314
+
315
+ /**
316
+ * Return target-specific post-install restart instructions.
317
+ */
318
+ export function getNextStepMessage(target: string): string {
319
+ const instructions: Record<string, string> = {
320
+ claude: 'Next step: Restart your Claude Code session (or run `/mcp` to reload MCP servers).',
321
+ codex: 'Next step: Start a new Codex conversation to load the MCP server.',
322
+ opencode: 'Next step: Restart OpenCode to load the MCP server.',
323
+ };
324
+
325
+ if (target === 'both' || target === 'all') {
326
+ return [instructions.claude, instructions.codex, instructions.opencode].join('\n');
327
+ }
328
+
329
+ if (!(target in instructions)) {
330
+ p.log.warn(`Unknown target "${target}" — defaulting to Claude instructions.`);
331
+ }
332
+ return instructions[target] ?? instructions.claude;
226
333
  }
227
334
 
228
335
  function escapeRegExp(s: string): string {
@@ -233,7 +340,20 @@ function escapeRegExp(s: string): string {
233
340
  * Create a global launcher script so the agent can be invoked by name from any directory.
234
341
  * e.g., typing `ernesto` opens Claude Code with that agent's MCP config.
235
342
  */
236
- function installLauncher(agentId: string, agentDir: string): void {
343
+ function installLauncher(agentId: string, agentDir: string, target: Target): void {
344
+ // Only create a launcher for Claude — other targets don't have a CLI equivalent
345
+ if (target === 'codex') {
346
+ p.log.info('Launcher skipped: Codex does not have a CLI equivalent.');
347
+ return;
348
+ }
349
+ if (target === 'opencode') {
350
+ p.log.info('Launcher skipped: OpenCode does not have a CLI equivalent.');
351
+ return;
352
+ }
353
+ if (target === 'all' || target === 'both') {
354
+ p.log.info('Note: Launcher is Claude-specific — other targets do not have a CLI equivalent.');
355
+ }
356
+
237
357
  // Launcher scripts to /usr/local/bin are Unix-only
238
358
  if (process.platform === 'win32') {
239
359
  p.log.info('Launcher scripts are not supported on Windows.');
@@ -264,13 +384,67 @@ function installLauncher(agentId: string, agentDir: string): void {
264
384
  }
265
385
  }
266
386
 
387
+ // ---------------------------------------------------------------------------
388
+ // Verify
389
+ // ---------------------------------------------------------------------------
390
+
391
+ export interface VerifyCheck {
392
+ label: string;
393
+ passed: boolean;
394
+ }
395
+
396
+ /**
397
+ * Verify the full install chain for an agent against a given target.
398
+ * Returns an array of pass/fail checks.
399
+ */
400
+ export function verifyInstall(agentId: string, agentDir: string, target: Target): VerifyCheck[] {
401
+ const checks: VerifyCheck[] = [];
402
+
403
+ // 1. Agent entry exists in config for each relevant target
404
+ const artifacts = detectArtifacts(agentId, agentDir);
405
+ const targetEntries = artifacts.mcpServerEntries;
406
+
407
+ const targets: ('claude' | 'codex' | 'opencode')[] =
408
+ target === 'all' || target === 'both'
409
+ ? ['claude', 'codex', 'opencode']
410
+ : [target as 'claude' | 'codex' | 'opencode'];
411
+
412
+ for (const t of targets) {
413
+ const hasEntry = targetEntries.some((e) => e.target === t);
414
+ checks.push({
415
+ label: `Agent entry in ${t} config`,
416
+ passed: hasEntry,
417
+ });
418
+ }
419
+
420
+ // 2. Engine binary resolves (local or npx fallback)
421
+ const engine = resolveEngineBin();
422
+ const isLocal = engine.command === 'node';
423
+ checks.push({
424
+ label: isLocal
425
+ ? `Engine binary resolves (${engine.bin})`
426
+ : 'Engine resolves via npx (fallback)',
427
+ passed: true,
428
+ });
429
+
430
+ // 3. agent.yaml exists at configured path
431
+ const agentYamlPath = join(agentDir, 'agent.yaml');
432
+ checks.push({
433
+ label: `agent.yaml exists (${agentYamlPath})`,
434
+ passed: existsSync(agentYamlPath),
435
+ });
436
+
437
+ return checks;
438
+ }
439
+
267
440
  export function registerInstall(program: Command): void {
268
441
  program
269
442
  .command('install')
270
443
  .argument('[dir]', 'Agent directory or agent name (checks ~/.soleri/<name> first, then cwd)')
271
444
  .option('--target <target>', 'Registration target: claude, opencode, codex, or all', 'claude')
445
+ .option('--verify', 'Verify the install chain (config, engine, agent.yaml)')
272
446
  .description('Register agent as MCP server in editor config')
273
- .action(async (dir?: string, opts?: { target?: string }) => {
447
+ .action(async (dir?: string, opts?: { target?: string; verify?: boolean }) => {
274
448
  let resolvedDir: string | undefined;
275
449
 
276
450
  if (dir) {
@@ -328,8 +502,35 @@ export function registerInstall(program: Command): void {
328
502
  }
329
503
 
330
504
  // Create global launcher script
331
- installLauncher(ctx.agentId, ctx.agentPath);
505
+ installLauncher(ctx.agentId, ctx.agentPath, target);
506
+
507
+ p.log.success(`Install complete for ${ctx.agentId}.`);
508
+ p.log.info(getNextStepMessage(target));
509
+
510
+ // Warn users running via npx — their cache may go stale on next release
511
+ if (resolveEngineBin().command === 'npx') {
512
+ p.log.warn(
513
+ `Running via npx — updates may be cached. For reliable updates: npm install -g soleri`,
514
+ );
515
+ }
332
516
 
333
- p.log.info(`Agent ${ctx.agentId} is now available as an MCP server.`);
517
+ // Run verification if --verify was passed
518
+ if (opts?.verify) {
519
+ const checks = verifyInstall(ctx.agentId, ctx.agentPath, target);
520
+ p.log.info('');
521
+ p.log.info('Install verification:');
522
+ let allPassed = true;
523
+ for (const check of checks) {
524
+ const icon = check.passed ? '\u2705' : '\u274C';
525
+ const logFn = check.passed ? p.log.success : p.log.error;
526
+ logFn(`${icon} ${check.label}`);
527
+ if (!check.passed) allPassed = false;
528
+ }
529
+ if (!allPassed) {
530
+ p.log.error('Verification failed — one or more checks did not pass.');
531
+ process.exit(1);
532
+ }
533
+ p.log.success('All checks passed.');
534
+ }
334
535
  });
335
536
  }
@@ -11,7 +11,13 @@ import { join, resolve as pathResolve } from 'node:path';
11
11
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
12
12
  import type { Command } from 'commander';
13
13
  import * as p from '@clack/prompts';
14
- import { PackLockfile, inferPackType, resolvePack, checkNpmVersion } from '@soleri/core';
14
+ import {
15
+ PackLockfile,
16
+ inferPackType,
17
+ resolvePack,
18
+ checkNpmVersion,
19
+ getBuiltinKnowledgePacksDirs,
20
+ } from '@soleri/core';
15
21
  import type { LockEntry, PackSource } from '@soleri/core';
16
22
 
17
23
  // ─── Tier display helpers ────────────────────────────────────────────
@@ -457,9 +463,14 @@ export function registerPack(program: Command): void {
457
463
  const candidates = [
458
464
  join(process.cwd(), 'knowledge-packs'),
459
465
  pathResolve(import.meta.dirname ?? '.', '..', '..', '..', '..', '..', 'knowledge-packs'),
466
+ ...getBuiltinKnowledgePacksDirs(),
460
467
  ];
468
+ const seen = new Set<string>();
461
469
  for (const c of candidates) {
462
- if (existsSync(c)) searchDirs.push(c);
470
+ if (existsSync(c) && !seen.has(c)) {
471
+ searchDirs.push(c);
472
+ seen.add(c);
473
+ }
463
474
  }
464
475
  }
465
476
 
@@ -0,0 +1,82 @@
1
+ import type { Command } from 'commander';
2
+ import { createRequire } from 'node:module';
3
+ import { execSync } from 'node:child_process';
4
+ import * as p from '@clack/prompts';
5
+
6
+ const require = createRequire(import.meta.url);
7
+
8
+ function getCurrentVersion(): string {
9
+ try {
10
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
11
+ return (require('../package.json') as { version: string }).version;
12
+ } catch {
13
+ return 'unknown';
14
+ }
15
+ }
16
+
17
+ function getLatestVersion(): string {
18
+ try {
19
+ return execSync('npm view @soleri/cli version', { stdio: ['pipe', 'pipe', 'pipe'] })
20
+ .toString()
21
+ .trim();
22
+ } catch {
23
+ throw new Error('Could not reach npm registry. Check your network connection and try again.');
24
+ }
25
+ }
26
+
27
+ function installLatest(): void {
28
+ execSync('npm install -g soleri@latest', { stdio: 'inherit' });
29
+ }
30
+
31
+ export function registerUpdate(program: Command): void {
32
+ program
33
+ .command('update')
34
+ .description('Update Soleri CLI to the latest version')
35
+ .action(async () => {
36
+ p.intro('Soleri Update');
37
+
38
+ const current = getCurrentVersion();
39
+ p.log.info(`Current version: ${current}`);
40
+
41
+ const spinner = p.spinner();
42
+ spinner.start('Checking latest version…');
43
+
44
+ let latest: string;
45
+ try {
46
+ latest = getLatestVersion();
47
+ } catch (err) {
48
+ spinner.stop('Failed');
49
+ p.log.error(err instanceof Error ? err.message : String(err));
50
+ process.exit(1);
51
+ return;
52
+ }
53
+
54
+ spinner.stop(`Latest version: ${latest}`);
55
+
56
+ if (current === latest) {
57
+ p.log.success(`Already on latest (${current})`);
58
+ p.outro('Nothing to do.');
59
+ return;
60
+ }
61
+
62
+ p.log.info(`Updating ${current} → ${latest}…`);
63
+
64
+ try {
65
+ installLatest();
66
+ } catch {
67
+ p.log.error('Update failed. Try manually: npm install -g soleri@latest');
68
+ process.exit(1);
69
+ }
70
+
71
+ const installed = getLatestVersion();
72
+ if (installed !== latest) {
73
+ p.log.warn(
74
+ `Installed version (${installed}) does not match expected (${latest}). Verify manually.`,
75
+ );
76
+ } else {
77
+ p.log.success(`Updated ${current} → ${installed}`);
78
+ }
79
+
80
+ p.outro('Restart your session to use the new version.');
81
+ });
82
+ }
@@ -2,8 +2,11 @@
2
2
  # Soleri Hook Pack: clean-commits
3
3
  # Version: 1.0.0
4
4
  # Rule: no-ai-attribution
5
+ # NOTE: This hook is intentionally disabled. The host agent (e.g. Claude Code)
6
+ # controls commit attribution via its own system prompt. Blocking attribution
7
+ # patterns here causes a deadlock when the host mandates them.
5
8
  name: no-ai-attribution
6
- enabled: true
9
+ enabled: false
7
10
  event: bash
8
11
  action: block
9
12
  conditions:
@@ -12,7 +15,7 @@ conditions:
12
15
  pattern: git\s+commit.*(-m|--message)
13
16
  - field: command
14
17
  operator: regex_match
15
- pattern: (🤖|Co-Authored-By|Generated with|AI-generated|by Claude|Claude Code|with Claude|noreply@anthropic\.com|Anthropic|Claude\s+(Opus|Sonnet|Haiku))
18
+ pattern: (placeholder-disabled-pattern)
16
19
  ---
17
20
 
18
- 🚫 **AI attribution blocked.** Use clean conventional commits: `feat:`, `fix:`, `refactor:`. No 🤖, Claude, Co-Authored-By, or Anthropic references.
21
+ This hook is intentionally disabled. Commit style is enforced by the engine rules, not by blocking patterns.
package/src/main.ts CHANGED
@@ -32,6 +32,7 @@ import { registerStaging } from './commands/staging.js';
32
32
  import { registerVault } from './commands/vault.js';
33
33
  import { registerYolo } from './commands/yolo.js';
34
34
  import { registerDream } from './commands/dream.js';
35
+ import { registerUpdate } from './commands/update.js';
35
36
 
36
37
  const require = createRequire(import.meta.url);
37
38
  const { version } = require('../package.json');
@@ -96,4 +97,5 @@ registerStaging(program);
96
97
  registerVault(program);
97
98
  registerYolo(program);
98
99
  registerDream(program);
100
+ registerUpdate(program);
99
101
  program.parse();