alvin-bot 4.8.1 โ 4.8.3
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/CHANGELOG.md +84 -0
- package/bin/cli.js +118 -12
- package/dist/providers/claude-sdk-provider.js +33 -10
- package/package.json +1 -1
- package/test/claude-sdk-provider.test.ts +51 -5
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,90 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to Alvin Bot are documented here.
|
|
4
4
|
|
|
5
|
+
## [4.8.3] โ 2026-04-11
|
|
6
|
+
|
|
7
|
+
### ๐ Critical: Claude SDK heartbeat false-positive "unavailable"
|
|
8
|
+
|
|
9
|
+
Caught in production on the Mac mini: the heartbeat monitor was marking `claude-sdk` as unhealthy every 5 minutes, triggering failover to Ollama, even though `claude -p "ping"` from the same user's terminal worked perfectly. After 9 consecutive heartbeat failures, the main Telegram bot was stuck serving responses via Gemma 4 instead of Claude Max.
|
|
10
|
+
|
|
11
|
+
**Root cause**: `isAvailable()` in the Claude SDK provider used `claude -p "ping" --output-format text` as an auth probe. That command spawns a full SDK query, takes **6-10 seconds warm** (longer on cold starts), and my timeout was only **10 seconds**. Under load or on cold starts it crossed the timeout threshold, was killed by Node, and execFileAsync rejected โ caught by the outer try/catch โ cached as "unavailable" for 60 seconds โ next heartbeat re-probed and failed the same way.
|
|
12
|
+
|
|
13
|
+
**Fix**: Replaced the `-p "ping"` probe with `claude auth status`. This is a purpose-built Claude CLI command that:
|
|
14
|
+
|
|
15
|
+
- Completes in ~150 ms (vs 6-10 s)
|
|
16
|
+
- Returns structured JSON with an explicit `loggedIn` boolean
|
|
17
|
+
- Consumes zero tokens
|
|
18
|
+
- Doesn't touch the SDK or model init path
|
|
19
|
+
|
|
20
|
+
The new code parses the JSON and returns `true` only when `loggedIn === true`. A fallback path keeps the old `-p "ping"` sniff for older Claude CLI versions that don't support `auth status` as JSON.
|
|
21
|
+
|
|
22
|
+
Before/after the fix:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Before: 6800ms warm probe, 10s timeout, consumed tokens,
|
|
26
|
+
failed under load โ 9 consecutive false-positive "unavailable"
|
|
27
|
+
After: 150ms probe, 5s timeout, no tokens, structured JSON check
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### โจ New CLI command: `alvin-bot status`
|
|
31
|
+
|
|
32
|
+
Offline-friendly status command โ no running bot required. Prints:
|
|
33
|
+
|
|
34
|
+
- **Version**: `Alvin Bot vX.Y.Z` + Node version + platform/arch
|
|
35
|
+
- **Data dir**: path + whether `.env` exists + configured `PRIMARY_PROVIDER`
|
|
36
|
+
- **Runtime state**:
|
|
37
|
+
- On macOS: LaunchAgent plist installed? PID from `launchctl list`?
|
|
38
|
+
- On Linux/Windows: `pm2 jlist` check for the `alvin-bot` process
|
|
39
|
+
- **Live info** (when the bot is running with the web UI on :3100): Uptime, active model
|
|
40
|
+
|
|
41
|
+
Answers Ali's request: *"alvin-bot status im Terminal soll auch die Version anzeigen"*. The command prominently features the version at the top so it's the first thing you see.
|
|
42
|
+
|
|
43
|
+
Example:
|
|
44
|
+
|
|
45
|
+
```
|
|
46
|
+
๐ค Alvin Bot v4.8.3
|
|
47
|
+
Node v25.9.0 ยท darwin/arm64
|
|
48
|
+
|
|
49
|
+
๐ Data dir: /Users/alvin_de/.alvin-bot
|
|
50
|
+
.env: โ
present
|
|
51
|
+
Provider: claude-sdk
|
|
52
|
+
|
|
53
|
+
๐ LaunchAgent: installed
|
|
54
|
+
Running: โ
yes (PID 43589)
|
|
55
|
+
Uptime: 0h 55m
|
|
56
|
+
Model: Gemma 4 E4B (Ollama)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### Tests
|
|
60
|
+
|
|
61
|
+
2 new test cases in `test/claude-sdk-provider.test.ts` cover the new flow:
|
|
62
|
+
|
|
63
|
+
- `claude auth status` returning `{loggedIn: true}` โ `isAvailable()` returns `true`
|
|
64
|
+
- `claude auth status` returning `{loggedIn: false}` โ `isAvailable()` returns `false`
|
|
65
|
+
- Older CLI where `auth status` throws โ fall back to `-p "ping"` path (preserves old behavior)
|
|
66
|
+
|
|
67
|
+
87 tests passing (up from 85).
|
|
68
|
+
|
|
69
|
+
## [4.8.2] โ 2026-04-11
|
|
70
|
+
|
|
71
|
+
### ๐ Offline setup: wait long enough for Ollama's first-run init
|
|
72
|
+
|
|
73
|
+
Second follow-up to 4.8.0's offline-gemma4 wizard. The 4.8.1 brew path successfully installs Ollama, but the subsequent `ensureOllamaServe()` was reporting "Could not start Ollama daemon" because it only waited **2 seconds** after spawning the server.
|
|
74
|
+
|
|
75
|
+
What actually happens on first run:
|
|
76
|
+
|
|
77
|
+
1. `nohup ollama serve &` spawns the server process
|
|
78
|
+
2. Server generates a fresh SSH keypair at `~/.ollama/id_ed25519` (~1 s)
|
|
79
|
+
3. Server discovers GPUs โ on Apple Silicon this initializes Metal (~5 s)
|
|
80
|
+
4. Server starts the runner subprocess (~1 s)
|
|
81
|
+
5. Server begins listening on `127.0.0.1:11434`
|
|
82
|
+
|
|
83
|
+
Total cold-start time: **5โ15 seconds**. The old 2-second wait was racing ahead of GPU discovery and failing the next `ollama list` call.
|
|
84
|
+
|
|
85
|
+
Fix: `ensureOllamaServe()` now polls `ollama list` every second for up to **30 seconds**. On success it reports which attempt worked (for visibility). On failure it dumps the last 15 lines of `/tmp/ollama-setup.log` so users can see what Ollama itself said.
|
|
86
|
+
|
|
87
|
+
Caught during the second run of the setup wizard on the fresh test MacBook โ brew install succeeded, daemon was actually running (PID confirmed via pgrep), but the wizard bailed out anyway because it gave up too soon.
|
|
88
|
+
|
|
5
89
|
## [4.8.1] โ 2026-04-11
|
|
6
90
|
|
|
7
91
|
### ๐ Offline setup: Homebrew preferred on macOS
|
package/bin/cli.js
CHANGED
|
@@ -219,28 +219,53 @@ function installOllama() {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
/**
|
|
222
|
-
* Ensure the Ollama daemon is running. Spawns it in the background if not
|
|
222
|
+
* Ensure the Ollama daemon is running. Spawns it in the background if not,
|
|
223
|
+
* then polls for readiness โ first-run initialization can take 5-15 seconds
|
|
224
|
+
* on macOS (SSH key generation + GPU discovery + runner startup).
|
|
223
225
|
*/
|
|
224
226
|
function ensureOllamaServe() {
|
|
227
|
+
// Fast path: already running
|
|
225
228
|
try {
|
|
226
|
-
// 'ollama list' needs the daemon running
|
|
227
229
|
execSync("ollama list", { stdio: "pipe", timeout: 5000 });
|
|
228
230
|
return true;
|
|
229
|
-
} catch {
|
|
230
|
-
|
|
231
|
+
} catch { /* not running โ try to start */ }
|
|
232
|
+
|
|
233
|
+
// Spawn in background (detached via `&` inside a shell)
|
|
234
|
+
try {
|
|
235
|
+
execSync("nohup ollama serve > /tmp/ollama-setup.log 2>&1 &", {
|
|
236
|
+
stdio: "pipe",
|
|
237
|
+
shell: "/bin/sh",
|
|
238
|
+
});
|
|
239
|
+
} catch (err) {
|
|
240
|
+
console.log(`\n โ ๏ธ Could not spawn 'ollama serve': ${err.message || err}`);
|
|
241
|
+
return false;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// Poll for readiness โ up to 30 seconds total. First-run init is slow
|
|
245
|
+
// because ollama generates an SSH key pair, discovers GPUs, and starts
|
|
246
|
+
// the runner subprocess.
|
|
247
|
+
const deadlineMs = Date.now() + 30_000;
|
|
248
|
+
let lastError = "";
|
|
249
|
+
let attempt = 0;
|
|
250
|
+
while (Date.now() < deadlineMs) {
|
|
251
|
+
attempt++;
|
|
231
252
|
try {
|
|
232
|
-
execSync("nohup ollama serve > /tmp/ollama-setup.log 2>&1 &", {
|
|
233
|
-
stdio: "pipe",
|
|
234
|
-
shell: "/bin/sh",
|
|
235
|
-
});
|
|
236
|
-
// Give it a moment
|
|
237
|
-
execSync("sleep 2", { stdio: "pipe" });
|
|
238
253
|
execSync("ollama list", { stdio: "pipe", timeout: 5000 });
|
|
254
|
+
if (attempt > 1) console.log(` โ
Ollama daemon ready after ${attempt} attempts`);
|
|
239
255
|
return true;
|
|
240
|
-
} catch {
|
|
241
|
-
|
|
256
|
+
} catch (err) {
|
|
257
|
+
lastError = err instanceof Error ? err.message : String(err);
|
|
242
258
|
}
|
|
259
|
+
// Sleep 1 second between polls via execSync (cross-platform, no promise in sync ctx)
|
|
260
|
+
try { execSync("sleep 1", { stdio: "pipe" }); } catch { /* shouldn't fail */ }
|
|
243
261
|
}
|
|
262
|
+
console.log(` โ ๏ธ Daemon did not become ready within 30s. Last error: ${lastError}`);
|
|
263
|
+
console.log(` Tail of /tmp/ollama-setup.log:`);
|
|
264
|
+
try {
|
|
265
|
+
const tail = execSync("tail -15 /tmp/ollama-setup.log", { encoding: "utf-8" });
|
|
266
|
+
tail.split("\n").forEach((line) => console.log(` ${line}`));
|
|
267
|
+
} catch { /* log missing */ }
|
|
268
|
+
return false;
|
|
244
269
|
}
|
|
245
270
|
|
|
246
271
|
/**
|
|
@@ -1822,6 +1847,86 @@ switch (cmd) {
|
|
|
1822
1847
|
case "-v":
|
|
1823
1848
|
version();
|
|
1824
1849
|
break;
|
|
1850
|
+
case "status": {
|
|
1851
|
+
// CLI `alvin-bot status` โ quick, offline-friendly status without
|
|
1852
|
+
// requiring a running bot. Prints version, node info, data dir,
|
|
1853
|
+
// configured provider, and โ on macOS โ LaunchAgent state.
|
|
1854
|
+
try {
|
|
1855
|
+
const pkg = JSON.parse(
|
|
1856
|
+
readFileSync(resolve(import.meta.dirname || ".", "../package.json"), "utf-8"),
|
|
1857
|
+
);
|
|
1858
|
+
console.log(`\n๐ค Alvin Bot v${pkg.version}`);
|
|
1859
|
+
} catch {
|
|
1860
|
+
console.log("\n๐ค Alvin Bot (version unknown)");
|
|
1861
|
+
}
|
|
1862
|
+
console.log(` Node ${process.version} ยท ${process.platform}/${process.arch}`);
|
|
1863
|
+
console.log("");
|
|
1864
|
+
|
|
1865
|
+
// Data dir + .env
|
|
1866
|
+
const envPath = join(DATA_DIR, ".env");
|
|
1867
|
+
console.log(`๐ Data dir: ${DATA_DIR}`);
|
|
1868
|
+
console.log(` .env: ${existsSync(envPath) ? "โ
present" : "โ missing"}`);
|
|
1869
|
+
|
|
1870
|
+
// Primary provider from .env
|
|
1871
|
+
if (existsSync(envPath)) {
|
|
1872
|
+
try {
|
|
1873
|
+
const env = readFileSync(envPath, "utf-8");
|
|
1874
|
+
const match = env.match(/^PRIMARY_PROVIDER=(.+)$/m);
|
|
1875
|
+
if (match) console.log(` Provider: ${match[1].trim()}`);
|
|
1876
|
+
} catch { /* ignore */ }
|
|
1877
|
+
}
|
|
1878
|
+
console.log("");
|
|
1879
|
+
|
|
1880
|
+
// Runtime state: LaunchAgent (macOS) or pm2 (Linux/Windows)
|
|
1881
|
+
if (process.platform === "darwin") {
|
|
1882
|
+
const { plistPath, label } = launchdPaths();
|
|
1883
|
+
const plistExists = existsSync(plistPath);
|
|
1884
|
+
console.log(`๐ LaunchAgent: ${plistExists ? "installed" : "not installed"}`);
|
|
1885
|
+
if (plistExists) {
|
|
1886
|
+
try {
|
|
1887
|
+
const out = execSync(`launchctl list | grep ${label} || true`, { encoding: "utf-8" });
|
|
1888
|
+
if (out.trim()) {
|
|
1889
|
+
const parts = out.trim().split(/\s+/);
|
|
1890
|
+
const pid = parts[0];
|
|
1891
|
+
const isRunning = pid !== "-" && pid !== "0";
|
|
1892
|
+
console.log(` Running: ${isRunning ? `โ
yes (PID ${pid})` : "โ no"}`);
|
|
1893
|
+
} else {
|
|
1894
|
+
console.log(` Running: โ not loaded`);
|
|
1895
|
+
}
|
|
1896
|
+
} catch {
|
|
1897
|
+
console.log(` Running: โ unknown`);
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
} else {
|
|
1901
|
+
// Linux/Windows: check pm2
|
|
1902
|
+
try {
|
|
1903
|
+
const out = execSync("pm2 jlist 2>/dev/null || echo '[]'", { encoding: "utf-8" });
|
|
1904
|
+
const procs = JSON.parse(out);
|
|
1905
|
+
const alvin = procs.find?.((p) => p && p.name === "alvin-bot");
|
|
1906
|
+
if (alvin) {
|
|
1907
|
+
console.log(`๐ pm2: ${alvin.pm2_env?.status || "unknown"} (PID ${alvin.pid || "?"})`);
|
|
1908
|
+
} else {
|
|
1909
|
+
console.log(`๐ pm2: alvin-bot not managed`);
|
|
1910
|
+
}
|
|
1911
|
+
} catch {
|
|
1912
|
+
console.log(`๐ pm2: not installed`);
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// Try to reach the running web API for live info
|
|
1917
|
+
try {
|
|
1918
|
+
const apiRes = execSync("curl -fsS -m 2 http://localhost:3100/api/status 2>/dev/null", { encoding: "utf-8" });
|
|
1919
|
+
const parsed = JSON.parse(apiRes);
|
|
1920
|
+
const uptimeSec = Math.floor(parsed.bot?.uptime || 0);
|
|
1921
|
+
const h = Math.floor(uptimeSec / 3600);
|
|
1922
|
+
const m = Math.floor((uptimeSec % 3600) / 60);
|
|
1923
|
+
console.log(` Uptime: ${h}h ${m}m`);
|
|
1924
|
+
if (parsed.model?.name) console.log(` Model: ${parsed.model.name}`);
|
|
1925
|
+
} catch { /* bot not running or web ui off โ skip */ }
|
|
1926
|
+
|
|
1927
|
+
console.log("");
|
|
1928
|
+
process.exit(0);
|
|
1929
|
+
}
|
|
1825
1930
|
default:
|
|
1826
1931
|
console.log(`
|
|
1827
1932
|
${t("cli.title")}
|
|
@@ -1837,6 +1942,7 @@ ${t("cli.commands")}
|
|
|
1837
1942
|
start ${t("cli.startDesc")} (background via PM2)
|
|
1838
1943
|
start -f Start in foreground (for debugging)
|
|
1839
1944
|
stop Stop the bot
|
|
1945
|
+
status Show bot version + LaunchAgent/pm2 state (offline)
|
|
1840
1946
|
launchd macOS only: install/uninstall/status as launchd user agent
|
|
1841
1947
|
version ${t("cli.versionDesc")}
|
|
1842
1948
|
|
|
@@ -237,19 +237,42 @@ export class ClaudeSDKProvider {
|
|
|
237
237
|
if (!claudePath)
|
|
238
238
|
return cache(false);
|
|
239
239
|
// Step 1: binary exists?
|
|
240
|
-
// Async execFile doesn't block the event loop. 5s timeout kills
|
|
241
|
-
// runaway probes without hanging the bot.
|
|
242
240
|
await execFileAsync(claudePath, ["--version"], { timeout: 5000 });
|
|
243
|
-
// Step 2: actually authenticated?
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
// the
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
241
|
+
// Step 2: actually authenticated?
|
|
242
|
+
//
|
|
243
|
+
// We used to use `claude -p "ping" --output-format text` and sniff
|
|
244
|
+
// the stdout for "Not logged in". That spawned a full SDK query,
|
|
245
|
+
// consumed tokens, and took 5-10 seconds warm โ occasionally
|
|
246
|
+
// crossing our timeout on cold starts or under load, leading to
|
|
247
|
+
// false-positive "unavailable" reports that cascaded into heartbeat
|
|
248
|
+
// failures and unnecessary fallback to Ollama.
|
|
249
|
+
//
|
|
250
|
+
// `claude auth status` is the purpose-built command: fast (~150ms),
|
|
251
|
+
// no token cost, no SDK init, returns structured JSON with an
|
|
252
|
+
// explicit `loggedIn` boolean. Much cleaner.
|
|
253
|
+
try {
|
|
254
|
+
const { stdout } = await execFileAsync(claudePath, ["auth", "status"], { timeout: 5000 });
|
|
255
|
+
const parsed = JSON.parse(stdout);
|
|
256
|
+
if (parsed.loggedIn === true) {
|
|
257
|
+
return cache(true);
|
|
258
|
+
}
|
|
259
|
+
// loggedIn === false (or missing) โ not authenticated
|
|
250
260
|
return cache(false);
|
|
251
261
|
}
|
|
252
|
-
|
|
262
|
+
catch (authErr) {
|
|
263
|
+
// Older claude CLI versions may not expose `auth status` as JSON,
|
|
264
|
+
// or may exit non-zero when not logged in. Fall back to the
|
|
265
|
+
// sniff-stdout approach for backward compat.
|
|
266
|
+
try {
|
|
267
|
+
const { stdout: probeOut } = await execFileAsync(claudePath, ["-p", "ping", "--output-format", "text"], { timeout: 15000 });
|
|
268
|
+
return cache(!isAuthErrorOutput(probeOut));
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Both checks failed โ treat as unavailable
|
|
272
|
+
void authErr;
|
|
273
|
+
return cache(false);
|
|
274
|
+
}
|
|
275
|
+
}
|
|
253
276
|
}
|
|
254
277
|
catch {
|
|
255
278
|
return cache(false);
|
package/package.json
CHANGED
|
@@ -25,15 +25,39 @@ describe("ClaudeSDKProvider.isAvailable", () => {
|
|
|
25
25
|
vi.resetModules();
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
-
it("returns
|
|
29
|
-
//
|
|
30
|
-
// Second call: -p 'ping' returns "Not logged in ยท Please run /login"
|
|
28
|
+
it("returns true when `claude auth status` reports loggedIn: true", async () => {
|
|
29
|
+
// Sequence: --version then auth status (JSON)
|
|
31
30
|
execFileMock
|
|
32
31
|
.mockImplementationOnce((_p, _a, _o, cb) =>
|
|
33
32
|
cb(null, { stdout: "1.0.0\n", stderr: "" }),
|
|
34
33
|
)
|
|
35
34
|
.mockImplementationOnce((_p, _a, _o, cb) =>
|
|
36
|
-
cb(null, {
|
|
35
|
+
cb(null, {
|
|
36
|
+
stdout: JSON.stringify({
|
|
37
|
+
loggedIn: true,
|
|
38
|
+
authMethod: "claude.ai",
|
|
39
|
+
subscriptionType: "max",
|
|
40
|
+
}),
|
|
41
|
+
stderr: "",
|
|
42
|
+
}),
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
const { ClaudeSDKProvider } = await import("../src/providers/claude-sdk-provider.js");
|
|
46
|
+
const p = new ClaudeSDKProvider();
|
|
47
|
+
const result = await p.isAvailable();
|
|
48
|
+
expect(result).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("returns false when `claude auth status` reports loggedIn: false", async () => {
|
|
52
|
+
execFileMock
|
|
53
|
+
.mockImplementationOnce((_p, _a, _o, cb) =>
|
|
54
|
+
cb(null, { stdout: "1.0.0\n", stderr: "" }),
|
|
55
|
+
)
|
|
56
|
+
.mockImplementationOnce((_p, _a, _o, cb) =>
|
|
57
|
+
cb(null, {
|
|
58
|
+
stdout: JSON.stringify({ loggedIn: false }),
|
|
59
|
+
stderr: "",
|
|
60
|
+
}),
|
|
37
61
|
);
|
|
38
62
|
|
|
39
63
|
const { ClaudeSDKProvider } = await import("../src/providers/claude-sdk-provider.js");
|
|
@@ -42,11 +66,15 @@ describe("ClaudeSDKProvider.isAvailable", () => {
|
|
|
42
66
|
expect(result).toBe(false);
|
|
43
67
|
});
|
|
44
68
|
|
|
45
|
-
it("
|
|
69
|
+
it("falls back to `claude -p` probe when `auth status` fails (older CLI)", async () => {
|
|
70
|
+
// Sequence: --version โ auth status rejects โ -p ping succeeds
|
|
46
71
|
execFileMock
|
|
47
72
|
.mockImplementationOnce((_p, _a, _o, cb) =>
|
|
48
73
|
cb(null, { stdout: "1.0.0\n", stderr: "" }),
|
|
49
74
|
)
|
|
75
|
+
.mockImplementationOnce((_p, _a, _o, cb) =>
|
|
76
|
+
cb(new Error("unknown command: auth status"), { stdout: "", stderr: "" }),
|
|
77
|
+
)
|
|
50
78
|
.mockImplementationOnce((_p, _a, _o, cb) =>
|
|
51
79
|
cb(null, { stdout: "pong", stderr: "" }),
|
|
52
80
|
);
|
|
@@ -56,6 +84,24 @@ describe("ClaudeSDKProvider.isAvailable", () => {
|
|
|
56
84
|
const result = await p.isAvailable();
|
|
57
85
|
expect(result).toBe(true);
|
|
58
86
|
});
|
|
87
|
+
|
|
88
|
+
it("falls back to `claude -p` probe and detects 'Not logged in' text", async () => {
|
|
89
|
+
execFileMock
|
|
90
|
+
.mockImplementationOnce((_p, _a, _o, cb) =>
|
|
91
|
+
cb(null, { stdout: "1.0.0\n", stderr: "" }),
|
|
92
|
+
)
|
|
93
|
+
.mockImplementationOnce((_p, _a, _o, cb) =>
|
|
94
|
+
cb(new Error("auth status not supported"), { stdout: "", stderr: "" }),
|
|
95
|
+
)
|
|
96
|
+
.mockImplementationOnce((_p, _a, _o, cb) =>
|
|
97
|
+
cb(null, { stdout: "Not logged in ยท Please run /login", stderr: "" }),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
const { ClaudeSDKProvider } = await import("../src/providers/claude-sdk-provider.js");
|
|
101
|
+
const p = new ClaudeSDKProvider();
|
|
102
|
+
const result = await p.isAvailable();
|
|
103
|
+
expect(result).toBe(false);
|
|
104
|
+
});
|
|
59
105
|
});
|
|
60
106
|
|
|
61
107
|
describe("ClaudeSDKProvider โ isAuthErrorOutput helper", () => {
|