@soleri/cli 9.7.2 → 9.9.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 (70) hide show
  1. package/dist/commands/add-domain.js +1 -0
  2. package/dist/commands/add-domain.js.map +1 -1
  3. package/dist/commands/add-pack.js +7 -147
  4. package/dist/commands/add-pack.js.map +1 -1
  5. package/dist/commands/agent.js +130 -0
  6. package/dist/commands/agent.js.map +1 -1
  7. package/dist/commands/create.js +96 -4
  8. package/dist/commands/create.js.map +1 -1
  9. package/dist/commands/dev.js +13 -3
  10. package/dist/commands/dev.js.map +1 -1
  11. package/dist/commands/doctor.js +2 -0
  12. package/dist/commands/doctor.js.map +1 -1
  13. package/dist/commands/extend.js +17 -0
  14. package/dist/commands/extend.js.map +1 -1
  15. package/dist/commands/install-knowledge.js +1 -0
  16. package/dist/commands/install-knowledge.js.map +1 -1
  17. package/dist/commands/install.d.ts +2 -0
  18. package/dist/commands/install.js +79 -20
  19. package/dist/commands/install.js.map +1 -1
  20. package/dist/commands/test.js +140 -1
  21. package/dist/commands/test.js.map +1 -1
  22. package/dist/commands/vault.d.ts +9 -0
  23. package/dist/commands/vault.js +66 -0
  24. package/dist/commands/vault.js.map +1 -0
  25. package/dist/hook-packs/flock-guard/manifest.json +2 -1
  26. package/dist/hook-packs/marketing-research/manifest.json +2 -1
  27. package/dist/hook-packs/registry.d.ts +2 -0
  28. package/dist/hook-packs/registry.js.map +1 -1
  29. package/dist/hook-packs/registry.ts +2 -0
  30. package/dist/main.js +7 -0
  31. package/dist/main.js.map +1 -1
  32. package/dist/prompts/create-wizard.d.ts +16 -2
  33. package/dist/prompts/create-wizard.js +84 -11
  34. package/dist/prompts/create-wizard.js.map +1 -1
  35. package/dist/utils/checks.d.ts +8 -5
  36. package/dist/utils/checks.js +105 -10
  37. package/dist/utils/checks.js.map +1 -1
  38. package/dist/utils/format-paths.d.ts +14 -0
  39. package/dist/utils/format-paths.js +27 -0
  40. package/dist/utils/format-paths.js.map +1 -0
  41. package/dist/utils/git.d.ts +29 -0
  42. package/dist/utils/git.js +88 -0
  43. package/dist/utils/git.js.map +1 -0
  44. package/dist/utils/logger.d.ts +1 -0
  45. package/dist/utils/logger.js +4 -0
  46. package/dist/utils/logger.js.map +1 -1
  47. package/package.json +1 -1
  48. package/src/__tests__/create-wizard-git.test.ts +208 -0
  49. package/src/__tests__/git-utils.test.ts +268 -0
  50. package/src/__tests__/install.test.ts +88 -0
  51. package/src/__tests__/scaffold-git-e2e.test.ts +112 -0
  52. package/src/commands/add-domain.ts +1 -0
  53. package/src/commands/add-pack.ts +10 -163
  54. package/src/commands/agent.ts +161 -0
  55. package/src/commands/create.ts +109 -5
  56. package/src/commands/dev.ts +13 -3
  57. package/src/commands/doctor.ts +1 -0
  58. package/src/commands/extend.ts +20 -1
  59. package/src/commands/install-knowledge.ts +1 -0
  60. package/src/commands/install.ts +87 -20
  61. package/src/commands/test.ts +141 -2
  62. package/src/commands/vault.ts +79 -0
  63. package/src/hook-packs/flock-guard/manifest.json +2 -1
  64. package/src/hook-packs/marketing-research/manifest.json +2 -1
  65. package/src/hook-packs/registry.ts +2 -0
  66. package/src/main.ts +10 -0
  67. package/src/prompts/create-wizard.ts +109 -14
  68. package/src/utils/checks.ts +122 -13
  69. package/src/utils/git.ts +118 -0
  70. package/src/utils/logger.ts +5 -0
@@ -1,7 +1,14 @@
1
1
  import type { Command } from 'commander';
2
2
  import { createRequire } from 'node:module';
3
- import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
4
- import { join, resolve } from 'node:path';
3
+ import {
4
+ accessSync,
5
+ constants as fsConstants,
6
+ existsSync,
7
+ readFileSync,
8
+ writeFileSync,
9
+ mkdirSync,
10
+ } from 'node:fs';
11
+ import { dirname, join, resolve } from 'node:path';
5
12
  import { homedir } from 'node:os';
6
13
  import * as p from '@clack/prompts';
7
14
  import { detectAgent } from '../utils/agent-context.js';
@@ -11,6 +18,9 @@ const SOLERI_HOME = process.env.SOLERI_HOME ?? join(homedir(), '.soleri');
11
18
 
12
19
  type Target = 'claude' | 'codex' | 'opencode' | 'both' | 'all';
13
20
 
21
+ /** Normalize a file path to forward slashes (POSIX) for cross-platform config files. */
22
+ export const toPosix = (p: string): string => p.replace(/\\/g, '/');
23
+
14
24
  /**
15
25
  * Resolve the absolute path to the soleri-engine binary.
16
26
  * Falls back to `npx @soleri/engine` if resolution fails (e.g. not installed globally).
@@ -28,17 +38,18 @@ function resolveEngineBin(): { command: string; bin: string } {
28
38
  /** MCP server entry for file-tree agents (resolved engine path, no npx) */
29
39
  function fileTreeMcpEntry(agentDir: string): Record<string, unknown> {
30
40
  const engine = resolveEngineBin();
41
+ const agentYaml = toPosix(join(agentDir, 'agent.yaml'));
31
42
  if (engine.command === 'node') {
32
43
  return {
33
44
  type: 'stdio',
34
45
  command: 'node',
35
- args: [engine.bin, '--agent', join(agentDir, 'agent.yaml')],
46
+ args: [toPosix(engine.bin), '--agent', agentYaml],
36
47
  };
37
48
  }
38
49
  return {
39
50
  type: 'stdio',
40
51
  command: 'npx',
41
- args: ['@soleri/engine', '--agent', join(agentDir, 'agent.yaml')],
52
+ args: ['@soleri/engine', '--agent', agentYaml],
42
53
  };
43
54
  }
44
55
 
@@ -47,13 +58,36 @@ function legacyMcpEntry(agentDir: string): Record<string, unknown> {
47
58
  return {
48
59
  type: 'stdio',
49
60
  command: 'node',
50
- args: [join(agentDir, 'dist', 'index.js')],
61
+ args: [toPosix(join(agentDir, 'dist', 'index.js'))],
51
62
  env: {},
52
63
  };
53
64
  }
54
65
 
66
+ /**
67
+ * Check if a file path is writable. If the file exists, checks write permission on the file.
68
+ * If the file does not exist, checks write permission on the parent directory.
69
+ */
70
+ function checkWritable(filePath: string): boolean {
71
+ try {
72
+ if (existsSync(filePath)) {
73
+ accessSync(filePath, fsConstants.W_OK);
74
+ } else {
75
+ accessSync(dirname(filePath), fsConstants.W_OK);
76
+ }
77
+ return true;
78
+ } catch {
79
+ return false;
80
+ }
81
+ }
82
+
55
83
  export function installClaude(agentId: string, agentDir: string, isFileTree: boolean): void {
56
84
  const configPath = join(homedir(), '.claude.json');
85
+
86
+ if (!checkWritable(configPath)) {
87
+ p.log.error(`Cannot write to ${configPath} — check file permissions`);
88
+ process.exit(1);
89
+ }
90
+
57
91
  let config: Record<string, unknown> = {};
58
92
 
59
93
  if (existsSync(configPath)) {
@@ -73,7 +107,12 @@ export function installClaude(agentId: string, agentDir: string, isFileTree: boo
73
107
  ? fileTreeMcpEntry(agentDir)
74
108
  : legacyMcpEntry(agentDir);
75
109
 
76
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
110
+ try {
111
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
112
+ } catch {
113
+ p.log.error(`Cannot write to ${configPath}. Check file permissions.`);
114
+ process.exit(1);
115
+ }
77
116
  p.log.success(`Registered ${agentId} in ~/.claude.json`);
78
117
  }
79
118
 
@@ -82,7 +121,12 @@ function installCodex(agentId: string, agentDir: string, isFileTree: boolean): v
82
121
  const configPath = join(codexDir, 'config.toml');
83
122
 
84
123
  if (!existsSync(codexDir)) {
85
- mkdirSync(codexDir, { recursive: true });
124
+ try {
125
+ mkdirSync(codexDir, { recursive: true });
126
+ } catch {
127
+ p.log.error(`Cannot create directory ${codexDir}. Check permissions.`);
128
+ process.exit(1);
129
+ }
86
130
  }
87
131
 
88
132
  let content = '';
@@ -97,21 +141,27 @@ function installCodex(agentId: string, agentDir: string, isFileTree: boolean): v
97
141
 
98
142
  let section: string;
99
143
  if (isFileTree) {
100
- const agentYamlPath = join(agentDir, 'agent.yaml');
144
+ const agentYamlPath = toPosix(join(agentDir, 'agent.yaml'));
101
145
  const engine = resolveEngineBin();
102
146
  if (engine.command === 'node') {
103
- section = `\n\n${sectionHeader}\ncommand = "node"\nargs = ["${engine.bin}", "--agent", "${agentYamlPath}"]\n`;
147
+ const bin = toPosix(engine.bin);
148
+ section = `\n\n${sectionHeader}\ncommand = "node"\nargs = ["${bin}", "--agent", "${agentYamlPath}"]\n`;
104
149
  } else {
105
150
  section = `\n\n${sectionHeader}\ncommand = "npx"\nargs = ["@soleri/engine", "--agent", "${agentYamlPath}"]\n`;
106
151
  }
107
152
  } else {
108
- const entryPoint = join(agentDir, 'dist', 'index.js');
153
+ const entryPoint = toPosix(join(agentDir, 'dist', 'index.js'));
109
154
  section = `\n\n${sectionHeader}\ncommand = "node"\nargs = ["${entryPoint}"]\n`;
110
155
  }
111
156
 
112
157
  content = content + section;
113
158
 
114
- writeFileSync(configPath, content.trim() + '\n', 'utf-8');
159
+ try {
160
+ writeFileSync(configPath, content.trim() + '\n', 'utf-8');
161
+ } catch {
162
+ p.log.error(`Cannot write to ${configPath}. Check file permissions.`);
163
+ process.exit(1);
164
+ }
115
165
  p.log.success(`Registered ${agentId} in ~/.codex/config.toml`);
116
166
  }
117
167
 
@@ -122,7 +172,12 @@ function installOpencode(agentId: string, agentDir: string, isFileTree: boolean)
122
172
  const configPath = join(configDir, 'opencode.json');
123
173
 
124
174
  if (!existsSync(configDir)) {
125
- mkdirSync(configDir, { recursive: true });
175
+ try {
176
+ mkdirSync(configDir, { recursive: true });
177
+ } catch {
178
+ p.log.error(`Cannot create directory ${configDir}. Check permissions.`);
179
+ process.exit(1);
180
+ }
126
181
  }
127
182
 
128
183
  let config: Record<string, unknown> = {};
@@ -132,7 +187,10 @@ function installOpencode(agentId: string, agentDir: string, isFileTree: boolean)
132
187
  const stripped = raw.replace(/^\s*\/\/.*$/gm, '');
133
188
  config = JSON.parse(stripped);
134
189
  } catch {
135
- config = {};
190
+ p.log.error(
191
+ `Failed to parse ${configPath}. The file may be corrupted. Delete it and try again.`,
192
+ );
193
+ process.exit(1);
136
194
  }
137
195
  }
138
196
 
@@ -143,21 +201,27 @@ function installOpencode(agentId: string, agentDir: string, isFileTree: boolean)
143
201
  const servers = config.mcp as Record<string, unknown>;
144
202
  if (isFileTree) {
145
203
  const engine = resolveEngineBin();
204
+ const agentYaml = toPosix(join(agentDir, 'agent.yaml'));
146
205
  servers[agentId] = {
147
206
  type: 'local',
148
207
  command:
149
208
  engine.command === 'node'
150
- ? ['node', engine.bin, '--agent', join(agentDir, 'agent.yaml')]
151
- : ['npx', '-y', '@soleri/engine', '--agent', join(agentDir, 'agent.yaml')],
209
+ ? ['node', toPosix(engine.bin), '--agent', agentYaml]
210
+ : ['npx', '-y', '@soleri/engine', '--agent', agentYaml],
152
211
  };
153
212
  } else {
154
213
  servers[agentId] = {
155
214
  type: 'local',
156
- command: ['node', join(agentDir, 'dist', 'index.js')],
215
+ command: ['node', toPosix(join(agentDir, 'dist', 'index.js'))],
157
216
  };
158
217
  }
159
218
 
160
- writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
219
+ try {
220
+ writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
221
+ } catch {
222
+ p.log.error(`Cannot write to ${configPath}. Check file permissions.`);
223
+ process.exit(1);
224
+ }
161
225
  p.log.success(`Registered ${agentId} in ~/.config/opencode/opencode.json`);
162
226
  }
163
227
 
@@ -172,7 +236,10 @@ function escapeRegExp(s: string): string {
172
236
  function installLauncher(agentId: string, agentDir: string): void {
173
237
  // Launcher scripts to /usr/local/bin are Unix-only
174
238
  if (process.platform === 'win32') {
175
- p.log.info('Launcher scripts are not supported on Windows — skipping');
239
+ p.log.info('Launcher scripts are not supported on Windows.');
240
+ p.log.info(
241
+ `On Windows, run your agent with: npx @soleri/cli dev --agent "${toPosix(agentDir)}"`,
242
+ );
176
243
  return;
177
244
  }
178
245
 
@@ -182,7 +249,7 @@ function installLauncher(agentId: string, agentDir: string): void {
182
249
  '#!/bin/bash',
183
250
  `# ${agentId} — Soleri second brain launcher`,
184
251
  `# Type "${agentId}" from any directory to open Claude Code with this agent`,
185
- `exec claude --mcp-config ${join(agentDir, '.mcp.json')}`,
252
+ `exec claude --mcp-config ${toPosix(join(agentDir, '.mcp.json'))}`,
186
253
  '',
187
254
  ].join('\n');
188
255
 
@@ -192,7 +259,7 @@ function installLauncher(agentId: string, agentDir: string): void {
192
259
  } catch {
193
260
  p.log.warn(`Could not create launcher at ${binPath} (may need sudo)`);
194
261
  p.log.info(
195
- `To create manually: sudo bash -c 'cat > ${binPath} << "EOF"\\n#!/bin/bash\\nexec claude --mcp-config ${join(agentDir, '.mcp.json')}\\nEOF' && chmod +x ${binPath}`,
262
+ `To create manually: sudo bash -c 'cat > ${binPath} << "EOF"\\n#!/bin/bash\\nexec claude --mcp-config ${toPosix(join(agentDir, '.mcp.json'))}\\nEOF' && chmod +x ${binPath}`,
196
263
  );
197
264
  }
198
265
  }
@@ -1,8 +1,139 @@
1
1
  import { spawn } from 'node:child_process';
2
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
3
+ import { join } from 'node:path';
2
4
  import type { Command } from 'commander';
3
5
  import * as p from '@clack/prompts';
6
+ import { parse as parseYaml } from 'yaml';
7
+ import { AgentYamlSchema } from '@soleri/forge/lib';
4
8
  import { detectAgent } from '../utils/agent-context.js';
5
9
 
10
+ /**
11
+ * Run validation checks for a file-tree agent (no vitest needed).
12
+ * Returns the process exit code (0 = all passed, 1 = failures).
13
+ */
14
+ function runFiletreeChecks(agentPath: string, _agentId: string): number {
15
+ let passed = 0;
16
+ let failed = 0;
17
+ const failures: string[] = [];
18
+
19
+ // ── 1. agent.yaml validation ───────────────────────
20
+ const yamlPath = join(agentPath, 'agent.yaml');
21
+ try {
22
+ const raw = readFileSync(yamlPath, 'utf-8');
23
+ const parsed = parseYaml(raw);
24
+ const result = AgentYamlSchema.safeParse(parsed);
25
+ if (result.success) {
26
+ p.log.success('agent.yaml — valid');
27
+ passed++;
28
+ } else {
29
+ const issues = result.error.issues
30
+ .map((i) => ` ${i.path.join('.')}: ${i.message}`)
31
+ .join('\n');
32
+ p.log.error(`agent.yaml — validation failed\n${issues}`);
33
+ failures.push('agent.yaml validation');
34
+ failed++;
35
+ }
36
+ } catch (err: unknown) {
37
+ const msg = err instanceof Error ? err.message : String(err);
38
+ p.log.error(`agent.yaml — could not read or parse: ${msg}`);
39
+ failures.push('agent.yaml read/parse');
40
+ failed++;
41
+ }
42
+
43
+ // ── 2. Skills syntax check ─────────────────────────
44
+ const skillsDir = join(agentPath, 'skills');
45
+ if (existsSync(skillsDir)) {
46
+ let validSkills = 0;
47
+ let invalidSkills = 0;
48
+ const invalidNames: string[] = [];
49
+
50
+ try {
51
+ const entries = readdirSync(skillsDir, { withFileTypes: true });
52
+ const skillDirs = entries.filter((e) => e.isDirectory());
53
+
54
+ for (const dir of skillDirs) {
55
+ const skillMd = join(skillsDir, dir.name, 'SKILL.md');
56
+ if (!existsSync(skillMd)) {
57
+ invalidSkills++;
58
+ invalidNames.push(`${dir.name}: missing SKILL.md`);
59
+ continue;
60
+ }
61
+
62
+ try {
63
+ const content = readFileSync(skillMd, 'utf-8');
64
+ const hasFrontmatter = content.startsWith('---');
65
+ const hasName = /^name:/m.test(content);
66
+ const hasDescription = /^description:/m.test(content);
67
+
68
+ if (hasFrontmatter && hasName && hasDescription) {
69
+ validSkills++;
70
+ } else {
71
+ invalidSkills++;
72
+ const missing: string[] = [];
73
+ if (!hasFrontmatter) missing.push('frontmatter (---)');
74
+ if (!hasName) missing.push('name:');
75
+ if (!hasDescription) missing.push('description:');
76
+ invalidNames.push(`${dir.name}: missing ${missing.join(', ')}`);
77
+ }
78
+ } catch {
79
+ invalidSkills++;
80
+ invalidNames.push(`${dir.name}: could not read SKILL.md`);
81
+ }
82
+ }
83
+
84
+ if (invalidSkills === 0) {
85
+ p.log.success(`skills — ${validSkills} valid, 0 invalid`);
86
+ passed++;
87
+ } else {
88
+ const details = invalidNames.map((n) => ` ${n}`).join('\n');
89
+ p.log.error(`skills — ${validSkills} valid, ${invalidSkills} invalid\n${details}`);
90
+ failures.push('skills syntax');
91
+ failed++;
92
+ }
93
+ } catch {
94
+ p.log.warn('skills — could not read skills/ directory');
95
+ // Not a failure — directory exists but unreadable is unusual, warn only
96
+ }
97
+ } else {
98
+ p.log.info('skills — no skills/ directory (skipped)');
99
+ }
100
+
101
+ // ── 3. Instructions check ──────────────────────────
102
+ const instructionsDir = join(agentPath, 'instructions');
103
+ if (existsSync(instructionsDir)) {
104
+ try {
105
+ const files = readdirSync(instructionsDir).filter((f) => f.endsWith('.md'));
106
+ if (files.length > 0) {
107
+ p.log.success(`instructions — ${files.length} .md file(s) found`);
108
+ passed++;
109
+ } else {
110
+ p.log.error('instructions — directory exists but contains no .md files');
111
+ failures.push('instructions empty');
112
+ failed++;
113
+ }
114
+ } catch {
115
+ p.log.error('instructions — could not read directory');
116
+ failures.push('instructions read');
117
+ failed++;
118
+ }
119
+ } else {
120
+ p.log.error('instructions — directory not found');
121
+ failures.push('instructions missing');
122
+ failed++;
123
+ }
124
+
125
+ // ── Summary ────────────────────────────────────────
126
+ if (failed === 0) {
127
+ p.log.success(`\n${passed} check(s) passed, 0 failed`);
128
+ } else {
129
+ p.log.error(
130
+ `\n${passed} check(s) passed, ${failed} failed:\n${failures.map((f) => ` - ${f}`).join('\n')}`,
131
+ );
132
+ }
133
+
134
+ return failed > 0 ? 1 : 0;
135
+ }
136
+
6
137
  export function registerTest(program: Command): void {
7
138
  program
8
139
  .command('test')
@@ -17,6 +148,16 @@ export function registerTest(program: Command): void {
17
148
  process.exit(1);
18
149
  }
19
150
 
151
+ p.log.info(`Running tests for ${ctx.agentId}...`);
152
+
153
+ // ── File-tree agents: run validation checks (no vitest) ──
154
+ if (ctx.format === 'filetree') {
155
+ const code = runFiletreeChecks(ctx.agentPath, ctx.agentId);
156
+ process.exit(code);
157
+ return;
158
+ }
159
+
160
+ // ── TypeScript agents: spawn vitest as before ──
20
161
  const args: string[] = [];
21
162
  if (opts.watch) {
22
163
  // vitest (no "run") enables watch mode
@@ -30,8 +171,6 @@ export function registerTest(program: Command): void {
30
171
  const extra = cmd.args as string[];
31
172
  if (extra.length > 0) args.push(...extra);
32
173
 
33
- p.log.info(`Running tests for ${ctx.agentId}...`);
34
-
35
174
  const child = spawn('npx', args, {
36
175
  cwd: ctx.agentPath,
37
176
  stdio: 'inherit',
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Vault CLI — export vault entries as browsable markdown files.
3
+ *
4
+ * `soleri vault export` — export to ./knowledge/vault/
5
+ * `soleri vault export --path ~/obsidian` — export to custom directory
6
+ * `soleri vault export --domain arch` — filter by domain
7
+ */
8
+
9
+ import { resolve } from 'node:path';
10
+ import { existsSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import type { Command } from 'commander';
13
+ import { detectAgent } from '../utils/agent-context.js';
14
+ import * as log from '../utils/logger.js';
15
+ import { SOLERI_HOME } from '@soleri/core';
16
+
17
+ export function registerVault(program: Command): void {
18
+ const vault = program.command('vault').description('Vault knowledge management');
19
+
20
+ vault
21
+ .command('export')
22
+ .description('Export vault entries as browsable markdown files')
23
+ .option('--path <dir>', 'Output directory (default: ./knowledge/)')
24
+ .option('--domain <name>', 'Filter by domain')
25
+ .action(async (opts: { path?: string; domain?: string }) => {
26
+ const agent = detectAgent();
27
+ if (!agent) {
28
+ log.fail('Not in a Soleri agent project', 'Run from an agent directory');
29
+ process.exit(1);
30
+ }
31
+
32
+ const outputDir = opts.path ? resolve(opts.path) : resolve('knowledge');
33
+
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;
42
+
43
+ if (!vaultDbPath) {
44
+ log.fail('Vault DB not found', 'Run the agent once to initialize its vault database.');
45
+ process.exit(1);
46
+ }
47
+
48
+ // Dynamic import to avoid loading better-sqlite3 unless needed
49
+ const { Vault } = await import('@soleri/core');
50
+ const vaultInstance = new Vault(vaultDbPath);
51
+
52
+ try {
53
+ log.heading('Vault Export');
54
+
55
+ if (opts.domain) {
56
+ const { syncEntryToMarkdown } = await import('@soleri/core');
57
+ const entries = vaultInstance.list({ limit: 10000, domain: opts.domain });
58
+ let synced = 0;
59
+ for (const entry of entries) {
60
+ await syncEntryToMarkdown(entry, outputDir);
61
+ synced++;
62
+ }
63
+ log.pass(
64
+ `Exported ${synced} entries from domain "${opts.domain}"`,
65
+ `${outputDir}/vault/`,
66
+ );
67
+ } else {
68
+ const { syncAllToMarkdown } = await import('@soleri/core');
69
+ const result = await syncAllToMarkdown(vaultInstance, outputDir);
70
+ log.pass(
71
+ `Exported ${result.synced} entries (${result.skipped} unchanged)`,
72
+ `${outputDir}/vault/`,
73
+ );
74
+ }
75
+ } finally {
76
+ vaultInstance.close();
77
+ }
78
+ });
79
+ }
@@ -32,5 +32,6 @@
32
32
  "timeout": 10,
33
33
  "statusMessage": "Releasing lockfile guard..."
34
34
  }
35
- ]
35
+ ],
36
+ "scaffoldDefault": false
36
37
  }
@@ -20,5 +20,6 @@
20
20
  "statusMessage": "Checking marketing context..."
21
21
  }
22
22
  ],
23
- "actionLevel": "remind"
23
+ "actionLevel": "remind",
24
+ "scaffoldDefault": false
24
25
  }
@@ -31,6 +31,8 @@ export interface HookPackManifest {
31
31
  lifecycleHooks?: HookPackLifecycleHook[];
32
32
  source?: 'built-in' | 'local';
33
33
  actionLevel?: 'remind' | 'warn' | 'block';
34
+ /** If false, pack is hidden from the scaffold picker but still installable via `hooks add-pack`. */
35
+ scaffoldDefault?: boolean;
34
36
  }
35
37
 
36
38
  const __filename = fileURLToPath(import.meta.url);
package/src/main.ts CHANGED
@@ -1,5 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const [major] = process.versions.node.split('.').map(Number);
4
+ if (major < 18) {
5
+ console.error(
6
+ `\n Soleri requires Node.js 18 or later.\n You have v${process.versions.node}.\n Upgrade at https://nodejs.org\n`,
7
+ );
8
+ process.exit(1);
9
+ }
10
+
3
11
  import { createRequire } from 'node:module';
4
12
  import { Command } from 'commander';
5
13
  import { registerCreate } from './commands/create.js';
@@ -21,6 +29,7 @@ import { registerSkills } from './commands/skills.js';
21
29
  import { registerAgent } from './commands/agent.js';
22
30
  import { registerTelegram } from './commands/telegram.js';
23
31
  import { registerStaging } from './commands/staging.js';
32
+ import { registerVault } from './commands/vault.js';
24
33
  import { registerYolo } from './commands/yolo.js';
25
34
 
26
35
  const require = createRequire(import.meta.url);
@@ -83,5 +92,6 @@ registerSkills(program);
83
92
  registerAgent(program);
84
93
  registerTelegram(program);
85
94
  registerStaging(program);
95
+ registerVault(program);
86
96
  registerYolo(program);
87
97
  program.parse();
@@ -8,6 +8,23 @@
8
8
  import * as p from '@clack/prompts';
9
9
  import type { AgentConfigInput } from '@soleri/forge/lib';
10
10
  import { ITALIAN_CRAFTSPERSON } from '@soleri/core/personas';
11
+ import { isGhInstalled } from '../utils/git.js';
12
+
13
+ /** Git configuration collected from the wizard. */
14
+ export interface WizardGitConfig {
15
+ init: boolean;
16
+ remote?: {
17
+ type: 'gh' | 'manual';
18
+ url?: string;
19
+ visibility?: 'public' | 'private';
20
+ };
21
+ }
22
+
23
+ /** Full result from the create wizard. */
24
+ export interface CreateWizardResult {
25
+ config: AgentConfigInput;
26
+ git: WizardGitConfig;
27
+ }
11
28
 
12
29
  /** Slugify a display name into a kebab-case ID. */
13
30
  function slugify(name: string): string {
@@ -19,9 +36,9 @@ function slugify(name: string): string {
19
36
 
20
37
  /**
21
38
  * Run the simplified create wizard.
22
- * Returns an AgentConfigInput or null if cancelled.
39
+ * Returns a CreateWizardResult or null if cancelled.
23
40
  */
24
- export async function runCreateWizard(initialName?: string): Promise<AgentConfigInput | null> {
41
+ export async function runCreateWizard(initialName?: string): Promise<CreateWizardResult | null> {
25
42
  p.intro('Create a new Soleri agent');
26
43
 
27
44
  // ─── Step 1: Name ───────────────────────────────────────────
@@ -119,17 +136,95 @@ export async function runCreateWizard(initialName?: string): Promise<AgentConfig
119
136
 
120
137
  if (p.isCancel(confirm) || !confirm) return null;
121
138
 
139
+ // ─── Step 3: Git setup ──────────────────────────────────────
140
+ const gitInit = await p.confirm({
141
+ message: 'Initialize as a git repository?',
142
+ initialValue: true,
143
+ });
144
+
145
+ if (p.isCancel(gitInit)) return null;
146
+
147
+ const git: WizardGitConfig = { init: gitInit as boolean };
148
+
149
+ if (git.init) {
150
+ const pushRemote = await p.confirm({
151
+ message: 'Push to a remote repository?',
152
+ initialValue: false,
153
+ });
154
+
155
+ if (p.isCancel(pushRemote)) return null;
156
+
157
+ if (pushRemote) {
158
+ const ghAvailable = await isGhInstalled();
159
+
160
+ let remoteType: 'gh' | 'manual';
161
+
162
+ if (ghAvailable) {
163
+ const remoteChoice = await p.select({
164
+ message: 'How would you like to set up the remote?',
165
+ options: [
166
+ { value: 'gh' as const, label: 'Create a new GitHub repository' },
167
+ { value: 'manual' as const, label: 'Add an existing remote URL' },
168
+ ],
169
+ });
170
+
171
+ if (p.isCancel(remoteChoice)) return null;
172
+ remoteType = remoteChoice as 'gh' | 'manual';
173
+ } else {
174
+ remoteType = 'manual';
175
+ }
176
+
177
+ if (remoteType === 'gh') {
178
+ const visibility = await p.select({
179
+ message: 'Repository visibility?',
180
+ options: [
181
+ { value: 'private' as const, label: 'Private' },
182
+ { value: 'public' as const, label: 'Public' },
183
+ ],
184
+ initialValue: 'private' as const,
185
+ });
186
+
187
+ if (p.isCancel(visibility)) return null;
188
+
189
+ git.remote = {
190
+ type: 'gh',
191
+ visibility: visibility as 'public' | 'private',
192
+ };
193
+ } else {
194
+ const remoteUrl = await p.text({
195
+ message: 'Remote repository URL:',
196
+ placeholder: 'https://github.com/user/repo.git',
197
+ validate: (v) => {
198
+ if (!v || v.trim().length === 0) return 'URL is required';
199
+ if (!v.startsWith('https://') && !v.startsWith('git@'))
200
+ return 'URL must start with https:// or git@';
201
+ },
202
+ });
203
+
204
+ if (p.isCancel(remoteUrl)) return null;
205
+
206
+ git.remote = {
207
+ type: 'manual',
208
+ url: (remoteUrl as string).trim(),
209
+ };
210
+ }
211
+ }
212
+ }
213
+
122
214
  return {
123
- id,
124
- name: name.trim(),
125
- role: 'Your universal second brain — learns, remembers, improves',
126
- description:
127
- 'A universal assistant that learns from your projects, captures knowledge, and gets smarter with every session.',
128
- domains: [],
129
- principles: [],
130
- skills: [],
131
- tone: 'mentor',
132
- greeting,
133
- persona,
134
- } as AgentConfigInput;
215
+ config: {
216
+ id,
217
+ name: name.trim(),
218
+ role: 'Your universal second brain — learns, remembers, improves',
219
+ description:
220
+ 'A universal assistant that learns from your projects, captures knowledge, and gets smarter with every session.',
221
+ domains: [],
222
+ principles: [],
223
+ skills: [],
224
+ tone: 'mentor',
225
+ greeting,
226
+ persona,
227
+ } as AgentConfigInput,
228
+ git,
229
+ };
135
230
  }