gsd-pi 2.78.0 → 2.78.1

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 (48) hide show
  1. package/README.md +52 -16
  2. package/dist/claude-cli-check.js +91 -32
  3. package/dist/resources/extensions/claude-code-cli/readiness.js +115 -31
  4. package/dist/tsconfig.extensions.tsbuildinfo +1 -1
  5. package/dist/web/standalone/.next/BUILD_ID +1 -1
  6. package/dist/web/standalone/.next/app-path-routes-manifest.json +15 -15
  7. package/dist/web/standalone/.next/build-manifest.json +2 -2
  8. package/dist/web/standalone/.next/server/app/_global-error.html +1 -1
  9. package/dist/web/standalone/.next/server/app/_global-error.rsc +1 -1
  10. package/dist/web/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  11. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  12. package/dist/web/standalone/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  13. package/dist/web/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  14. package/dist/web/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  15. package/dist/web/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  16. package/dist/web/standalone/.next/server/app/_not-found.html +1 -1
  17. package/dist/web/standalone/.next/server/app/_not-found.rsc +1 -1
  18. package/dist/web/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  19. package/dist/web/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  20. package/dist/web/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  21. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  22. package/dist/web/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  23. package/dist/web/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  24. package/dist/web/standalone/.next/server/app/index.html +1 -1
  25. package/dist/web/standalone/.next/server/app/index.rsc +1 -1
  26. package/dist/web/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  27. package/dist/web/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  28. package/dist/web/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  29. package/dist/web/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  30. package/dist/web/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  31. package/dist/web/standalone/.next/server/app-paths-manifest.json +15 -15
  32. package/dist/web/standalone/.next/server/middleware-build-manifest.js +1 -1
  33. package/dist/web/standalone/.next/server/middleware-manifest.json +1 -1
  34. package/dist/web/standalone/.next/server/pages/404.html +1 -1
  35. package/dist/web/standalone/.next/server/pages/500.html +1 -1
  36. package/package.json +1 -1
  37. package/packages/daemon/package.json +2 -2
  38. package/packages/mcp-server/package.json +2 -2
  39. package/packages/native/package.json +1 -1
  40. package/packages/pi-agent-core/package.json +1 -1
  41. package/packages/pi-ai/package.json +1 -1
  42. package/packages/pi-coding-agent/package.json +1 -1
  43. package/packages/pi-tui/package.json +1 -1
  44. package/packages/rpc-client/package.json +1 -1
  45. package/pkg/package.json +1 -1
  46. package/src/resources/extensions/claude-code-cli/readiness.ts +116 -29
  47. /package/dist/web/standalone/.next/static/{C1zT2kEfoLhDdbWPWKrXd → 7afp7gq8-DVbxum83zRQ-}/_buildManifest.js +0 -0
  48. /package/dist/web/standalone/.next/static/{C1zT2kEfoLhDdbWPWKrXd → 7afp7gq8-DVbxum83zRQ-}/_ssgManifest.js +0 -0
@@ -5,8 +5,13 @@
5
5
  * Results are cached for 30 seconds to avoid shelling out on every
6
6
  * model-availability check.
7
7
  *
8
- * Auth verification follows the T3 Code pattern: run `claude auth status`
9
- * and check the exit code + output for an authenticated session.
8
+ * Auth verification runs `claude auth status --json` and inspects the
9
+ * `loggedIn` field, falling back to plain `claude auth status` and a text
10
+ * heuristic when the JSON shape is unavailable (older Claude CLI builds).
11
+ *
12
+ * Set GSD_CLAUDE_DEBUG=1 to print the probe's binary selection and auth
13
+ * outputs to stderr — useful when diagnosing platform-specific detection
14
+ * failures (Issue #4997).
10
15
  */
11
16
 
12
17
  import { execFileSync } from "node:child_process";
@@ -27,27 +32,109 @@ const CLAUDE_COMMAND = process.platform === "win32" ? "claude.cmd" : "claude";
27
32
  */
28
33
  const CLAUDE_COMMAND_CANDIDATES = process.platform === "win32" ? [CLAUDE_COMMAND, "claude.exe", "claude"] : [CLAUDE_COMMAND];
29
34
 
30
- function execClaude(args: string[]): Buffer {
31
- let lastError: unknown;
35
+ // Keep the version probe snappy — `claude --version` is a quick path.
36
+ const VERSION_TIMEOUT_MS = 5_000;
37
+ // Auth status can be much slower on Windows because the spawn goes through
38
+ // cmd.exe → claude.cmd → node → Claude CLI. 15s leaves headroom on cold spawns
39
+ // without making startup feel hung when the CLI is genuinely missing.
40
+ const AUTH_TIMEOUT_MS = 15_000;
41
+
42
+ function debugLog(...parts: unknown[]): void {
43
+ if (process.env.GSD_CLAUDE_DEBUG) {
44
+ process.stderr.write(`[claude-readiness] ${parts.map((p) => (typeof p === "string" ? p : JSON.stringify(p))).join(" ")}\n`);
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Find the first candidate that responds to `--version`. Returns the
50
+ * candidate name on success, null if none worked.
51
+ *
52
+ * On Windows with `shell: true`, a missing candidate surfaces as a
53
+ * non-zero exit from cmd.exe rather than ENOENT — so we cannot rely on
54
+ * the error code to decide "try next". Treat any failure as "try next"
55
+ * for the version probe; the only thing that matters for binary
56
+ * detection is whether *some* candidate produces a `claude --version`
57
+ * line.
58
+ */
59
+ function findWorkingCommand(): string | null {
32
60
  for (const command of CLAUDE_COMMAND_CANDIDATES) {
33
61
  try {
34
- return execFileSync(command, args, {
35
- timeout: 5_000,
62
+ execFileSync(command, ["--version"], {
63
+ timeout: VERSION_TIMEOUT_MS,
36
64
  stdio: "pipe",
37
65
  shell: process.platform === "win32",
38
66
  });
67
+ debugLog("version probe ok via", command);
68
+ return command;
39
69
  } catch (error) {
40
- lastError = error;
41
- const code = (error as NodeJS.ErrnoException | undefined)?.code;
42
- // Windows Git Bash can surface `.cmd` spawn failures as EINVAL instead
43
- // of ENOENT. Treat both as "try next candidate".
44
- if (code === "ENOENT" || code === "EINVAL") {
45
- continue;
70
+ debugLog("version probe failed for", command, "code=", (error as NodeJS.ErrnoException | undefined)?.code);
71
+ continue;
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+
77
+ /**
78
+ * Decide auth state from `claude auth status` output.
79
+ *
80
+ * Newer Claude CLI builds emit JSON by default with a `loggedIn` boolean.
81
+ * Older builds emit free-form text. We prefer the structured signal and fall
82
+ * back to a text heuristic. Note: the text heuristic only covers English
83
+ * phrasing — the JSON path is the durable signal.
84
+ */
85
+ function parseAuthStatus(output: string): boolean | null {
86
+ const trimmed = output.trim();
87
+ if (!trimmed) return null;
88
+
89
+ if (trimmed.startsWith("{")) {
90
+ try {
91
+ const parsed = JSON.parse(trimmed) as { loggedIn?: unknown };
92
+ if (typeof parsed.loggedIn === "boolean") {
93
+ return parsed.loggedIn;
46
94
  }
47
- throw error;
95
+ } catch {
96
+ // Fall through to text heuristic.
48
97
  }
49
98
  }
50
- throw lastError ?? new Error(`Claude CLI executable not found (tried: ${CLAUDE_COMMAND_CANDIDATES.join(", ")})`);
99
+
100
+ const lower = trimmed.toLowerCase();
101
+ if (/not logged in|no credentials|unauthenticated|not authenticated/.test(lower)) {
102
+ return false;
103
+ }
104
+ if (/logged in|authenticated|signed in|email|subscription/.test(lower)) {
105
+ return true;
106
+ }
107
+ return null;
108
+ }
109
+
110
+ function probeAuth(command: string): boolean | null {
111
+ // Try --json first (newer CLIs).
112
+ try {
113
+ const out = execFileSync(command, ["auth", "status", "--json"], {
114
+ timeout: AUTH_TIMEOUT_MS,
115
+ stdio: "pipe",
116
+ shell: process.platform === "win32",
117
+ }).toString();
118
+ debugLog("auth status --json output:", out.slice(0, 200));
119
+ const parsed = parseAuthStatus(out);
120
+ if (parsed !== null) return parsed;
121
+ } catch (error) {
122
+ debugLog("auth status --json threw:", (error as Error).message?.slice(0, 200));
123
+ }
124
+
125
+ // Fallback: plain `auth status` (older CLIs that don't accept --json).
126
+ try {
127
+ const out = execFileSync(command, ["auth", "status"], {
128
+ timeout: AUTH_TIMEOUT_MS,
129
+ stdio: "pipe",
130
+ shell: process.platform === "win32",
131
+ }).toString();
132
+ debugLog("auth status output:", out.slice(0, 200));
133
+ return parseAuthStatus(out);
134
+ } catch (error) {
135
+ debugLog("auth status threw:", (error as Error).message?.slice(0, 200));
136
+ return null;
137
+ }
51
138
  }
52
139
 
53
140
  let cachedBinaryPresent: boolean | null = null;
@@ -55,6 +142,10 @@ let cachedAuthed: boolean | null = null;
55
142
  let lastCheckMs = 0;
56
143
  const CHECK_INTERVAL_MS = 30_000;
57
144
 
145
+ /**
146
+ * Refresh the cached binary/auth state when the cache window has expired.
147
+ * Preserves a known auth state across soft-fail auth probes.
148
+ */
58
149
  function refreshCache(): void {
59
150
  const now = Date.now();
60
151
  if (cachedBinaryPresent !== null && now - lastCheckMs < CHECK_INTERVAL_MS) {
@@ -64,27 +155,23 @@ function refreshCache(): void {
64
155
  // Set timestamp first to prevent re-entrant checks during the same window
65
156
  lastCheckMs = now;
66
157
 
67
- // Check binary presence
68
- try {
69
- execClaude(["--version"]);
70
- cachedBinaryPresent = true;
71
- } catch {
158
+ const command = findWorkingCommand();
159
+ if (!command) {
72
160
  cachedBinaryPresent = false;
73
161
  cachedAuthed = false;
74
162
  return;
75
163
  }
164
+ cachedBinaryPresent = true;
76
165
 
77
- // Check auth status — exit code 0 with non-error output means authenticated
78
- try {
79
- const output = execClaude(["auth", "status"])
80
- .toString()
81
- .toLowerCase();
82
- // The CLI outputs "not logged in", "no credentials", or similar when unauthenticated
83
- cachedAuthed = !(/not logged in|no credentials|unauthenticated|not authenticated/i.test(output));
84
- } catch {
85
- // Non-zero exit code means not authenticated
86
- cachedAuthed = false;
166
+ const authed = probeAuth(command);
167
+ if (authed === null) {
168
+ // Couldn't determine auth state from CLI output. Don't clobber a
169
+ // previously known-good cache; otherwise default to false so we don't
170
+ // silently route requests to an unauthenticated CLI.
171
+ if (cachedAuthed === null) cachedAuthed = false;
172
+ return;
87
173
  }
174
+ cachedAuthed = authed;
88
175
  }
89
176
 
90
177
  /**