tau-all-agent 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.
Files changed (95) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/extensions/answer.ts +601 -0
  4. package/extensions/branch-term.ts +405 -0
  5. package/extensions/btw.ts +444 -0
  6. package/extensions/ghostty.ts +301 -0
  7. package/extensions/git-diff-stats.ts +277 -0
  8. package/extensions/git-pr-status.ts +286 -0
  9. package/extensions/insights.ts +2367 -0
  10. package/extensions/interlude.ts +144 -0
  11. package/extensions/loop.ts +529 -0
  12. package/extensions/memory.ts +1889 -0
  13. package/extensions/notify.ts +161 -0
  14. package/extensions/openai-fast.ts +227 -0
  15. package/extensions/openai-verbosity.ts +223 -0
  16. package/extensions/review.ts +4347 -0
  17. package/extensions/sandbox/index.ts +2578 -0
  18. package/extensions/telegram/README.md +81 -0
  19. package/extensions/telegram/daemon.mjs +1942 -0
  20. package/extensions/telegram/index.ts +1073 -0
  21. package/extensions/telegram/message-text.d.mts +1 -0
  22. package/extensions/telegram/message-text.mjs +16 -0
  23. package/extensions/usage/anthropic.ts +198 -0
  24. package/extensions/usage/github-copilot.ts +204 -0
  25. package/extensions/usage/google-gemini-cli.ts +232 -0
  26. package/extensions/usage/index.ts +2310 -0
  27. package/extensions/usage/minimax.ts +208 -0
  28. package/extensions/usage/openai-codex.ts +180 -0
  29. package/extensions/usage/openrouter.ts +168 -0
  30. package/extensions/usage/providers.ts +26 -0
  31. package/extensions/usage/shared.ts +156 -0
  32. package/extensions/usage/types.ts +53 -0
  33. package/extensions/usage/zai.ts +186 -0
  34. package/extensions/websearch/README.md +68 -0
  35. package/extensions/websearch/browser/chromium.ts +330 -0
  36. package/extensions/websearch/browser/discovery.ts +75 -0
  37. package/extensions/websearch/browser/firefox.ts +150 -0
  38. package/extensions/websearch/browser/sqlite.ts +38 -0
  39. package/extensions/websearch/config.ts +76 -0
  40. package/extensions/websearch/index.ts +312 -0
  41. package/extensions/websearch/normalize.ts +155 -0
  42. package/extensions/websearch/providers/anthropic.pi.ts +139 -0
  43. package/extensions/websearch/providers/gemini.browser.ts +185 -0
  44. package/extensions/websearch/providers/gemini.pi.ts +108 -0
  45. package/extensions/websearch/providers/openai-codex.browser.ts +77 -0
  46. package/extensions/websearch/providers/openai-codex.pi.ts +49 -0
  47. package/extensions/websearch/providers/openai-codex.shared.ts +123 -0
  48. package/extensions/websearch/providers/pi-model.shared.ts +86 -0
  49. package/extensions/websearch/providers/search-prompt.shared.ts +17 -0
  50. package/extensions/websearch/providers/shared.ts +76 -0
  51. package/extensions/websearch/types.ts +52 -0
  52. package/extensions/worktree.ts +2223 -0
  53. package/package.json +76 -0
  54. package/skills/browser-tools/SKILL.md +253 -0
  55. package/skills/browser-tools/scripts/browser-content.js +100 -0
  56. package/skills/browser-tools/scripts/browser-cookies.js +33 -0
  57. package/skills/browser-tools/scripts/browser-dismiss-cookies.js +455 -0
  58. package/skills/browser-tools/scripts/browser-eval.js +75 -0
  59. package/skills/browser-tools/scripts/browser-logs-tail.js +89 -0
  60. package/skills/browser-tools/scripts/browser-nav.js +28 -0
  61. package/skills/browser-tools/scripts/browser-net-summary.js +115 -0
  62. package/skills/browser-tools/scripts/browser-pick.js +133 -0
  63. package/skills/browser-tools/scripts/browser-screenshot.js +18 -0
  64. package/skills/browser-tools/scripts/browser-start.js +281 -0
  65. package/skills/browser-tools/scripts/browser-watch.js +321 -0
  66. package/skills/browser-tools/scripts/utils.js +60 -0
  67. package/skills/git-clean-history/SKILL.md +45 -0
  68. package/skills/git-commit/SKILL.md +55 -0
  69. package/skills/homeassistant-ops/SKILL.md +184 -0
  70. package/skills/homeassistant-ops/references/api.md +64 -0
  71. package/skills/homeassistant-ops/references/id_conventions.md +71 -0
  72. package/skills/homeassistant-ops/references/playbook.md +150 -0
  73. package/skills/homeassistant-ops/scripts/ha_ops.js +2942 -0
  74. package/skills/openscad/SKILL.md +149 -0
  75. package/skills/openscad/examples/parametric_box.scad +92 -0
  76. package/skills/openscad/examples/phone_stand.scad +95 -0
  77. package/skills/openscad/scripts/common.sh +49 -0
  78. package/skills/openscad/scripts/export-stl.sh +56 -0
  79. package/skills/openscad/scripts/extract-params.sh +147 -0
  80. package/skills/openscad/scripts/multi-preview.sh +68 -0
  81. package/skills/openscad/scripts/preview.sh +74 -0
  82. package/skills/openscad/scripts/render-with-params.sh +91 -0
  83. package/skills/openscad/scripts/validate.sh +46 -0
  84. package/skills/oracle/SKILL.md +64 -0
  85. package/skills/oracle/scripts/oracle +249 -0
  86. package/skills/oracle/scripts/oracle-bundle +227 -0
  87. package/skills/sentry/SKILL.md +198 -0
  88. package/skills/sentry/lib/auth.js +99 -0
  89. package/skills/sentry/scripts/fetch-event.js +325 -0
  90. package/skills/sentry/scripts/fetch-issue.js +368 -0
  91. package/skills/sentry/scripts/list-issues.js +245 -0
  92. package/skills/sentry/scripts/search-events.js +291 -0
  93. package/skills/sentry/scripts/search-logs.js +234 -0
  94. package/skills/update-changelog/SKILL.md +135 -0
  95. package/skills/web-design/SKILL.md +117 -0
@@ -0,0 +1,74 @@
1
+ #!/bin/bash
2
+ # Generate a preview PNG from an OpenSCAD file
3
+ # Usage: preview.sh input.scad output.png [options]
4
+ #
5
+ # Options:
6
+ # --camera=x,y,z,rx,ry,rz,dist Camera position
7
+ # --size=WxH Image size (default: 800x600)
8
+ # -D 'var=value' Set parameter value
9
+
10
+ set -e
11
+
12
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
13
+ source "$SCRIPT_DIR/common.sh"
14
+
15
+ check_openscad
16
+
17
+ if [ $# -lt 2 ]; then
18
+ echo "Usage: $0 input.scad output.png [--camera=...] [--size=WxH] [-D 'var=val']"
19
+ echo ""
20
+ echo "Camera format: x,y,z,rotx,roty,rotz,distance"
21
+ echo "Common cameras:"
22
+ echo " Isometric: --camera=0,0,0,55,0,25,200"
23
+ echo " Front: --camera=0,0,0,90,0,0,200"
24
+ echo " Top: --camera=0,0,0,0,0,0,200"
25
+ exit 1
26
+ fi
27
+
28
+ INPUT="$1"
29
+ OUTPUT="$2"
30
+ shift 2
31
+
32
+ # Defaults
33
+ CAMERA="0,0,0,55,0,25,0"
34
+ SIZE="800,600"
35
+ DEFINES=()
36
+
37
+ # Parse options
38
+ while [ $# -gt 0 ]; do
39
+ case "$1" in
40
+ --camera=*)
41
+ CAMERA="${1#--camera=}"
42
+ ;;
43
+ --size=*)
44
+ SIZE="${1#--size=}"
45
+ SIZE="${SIZE/x/,}"
46
+ ;;
47
+ -D)
48
+ shift
49
+ DEFINES+=("-D" "$1")
50
+ ;;
51
+ *)
52
+ echo "Unknown option: $1"
53
+ exit 1
54
+ ;;
55
+ esac
56
+ shift
57
+ done
58
+
59
+ # Ensure output directory exists
60
+ mkdir -p "$(dirname "$OUTPUT")"
61
+
62
+ # Run OpenSCAD
63
+ echo "Rendering preview: $INPUT -> $OUTPUT"
64
+ $OPENSCAD \
65
+ --camera="$CAMERA" \
66
+ --imgsize="${SIZE}" \
67
+ --colorscheme="Tomorrow Night" \
68
+ --autocenter \
69
+ --viewall \
70
+ "${DEFINES[@]}" \
71
+ -o "$OUTPUT" \
72
+ "$INPUT"
73
+
74
+ echo "Preview saved to: $OUTPUT"
@@ -0,0 +1,91 @@
1
+ #!/bin/bash
2
+ # Render OpenSCAD with parameters from a JSON file
3
+ # Usage: render-with-params.sh input.scad params.json output.stl|output.png
4
+
5
+ set -e
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ source "$SCRIPT_DIR/common.sh"
9
+
10
+ check_openscad
11
+
12
+ if [ $# -lt 3 ]; then
13
+ echo "Usage: $0 input.scad params.json output.[stl|png]"
14
+ echo ""
15
+ echo "params.json format:"
16
+ echo ' {"width": 60, "height": 40, "include_lid": true}'
17
+ exit 1
18
+ fi
19
+
20
+ INPUT="$1"
21
+ PARAMS_FILE="$2"
22
+ OUTPUT="$3"
23
+
24
+ if [ ! -f "$INPUT" ]; then
25
+ echo "Error: Input file not found: $INPUT"
26
+ exit 1
27
+ fi
28
+
29
+ if [ ! -f "$PARAMS_FILE" ]; then
30
+ echo "Error: Params file not found: $PARAMS_FILE"
31
+ exit 1
32
+ fi
33
+
34
+ # Build -D arguments from JSON
35
+ DEFINES=()
36
+ while IFS= read -r line; do
37
+ # Parse each key-value pair
38
+ key=$(echo "$line" | cut -d'=' -f1)
39
+ value=$(echo "$line" | cut -d'=' -f2-)
40
+
41
+ if [ -n "$key" ]; then
42
+ DEFINES+=("-D" "$key=$value")
43
+ fi
44
+ done < <(
45
+ # Use python or jq to parse JSON to key=value lines
46
+ if command -v python3 &> /dev/null; then
47
+ python3 -c "
48
+ import json
49
+ with open('$PARAMS_FILE') as f:
50
+ params = json.load(f)
51
+ for k, v in params.items():
52
+ if isinstance(v, bool):
53
+ print(f'{k}={str(v).lower()}')
54
+ elif isinstance(v, str):
55
+ print(f'{k}=\"{v}\"')
56
+ else:
57
+ print(f'{k}={v}')
58
+ "
59
+ elif command -v jq &> /dev/null; then
60
+ jq -r 'to_entries | .[] | "\(.key)=\(.value)"' "$PARAMS_FILE"
61
+ else
62
+ echo "Error: Requires python3 or jq to parse JSON"
63
+ exit 1
64
+ fi
65
+ )
66
+
67
+ echo "Rendering with parameters from: $PARAMS_FILE"
68
+ echo "Parameters: ${DEFINES[*]}"
69
+
70
+ # Determine output type and set appropriate options
71
+ EXT="${OUTPUT##*.}"
72
+ case "$EXT" in
73
+ stl|STL)
74
+ $OPENSCAD "${DEFINES[@]}" -o "$OUTPUT" "$INPUT"
75
+ ;;
76
+ png|PNG)
77
+ $OPENSCAD "${DEFINES[@]}" \
78
+ --camera="0,0,0,55,0,25,0" \
79
+ --imgsize="800,600" \
80
+ --colorscheme="Tomorrow Night" \
81
+ --autocenter --viewall \
82
+ -o "$OUTPUT" "$INPUT"
83
+ ;;
84
+ *)
85
+ echo "Unsupported output format: $EXT"
86
+ echo "Supported: stl, png"
87
+ exit 1
88
+ ;;
89
+ esac
90
+
91
+ echo "Output saved: $OUTPUT"
@@ -0,0 +1,46 @@
1
+ #!/bin/bash
2
+ # Validate an OpenSCAD file for syntax errors
3
+ # Usage: validate.sh input.scad
4
+
5
+ set -e
6
+
7
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
8
+ source "$SCRIPT_DIR/common.sh"
9
+
10
+ check_openscad
11
+
12
+ if [ $# -lt 1 ]; then
13
+ echo "Usage: $0 input.scad"
14
+ exit 1
15
+ fi
16
+
17
+ INPUT="$1"
18
+
19
+ if [ ! -f "$INPUT" ]; then
20
+ echo "Error: File not found: $INPUT"
21
+ exit 1
22
+ fi
23
+
24
+ echo "Validating: $INPUT"
25
+
26
+ # Create temp file for output
27
+ TEMP_OUTPUT=$(mktemp /tmp/openscad_validate.XXXXXX.echo)
28
+ trap "rm -f $TEMP_OUTPUT" EXIT
29
+
30
+ # Run OpenSCAD with echo output (fastest way to check syntax)
31
+ # Using --export-format=echo just parses and evaluates without rendering
32
+ if $OPENSCAD -o "$TEMP_OUTPUT" --export-format=echo "$INPUT" 2>&1; then
33
+ echo "✓ Syntax OK"
34
+
35
+ # Check for warnings in stderr
36
+ if [ -s "$TEMP_OUTPUT" ]; then
37
+ echo ""
38
+ echo "Echo output:"
39
+ cat "$TEMP_OUTPUT"
40
+ fi
41
+
42
+ exit 0
43
+ else
44
+ echo "✗ Validation failed"
45
+ exit 1
46
+ fi
@@ -0,0 +1,64 @@
1
+ ---
2
+ name: oracle
3
+ description: Get a second opinion by bundling a prompt + a curated file set, then asking a strong model from a different family through a separate Pi invocation.
4
+ ---
5
+
6
+ # Oracle
7
+
8
+ Use this skill when you want a second opinion from a capable model in a different family than the current session.
9
+
10
+ The oracle workflow bundles selected files into a standalone prompt, selects an alternate model with `pi --list-models`, and sends the bundle to `pi -p --no-tools`. It is read-only by default because the oracle receives the selected context directly and does not get tools.
11
+
12
+ ## Model selection
13
+
14
+ The `./scripts/oracle` wrapper chooses the strongest known model it can find in the first usable family from this order:
15
+
16
+ | Current family | Oracle preference |
17
+ | ------------------ | ----------------------------------------------------- |
18
+ | OpenAI / Codex | Claude / Anthropic → Gemini / Google → OpenAI / Codex |
19
+ | Claude / Anthropic | OpenAI / Codex → Gemini / Google → Claude / Anthropic |
20
+ | Gemini / Google | OpenAI / Codex → Claude / Anthropic → Gemini / Google |
21
+ | Unknown | Claude / Anthropic → OpenAI / Codex → Gemini / Google |
22
+
23
+ It prefers models listed in Pi's `enabledModels`, then falls back to the best matching model shown by `pi --list-models`. Override only when needed with `--model provider/model`.
24
+
25
+ If the current model is not obvious to the script, pass it explicitly with `--current provider/model`. Otherwise it falls back to Pi's default model from `settings.json`.
26
+
27
+ ## Workflow
28
+
29
+ 1. Pick the smallest file set that contains the truth. Avoid secrets by default.
30
+ 2. Preview selected files before sending.
31
+ 3. Ask the oracle with a standalone prompt that states the task, constraints, and desired output format.
32
+ 4. Treat the result as advice, not authority. Reason from first principles before applying recommendations.
33
+
34
+ ## Commands
35
+
36
+ Run commands from this skill directory:
37
+
38
+ ```bash
39
+ # Preview selected files
40
+ ./scripts/oracle-bundle --dry-run -p "<task>" --file "src/**" --file "!**/*.test.*"
41
+
42
+ # Preview the full bundle
43
+ ./scripts/oracle-bundle -p "<task>" --file "src/**" --file "!**/*.test.*"
44
+
45
+ # Show which oracle model would be selected
46
+ ./scripts/oracle --list-models --current openai-codex/gpt-5.5
47
+
48
+ # Ask the automatically selected oracle model
49
+ ./scripts/oracle --current openai-codex/gpt-5.5 \
50
+ -p "<task>" --file "src/**" --file "!**/*.test.*"
51
+
52
+ # Override the oracle model when the automatic choice is wrong
53
+ ./scripts/oracle --model github-copilot/claude-opus-4.7 \
54
+ -p "<task>" --file "src/**" --file "!**/*.test.*"
55
+ ```
56
+
57
+ ## Tips
58
+
59
+ - Prefer a minimal file set over the whole repo.
60
+ - If you need diffs reviewed, paste the diff into the prompt or attach the diff file via `--file`.
61
+ - Make the prompt completely standalone: include intent, goals, constraints, error text, and whether you want a plan, review, pros/cons, or patch guidance.
62
+ - Never include secrets (`.env`, tokens, key files).
63
+ - For general code review, ask for feedback on correctness, consistency with existing code, simplicity, maintainability, performance, security, and edge cases.
64
+ - Oracle can be slow while it reasons. Allow it several minutes to process.
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+ import { spawnSync } from "node:child_process";
3
+ import { existsSync, readFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+
8
+ const THINKING_LEVEL = "xhigh";
9
+ const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
10
+
11
+ const FAMILY_ORDER = {
12
+ openai: ["anthropic", "google", "openai"],
13
+ anthropic: ["openai", "google", "anthropic"],
14
+ google: ["openai", "anthropic", "google"],
15
+ unknown: ["anthropic", "openai", "google"],
16
+ };
17
+
18
+ const args = parseArgs(process.argv.slice(2));
19
+ const settings = readSettings();
20
+ const currentModel = args.currentModel ?? process.env.ORACLE_CURRENT_MODEL ?? defaultModel(settings);
21
+ const currentFamily = modelFamily(currentModel ?? "") ?? "unknown";
22
+ const availableModels = listPiModels();
23
+ const enabledModels = new Set((settings.enabledModels ?? []).map(normalizeModelSpec));
24
+
25
+ if (args.listModels) {
26
+ printModelPlan(availableModels, enabledModels, currentModel, currentFamily);
27
+ process.exit(0);
28
+ }
29
+
30
+ const oracleModel = args.model ?? selectOracleModel(availableModels, enabledModels, currentFamily);
31
+ if (!oracleModel) {
32
+ die("Could not find a suitable oracle model from `pi --list-models`.");
33
+ }
34
+
35
+ console.error(
36
+ `[oracle] current=${currentModel ?? "unknown"} family=${currentFamily}; oracle=${oracleModel}`,
37
+ );
38
+
39
+ const bundle = run(path.join(SCRIPT_DIR, "oracle-bundle"), args.bundleArgs, {
40
+ cwd: repoRoot(),
41
+ encoding: "utf8",
42
+ maxBuffer: 64 * 1024 * 1024,
43
+ });
44
+
45
+ if (bundle.status !== 0) {
46
+ process.stderr.write(bundle.stderr ?? "");
47
+ process.exit(bundle.status ?? 1);
48
+ }
49
+
50
+ const pi = spawnSync("pi", ["-p", "--no-tools", "--model", oracleModel, "--thinking", THINKING_LEVEL], {
51
+ cwd: repoRoot(),
52
+ input: bundle.stdout,
53
+ stdio: ["pipe", "inherit", "inherit"],
54
+ });
55
+
56
+ if (pi.error) die(pi.error.message);
57
+ process.exit(pi.status ?? 1);
58
+
59
+ function parseArgs(argv) {
60
+ const bundleArgs = [];
61
+ let currentModel;
62
+ let model;
63
+ let listModels = false;
64
+
65
+ for (let index = 0; index < argv.length; index += 1) {
66
+ const arg = argv[index];
67
+ if (arg === "--current" || arg === "--current-model") {
68
+ currentModel = requireValue(argv, (index += 1), arg);
69
+ } else if (arg === "--model" || arg === "--oracle-model") {
70
+ model = normalizeModelSpec(requireValue(argv, (index += 1), arg));
71
+ } else if (arg === "--list-models") {
72
+ listModels = true;
73
+ } else {
74
+ bundleArgs.push(arg);
75
+ }
76
+ }
77
+
78
+ return { bundleArgs, currentModel: currentModel ? normalizeModelSpec(currentModel) : undefined, model, listModels };
79
+ }
80
+
81
+ function requireValue(argv, index, flag) {
82
+ const value = argv[index];
83
+ if (!value) die(`${flag} requires a value`);
84
+ return value;
85
+ }
86
+
87
+ function readSettings() {
88
+ const agentDir = process.env.PI_CODING_AGENT_DIR ?? path.join(homedir(), ".pi", "agent");
89
+ const settingsPath = path.join(agentDir, "settings.json");
90
+ if (!existsSync(settingsPath)) return {};
91
+
92
+ try {
93
+ return JSON.parse(readFileSync(settingsPath, "utf8"));
94
+ } catch (error) {
95
+ console.error(`[oracle] warning: failed to parse ${settingsPath}: ${error.message}`);
96
+ return {};
97
+ }
98
+ }
99
+
100
+ function defaultModel(settings) {
101
+ if (!settings.defaultProvider || !settings.defaultModel) return undefined;
102
+ return normalizeModelSpec(`${settings.defaultProvider}/${settings.defaultModel}`);
103
+ }
104
+
105
+ function listPiModels() {
106
+ const result = run("pi", ["--list-models"], { encoding: "utf8", maxBuffer: 16 * 1024 * 1024 });
107
+ if (result.error) die(result.error.message);
108
+ if (result.status !== 0) die(result.stderr || "pi --list-models failed");
109
+
110
+ const models = [];
111
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`;
112
+ for (const line of output.split("\n")) {
113
+ const match = line.match(/^(\S+)\s+(\S+)\s+/);
114
+ if (!match || match[1] === "provider") continue;
115
+ models.push({ provider: match[1], model: match[2], spec: `${match[1]}/${match[2]}` });
116
+ }
117
+ return models;
118
+ }
119
+
120
+ function selectOracleModel(models, enabledModels, currentFamily) {
121
+ for (const family of FAMILY_ORDER[currentFamily] ?? FAMILY_ORDER.unknown) {
122
+ const best = bestModelForFamily(models, enabledModels, family);
123
+ if (best) return best.spec;
124
+ }
125
+ return undefined;
126
+ }
127
+
128
+ function bestModelForFamily(models, enabledModels, family) {
129
+ return models
130
+ .map((model) => ({ model, score: scoreModel(model, enabledModels, family) }))
131
+ .filter((entry) => entry.score >= 0)
132
+ .sort((a, b) => b.score - a.score || a.model.spec.localeCompare(b.model.spec))[0]?.model;
133
+ }
134
+
135
+ function scoreModel(entry, enabledModels, targetFamily) {
136
+ if (modelFamily(entry.spec) !== targetFamily) return -1;
137
+
138
+ let score = enabledModels.has(normalizeModelSpec(entry.spec)) ? 10_000 : 0;
139
+ score += providerScore(entry.provider, targetFamily);
140
+ score += nameScore(entry.model, targetFamily);
141
+ return score;
142
+ }
143
+
144
+ function providerScore(provider, family) {
145
+ const normalized = provider.toLowerCase();
146
+ if (family === "openai") {
147
+ if (normalized === "openai-codex") return 700;
148
+ if (normalized === "openai") return 600;
149
+ if (normalized === "github-copilot") return 500;
150
+ if (normalized.includes("azure")) return 450;
151
+ if (normalized === "openrouter") return 300;
152
+ return 100;
153
+ }
154
+
155
+ if (family === "anthropic") {
156
+ if (normalized === "github-copilot") return 700;
157
+ if (normalized === "anthropic") return 600;
158
+ if (normalized === "amazon-bedrock") return 450;
159
+ if (normalized === "openrouter") return 300;
160
+ return 100;
161
+ }
162
+
163
+ if (family === "google") {
164
+ if (normalized === "google-gemini-cli") return 700;
165
+ if (normalized === "google") return 600;
166
+ if (normalized === "github-copilot") return 500;
167
+ if (normalized.includes("vertex")) return 450;
168
+ if (normalized === "openrouter") return 300;
169
+ return 100;
170
+ }
171
+
172
+ return 0;
173
+ }
174
+
175
+ function nameScore(model, family) {
176
+ const normalized = model.toLowerCase();
177
+
178
+ if (family === "openai") {
179
+ if (/gpt-5\.5/.test(normalized)) return 1_000;
180
+ if (/gpt-5\.4/.test(normalized)) return 950;
181
+ if (/gpt-5\.3.*codex/.test(normalized)) return 925;
182
+ if (/gpt-5\.3/.test(normalized)) return 900;
183
+ if (/gpt-5\.2/.test(normalized)) return 850;
184
+ if (/gpt-5\.1/.test(normalized)) return 800;
185
+ if (/gpt-5/.test(normalized)) return 750;
186
+ if (/gpt-4\.1/.test(normalized)) return 500;
187
+ return 100;
188
+ }
189
+
190
+ if (family === "anthropic") {
191
+ if (/claude[-_.]opus[-_.]4[-_.]7/.test(normalized)) return 1_000;
192
+ if (/claude[-_.]opus[-_.]4[-_.]6/.test(normalized)) return 950;
193
+ if (/claude[-_.]opus[-_.]4[-_.]5/.test(normalized)) return 900;
194
+ if (/claude[-_.]opus[-_.]4[-_.]1/.test(normalized)) return 850;
195
+ if (/claude[-_.]opus[-_.]4/.test(normalized)) return 800;
196
+ if (/claude[-_.]sonnet[-_.]4[-_.]6/.test(normalized)) return 700;
197
+ if (/claude[-_.]sonnet[-_.]4[-_.]5/.test(normalized)) return 650;
198
+ if (/claude[-_.]sonnet[-_.]4/.test(normalized)) return 600;
199
+ return 100;
200
+ }
201
+
202
+ if (family === "google") {
203
+ if (/gemini[-_.]3\.1[-_.]pro/.test(normalized)) return 1_000;
204
+ if (/gemini[-_.]3[-_.]pro/.test(normalized)) return 950;
205
+ if (/gemini[-_.]2\.5[-_.]pro/.test(normalized)) return 800;
206
+ return 100;
207
+ }
208
+
209
+ return 0;
210
+ }
211
+
212
+ function modelFamily(spec) {
213
+ const normalized = normalizeModelSpec(spec).toLowerCase();
214
+ if (!normalized) return undefined;
215
+ if (/(anthropic|claude)/.test(normalized)) return "anthropic";
216
+ if (/(google|gemini)/.test(normalized)) return "google";
217
+ if (/(openai|gpt|codex)/.test(normalized)) return "openai";
218
+ return undefined;
219
+ }
220
+
221
+ function normalizeModelSpec(spec) {
222
+ return String(spec).trim().replace(/:(off|minimal|low|medium|high|xhigh)$/i, "");
223
+ }
224
+
225
+ function printModelPlan(models, enabledModels, currentModel, currentFamily) {
226
+ console.log(`Current model: ${currentModel ?? "unknown"}`);
227
+ console.log(`Current family: ${currentFamily}`);
228
+ console.log(`Family order: ${(FAMILY_ORDER[currentFamily] ?? FAMILY_ORDER.unknown).join(" -> ")}`);
229
+ console.log("");
230
+
231
+ for (const family of ["anthropic", "openai", "google"]) {
232
+ const best = bestModelForFamily(models, enabledModels, family);
233
+ console.log(`${family}: ${best?.spec ?? "not found"}`);
234
+ }
235
+ }
236
+
237
+ function repoRoot() {
238
+ const result = run("git", ["rev-parse", "--show-toplevel"], { encoding: "utf8" });
239
+ return result.status === 0 ? result.stdout.trim() : process.cwd();
240
+ }
241
+
242
+ function run(command, args, options = {}) {
243
+ return spawnSync(command, args, { ...options });
244
+ }
245
+
246
+ function die(message) {
247
+ console.error(`oracle: ${message}`);
248
+ process.exit(1);
249
+ }