@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.
- package/dist/commands/add-domain.js +1 -0
- package/dist/commands/add-domain.js.map +1 -1
- package/dist/commands/add-pack.js +7 -147
- package/dist/commands/add-pack.js.map +1 -1
- package/dist/commands/agent.js +130 -0
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/create.js +96 -4
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/dev.js +13 -3
- package/dist/commands/dev.js.map +1 -1
- package/dist/commands/doctor.js +2 -0
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/extend.js +17 -0
- package/dist/commands/extend.js.map +1 -1
- package/dist/commands/install-knowledge.js +1 -0
- package/dist/commands/install-knowledge.js.map +1 -1
- package/dist/commands/install.d.ts +2 -0
- package/dist/commands/install.js +79 -20
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/test.js +140 -1
- package/dist/commands/test.js.map +1 -1
- package/dist/commands/vault.d.ts +9 -0
- package/dist/commands/vault.js +66 -0
- package/dist/commands/vault.js.map +1 -0
- package/dist/hook-packs/flock-guard/manifest.json +2 -1
- package/dist/hook-packs/marketing-research/manifest.json +2 -1
- package/dist/hook-packs/registry.d.ts +2 -0
- package/dist/hook-packs/registry.js.map +1 -1
- package/dist/hook-packs/registry.ts +2 -0
- package/dist/main.js +7 -0
- package/dist/main.js.map +1 -1
- package/dist/prompts/create-wizard.d.ts +16 -2
- package/dist/prompts/create-wizard.js +84 -11
- package/dist/prompts/create-wizard.js.map +1 -1
- package/dist/utils/checks.d.ts +8 -5
- package/dist/utils/checks.js +105 -10
- package/dist/utils/checks.js.map +1 -1
- package/dist/utils/format-paths.d.ts +14 -0
- package/dist/utils/format-paths.js +27 -0
- package/dist/utils/format-paths.js.map +1 -0
- package/dist/utils/git.d.ts +29 -0
- package/dist/utils/git.js +88 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +1 -0
- package/dist/utils/logger.js +4 -0
- package/dist/utils/logger.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/create-wizard-git.test.ts +208 -0
- package/src/__tests__/git-utils.test.ts +268 -0
- package/src/__tests__/install.test.ts +88 -0
- package/src/__tests__/scaffold-git-e2e.test.ts +112 -0
- package/src/commands/add-domain.ts +1 -0
- package/src/commands/add-pack.ts +10 -163
- package/src/commands/agent.ts +161 -0
- package/src/commands/create.ts +109 -5
- package/src/commands/dev.ts +13 -3
- package/src/commands/doctor.ts +1 -0
- package/src/commands/extend.ts +20 -1
- package/src/commands/install-knowledge.ts +1 -0
- package/src/commands/install.ts +87 -20
- package/src/commands/test.ts +141 -2
- package/src/commands/vault.ts +79 -0
- package/src/hook-packs/flock-guard/manifest.json +2 -1
- package/src/hook-packs/marketing-research/manifest.json +2 -1
- package/src/hook-packs/registry.ts +2 -0
- package/src/main.ts +10 -0
- package/src/prompts/create-wizard.ts +109 -14
- package/src/utils/checks.ts +122 -13
- package/src/utils/git.ts +118 -0
- package/src/utils/logger.ts +5 -0
package/src/commands/install.ts
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
2
|
import { createRequire } from 'node:module';
|
|
3
|
-
import {
|
|
4
|
-
|
|
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',
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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',
|
|
151
|
-
: ['npx', '-y', '@soleri/engine', '--agent',
|
|
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
|
-
|
|
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
|
|
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
|
}
|
package/src/commands/test.ts
CHANGED
|
@@ -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
|
+
}
|
|
@@ -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
|
|
39
|
+
* Returns a CreateWizardResult or null if cancelled.
|
|
23
40
|
*/
|
|
24
|
-
export async function runCreateWizard(initialName?: string): Promise<
|
|
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
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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
|
}
|