@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.
@@ -28,11 +28,26 @@ import {
28
28
  generateInjectClaudeMd,
29
29
  generateSkills,
30
30
  } from '@soleri/forge/lib';
31
- import { composeClaudeMd, getEngineRulesContent } from '@soleri/forge/lib';
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(enginePath, getEngineRulesContent(), 'utf-8');
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(enginePath, getEngineRulesContent(), 'utf-8');
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
@@ -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
- const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
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) {
@@ -35,7 +35,14 @@ function runFileTreeDev(agentPath: string, agentId: string): void {
35
35
  regenerateClaudeMd(agentPath);
36
36
 
37
37
  // Start the engine server
38
- const engineBin = require.resolve('@soleri/core/dist/engine/bin/soleri-engine.js');
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
- // Directory may not exist yet that's OK
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
 
@@ -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
  }
@@ -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
- }