@panchr/tyr 0.2.0 → 0.2.2
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/README.md +22 -127
- package/package.json +1 -1
- package/src/commands/config.ts +48 -0
- package/src/commands/db.ts +58 -0
- package/src/commands/debug.ts +2 -1
- package/src/commands/log.ts +2 -1
- package/src/commands/suggest.ts +30 -18
- package/src/index.ts +0 -0
- package/src/install.ts +2 -1
- package/src/repo.ts +24 -0
package/README.md
CHANGED
|
@@ -54,37 +54,15 @@ tyr uninstall # project
|
|
|
54
54
|
tyr uninstall --global # global
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
Use `--dry-run` with either command to preview changes without modifying anything.
|
|
57
|
+
Use `--dry-run` with either command to preview changes without modifying anything. Run `tyr --help` for the full command reference.
|
|
58
58
|
|
|
59
|
-
##
|
|
60
|
-
|
|
61
|
-
### Commands
|
|
62
|
-
|
|
63
|
-
```
|
|
64
|
-
tyr install [--global] [--project] [--dry-run] [--shadow|--audit]
|
|
65
|
-
tyr uninstall [--global] [--project] [--dry-run]
|
|
66
|
-
tyr config show
|
|
67
|
-
tyr config set <key> <value>
|
|
68
|
-
tyr config path
|
|
69
|
-
tyr config env set <key> <value>
|
|
70
|
-
tyr config env show
|
|
71
|
-
tyr config env path
|
|
72
|
-
tyr log [--last N] [--json] [--since T] [--until T] [--decision D] [--provider P] [--cwd C] [--verbose]
|
|
73
|
-
tyr log clear
|
|
74
|
-
tyr db migrate
|
|
75
|
-
tyr stats [--since T] [--json]
|
|
76
|
-
tyr suggest [--global|--project] [--min-count N] [--all]
|
|
77
|
-
tyr debug claude-config [--cwd C]
|
|
78
|
-
tyr version
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
### Configuration
|
|
59
|
+
## Configuration
|
|
82
60
|
|
|
83
61
|
Tyr reads its own config from `~/.config/tyr/config.json` (overridable via `TYR_CONFIG_FILE`). The config file supports JSON with comments (JSONC).
|
|
84
62
|
|
|
85
63
|
| Key | Type | Default | Description |
|
|
86
64
|
|-----|------|---------|-------------|
|
|
87
|
-
| `providers` | string[] | `["chained-commands"
|
|
65
|
+
| `providers` | string[] | `["chained-commands"]` | Ordered list of providers to run |
|
|
88
66
|
| `failOpen` | boolean | `false` | Approve on error instead of failing closed |
|
|
89
67
|
| `claude.model` | string | `"haiku"` | Model identifier for the Claude CLI |
|
|
90
68
|
| `claude.timeout` | number | `10` | Claude request timeout in seconds |
|
|
@@ -97,40 +75,25 @@ Tyr reads its own config from `~/.config/tyr/config.json` (overridable via `TYR_
|
|
|
97
75
|
| `verboseLog` | boolean | `false` | Include LLM prompt/params in log entries |
|
|
98
76
|
| `logRetention` | string | `"30d"` | Auto-prune logs older than this (`"0"` to disable) |
|
|
99
77
|
|
|
100
|
-
|
|
78
|
+
Use `tyr config show` to view the current config, `tyr config set <key> <value>` to update a value, `tyr config example` to print a recommended starter config, and `tyr config schema` to print the JSON Schema.
|
|
101
79
|
|
|
102
80
|
### Environment variables
|
|
103
81
|
|
|
104
|
-
Tyr loads environment variables from `~/.config/tyr/.env` (next to the config file). This is the recommended place to store API keys.
|
|
105
|
-
|
|
106
|
-
```bash
|
|
107
|
-
# Store your OpenRouter API key
|
|
108
|
-
tyr config env set OPENROUTER_API_KEY sk-or-...
|
|
109
|
-
|
|
110
|
-
# View stored variables (values masked)
|
|
111
|
-
tyr config env show
|
|
112
|
-
|
|
113
|
-
# Print .env file path
|
|
114
|
-
tyr config env path
|
|
115
|
-
```
|
|
116
|
-
|
|
117
|
-
Existing process environment variables take precedence over `.env` values.
|
|
82
|
+
Tyr loads environment variables from `~/.config/tyr/.env` (next to the config file). This is the recommended place to store API keys (e.g., `OPENROUTER_API_KEY`). Use `tyr config env set <key> <value>` to manage them. Existing process environment variables take precedence.
|
|
118
83
|
|
|
119
|
-
|
|
84
|
+
## Providers
|
|
120
85
|
|
|
121
86
|
Tyr uses a **pipeline architecture** where providers are evaluated in sequence. The first provider to return a definitive `allow` or `deny` wins --- remaining providers are skipped. If all providers `abstain`, the request falls through to Claude Code's default behavior (prompting the user), unless `failOpen` is `true`, in which case the request is approved.
|
|
122
87
|
|
|
123
88
|
Configure the pipeline via the `providers` array. **Order matters** -- providers run in order.
|
|
124
89
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
#### `cache`
|
|
90
|
+
### `cache`
|
|
128
91
|
|
|
129
92
|
Caches prior decisions in SQLite. If the same command was previously allowed or denied (with the same config and permission rules), returns the cached result immediately. The cache auto-invalidates when your config or Claude Code permission rules change.
|
|
130
93
|
|
|
131
94
|
**Best practice:** Place first in the pipeline to skip expensive downstream evaluations.
|
|
132
95
|
|
|
133
|
-
|
|
96
|
+
### `chained-commands`
|
|
134
97
|
|
|
135
98
|
Parses compound shell commands (`&&`, `||`, `|`, `;`, subshells, command substitution) and checks each sub-command against your Claude Code allow/deny permission patterns.
|
|
136
99
|
|
|
@@ -138,7 +101,7 @@ Parses compound shell commands (`&&`, `||`, `|`, `;`, subshells, command substit
|
|
|
138
101
|
- **Deny:** _Any_ sub-command matches a deny pattern
|
|
139
102
|
- **Abstain:** Any sub-command has no matching pattern
|
|
140
103
|
|
|
141
|
-
|
|
104
|
+
### `claude`
|
|
142
105
|
|
|
143
106
|
Sends ambiguous commands to the local Claude CLI for semantic evaluation. The LLM sees your permission rules, the command, and the working directory, then reasons about whether the command is safe.
|
|
144
107
|
|
|
@@ -146,19 +109,13 @@ When `claude.canDeny` is `false` (the default), the LLM can only approve command
|
|
|
146
109
|
|
|
147
110
|
When `conversationContext` is enabled, the LLM also sees recent conversation messages from the Claude Code session. This lets it allow commands that don't match any configured pattern if the user clearly requested the action and it's a typical, safe development command. The deny list is always checked first -- no amount of context overrides a denied pattern.
|
|
148
111
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
The main benefit of this provider is that it reuses the authentication that `claude` is already configured with, whether that's an Anthropic API key or a subscription.
|
|
152
|
-
|
|
153
|
-
#### `openrouter`
|
|
112
|
+
Note: this provider adds ~5 seconds of latency per evaluation due to the subprocess overhead, but this is still faster than a human reviewing and approving a command. It also reuses whatever authentication `claude` is already configured with.
|
|
154
113
|
|
|
155
|
-
|
|
114
|
+
### `openrouter`
|
|
156
115
|
|
|
157
|
-
|
|
116
|
+
Same semantics as the `claude` provider but uses the OpenRouter HTTP API instead of the local CLI. Supports `conversationContext` in the same way. Requires `OPENROUTER_API_KEY`.
|
|
158
117
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
#### Pipeline examples
|
|
118
|
+
### Pipeline examples
|
|
162
119
|
|
|
163
120
|
```jsonc
|
|
164
121
|
// Safe & fast (default) -- pattern matching only
|
|
@@ -174,82 +131,20 @@ Only evaluates `Bash` tool requests; abstains on all other tools.
|
|
|
174
131
|
{ "providers": ["cache", "chained-commands", "openrouter"] }
|
|
175
132
|
```
|
|
176
133
|
|
|
177
|
-
###
|
|
178
|
-
|
|
179
|
-
Every permission decision is logged to a SQLite database at `~/.local/share/tyr/tyr.db` (overridable via `TYR_DB_PATH`).
|
|
180
|
-
|
|
181
|
-
```bash
|
|
182
|
-
# View recent decisions (default: last 20)
|
|
183
|
-
tyr log
|
|
184
|
-
|
|
185
|
-
# Show more entries
|
|
186
|
-
tyr log --last 50
|
|
187
|
-
|
|
188
|
-
# Filter by decision type
|
|
189
|
-
tyr log --decision allow
|
|
190
|
-
tyr log --decision deny
|
|
191
|
-
|
|
192
|
-
# Filter by time range (ISO or relative: 1h, 30m, 7d)
|
|
193
|
-
tyr log --since 1h
|
|
194
|
-
tyr log --since 2025-01-01 --until 2025-01-31
|
|
195
|
-
|
|
196
|
-
# Filter by provider or working directory
|
|
197
|
-
tyr log --provider chained-commands
|
|
198
|
-
tyr log --cwd /path/to/project
|
|
199
|
-
|
|
200
|
-
# JSON output
|
|
201
|
-
tyr log --json
|
|
202
|
-
|
|
203
|
-
# Show LLM prompts for verbose-logged entries
|
|
204
|
-
tyr log --verbose
|
|
205
|
-
|
|
206
|
-
# Clear all logs
|
|
207
|
-
tyr log clear
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Log entries are automatically pruned based on the `logRetention` config setting (default: 30 days).
|
|
211
|
-
|
|
212
|
-
### Statistics
|
|
213
|
-
|
|
214
|
-
```bash
|
|
215
|
-
# View overall stats
|
|
216
|
-
tyr stats
|
|
217
|
-
|
|
218
|
-
# Stats for the last 7 days
|
|
219
|
-
tyr stats --since 7d
|
|
220
|
-
|
|
221
|
-
# Machine-readable JSON
|
|
222
|
-
tyr stats --json
|
|
223
|
-
```
|
|
224
|
-
|
|
225
|
-
Shows: total checks, decision breakdown (allow/deny/abstain/error), cache hit rate, provider distribution, and auto-approval count.
|
|
134
|
+
### Permission prompt delay
|
|
226
135
|
|
|
227
|
-
|
|
136
|
+
When tyr is installed as a hook, Claude Code shows the permission prompt and calls the hook concurrently. You'll see the prompt appear immediately while tyr evaluates the command in the background. If the hook decides to allow or deny, the prompt is automatically resolved; if the hook abstains, the prompt remains for you to decide manually.
|
|
228
137
|
|
|
229
|
-
|
|
138
|
+
## Database management
|
|
230
139
|
|
|
231
|
-
|
|
232
|
-
# Start an interactive session with suggested rules (commands approved >= 5 times)
|
|
233
|
-
tyr suggest
|
|
234
|
-
|
|
235
|
-
# Lower the threshold for which commands are surfaced
|
|
236
|
-
tyr suggest --min-count 3
|
|
140
|
+
Tyr stores all decision logs and cached results in a SQLite database at `~/.local/share/tyr/tyr.db` (overridable via `TYR_DB_PATH`).
|
|
237
141
|
|
|
238
|
-
|
|
239
|
-
|
|
142
|
+
| Command | Description |
|
|
143
|
+
|---------|-------------|
|
|
144
|
+
| `tyr db migrate` | Run pending schema migrations after upgrading tyr |
|
|
145
|
+
| `tyr db rename <old> <new>` | Update stored project paths after moving a directory |
|
|
240
146
|
|
|
241
|
-
|
|
242
|
-
tyr suggest --all
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
### Database migrations
|
|
246
|
-
|
|
247
|
-
When upgrading from one `tyr` version to another, run
|
|
248
|
-
|
|
249
|
-
```bash
|
|
250
|
-
# Run pending schema migrations
|
|
251
|
-
tyr db migrate
|
|
252
|
-
```
|
|
147
|
+
`tyr db rename` is useful when you relocate a project on disk. It rewrites the `cwd` column in both the logs and cache tables (including subpaths) so that `tyr log`, `tyr stats`, and cache lookups continue to work for the moved project.
|
|
253
148
|
|
|
254
149
|
## Development
|
|
255
150
|
|
package/package.json
CHANGED
package/src/commands/config.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
|
+
import { z } from "zod/v4";
|
|
2
3
|
import {
|
|
3
4
|
getConfigPath,
|
|
4
5
|
getEnvPath,
|
|
@@ -167,6 +168,51 @@ const env = defineCommand({
|
|
|
167
168
|
},
|
|
168
169
|
});
|
|
169
170
|
|
|
171
|
+
const EXAMPLE_CONFIG = `{
|
|
172
|
+
// Pattern matching with caching for fast repeated evaluations.
|
|
173
|
+
// Add "claude" or "openrouter" after "chained-commands" to use an
|
|
174
|
+
// LLM for commands that don't match any allow/deny pattern.
|
|
175
|
+
"providers": ["cache", "chained-commands"],
|
|
176
|
+
|
|
177
|
+
// Fail closed: deny on error rather than auto-approving.
|
|
178
|
+
"failOpen": false,
|
|
179
|
+
|
|
180
|
+
// Auto-prune log entries older than 30 days.
|
|
181
|
+
"logRetention": "30d"
|
|
182
|
+
}
|
|
183
|
+
`;
|
|
184
|
+
|
|
185
|
+
const example = defineCommand({
|
|
186
|
+
meta: {
|
|
187
|
+
name: "example",
|
|
188
|
+
description: "Print a recommended example config",
|
|
189
|
+
},
|
|
190
|
+
run() {
|
|
191
|
+
process.stdout.write(EXAMPLE_CONFIG);
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const schema = defineCommand({
|
|
196
|
+
meta: {
|
|
197
|
+
name: "schema",
|
|
198
|
+
description: "Print the config JSON Schema",
|
|
199
|
+
},
|
|
200
|
+
run() {
|
|
201
|
+
const jsonSchema = z.toJSONSchema(TyrConfigSchema, {
|
|
202
|
+
target: "draft-2020-12",
|
|
203
|
+
});
|
|
204
|
+
// z.toJSONSchema cannot represent .refine() constraints;
|
|
205
|
+
// manually add the pattern for logRetention.
|
|
206
|
+
const props = (jsonSchema as { properties?: Record<string, object> })
|
|
207
|
+
.properties;
|
|
208
|
+
if (props?.logRetention) {
|
|
209
|
+
(props.logRetention as Record<string, unknown>).pattern =
|
|
210
|
+
"^(0|\\d+[smhd])$";
|
|
211
|
+
}
|
|
212
|
+
console.log(JSON.stringify(jsonSchema, null, 2));
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
|
|
170
216
|
export default defineCommand({
|
|
171
217
|
meta: {
|
|
172
218
|
name: "config",
|
|
@@ -176,6 +222,8 @@ export default defineCommand({
|
|
|
176
222
|
show,
|
|
177
223
|
set,
|
|
178
224
|
path,
|
|
225
|
+
example,
|
|
226
|
+
schema,
|
|
179
227
|
env,
|
|
180
228
|
},
|
|
181
229
|
});
|
package/src/commands/db.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { resolve } from "node:path";
|
|
1
2
|
import { defineCommand } from "citty";
|
|
2
3
|
import {
|
|
3
4
|
CURRENT_SCHEMA_VERSION,
|
|
@@ -55,6 +56,62 @@ const migrate = defineCommand({
|
|
|
55
56
|
},
|
|
56
57
|
});
|
|
57
58
|
|
|
59
|
+
const rename = defineCommand({
|
|
60
|
+
meta: {
|
|
61
|
+
name: "rename",
|
|
62
|
+
description:
|
|
63
|
+
"Rename a project directory in the database (e.g. after moving a project)",
|
|
64
|
+
},
|
|
65
|
+
args: {
|
|
66
|
+
oldPath: {
|
|
67
|
+
type: "positional",
|
|
68
|
+
description: "Current project directory path",
|
|
69
|
+
required: true,
|
|
70
|
+
},
|
|
71
|
+
newPath: {
|
|
72
|
+
type: "positional",
|
|
73
|
+
description: "New project directory path",
|
|
74
|
+
required: true,
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
async run({ args }) {
|
|
78
|
+
const oldPath = resolve(args.oldPath as string);
|
|
79
|
+
const newPath = resolve(args.newPath as string);
|
|
80
|
+
|
|
81
|
+
if (oldPath === newPath) {
|
|
82
|
+
console.error("Old and new paths are the same.");
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const db = openRawDb();
|
|
87
|
+
|
|
88
|
+
if (!hasMetaTable(db)) {
|
|
89
|
+
console.error("Database is uninitialized. Nothing to rename.");
|
|
90
|
+
db.close();
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const escapedOld = oldPath.replace(/[\\%_]/g, "\\$&");
|
|
95
|
+
|
|
96
|
+
const updated = db.transaction(() => {
|
|
97
|
+
let total = 0;
|
|
98
|
+
for (const table of ["logs", "cache"] as const) {
|
|
99
|
+
const result = db
|
|
100
|
+
.query(
|
|
101
|
+
`UPDATE ${table} SET cwd = ? || substr(cwd, length(?) + 1)
|
|
102
|
+
WHERE cwd = ? OR cwd LIKE ? || '/%' ESCAPE '\\'`,
|
|
103
|
+
)
|
|
104
|
+
.run(newPath, oldPath, oldPath, escapedOld);
|
|
105
|
+
total += result.changes;
|
|
106
|
+
}
|
|
107
|
+
return total;
|
|
108
|
+
})();
|
|
109
|
+
|
|
110
|
+
console.log(`Renamed ${oldPath} → ${newPath} (${updated} row(s) updated)`);
|
|
111
|
+
db.close();
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
|
|
58
115
|
export default defineCommand({
|
|
59
116
|
meta: {
|
|
60
117
|
name: "db",
|
|
@@ -62,5 +119,6 @@ export default defineCommand({
|
|
|
62
119
|
},
|
|
63
120
|
subCommands: {
|
|
64
121
|
migrate,
|
|
122
|
+
rename,
|
|
65
123
|
},
|
|
66
124
|
});
|
package/src/commands/debug.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { defineCommand } from "citty";
|
|
2
2
|
import { ClaudeAgent } from "../agents/claude.ts";
|
|
3
|
+
import { getRepoRoot } from "../repo.ts";
|
|
3
4
|
|
|
4
5
|
const claudeConfig = defineCommand({
|
|
5
6
|
meta: {
|
|
@@ -13,7 +14,7 @@ const claudeConfig = defineCommand({
|
|
|
13
14
|
},
|
|
14
15
|
},
|
|
15
16
|
async run({ args }) {
|
|
16
|
-
const cwd = (args.cwd as string | undefined) ??
|
|
17
|
+
const cwd = (args.cwd as string | undefined) ?? getRepoRoot();
|
|
17
18
|
const agent = new ClaudeAgent();
|
|
18
19
|
await agent.init(cwd);
|
|
19
20
|
const info = agent.getDebugInfo();
|
package/src/commands/log.ts
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
readLogEntries,
|
|
11
11
|
truncateOldLogs,
|
|
12
12
|
} from "../log.ts";
|
|
13
|
+
import { getRepoRoot } from "../repo.ts";
|
|
13
14
|
|
|
14
15
|
function formatTime(ts: number): string {
|
|
15
16
|
const d = new Date(ts);
|
|
@@ -145,7 +146,7 @@ export default defineCommand({
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
// Default to current directory unless --all or --cwd is provided
|
|
148
|
-
const cwdFilter = args.all ? undefined : (args.cwd ??
|
|
149
|
+
const cwdFilter = args.all ? undefined : (args.cwd ?? getRepoRoot());
|
|
149
150
|
|
|
150
151
|
const entries = readLogEntries({
|
|
151
152
|
last: last > 0 ? last : undefined,
|
package/src/commands/suggest.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
1
2
|
import { homedir } from "node:os";
|
|
2
3
|
import { join } from "node:path";
|
|
3
4
|
import { defineCommand } from "citty";
|
|
@@ -9,6 +10,7 @@ import {
|
|
|
9
10
|
import { rejectUnknownArgs } from "../args.ts";
|
|
10
11
|
import { closeDb, getDb } from "../db.ts";
|
|
11
12
|
import { readSettings } from "../install.ts";
|
|
13
|
+
import { getRepoRoot } from "../repo.ts";
|
|
12
14
|
|
|
13
15
|
const suggestArgs = {
|
|
14
16
|
global: {
|
|
@@ -92,35 +94,43 @@ export function getSuggestions(
|
|
|
92
94
|
return suggestions;
|
|
93
95
|
}
|
|
94
96
|
|
|
95
|
-
function
|
|
97
|
+
export function buildSuggestPrompt(
|
|
96
98
|
suggestions: Suggestion[],
|
|
97
|
-
|
|
99
|
+
targetPath: string,
|
|
100
|
+
allPaths: string[],
|
|
98
101
|
): string {
|
|
99
102
|
const commandList = suggestions
|
|
100
103
|
.map((s) => `- \`${s.command}\` (approved ${s.count} times)`)
|
|
101
104
|
.join("\n");
|
|
102
105
|
|
|
103
|
-
|
|
106
|
+
const pathList = allPaths.map((p) => `- \`${p}\``).join("\n");
|
|
104
107
|
|
|
105
|
-
|
|
108
|
+
return `I've been manually approving shell commands while using Claude Code. Tyr has identified frequently-approved commands that could be added as permanent allow rules.
|
|
106
109
|
|
|
107
110
|
## Frequently Approved Commands (not yet in allow rules)
|
|
108
111
|
|
|
109
112
|
${commandList}
|
|
110
113
|
|
|
111
|
-
## Settings
|
|
112
|
-
|
|
114
|
+
## Settings Files
|
|
115
|
+
|
|
116
|
+
Claude Code reads permissions from multiple settings files:
|
|
117
|
+
|
|
118
|
+
${pathList}
|
|
119
|
+
|
|
120
|
+
## Target Settings File
|
|
121
|
+
- Write new rules to: \`${targetPath}\`
|
|
113
122
|
- Format: JSON with a \`permissions.allow\` array of strings
|
|
114
|
-
- Each rule
|
|
123
|
+
- Each rule MUST use the exact format \`Bash(pattern)\` where \`pattern\` can use \`*\` as a glob wildcard
|
|
115
124
|
- Example: \`Bash(bun *)\` allows any command starting with \`bun \`
|
|
125
|
+
- IMPORTANT: Before writing rules, read the settings files listed above (those that exist) to understand existing permissions, then merge new rules into the target file's \`permissions.allow\` array
|
|
116
126
|
|
|
117
|
-
##
|
|
118
|
-
Help
|
|
127
|
+
## Instructions
|
|
128
|
+
Help me decide which commands to add as allow rules:
|
|
119
129
|
1. Suggest generalized glob patterns that group similar commands (e.g., "bun test" and "bun lint" → "Bash(bun *)")
|
|
120
130
|
2. Explain what each pattern would match
|
|
121
|
-
3. When
|
|
131
|
+
3. When I'm ready, write the rules to the target settings file
|
|
122
132
|
|
|
123
|
-
Be concise. Start by presenting your suggested rules and ask if
|
|
133
|
+
Be concise. Start by presenting your suggested rules and ask if I want to adjust them.`;
|
|
124
134
|
}
|
|
125
135
|
|
|
126
136
|
export default defineCommand({
|
|
@@ -146,7 +156,8 @@ export default defineCommand({
|
|
|
146
156
|
return;
|
|
147
157
|
}
|
|
148
158
|
|
|
149
|
-
const
|
|
159
|
+
const repoRoot = getRepoRoot();
|
|
160
|
+
const allPaths = settingsPaths(repoRoot);
|
|
150
161
|
const allowPatterns: string[] = [];
|
|
151
162
|
for (const path of allPaths) {
|
|
152
163
|
const settings = await readSettings(path);
|
|
@@ -156,7 +167,7 @@ export default defineCommand({
|
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
169
|
|
|
159
|
-
const cwdFilter = args.all ? undefined :
|
|
170
|
+
const cwdFilter = args.all ? undefined : repoRoot;
|
|
160
171
|
const suggestions = getSuggestions(minCount, allowPatterns, cwdFilter);
|
|
161
172
|
closeDb();
|
|
162
173
|
|
|
@@ -165,17 +176,18 @@ export default defineCommand({
|
|
|
165
176
|
return;
|
|
166
177
|
}
|
|
167
178
|
|
|
168
|
-
const scope: "global" | "project" = args.
|
|
179
|
+
const scope: "global" | "project" = args.global ? "global" : "project";
|
|
169
180
|
const configDir =
|
|
170
181
|
process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude");
|
|
171
|
-
const
|
|
182
|
+
const targetPath =
|
|
172
183
|
scope === "global"
|
|
173
184
|
? join(configDir, "settings.json")
|
|
174
|
-
: join(
|
|
185
|
+
: join(repoRoot, ".claude", "settings.json");
|
|
175
186
|
|
|
176
|
-
const
|
|
187
|
+
const existingPaths = allPaths.filter((p) => existsSync(p));
|
|
188
|
+
const prompt = buildSuggestPrompt(suggestions, targetPath, existingPaths);
|
|
177
189
|
|
|
178
|
-
const proc = Bun.spawn(["claude",
|
|
190
|
+
const proc = Bun.spawn(["claude", prompt], {
|
|
179
191
|
stdin: "inherit",
|
|
180
192
|
stdout: "inherit",
|
|
181
193
|
stderr: "inherit",
|
package/src/index.ts
CHANGED
|
File without changes
|
package/src/install.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
|
+
import { getRepoRoot } from "./repo.ts";
|
|
4
5
|
|
|
5
6
|
export type JudgeMode = "shadow" | "audit" | undefined;
|
|
6
7
|
|
|
@@ -20,7 +21,7 @@ export function getSettingsPath(scope: "global" | "project"): string {
|
|
|
20
21
|
if (scope === "global") {
|
|
21
22
|
return join(homedir(), ".claude", "settings.json");
|
|
22
23
|
}
|
|
23
|
-
return join(
|
|
24
|
+
return join(getRepoRoot(), ".claude", "settings.json");
|
|
24
25
|
}
|
|
25
26
|
|
|
26
27
|
/** Read and parse a settings.json, returning {} if it doesn't exist. */
|
package/src/repo.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Walk up from `startDir` looking for a `.git` directory.
|
|
6
|
+
* Returns the repo root or `null` if not inside a git repository.
|
|
7
|
+
*/
|
|
8
|
+
export function findRepoRoot(startDir: string): string | null {
|
|
9
|
+
let dir = resolve(startDir);
|
|
10
|
+
for (;;) {
|
|
11
|
+
if (existsSync(join(dir, ".git"))) return dir;
|
|
12
|
+
const parent = dirname(dir);
|
|
13
|
+
if (parent === dir) return null;
|
|
14
|
+
dir = parent;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returns the repo root for the current directory, falling back to
|
|
20
|
+
* `process.cwd()` when not inside a git repository.
|
|
21
|
+
*/
|
|
22
|
+
export function getRepoRoot(): string {
|
|
23
|
+
return findRepoRoot(process.cwd()) ?? process.cwd();
|
|
24
|
+
}
|