@zeulewan/glueclaw-provider 1.1.0 → 1.2.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.
- package/package.json +3 -1
- package/src/healthcheck.ts +106 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeulewan/glueclaw-provider",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "GlueClaw - Claude CLI subprocess provider for Max subscription",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
"index.ts",
|
|
11
11
|
"src/stream.ts",
|
|
12
12
|
"src/catalog.ts",
|
|
13
|
+
"src/healthcheck.ts",
|
|
13
14
|
"src/openclaw.d.ts",
|
|
14
15
|
"openclaw.plugin.json",
|
|
15
16
|
"install.sh",
|
|
@@ -31,6 +32,7 @@
|
|
|
31
32
|
"test:integration": "vitest run src/__tests__/integration",
|
|
32
33
|
"test:e2e": "vitest run src/__tests__/e2e",
|
|
33
34
|
"typecheck": "tsc --noEmit",
|
|
35
|
+
"healthcheck": "npx tsx src/healthcheck.ts",
|
|
34
36
|
"format": "prettier --check .",
|
|
35
37
|
"format:fix": "prettier --write ."
|
|
36
38
|
},
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Binary-search a prompt (split into lines) to find the single line
|
|
5
|
+
* that causes a test function to reject. The testFn receives a joined
|
|
6
|
+
* prompt string and should return true if it passes, false if it fails.
|
|
7
|
+
*
|
|
8
|
+
* Returns the offending line, or null if the full prompt passes.
|
|
9
|
+
*/
|
|
10
|
+
export async function binarySearchTrigger(
|
|
11
|
+
lines: string[],
|
|
12
|
+
testFn: (chunk: string) => Promise<boolean>,
|
|
13
|
+
): Promise<string | null> {
|
|
14
|
+
if (lines.length === 0) return null;
|
|
15
|
+
|
|
16
|
+
const fullPasses = await testFn(lines.join("\n"));
|
|
17
|
+
if (fullPasses) return null;
|
|
18
|
+
|
|
19
|
+
if (lines.length === 1) return lines[0] ?? null;
|
|
20
|
+
|
|
21
|
+
let lo = 0;
|
|
22
|
+
let hi = lines.length;
|
|
23
|
+
|
|
24
|
+
while (hi - lo > 1) {
|
|
25
|
+
const mid = Math.floor((lo + hi) / 2);
|
|
26
|
+
const firstHalf = lines.slice(lo, mid).join("\n");
|
|
27
|
+
const fails = !(await testFn(firstHalf));
|
|
28
|
+
if (fails) {
|
|
29
|
+
hi = mid;
|
|
30
|
+
} else {
|
|
31
|
+
lo = mid;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return lines[lo] ?? null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Test whether the current scrubbed system prompt passes Claude CLI
|
|
40
|
+
* without triggering a 400 error. If it fails, binary-searches to
|
|
41
|
+
* identify the offending line.
|
|
42
|
+
*/
|
|
43
|
+
export async function runHealthcheck(opts: {
|
|
44
|
+
claudeBin?: string;
|
|
45
|
+
systemPrompt: string;
|
|
46
|
+
scenario?: string;
|
|
47
|
+
}): Promise<{ ok: boolean; trigger: string | null }> {
|
|
48
|
+
const claudeBin = opts.claudeBin ?? "claude";
|
|
49
|
+
const scenario = opts.scenario ?? "healthcheck";
|
|
50
|
+
|
|
51
|
+
const testPrompt = async (prompt: string): Promise<boolean> => {
|
|
52
|
+
return new Promise((resolve) => {
|
|
53
|
+
const env = { ...process.env, MOCK_SCENARIO: scenario };
|
|
54
|
+
const args = [
|
|
55
|
+
"--system-prompt",
|
|
56
|
+
prompt,
|
|
57
|
+
"--output-format",
|
|
58
|
+
"stream-json",
|
|
59
|
+
"-p",
|
|
60
|
+
"say pong",
|
|
61
|
+
];
|
|
62
|
+
const proc = spawn(claudeBin, args, {
|
|
63
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
64
|
+
env,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
let exitCode = 0;
|
|
68
|
+
proc.on("close", (code: number | null) => {
|
|
69
|
+
exitCode = code ?? 0;
|
|
70
|
+
resolve(exitCode === 0);
|
|
71
|
+
});
|
|
72
|
+
proc.on("error", () => resolve(false));
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const lines = opts.systemPrompt.split("\n");
|
|
77
|
+
const fullPasses = await testPrompt(opts.systemPrompt);
|
|
78
|
+
if (fullPasses) return { ok: true, trigger: null };
|
|
79
|
+
|
|
80
|
+
const trigger = await binarySearchTrigger(lines, testPrompt);
|
|
81
|
+
return { ok: false, trigger };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// CLI entry point: npx tsx src/healthcheck.ts
|
|
85
|
+
const isMain =
|
|
86
|
+
typeof process !== "undefined" && process.argv[1]?.endsWith("healthcheck.ts");
|
|
87
|
+
if (isMain) {
|
|
88
|
+
const { scrubPrompt } = await import("./stream.js");
|
|
89
|
+
const testPrompt =
|
|
90
|
+
"You are a personal assistant running inside OpenClaw. " +
|
|
91
|
+
"HEARTBEAT_OK reply_to_current [[reply_to:user]] " +
|
|
92
|
+
"openclaw.inbound_meta generated by OpenClaw";
|
|
93
|
+
const scrubbed = scrubPrompt(testPrompt);
|
|
94
|
+
|
|
95
|
+
console.log("GlueClaw healthcheck");
|
|
96
|
+
console.log(" Testing scrubbed prompt against Claude CLI...\n");
|
|
97
|
+
|
|
98
|
+
const result = await runHealthcheck({ systemPrompt: scrubbed });
|
|
99
|
+
if (result.ok) {
|
|
100
|
+
console.log(" PASS: scrubbed prompt accepted");
|
|
101
|
+
} else {
|
|
102
|
+
console.log(" FAIL: prompt rejected");
|
|
103
|
+
console.log(` Trigger line: ${result.trigger}`);
|
|
104
|
+
}
|
|
105
|
+
process.exit(result.ok ? 0 : 1);
|
|
106
|
+
}
|