@panchr/tyr 0.1.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,181 @@
1
+ import { defineCommand } from "citty";
2
+ import {
3
+ getConfigPath,
4
+ getEnvPath,
5
+ isValidKey,
6
+ parseValue,
7
+ readConfig,
8
+ readEnvFile,
9
+ readRawConfig,
10
+ writeConfig,
11
+ writeEnvVar,
12
+ } from "../config.ts";
13
+ import { type TyrConfig, TyrConfigSchema } from "../types.ts";
14
+
15
+ const show = defineCommand({
16
+ meta: {
17
+ name: "show",
18
+ description: "Display current configuration",
19
+ },
20
+ async run() {
21
+ const config = await readConfig().catch((err) => {
22
+ console.error(
23
+ `Invalid config: ${err instanceof Error ? err.message : err}`,
24
+ );
25
+ return process.exit(1) as never;
26
+ });
27
+ console.log(JSON.stringify(config, null, 2));
28
+ },
29
+ });
30
+
31
+ const set = defineCommand({
32
+ meta: {
33
+ name: "set",
34
+ description:
35
+ "Set a configuration value (e.g. tyr config set failOpen true)",
36
+ },
37
+ args: {
38
+ key: { type: "positional", description: "Config key", required: true },
39
+ value: { type: "positional", description: "Config value", required: true },
40
+ },
41
+ async run({ args }) {
42
+ const key = args.key as string;
43
+ const value = args.value as string;
44
+
45
+ if (!isValidKey(key)) {
46
+ console.error(`Unknown config key: ${key}`);
47
+ process.exit(1);
48
+ return;
49
+ }
50
+
51
+ const parsed = parseValue(key, value);
52
+ if (parsed === null) {
53
+ console.error(`Invalid value for ${key}: ${value}`);
54
+ process.exit(1);
55
+ return;
56
+ }
57
+
58
+ const raw = await readRawConfig().catch((err) => {
59
+ console.error(
60
+ `Cannot read config: ${err instanceof Error ? err.message : err}`,
61
+ );
62
+ return process.exit(1) as never;
63
+ });
64
+ const parts = key.split(".");
65
+ if (parts.length === 2) {
66
+ const group = parts[0] as string;
67
+ const field = parts[1] as string;
68
+ if (!raw[group] || typeof raw[group] !== "object") raw[group] = {};
69
+ (raw[group] as Record<string, unknown>)[field] = parsed;
70
+ } else {
71
+ raw[key] = parsed;
72
+ }
73
+
74
+ let config: TyrConfig;
75
+ try {
76
+ config = TyrConfigSchema.strict().parse(raw);
77
+ } catch (err) {
78
+ console.error(
79
+ `Config would still be invalid after this change: ${err instanceof Error ? err.message : err}`,
80
+ );
81
+ process.exit(1);
82
+ return;
83
+ }
84
+ await writeConfig(config);
85
+ console.log(`Set ${key} = ${String(parsed)}`);
86
+ },
87
+ });
88
+
89
+ const path = defineCommand({
90
+ meta: {
91
+ name: "path",
92
+ description: "Print the config file path",
93
+ },
94
+ run() {
95
+ console.log(getConfigPath());
96
+ },
97
+ });
98
+
99
+ function maskValue(value: string): string {
100
+ if (value.length <= 4) return "****";
101
+ return `${value.slice(0, 4)}...`;
102
+ }
103
+
104
+ const envSet = defineCommand({
105
+ meta: {
106
+ name: "set",
107
+ description:
108
+ "Set an environment variable (e.g. tyr config env set KEY VALUE)",
109
+ },
110
+ args: {
111
+ key: { type: "positional", description: "Variable name", required: true },
112
+ value: {
113
+ type: "positional",
114
+ description: "Variable value",
115
+ required: true,
116
+ },
117
+ },
118
+ run({ args }) {
119
+ const key = args.key as string;
120
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
121
+ console.error(`Invalid variable name: ${key}`);
122
+ process.exit(1);
123
+ return;
124
+ }
125
+ writeEnvVar(key, args.value as string);
126
+ console.log(`Set ${key} in ${getEnvPath()}`);
127
+ },
128
+ });
129
+
130
+ const envShow = defineCommand({
131
+ meta: {
132
+ name: "show",
133
+ description: "Show environment variables (values masked)",
134
+ },
135
+ run() {
136
+ const vars = readEnvFile();
137
+ const entries = Object.entries(vars);
138
+ if (entries.length === 0) {
139
+ console.log("No environment variables set.");
140
+ return;
141
+ }
142
+ for (const [key, value] of entries) {
143
+ console.log(`${key}=${maskValue(value)}`);
144
+ }
145
+ },
146
+ });
147
+
148
+ const envPath = defineCommand({
149
+ meta: {
150
+ name: "path",
151
+ description: "Print the env file path",
152
+ },
153
+ run() {
154
+ console.log(getEnvPath());
155
+ },
156
+ });
157
+
158
+ const env = defineCommand({
159
+ meta: {
160
+ name: "env",
161
+ description: "Manage environment variables in tyr's .env file",
162
+ },
163
+ subCommands: {
164
+ set: envSet,
165
+ show: envShow,
166
+ path: envPath,
167
+ },
168
+ });
169
+
170
+ export default defineCommand({
171
+ meta: {
172
+ name: "config",
173
+ description: "View and manage tyr configuration",
174
+ },
175
+ subCommands: {
176
+ show,
177
+ set,
178
+ path,
179
+ env,
180
+ },
181
+ });
@@ -0,0 +1,66 @@
1
+ import { defineCommand } from "citty";
2
+ import {
3
+ CURRENT_SCHEMA_VERSION,
4
+ getDbPath,
5
+ getSchemaVersion,
6
+ hasMetaTable,
7
+ openRawDb,
8
+ runMigrations,
9
+ } from "../db.ts";
10
+
11
+ const migrate = defineCommand({
12
+ meta: {
13
+ name: "migrate",
14
+ description: "Run pending database schema migrations",
15
+ },
16
+ async run() {
17
+ const dbPath = getDbPath();
18
+ const db = openRawDb(dbPath);
19
+
20
+ if (!hasMetaTable(db)) {
21
+ console.log(
22
+ "Database is uninitialized. Run any tyr command to create the schema.",
23
+ );
24
+ db.close();
25
+ return;
26
+ }
27
+
28
+ const version = getSchemaVersion(db);
29
+ if (version === null) {
30
+ console.error(
31
+ "[tyr] database is missing schema_version. Cannot migrate.",
32
+ );
33
+ db.close();
34
+ process.exit(1);
35
+ }
36
+
37
+ if (version === CURRENT_SCHEMA_VERSION) {
38
+ console.log(
39
+ `Database is already at v${CURRENT_SCHEMA_VERSION}. Nothing to do.`,
40
+ );
41
+ db.close();
42
+ return;
43
+ }
44
+
45
+ console.log(`Database: ${dbPath}`);
46
+ console.log(`Current version: v${version}`);
47
+ console.log(`Target version: v${CURRENT_SCHEMA_VERSION}`);
48
+
49
+ const result = runMigrations(db);
50
+ console.log(
51
+ `Applied ${result.applied} migration(s): v${result.fromVersion} → v${result.toVersion}`,
52
+ );
53
+
54
+ db.close();
55
+ },
56
+ });
57
+
58
+ export default defineCommand({
59
+ meta: {
60
+ name: "db",
61
+ description: "Database management commands",
62
+ },
63
+ subCommands: {
64
+ migrate,
65
+ },
66
+ });
@@ -0,0 +1,34 @@
1
+ import { defineCommand } from "citty";
2
+ import { ClaudeAgent } from "../agents/claude.ts";
3
+
4
+ const claudeConfig = defineCommand({
5
+ meta: {
6
+ name: "claude-config",
7
+ description: "Print the merged Claude Code permission config",
8
+ },
9
+ args: {
10
+ cwd: {
11
+ type: "string",
12
+ description: "Project directory (defaults to current directory)",
13
+ },
14
+ },
15
+ async run({ args }) {
16
+ const cwd = (args.cwd as string | undefined) ?? process.cwd();
17
+ const agent = new ClaudeAgent();
18
+ await agent.init(cwd);
19
+ const info = agent.getDebugInfo();
20
+ agent.close();
21
+
22
+ console.log(JSON.stringify(info, null, 2));
23
+ },
24
+ });
25
+
26
+ export default defineCommand({
27
+ meta: {
28
+ name: "debug",
29
+ description: "Debugging and diagnostic tools",
30
+ },
31
+ subCommands: {
32
+ "claude-config": claudeConfig,
33
+ },
34
+ });
@@ -0,0 +1,77 @@
1
+ import { defineCommand } from "citty";
2
+ import { rejectUnknownArgs } from "../args.ts";
3
+ import type { JudgeMode } from "../install.ts";
4
+ import {
5
+ getSettingsPath,
6
+ isInstalled,
7
+ mergeHook,
8
+ readSettings,
9
+ writeSettings,
10
+ } from "../install.ts";
11
+
12
+ const installArgs = {
13
+ global: {
14
+ type: "boolean" as const,
15
+ description: "Write to ~/.claude/settings.json",
16
+ },
17
+ project: {
18
+ type: "boolean" as const,
19
+ description: "Write to .claude/settings.json (default)",
20
+ },
21
+ "dry-run": {
22
+ type: "boolean" as const,
23
+ description: "Print what would be written without modifying anything",
24
+ },
25
+ shadow: {
26
+ type: "boolean" as const,
27
+ description: "Install in shadow mode (run pipeline but always abstain)",
28
+ },
29
+ audit: {
30
+ type: "boolean" as const,
31
+ description:
32
+ "Install in audit mode (log requests without running pipeline)",
33
+ },
34
+ };
35
+
36
+ export default defineCommand({
37
+ meta: {
38
+ name: "install",
39
+ description: "Register tyr as a Claude Code hook",
40
+ },
41
+ args: installArgs,
42
+ async run({ args, rawArgs }) {
43
+ rejectUnknownArgs(rawArgs, installArgs);
44
+
45
+ if (args.shadow && args.audit) {
46
+ console.error("--shadow and --audit are mutually exclusive");
47
+ process.exit(1);
48
+ return;
49
+ }
50
+
51
+ const scope = args.global ? "global" : "project";
52
+ const dryRun = args["dry-run"] ?? false;
53
+ const mode: JudgeMode = args.shadow
54
+ ? "shadow"
55
+ : args.audit
56
+ ? "audit"
57
+ : undefined;
58
+ const settingsPath = getSettingsPath(scope);
59
+
60
+ const settings = await readSettings(settingsPath);
61
+ const alreadyInstalled = isInstalled(settings);
62
+ const updated = mergeHook(settings, mode);
63
+
64
+ if (dryRun) {
65
+ console.log(`Would write to ${settingsPath}:\n`);
66
+ console.log(JSON.stringify(updated, null, 2));
67
+ return;
68
+ }
69
+
70
+ await writeSettings(settingsPath, updated);
71
+ if (alreadyInstalled) {
72
+ console.log(`Updated tyr hook in ${settingsPath}`);
73
+ } else {
74
+ console.log(`Installed tyr hook in ${settingsPath}`);
75
+ }
76
+ },
77
+ });