@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
|
@@ -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":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AACxD,OAAO,KAAK,GAAG,MAAM,oBAAoB,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,WAAW,CAAC,4BAA4B,CAAC,CAAC;IAEjF,KAAK;SACF,OAAO,CAAC,QAAQ,CAAC;SACjB,WAAW,CAAC,kDAAkD,CAAC;SAC/D,MAAM,CAAC,cAAc,EAAE,0CAA0C,CAAC;SAClE,MAAM,CAAC,iBAAiB,EAAE,kBAAkB,CAAC;SAC7C,MAAM,CAAC,KAAK,EAAE,IAAwC,EAAE,EAAE;QACzD,MAAM,KAAK,GAAG,WAAW,EAAE,CAAC;QAC5B,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,GAAG,CAAC,IAAI,CAAC,+BAA+B,EAAE,6BAA6B,CAAC,CAAC;YACzE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,SAAS,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QAExE,oDAAoD;QACpD,MAAM,SAAS,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QAC/D,MAAM,YAAY,GAAG,IAAI,CAAC,WAAW,EAAE,IAAI,EAAE,IAAI,KAAK,CAAC,OAAO,EAAE,EAAE,UAAU,CAAC,CAAC;QAC9E,MAAM,WAAW,GAAG,UAAU,CAAC,SAAS,CAAC;YACvC,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,UAAU,CAAC,YAAY,CAAC;gBACxB,CAAC,CAAC,YAAY;gBACd,CAAC,CAAC,IAAI,CAAC;QAEX,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,GAAG,CAAC,IAAI,CAAC,oBAAoB,EAAE,sDAAsD,CAAC,CAAC;YACvF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,+DAA+D;QAC/D,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;QAC/C,MAAM,aAAa,GAAG,IAAI,KAAK,CAAC,WAAW,CAAC,CAAC;QAE7C,IAAI,CAAC;YACH,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;YAE5B,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;gBAChB,MAAM,EAAE,mBAAmB,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;gBAC7D,MAAM,OAAO,GAAG,aAAa,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;gBAC1E,IAAI,MAAM,GAAG,CAAC,CAAC;gBACf,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;oBAC5B,MAAM,mBAAmB,CAAC,KAAK,EAAE,SAAS,CAAC,CAAC;oBAC5C,MAAM,EAAE,CAAC;gBACX,CAAC;gBACD,GAAG,CAAC,IAAI,CACN,YAAY,MAAM,yBAAyB,IAAI,CAAC,MAAM,GAAG,EACzD,GAAG,SAAS,SAAS,CACtB,CAAC;YACJ,CAAC;iBAAM,CAAC;gBACN,MAAM,EAAE,iBAAiB,EAAE,GAAG,MAAM,MAAM,CAAC,cAAc,CAAC,CAAC;gBAC3D,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,aAAa,EAAE,SAAS,CAAC,CAAC;gBACjE,GAAG,CAAC,IAAI,CACN,YAAY,MAAM,CAAC,MAAM,aAAa,MAAM,CAAC,OAAO,aAAa,EACjE,GAAG,SAAS,SAAS,CACtB,CAAC;YACJ,CAAC;QACH,CAAC;gBAAS,CAAC;YACT,aAAa,CAAC,KAAK,EAAE,CAAC;QACxB,CAAC;IACH,CAAC,CAAC,CAAC;AACP,CAAC"}
|
|
@@ -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
|
@@ -23,9 +23,9 @@ describe('hook-packs', () => {
|
|
|
23
23
|
});
|
|
24
24
|
|
|
25
25
|
describe('registry', () => {
|
|
26
|
-
it('should list all
|
|
26
|
+
it('should list all 10 built-in packs', () => {
|
|
27
27
|
const packs = listPacks();
|
|
28
|
-
expect(packs.length).toBe(
|
|
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');
|