@soleri/cli 9.8.0 → 9.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent.js +19 -3
- package/dist/commands/agent.js.map +1 -1
- package/dist/commands/create.js +18 -2
- 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/install.d.ts +2 -0
- package/dist/commands/install.js +79 -20
- package/dist/commands/install.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/rtk/README.md +31 -0
- package/dist/hook-packs/rtk/manifest.json +24 -0
- package/dist/hook-packs/rtk/scripts/rtk-rewrite.sh +70 -0
- package/dist/main.js +7 -0
- package/dist/main.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/hook-packs.test.ts +63 -2
- package/src/__tests__/install.test.ts +88 -0
- package/src/__tests__/rtk-rewrite.test.ts +223 -0
- package/src/__tests__/scaffold-git-e2e.test.ts +7 -0
- package/src/commands/agent.ts +27 -4
- package/src/commands/create.ts +20 -2
- package/src/commands/dev.ts +13 -3
- package/src/commands/install.ts +87 -20
- package/src/commands/vault.ts +79 -0
- package/src/hook-packs/rtk/README.md +31 -0
- package/src/hook-packs/rtk/manifest.json +24 -0
- package/src/hook-packs/rtk/scripts/rtk-rewrite.sh +70 -0
- package/src/main.ts +10 -0
- package/src/utils/format-paths.ts +0 -41
package/src/commands/agent.ts
CHANGED
|
@@ -28,11 +28,26 @@ import {
|
|
|
28
28
|
generateInjectClaudeMd,
|
|
29
29
|
generateSkills,
|
|
30
30
|
} from '@soleri/forge/lib';
|
|
31
|
-
import { composeClaudeMd,
|
|
32
|
-
import type { AgentConfig } from '@soleri/forge/lib';
|
|
31
|
+
import { composeClaudeMd, getModularEngineRules, AgentYamlSchema } from '@soleri/forge/lib';
|
|
32
|
+
import type { AgentConfig, EngineFeature } from '@soleri/forge/lib';
|
|
33
33
|
import { detectAgent } from '../utils/agent-context.js';
|
|
34
34
|
import { installClaude } from './install.js';
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Read engine.features from agent.yaml at the given agent path.
|
|
38
|
+
* Returns undefined if not specified (= include all modules).
|
|
39
|
+
*/
|
|
40
|
+
function readEngineFeatures(agentPath: string): EngineFeature[] | undefined {
|
|
41
|
+
try {
|
|
42
|
+
const yamlPath = join(agentPath, 'agent.yaml');
|
|
43
|
+
if (!existsSync(yamlPath)) return undefined;
|
|
44
|
+
const parsed = AgentYamlSchema.parse(parseYaml(readFileSync(yamlPath, 'utf-8')));
|
|
45
|
+
return parsed.engine?.features as EngineFeature[] | undefined;
|
|
46
|
+
} catch {
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
export function registerAgent(program: Command): void {
|
|
37
52
|
const agent = program.command('agent').description('Agent lifecycle management');
|
|
38
53
|
|
|
@@ -263,7 +278,11 @@ export function registerAgent(program: Command): void {
|
|
|
263
278
|
|
|
264
279
|
// 2. Regenerate _engine.md
|
|
265
280
|
mkdirSync(join(ctx.agentPath, 'instructions'), { recursive: true });
|
|
266
|
-
writeFileSync(
|
|
281
|
+
writeFileSync(
|
|
282
|
+
enginePath,
|
|
283
|
+
getModularEngineRules(readEngineFeatures(ctx.agentPath)),
|
|
284
|
+
'utf-8',
|
|
285
|
+
);
|
|
267
286
|
p.log.success(`Regenerated ${enginePath}`);
|
|
268
287
|
|
|
269
288
|
// 3. Recompose CLAUDE.md
|
|
@@ -407,7 +426,11 @@ export function registerAgent(program: Command): void {
|
|
|
407
426
|
|
|
408
427
|
// 2. Regenerate _engine.md from latest shared-rules
|
|
409
428
|
mkdirSync(join(ctx.agentPath, 'instructions'), { recursive: true });
|
|
410
|
-
writeFileSync(
|
|
429
|
+
writeFileSync(
|
|
430
|
+
enginePath,
|
|
431
|
+
getModularEngineRules(readEngineFeatures(ctx.agentPath)),
|
|
432
|
+
'utf-8',
|
|
433
|
+
);
|
|
411
434
|
p.log.success(`Regenerated ${enginePath}`);
|
|
412
435
|
|
|
413
436
|
// 3. Recompose CLAUDE.md from agent.yaml + instructions + workflows + skills
|
package/src/commands/create.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { readFileSync, existsSync } from 'node:fs';
|
|
1
|
+
import { accessSync, constants as fsConstants, readFileSync, existsSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import type { Command } from 'commander';
|
|
4
4
|
import * as p from '@clack/prompts';
|
|
@@ -97,7 +97,16 @@ export function registerCreate(program: Command): void {
|
|
|
97
97
|
p.log.error(`Config file not found: ${configPath}`);
|
|
98
98
|
process.exit(1);
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
101
|
+
let raw: any;
|
|
102
|
+
try {
|
|
103
|
+
raw = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
104
|
+
} catch {
|
|
105
|
+
p.log.error(
|
|
106
|
+
`Failed to parse ${configPath}. The file may be corrupted. Delete it and try again.`,
|
|
107
|
+
);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
101
110
|
const parsed = AgentConfigSchema.safeParse(raw);
|
|
102
111
|
if (!parsed.success) {
|
|
103
112
|
p.log.error(`Invalid config: ${parsed.error.message}`);
|
|
@@ -175,6 +184,15 @@ export function registerCreate(program: Command): void {
|
|
|
175
184
|
};
|
|
176
185
|
|
|
177
186
|
const outputDir = opts?.dir ? resolve(opts.dir) : (config.outputDir ?? process.cwd());
|
|
187
|
+
|
|
188
|
+
// Preflight: check output directory is writable
|
|
189
|
+
try {
|
|
190
|
+
accessSync(outputDir, fsConstants.W_OK);
|
|
191
|
+
} catch {
|
|
192
|
+
p.log.error(`Cannot write to ${outputDir} — check permissions`);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
|
|
178
196
|
const nonInteractive = !!(opts?.yes || opts?.config);
|
|
179
197
|
|
|
180
198
|
if (!nonInteractive) {
|
package/src/commands/dev.ts
CHANGED
|
@@ -35,7 +35,14 @@ function runFileTreeDev(agentPath: string, agentId: string): void {
|
|
|
35
35
|
regenerateClaudeMd(agentPath);
|
|
36
36
|
|
|
37
37
|
// Start the engine server
|
|
38
|
-
|
|
38
|
+
let engineBin: string;
|
|
39
|
+
try {
|
|
40
|
+
engineBin = require.resolve('@soleri/core/dist/engine/bin/soleri-engine.js');
|
|
41
|
+
} catch {
|
|
42
|
+
p.log.error('Engine not found. Run: npm install @soleri/core');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
39
46
|
const engine = spawn('node', [engineBin, '--agent', join(agentPath, 'agent.yaml')], {
|
|
40
47
|
stdio: ['pipe', 'inherit', 'inherit'],
|
|
41
48
|
env: { ...process.env },
|
|
@@ -72,8 +79,11 @@ function runFileTreeDev(agentPath: string, agentId: string): void {
|
|
|
72
79
|
regenerateClaudeMd(agentPath);
|
|
73
80
|
}, 200);
|
|
74
81
|
});
|
|
75
|
-
} catch {
|
|
76
|
-
|
|
82
|
+
} catch (err: unknown) {
|
|
83
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
84
|
+
if (!msg.includes('ENOENT')) {
|
|
85
|
+
p.log.warn(`File watch stopped: ${msg}. Restart soleri dev if changes stop updating.`);
|
|
86
|
+
}
|
|
77
87
|
}
|
|
78
88
|
}
|
|
79
89
|
|
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
|
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# RTK Hook Pack
|
|
2
|
+
|
|
3
|
+
Reduces LLM token usage by 60-90% by routing shell commands through [RTK](https://github.com/rtk-ai/rtk), a Rust-based CLI proxy that compresses verbose command output.
|
|
4
|
+
|
|
5
|
+
## How it works
|
|
6
|
+
|
|
7
|
+
A PreToolUse hook intercepts Bash commands and rewrites them through RTK:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
git status → rtk git status → "M 3 files" (instead of 15 lines)
|
|
11
|
+
npm test → rtk npm test → "2 failed" (instead of 200+ lines)
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
RTK supports 70+ commands across git, JS/TS, Python, Go, Ruby, Docker, and file operations.
|
|
15
|
+
|
|
16
|
+
## Prerequisites
|
|
17
|
+
|
|
18
|
+
- [RTK](https://github.com/rtk-ai/rtk) >= 0.23.0 (`brew install rtk-ai/tap/rtk` or `cargo install rtk`)
|
|
19
|
+
- `jq` (usually pre-installed on macOS/Linux)
|
|
20
|
+
|
|
21
|
+
## Install
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
soleri hooks add-pack rtk
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Uninstall
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
soleri hooks remove-pack rtk
|
|
31
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "rtk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "RTK token compression — rewrites shell commands through RTK proxy to reduce LLM token usage by 60-90%",
|
|
5
|
+
"hooks": [],
|
|
6
|
+
"scripts": [
|
|
7
|
+
{
|
|
8
|
+
"name": "rtk-rewrite",
|
|
9
|
+
"file": "rtk-rewrite.sh",
|
|
10
|
+
"targetDir": "hooks"
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"lifecycleHooks": [
|
|
14
|
+
{
|
|
15
|
+
"event": "PreToolUse",
|
|
16
|
+
"matcher": "Bash",
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "sh ~/.claude/hooks/rtk-rewrite.sh",
|
|
19
|
+
"timeout": 10,
|
|
20
|
+
"statusMessage": "RTK: compressing output..."
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"scaffoldDefault": false
|
|
24
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# RTK Rewrite — PreToolUse command rewriter (Soleri Hook Pack: rtk)
|
|
3
|
+
# Intercepts Bash commands and rewrites them through RTK proxy for token compression.
|
|
4
|
+
# RTK (https://github.com/rtk-ai/rtk) reduces LLM token usage by 60-90%.
|
|
5
|
+
# Dependencies: jq, rtk (>= 0.23.0)
|
|
6
|
+
# POSIX sh compatible.
|
|
7
|
+
|
|
8
|
+
set -eu
|
|
9
|
+
|
|
10
|
+
# ── Dependency checks ──────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
WARN_FLAG="${HOME}/.soleri/.rtk-warned"
|
|
13
|
+
|
|
14
|
+
warn_once() {
|
|
15
|
+
# Warn at most once per day (86400 seconds)
|
|
16
|
+
if [ -f "$WARN_FLAG" ]; then
|
|
17
|
+
# Get file mtime — try macOS stat, then Linux stat, fallback to 0
|
|
18
|
+
FILE_MTIME=0
|
|
19
|
+
if stat -f %m "$WARN_FLAG" >/dev/null 2>&1; then
|
|
20
|
+
FILE_MTIME=$(stat -f %m "$WARN_FLAG")
|
|
21
|
+
elif stat -c %Y "$WARN_FLAG" >/dev/null 2>&1; then
|
|
22
|
+
FILE_MTIME=$(stat -c %Y "$WARN_FLAG")
|
|
23
|
+
fi
|
|
24
|
+
NOW=$(date +%s)
|
|
25
|
+
WARN_AGE=$(( NOW - FILE_MTIME ))
|
|
26
|
+
[ "$WARN_AGE" -lt 86400 ] && return
|
|
27
|
+
fi
|
|
28
|
+
mkdir -p "$(dirname "$WARN_FLAG")"
|
|
29
|
+
echo "$1" >&2
|
|
30
|
+
touch "$WARN_FLAG"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if ! command -v jq >/dev/null 2>&1; then
|
|
34
|
+
warn_once "[soleri:rtk] jq not found — RTK hook disabled. Install: https://stedolan.github.io/jq/"
|
|
35
|
+
exit 0
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
if ! command -v rtk >/dev/null 2>&1; then
|
|
39
|
+
warn_once "[soleri:rtk] rtk not found — RTK hook disabled. Install: https://github.com/rtk-ai/rtk"
|
|
40
|
+
exit 0
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
# ── Read stdin JSON ────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
INPUT=$(cat)
|
|
46
|
+
|
|
47
|
+
# Extract command from Claude Code PreToolUse JSON
|
|
48
|
+
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)
|
|
49
|
+
if [ -z "$CMD" ]; then
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
|
|
53
|
+
# ── Rewrite via RTK ────────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
# Ask RTK if it can compress this command.
|
|
56
|
+
# Exit codes: 0 = rewritten, 1 = no match (pass through), 2+ = error
|
|
57
|
+
REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null) || exit 0
|
|
58
|
+
|
|
59
|
+
# If RTK returned empty or same command, pass through
|
|
60
|
+
if [ -z "$REWRITTEN" ] || [ "$REWRITTEN" = "$CMD" ]; then
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# ── Return rewritten command ───────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
# Build updatedInput from original tool_input with rewritten command
|
|
67
|
+
UPDATED_INPUT=$(printf '%s' "$INPUT" | jq -c --arg cmd "$REWRITTEN" '.tool_input | .command = $cmd')
|
|
68
|
+
|
|
69
|
+
# Output Claude Code hookSpecificOutput contract
|
|
70
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"allow","permissionDecisionReason":"RTK token compression","updatedInput":%s}}' "$UPDATED_INPUT"
|
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();
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Format-aware path resolution for filetree vs typescript agents.
|
|
3
|
-
*/
|
|
4
|
-
import { join } from 'node:path';
|
|
5
|
-
|
|
6
|
-
export interface FormatPaths {
|
|
7
|
-
knowledgeDir: string;
|
|
8
|
-
extensionsDir: string;
|
|
9
|
-
facadesDir: string;
|
|
10
|
-
agentConfigFile: string;
|
|
11
|
-
entryPoint: string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function getFormatPaths(ctx: {
|
|
15
|
-
format: 'filetree' | 'typescript';
|
|
16
|
-
agentPath: string;
|
|
17
|
-
}): FormatPaths {
|
|
18
|
-
const { format, agentPath } = ctx;
|
|
19
|
-
|
|
20
|
-
if (format === 'filetree') {
|
|
21
|
-
return {
|
|
22
|
-
knowledgeDir: join(agentPath, 'knowledge'),
|
|
23
|
-
extensionsDir: join(agentPath, 'extensions'),
|
|
24
|
-
facadesDir: '',
|
|
25
|
-
agentConfigFile: join(agentPath, 'agent.yaml'),
|
|
26
|
-
entryPoint: join(agentPath, 'agent.yaml'),
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
knowledgeDir: join(agentPath, 'src', 'intelligence', 'data'),
|
|
32
|
-
extensionsDir: join(agentPath, 'src', 'extensions'),
|
|
33
|
-
facadesDir: join(agentPath, 'src', 'facades'),
|
|
34
|
-
agentConfigFile: join(agentPath, 'package.json'),
|
|
35
|
-
entryPoint: join(agentPath, 'src', 'index.ts'),
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export function isFileTree(ctx: { format: string }): boolean {
|
|
40
|
-
return ctx.format === 'filetree';
|
|
41
|
-
}
|