@melaya/runner 1.0.73 → 1.0.74

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 (2) hide show
  1. package/dist/detect.js +37 -21
  2. package/package.json +1 -1
package/dist/detect.js CHANGED
@@ -4,32 +4,48 @@
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
+ /** Claude Code is a subscription CLI — when it's logged in on this machine,
12
+ * report its model aliases so the platform's model picker shows TRUE
13
+ * availability for local runs (claude_code pipelines execute HERE).
14
+ *
15
+ * We read the CLI's OAuth credentials FILE directly rather than spawning
16
+ * `claude auth status`. That subprocess flashed a console window on Windows
17
+ * every time the runner started (`windowsHide` is unreliable through the
18
+ * `.cmd` shell wrapper, which spawns its own conhost). The file is the source
19
+ * of truth on Windows/Linux; macOS keeps the grant in the Keychain, where
20
+ * `security` is a subprocess but pops NO window. Zero windows, faster, and
21
+ * it mirrors exactly what the runtime (model.py `_read_claude_oauth`) reads. */
12
22
  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) {
23
+ let raw = null;
24
+ try {
25
+ raw = await readFile(join(homedir(), ".claude", ".credentials.json"), "utf8");
26
+ }
27
+ catch {
28
+ raw = null;
29
+ }
30
+ if (!raw && process.platform === "darwin") {
17
31
  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
32
+ const { stdout } = await execFileAsync("security", ["find-generic-password", "-s", "Claude Code-credentials", "-w"], { timeout: 8000 });
33
+ if (stdout && stdout.trim())
34
+ raw = stdout.trim();
35
+ }
36
+ catch { /* not in keychain */ }
37
+ }
38
+ if (!raw)
39
+ return [];
40
+ try {
41
+ const oauth = (JSON.parse(raw)?.claudeAiOauth ?? {});
42
+ const expiresAt = Number(oauth.expiresAt ?? 0);
43
+ if (oauth.accessToken && (!expiresAt || Date.now() < expiresAt)) {
44
+ return ["sonnet", "opus", "haiku"].map((name) => ({ provider: "claude_code", name }));
29
45
  }
30
- catch { /* not installed / not on PATH / non-JSON — try next candidate */ }
31
46
  }
32
- return [];
47
+ catch { /* malformed creds → treat as logged out */ }
48
+ return []; // not logged in / expired → don't advertise models
33
49
  }
34
50
  export async function detectModels() {
35
51
  const models = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@melaya/runner",
3
- "version": "1.0.73",
3
+ "version": "1.0.74",
4
4
  "description": "Run Melaya AI pipelines locally with your own LM Studio or Ollama models",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,