@melaya/runner 1.0.73 → 1.0.76

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/detect.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export interface DetectedModel {
2
- provider: "lmstudio" | "ollama" | "claude_code";
2
+ provider: "lmstudio" | "ollama" | "claude_code" | "codex";
3
3
  name: string;
4
4
  }
5
5
  export declare function detectModels(): Promise<DetectedModel[]>;
package/dist/detect.js CHANGED
@@ -4,32 +4,137 @@
4
4
  */
5
5
  import { execFile } from "child_process";
6
6
  import { promisify } from "util";
7
+ import { readFile } from "fs/promises";
8
+ import { homedir } from "os";
9
+ import { join } from "path";
7
10
  const execFileAsync = promisify(execFile);
8
- /** Claude Code is a subscription CLI — when it's installed AND logged in on
9
- * this machine, report its model aliases so the platform's model picker can
10
- * show the TRUE availability for local runs (the cloud host's CLI status is
11
- * the wrong signal: claude_code pipelines execute HERE, on the runner). */
11
+ /** Fallbacks used ONLY when live discovery fails (offline runner, transient
12
+ * error, missing cache). The REAL lists are fetched dynamically below so a new
13
+ * claude/codex release is available on Melaya automatically no code change,
14
+ * no redeploy. Kept minimal + in step with the registry `staticModels`. */
15
+ const CODEX_MODELS_FALLBACK = ["gpt-5.5", "gpt-5.4", "gpt-5.4-mini"];
16
+ const CLAUDE_ALIASES_FALLBACK = ["sonnet", "opus", "haiku"];
17
+ /** Discover the claude model FAMILIES this subscription serves, so a new
18
+ * family (a future tier) appears on Melaya automatically. Hits the same
19
+ * GET /v1/models the runtime resolver uses (model.py::_resolve_claude_model_id)
20
+ * with the OAuth token, then reduces concrete ids (claude-<family>-<ver>-<date>)
21
+ * to their family aliases (opus/sonnet/haiku/…). Each alias resolves to the
22
+ * NEWEST model of its family at call time, so within-family releases are picked
23
+ * up for free too. Best-effort: any failure falls back to the known aliases. */
24
+ async function fetchClaudeFamilies(token) {
25
+ try {
26
+ const r = await fetch("https://api.anthropic.com/v1/models?limit=100", {
27
+ headers: {
28
+ Authorization: `Bearer ${token}`,
29
+ "anthropic-version": "2023-06-01",
30
+ "anthropic-beta": "oauth-2025-04-20",
31
+ "x-app": "cli",
32
+ "User-Agent": "claude-code/2.1.81",
33
+ },
34
+ signal: AbortSignal.timeout(6000),
35
+ });
36
+ if (r.ok) {
37
+ const data = (await r.json());
38
+ const fams = new Set();
39
+ for (const m of data.data ?? []) {
40
+ const mm = /^claude-([a-z]+)-/.exec(m?.id ?? "");
41
+ if (mm)
42
+ fams.add(mm[1]);
43
+ }
44
+ if (fams.size)
45
+ return [...fams];
46
+ }
47
+ }
48
+ catch { /* offline / transient → fallback */ }
49
+ return CLAUDE_ALIASES_FALLBACK;
50
+ }
51
+ /** Read the codex model catalog from the CLI's OWN auto-refreshed cache
52
+ * (~/.codex/models_cache.json, written by codex when it lists models), so a
53
+ * new codex release appears on Melaya automatically. File-based like auth.json
54
+ * — no API call, no subprocess/terminal. Filters non-chat helper slugs
55
+ * (e.g. codex-auto-review). Falls back to a known set. */
56
+ async function readCodexModels() {
57
+ try {
58
+ const raw = await readFile(join(homedir(), ".codex", "models_cache.json"), "utf8");
59
+ const j = JSON.parse(raw);
60
+ const slugs = (j.models ?? [])
61
+ .map((m) => m?.slug ?? "")
62
+ .filter((s) => s && !/(^|-)(auto|review|embed|moderation)(-|$)/i.test(s));
63
+ if (slugs.length)
64
+ return slugs;
65
+ }
66
+ catch { /* no cache / malformed → fallback */ }
67
+ return CODEX_MODELS_FALLBACK;
68
+ }
69
+ /** Claude Code is a subscription CLI — when it's logged in on this machine,
70
+ * report its model aliases so the platform's model picker shows TRUE
71
+ * availability for local runs (claude_code pipelines execute HERE).
72
+ *
73
+ * We read the CLI's OAuth credentials FILE directly rather than spawning
74
+ * `claude auth status`. That subprocess flashed a console window on Windows
75
+ * every time the runner started (`windowsHide` is unreliable through the
76
+ * `.cmd` shell wrapper, which spawns its own conhost). The file is the source
77
+ * of truth on Windows/Linux; macOS keeps the grant in the Keychain, where
78
+ * `security` is a subprocess but pops NO window. Zero windows, faster, and
79
+ * it mirrors exactly what the runtime (model.py `_read_claude_oauth`) reads. */
12
80
  async function detectClaudeCode() {
13
- // npm global installs on Windows expose claude.cmd — execFile with a bare
14
- // name won't resolve .cmd without a shell, so try the .cmd wrapper first.
15
- const candidates = process.platform === "win32" ? ["claude.cmd", "claude"] : ["claude"];
16
- for (const exe of candidates) {
81
+ let raw = null;
82
+ try {
83
+ raw = await readFile(join(homedir(), ".claude", ".credentials.json"), "utf8");
84
+ }
85
+ catch {
86
+ raw = null;
87
+ }
88
+ if (!raw && process.platform === "darwin") {
17
89
  try {
18
- const { stdout } = await execFileAsync(exe, ["auth", "status"], {
19
- timeout: 10_000,
20
- windowsHide: true,
21
- // .cmd files need a shell on win32; harmless elsewhere.
22
- shell: process.platform === "win32",
23
- });
24
- const status = JSON.parse(stdout);
25
- if (status.loggedIn) {
26
- return ["sonnet", "opus", "haiku"].map((name) => ({ provider: "claude_code", name }));
27
- }
28
- return []; // installed but logged out — don't advertise models
90
+ const { stdout } = await execFileAsync("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { timeout: 8000 });
91
+ if (stdout && stdout.trim())
92
+ raw = stdout.trim();
93
+ }
94
+ catch { /* not in keychain */ }
95
+ }
96
+ if (!raw)
97
+ return [];
98
+ try {
99
+ const oauth = (JSON.parse(raw)?.claudeAiOauth ?? {});
100
+ const expiresAt = Number(oauth.expiresAt ?? 0);
101
+ if (oauth.accessToken && (!expiresAt || Date.now() < expiresAt)) {
102
+ const names = await fetchClaudeFamilies(oauth.accessToken);
103
+ return names.map((name) => ({ provider: "claude_code", name }));
104
+ }
105
+ }
106
+ catch { /* malformed creds → treat as logged out */ }
107
+ return []; // not logged in / expired → don't advertise models
108
+ }
109
+ /** OpenAI Codex CLI is a subscription CLI (ChatGPT Plus/Pro) — when it's
110
+ * logged in on this machine, advertise its models so the platform's picker
111
+ * shows TRUE availability for local runs (codex pipelines execute HERE).
112
+ *
113
+ * Same file-based approach as `detectClaudeCode`: read `~/.codex/auth.json`
114
+ * directly rather than spawning `codex login status`. The subprocess flashed
115
+ * a console window on Windows on every runner start (`windowsHide` is
116
+ * unreliable through the `.cmd` shell wrapper). The file is the source of
117
+ * truth cross-platform and mirrors what the runtime (model.py) authenticates
118
+ * against. A ChatGPT login stores `tokens.access_token`; an API-key login
119
+ * stores `OPENAI_API_KEY`. Either means "logged in". */
120
+ async function detectCodex() {
121
+ let raw = null;
122
+ try {
123
+ raw = await readFile(join(homedir(), ".codex", "auth.json"), "utf8");
124
+ }
125
+ catch {
126
+ return [];
127
+ }
128
+ try {
129
+ const auth = JSON.parse(raw);
130
+ const loggedIn = !!(auth?.tokens?.access_token || auth?.OPENAI_API_KEY);
131
+ if (loggedIn) {
132
+ const names = await readCodexModels();
133
+ return names.map((name) => ({ provider: "codex", name }));
29
134
  }
30
- catch { /* not installed / not on PATH / non-JSON — try next candidate */ }
31
135
  }
32
- return [];
136
+ catch { /* malformed creds → treat as logged out */ }
137
+ return []; // not logged in → don't advertise models
33
138
  }
34
139
  export async function detectModels() {
35
140
  const models = [];
@@ -57,5 +162,7 @@ export async function detectModels() {
57
162
  catch { /* not running */ }
58
163
  // Claude Code CLI — subscription auth on THIS machine
59
164
  models.push(...await detectClaudeCode());
165
+ // OpenAI Codex CLI — subscription auth on THIS machine
166
+ models.push(...await detectCodex());
60
167
  return models;
61
168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.0.73",
3
+ "version": "1.0.76",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,