@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.
@@ -0,0 +1,66 @@
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
+ import { resolve } from 'node:path';
9
+ import { existsSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import { detectAgent } from '../utils/agent-context.js';
12
+ import * as log from '../utils/logger.js';
13
+ import { SOLERI_HOME } from '@soleri/core';
14
+ export function registerVault(program) {
15
+ const vault = program.command('vault').description('Vault knowledge management');
16
+ vault
17
+ .command('export')
18
+ .description('Export vault entries as browsable markdown files')
19
+ .option('--path <dir>', 'Output directory (default: ./knowledge/)')
20
+ .option('--domain <name>', 'Filter by domain')
21
+ .action(async (opts) => {
22
+ const agent = detectAgent();
23
+ if (!agent) {
24
+ log.fail('Not in a Soleri agent project', 'Run from an agent directory');
25
+ process.exit(1);
26
+ }
27
+ const outputDir = opts.path ? resolve(opts.path) : resolve('knowledge');
28
+ // Find vault DB — check new path first, then legacy
29
+ const newDbPath = join(SOLERI_HOME, agent.agentId, 'vault.db');
30
+ const legacyDbPath = join(SOLERI_HOME, '..', `.${agent.agentId}`, 'vault.db');
31
+ const vaultDbPath = existsSync(newDbPath)
32
+ ? newDbPath
33
+ : existsSync(legacyDbPath)
34
+ ? legacyDbPath
35
+ : null;
36
+ if (!vaultDbPath) {
37
+ log.fail('Vault DB not found', 'Run the agent once to initialize its vault database.');
38
+ process.exit(1);
39
+ }
40
+ // Dynamic import to avoid loading better-sqlite3 unless needed
41
+ const { Vault } = await import('@soleri/core');
42
+ const vaultInstance = new Vault(vaultDbPath);
43
+ try {
44
+ log.heading('Vault Export');
45
+ if (opts.domain) {
46
+ const { syncEntryToMarkdown } = await import('@soleri/core');
47
+ const entries = vaultInstance.list({ limit: 10000, domain: opts.domain });
48
+ let synced = 0;
49
+ for (const entry of entries) {
50
+ await syncEntryToMarkdown(entry, outputDir);
51
+ synced++;
52
+ }
53
+ log.pass(`Exported ${synced} entries from domain "${opts.domain}"`, `${outputDir}/vault/`);
54
+ }
55
+ else {
56
+ const { syncAllToMarkdown } = await import('@soleri/core');
57
+ const result = await syncAllToMarkdown(vaultInstance, outputDir);
58
+ log.pass(`Exported ${result.synced} entries (${result.skipped} unchanged)`, `${outputDir}/vault/`);
59
+ }
60
+ }
61
+ finally {
62
+ vaultInstance.close();
63
+ }
64
+ });
65
+ }
66
+ //# sourceMappingURL=vault.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vault.js","sourceRoot":"","sources":["../../src/commands/vault.ts"],"names":[],"mappings":"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"}
@@ -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/dist/main.js CHANGED
@@ -1,4 +1,9 @@
1
1
  #!/usr/bin/env node
2
+ const [major] = process.versions.node.split('.').map(Number);
3
+ if (major < 18) {
4
+ console.error(`\n Soleri requires Node.js 18 or later.\n You have v${process.versions.node}.\n Upgrade at https://nodejs.org\n`);
5
+ process.exit(1);
6
+ }
2
7
  import { createRequire } from 'node:module';
3
8
  import { Command } from 'commander';
4
9
  import { registerCreate } from './commands/create.js';
@@ -20,6 +25,7 @@ import { registerSkills } from './commands/skills.js';
20
25
  import { registerAgent } from './commands/agent.js';
21
26
  import { registerTelegram } from './commands/telegram.js';
22
27
  import { registerStaging } from './commands/staging.js';
28
+ import { registerVault } from './commands/vault.js';
23
29
  import { registerYolo } from './commands/yolo.js';
24
30
  const require = createRequire(import.meta.url);
25
31
  const { version } = require('../package.json');
@@ -76,6 +82,7 @@ registerSkills(program);
76
82
  registerAgent(program);
77
83
  registerTelegram(program);
78
84
  registerStaging(program);
85
+ registerVault(program);
79
86
  registerYolo(program);
80
87
  program.parse();
81
88
  //# sourceMappingURL=main.js.map
package/dist/main.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"main.js","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soleri/cli",
3
- "version": "9.8.0",
3
+ "version": "9.10.0",
4
4
  "description": "Developer CLI for creating and managing Soleri AI agents.",
5
5
  "keywords": [
6
6
  "agent",
@@ -23,9 +23,9 @@ describe('hook-packs', () => {
23
23
  });
24
24
 
25
25
  describe('registry', () => {
26
- it('should list all 9 built-in packs', () => {
26
+ it('should list all 10 built-in packs', () => {
27
27
  const packs = listPacks();
28
- expect(packs.length).toBe(9);
28
+ expect(packs.length).toBe(10);
29
29
  const names = packs.map((p) => p.name).sort();
30
30
  expect(names).toEqual([
31
31
  'a11y',
@@ -34,6 +34,7 @@ describe('hook-packs', () => {
34
34
  'flock-guard',
35
35
  'full',
36
36
  'marketing-research',
37
+ 'rtk',
37
38
  'safety',
38
39
  'typescript-safety',
39
40
  'yolo-safety',
@@ -263,4 +264,64 @@ describe('hook-packs', () => {
263
264
  expect(installed).toContain('yolo-safety');
264
265
  });
265
266
  });
267
+
268
+ describe('rtk pack', () => {
269
+ it('should get rtk pack with script and lifecycle hook', () => {
270
+ const pack = getPack('rtk');
271
+ expect(pack).not.toBeNull();
272
+ expect(pack!.manifest.name).toBe('rtk');
273
+ expect(pack!.manifest.hooks).toEqual([]);
274
+ expect(pack!.manifest.scripts).toHaveLength(1);
275
+ expect(pack!.manifest.scripts![0].name).toBe('rtk-rewrite');
276
+ expect(pack!.manifest.scripts![0].file).toBe('rtk-rewrite.sh');
277
+ expect(pack!.manifest.scripts![0].targetDir).toBe('hooks');
278
+ expect(pack!.manifest.lifecycleHooks).toHaveLength(1);
279
+ expect(pack!.manifest.lifecycleHooks![0].event).toBe('PreToolUse');
280
+ expect(pack!.manifest.lifecycleHooks![0].matcher).toBe('Bash');
281
+ expect(pack!.manifest.lifecycleHooks![0].command).toBe('sh ~/.claude/hooks/rtk-rewrite.sh');
282
+ });
283
+
284
+ it('should install rtk pack with script and lifecycle hook', () => {
285
+ const result = installPack('rtk');
286
+ expect(result.installed).toEqual([]);
287
+ expect(result.scripts).toHaveLength(1);
288
+ expect(result.scripts[0]).toBe('hooks/rtk-rewrite.sh');
289
+ expect(result.lifecycleHooks).toHaveLength(1);
290
+ expect(result.lifecycleHooks[0]).toBe('PreToolUse:Bash');
291
+ const claudeDir = join(tempHome, '.claude');
292
+ expect(existsSync(join(claudeDir, 'hooks', 'rtk-rewrite.sh'))).toBe(true);
293
+ const settings = JSON.parse(readFileSync(join(claudeDir, 'settings.json'), 'utf-8'));
294
+ expect(settings.hooks.PreToolUse).toHaveLength(1);
295
+ expect(settings.hooks.PreToolUse[0].matcher).toBe('Bash');
296
+ expect(settings.hooks.PreToolUse[0].hooks[0].command).toBe(
297
+ 'sh ~/.claude/hooks/rtk-rewrite.sh',
298
+ );
299
+ expect(settings.hooks.PreToolUse[0]._soleriPack).toBe('rtk');
300
+ });
301
+
302
+ it('should remove rtk pack including script and lifecycle hook', () => {
303
+ installPack('rtk');
304
+ const result = removePack('rtk');
305
+ expect(result.scripts).toHaveLength(1);
306
+ expect(result.lifecycleHooks).toHaveLength(1);
307
+ const claudeDir = join(tempHome, '.claude');
308
+ expect(existsSync(join(claudeDir, 'hooks', 'rtk-rewrite.sh'))).toBe(false);
309
+ const settings = JSON.parse(readFileSync(join(claudeDir, 'settings.json'), 'utf-8'));
310
+ expect(settings.hooks.PreToolUse).toBeUndefined();
311
+ });
312
+
313
+ it('should be idempotent for rtk lifecycle hooks', () => {
314
+ installPack('rtk');
315
+ const result2 = installPack('rtk');
316
+ expect(result2.lifecycleHooks).toEqual([]);
317
+ const claudeDir = join(tempHome, '.claude');
318
+ const settings = JSON.parse(readFileSync(join(claudeDir, 'settings.json'), 'utf-8'));
319
+ expect(settings.hooks.PreToolUse).toHaveLength(1);
320
+ });
321
+
322
+ it('should not be scaffoldDefault', () => {
323
+ const pack = getPack('rtk');
324
+ expect(pack!.manifest.scaffoldDefault).toBe(false);
325
+ });
326
+ });
266
327
  });
@@ -0,0 +1,88 @@
1
+ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
2
+ import { mkdirSync, rmSync, writeFileSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { toPosix } from '../commands/install.js';
6
+
7
+ // Mock @clack/prompts to suppress console output during tests
8
+ vi.mock('@clack/prompts', () => ({
9
+ log: {
10
+ success: vi.fn(),
11
+ error: vi.fn(),
12
+ warn: vi.fn(),
13
+ info: vi.fn(),
14
+ },
15
+ }));
16
+
17
+ describe('toPosix', () => {
18
+ it('converts backslashes to forward slashes', () => {
19
+ expect(toPosix('C:\\Users\\foo\\agent.yaml')).toBe('C:/Users/foo/agent.yaml');
20
+ });
21
+
22
+ it('leaves forward slashes unchanged', () => {
23
+ expect(toPosix('/home/user/agent.yaml')).toBe('/home/user/agent.yaml');
24
+ });
25
+
26
+ it('handles mixed separators', () => {
27
+ expect(toPosix('C:\\Users/foo\\bar/agent.yaml')).toBe('C:/Users/foo/bar/agent.yaml');
28
+ });
29
+
30
+ it('handles empty string', () => {
31
+ expect(toPosix('')).toBe('');
32
+ });
33
+ });
34
+
35
+ describe('installClaude path normalization', () => {
36
+ let tempDir: string;
37
+ let originalHome: string;
38
+
39
+ beforeEach(() => {
40
+ tempDir = join(tmpdir(), `cli-install-test-${Date.now()}`);
41
+ mkdirSync(tempDir, { recursive: true });
42
+ originalHome = process.env.HOME ?? '';
43
+ // Point homedir() to our temp dir so ~/.claude.json lands there
44
+ process.env.HOME = tempDir;
45
+ // Windows uses USERPROFILE instead of HOME
46
+ process.env.USERPROFILE = tempDir;
47
+ });
48
+
49
+ afterEach(() => {
50
+ process.env.HOME = originalHome;
51
+ if (originalHome) process.env.USERPROFILE = originalHome;
52
+ rmSync(tempDir, { recursive: true, force: true });
53
+ });
54
+
55
+ it('should not contain backslashes in written config paths (file-tree agent)', async () => {
56
+ // Dynamic import to pick up the mocked homedir
57
+ const { installClaude } = await import('../commands/install.js');
58
+
59
+ // Simulate a Windows-style path
60
+ const fakeAgentDir = 'C:\\Users\\testuser\\my-agent';
61
+ installClaude('test-agent', fakeAgentDir, true);
62
+
63
+ const configPath = join(tempDir, '.claude.json');
64
+ const raw = readFileSync(configPath, 'utf-8');
65
+
66
+ // The raw JSON should not contain any backslash-based paths
67
+ // (backslashes in JSON would appear as \\ in the raw string)
68
+ const config = JSON.parse(raw);
69
+ const entry = config.mcpServers['test-agent'];
70
+ for (const arg of entry.args as string[]) {
71
+ expect(arg).not.toContain('\\');
72
+ }
73
+ });
74
+
75
+ it('should not contain backslashes in written config paths (legacy agent)', async () => {
76
+ const { installClaude } = await import('../commands/install.js');
77
+
78
+ const fakeAgentDir = 'C:\\Users\\testuser\\my-agent';
79
+ installClaude('test-agent', fakeAgentDir, false);
80
+
81
+ const configPath = join(tempDir, '.claude.json');
82
+ const config = JSON.parse(readFileSync(configPath, 'utf-8'));
83
+ const entry = config.mcpServers['test-agent'];
84
+ for (const arg of entry.args as string[]) {
85
+ expect(arg).not.toContain('\\');
86
+ }
87
+ });
88
+ });
@@ -0,0 +1,223 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { execSync, spawnSync } from 'node:child_process';
3
+ import { join, dirname } from 'node:path';
4
+ import { mkdtempSync, rmSync, existsSync, symlinkSync, readdirSync, mkdirSync } from 'node:fs';
5
+ import { tmpdir } from 'node:os';
6
+
7
+ const SCRIPTS_DIR = join(__dirname, '..', 'hook-packs', 'rtk', 'scripts');
8
+ const SCRIPT = join(SCRIPTS_DIR, 'rtk-rewrite.sh').replace(/\\/g, '/');
9
+
10
+ function makePayload(command: string, extra?: Record<string, unknown>): string {
11
+ return JSON.stringify({
12
+ tool_name: 'Bash',
13
+ tool_input: { command, ...extra },
14
+ });
15
+ }
16
+
17
+ function runHook(
18
+ command: string,
19
+ extra?: Record<string, unknown>,
20
+ ): { stdout: string; stderr: string; exitCode: number } {
21
+ try {
22
+ const payload = makePayload(command, extra);
23
+ const result = execSync(`printf '%s' '${payload.replace(/'/g, "'\\''")}' | sh '${SCRIPT}'`, {
24
+ encoding: 'utf-8',
25
+ stdio: 'pipe',
26
+ });
27
+ return { stdout: result, stderr: '', exitCode: 0 };
28
+ } catch (err: any) {
29
+ return { stdout: err.stdout ?? '', stderr: err.stderr ?? '', exitCode: err.status ?? 1 };
30
+ }
31
+ }
32
+
33
+ // RTK and jq must be installed to run these tests.
34
+ const hasRtk = (() => {
35
+ try {
36
+ execSync('command -v rtk', { stdio: 'pipe' });
37
+ return true;
38
+ } catch {
39
+ return false;
40
+ }
41
+ })();
42
+
43
+ const hasJq = (() => {
44
+ try {
45
+ execSync('command -v jq', { stdio: 'pipe' });
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ })();
51
+
52
+ const isWindows = process.platform === 'win32';
53
+
54
+ describe.skipIf(isWindows || !hasRtk || !hasJq)('rtk-rewrite hook script', () => {
55
+ it('rewrites git status to rtk git status', () => {
56
+ const { stdout, exitCode } = runHook('git status');
57
+ expect(exitCode).toBe(0);
58
+ expect(stdout.trim()).not.toBe('');
59
+
60
+ const output = JSON.parse(stdout.trim());
61
+ expect(output.hookSpecificOutput).toBeDefined();
62
+ expect(output.hookSpecificOutput.hookEventName).toBe('PreToolUse');
63
+ expect(output.hookSpecificOutput.permissionDecision).toBe('allow');
64
+ expect(output.hookSpecificOutput.updatedInput).toBeDefined();
65
+ expect(output.hookSpecificOutput.updatedInput.command).toBe('rtk git status');
66
+ });
67
+
68
+ it('preserves original tool_input fields in updatedInput', () => {
69
+ const { stdout } = runHook('git diff', { description: 'Show diff', timeout: 60000 });
70
+ const output = JSON.parse(stdout.trim());
71
+ expect(output.hookSpecificOutput.updatedInput.description).toBe('Show diff');
72
+ expect(output.hookSpecificOutput.updatedInput.timeout).toBe(60000);
73
+ expect(output.hookSpecificOutput.updatedInput.command).toBe('rtk git diff');
74
+ });
75
+
76
+ it('passes through non-rewritable commands (exit 0, no output)', () => {
77
+ const { stdout, exitCode } = runHook('echo hello');
78
+ expect(exitCode).toBe(0);
79
+ expect(stdout.trim()).toBe('');
80
+ });
81
+
82
+ it('passes through empty command', () => {
83
+ const { stdout, exitCode } = runHook('');
84
+ expect(exitCode).toBe(0);
85
+ expect(stdout.trim()).toBe('');
86
+ });
87
+
88
+ it('rewrites ls commands', () => {
89
+ const { stdout } = runHook('ls -la');
90
+ const output = JSON.parse(stdout.trim());
91
+ expect(output.hookSpecificOutput.updatedInput.command).toMatch(/^rtk (ls|read)/);
92
+ });
93
+
94
+ it('rewrites git log commands', () => {
95
+ const { stdout } = runHook('git log --oneline -5');
96
+ const output = JSON.parse(stdout.trim());
97
+ expect(output.hookSpecificOutput.updatedInput.command).toContain('rtk git log');
98
+ });
99
+ });
100
+
101
+ /**
102
+ * Build a PATH string that excludes specific commands by creating shadow
103
+ * directories with symlinks to everything except the hidden commands.
104
+ */
105
+ function buildPathWithout(hide: string[]): string {
106
+ const originalDirs = (process.env.PATH || '').split(':');
107
+ const resultDirs: string[] = [];
108
+
109
+ for (const dir of originalDirs) {
110
+ if (!existsSync(dir)) continue;
111
+
112
+ // Check if this dir contains any of the commands we want to hide
113
+ const hasHidden = hide.some((cmd) => existsSync(join(dir, cmd)));
114
+
115
+ if (!hasHidden) {
116
+ resultDirs.push(dir);
117
+ continue;
118
+ }
119
+
120
+ // Create a shadow dir with symlinks to everything except hidden commands
121
+ const shadowDir = mkdtempSync(join(tmpdir(), 'rtk-shadow-'));
122
+ shadowDirsToCleanup.push(shadowDir);
123
+
124
+ try {
125
+ const entries = readdirSync(dir);
126
+ for (const entry of entries) {
127
+ if (hide.includes(entry)) continue;
128
+ const src = join(dir, entry);
129
+ const dst = join(shadowDir, entry);
130
+ try {
131
+ symlinkSync(src, dst);
132
+ } catch {
133
+ // Skip entries that fail (broken symlinks, permission issues)
134
+ }
135
+ }
136
+ } catch {
137
+ // If we can't read the dir, skip it
138
+ continue;
139
+ }
140
+
141
+ resultDirs.push(shadowDir);
142
+ }
143
+
144
+ return resultDirs.join(':');
145
+ }
146
+
147
+ // Track shadow dirs for cleanup
148
+ let shadowDirsToCleanup: string[] = [];
149
+
150
+ describe.skipIf(isWindows)('rtk-rewrite dependency warnings', () => {
151
+ let tempHome: string;
152
+
153
+ beforeEach(() => {
154
+ tempHome = mkdtempSync(join(tmpdir(), 'rtk-warn-test-'));
155
+ shadowDirsToCleanup = [];
156
+ });
157
+
158
+ afterEach(() => {
159
+ rmSync(tempHome, { recursive: true, force: true });
160
+ for (const d of shadowDirsToCleanup) {
161
+ rmSync(d, { recursive: true, force: true });
162
+ }
163
+ shadowDirsToCleanup = [];
164
+ });
165
+
166
+ function runWithMissingDep(hide: string[]): {
167
+ stdout: string;
168
+ stderr: string;
169
+ exitCode: number;
170
+ } {
171
+ const payload = makePayload('git status');
172
+ const restrictedPath = buildPathWithout(hide);
173
+
174
+ const result = spawnSync(
175
+ 'sh',
176
+ ['-c', `printf '%s' '${payload.replace(/'/g, "'\\''")}' | sh '${SCRIPT}'`],
177
+ {
178
+ encoding: 'utf-8',
179
+ stdio: 'pipe',
180
+ env: {
181
+ PATH: restrictedPath,
182
+ HOME: tempHome,
183
+ TERM: process.env.TERM || 'xterm',
184
+ },
185
+ },
186
+ );
187
+
188
+ return {
189
+ stdout: result.stdout ?? '',
190
+ stderr: result.stderr ?? '',
191
+ exitCode: result.status ?? 1,
192
+ };
193
+ }
194
+
195
+ it('warns on stderr when jq is missing', () => {
196
+ const { stdout, stderr, exitCode } = runWithMissingDep(['jq']);
197
+ expect(exitCode).toBe(0);
198
+ expect(stdout.trim()).toBe('');
199
+ expect(stderr).toContain('[soleri:rtk] jq not found');
200
+ expect(stderr).toContain('RTK hook disabled');
201
+ });
202
+
203
+ it('warns on stderr when rtk is missing', () => {
204
+ if (!hasJq) return; // jq must be present for the rtk check to be reached
205
+ const { stdout, stderr, exitCode } = runWithMissingDep(['rtk']);
206
+ expect(exitCode).toBe(0);
207
+ expect(stdout.trim()).toBe('');
208
+ expect(stderr).toContain('[soleri:rtk] rtk not found');
209
+ expect(stderr).toContain('RTK hook disabled');
210
+ });
211
+
212
+ it('suppresses warning on second run within same day', () => {
213
+ // First run — should warn
214
+ const first = runWithMissingDep(['jq']);
215
+ expect(first.exitCode).toBe(0);
216
+ expect(first.stderr).toContain('[soleri:rtk] jq not found');
217
+
218
+ // Second run — flag file exists and is fresh, should suppress
219
+ const second = runWithMissingDep(['jq']);
220
+ expect(second.exitCode).toBe(0);
221
+ expect(second.stderr.trim()).toBe('');
222
+ });
223
+ });
@@ -53,6 +53,10 @@ describe('scaffold + git init (E2E)', () => {
53
53
  const initResult = await gitInit(agentDir);
54
54
  expect(initResult.ok).toBe(true);
55
55
 
56
+ // Ensure git config is set (CI runners may not have global user.name/user.email)
57
+ gitCommand(agentDir, 'config', 'user.name', 'Test');
58
+ gitCommand(agentDir, 'config', 'user.email', 'test@test.local');
59
+
56
60
  const commitResult = await gitInitialCommit(agentDir, 'feat: scaffold agent "test-agent"');
57
61
  expect(commitResult.ok).toBe(true);
58
62
 
@@ -80,6 +84,9 @@ describe('scaffold + git init (E2E)', () => {
80
84
  const agentDir = result.agentDir;
81
85
 
82
86
  await gitInit(agentDir);
87
+ // Ensure git config is set (CI runners may not have global user.name/user.email)
88
+ gitCommand(agentDir, 'config', 'user.name', 'Test');
89
+ gitCommand(agentDir, 'config', 'user.email', 'test@test.local');
83
90
  await gitInitialCommit(agentDir, 'feat: scaffold agent "test-agent"');
84
91
 
85
92
  const trackedFiles = gitCommand(agentDir, 'ls-files');