glm-coding-router 1.1.2 → 2.1.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/LICENSE +21 -21
- package/README.md +542 -426
- package/dist/bin/glm-review.js +28 -3
- package/dist/bin/glm-worker.js +30 -4
- package/dist/budget/estimator.js +218 -0
- package/dist/budget/manager.js +223 -0
- package/dist/cli.js +46 -3
- package/dist/commands/dashboard.js +348 -0
- package/dist/commands/doctor-auth.js +107 -0
- package/dist/commands/doctor-command.js +171 -41
- package/dist/commands/landing.js +47 -0
- package/dist/commands/runs.js +568 -0
- package/dist/commands/status.js +28 -15
- package/dist/commands/usage.js +34 -58
- package/dist/commands/watch.js +289 -0
- package/dist/core/config.js +61 -0
- package/dist/core/errors.js +24 -0
- package/dist/core/key-inspector.js +45 -0
- package/dist/core/paths.js +32 -0
- package/dist/core/process.js +83 -0
- package/dist/core/prompt.js +18 -5
- package/dist/core/routing-flags.js +59 -0
- package/dist/core/user-env.js +17 -7
- package/dist/core/zai-quota.js +148 -0
- package/dist/events/bus.js +64 -0
- package/dist/events/claude-adapter.js +416 -0
- package/dist/events/types.js +9 -0
- package/dist/handoff/bundle.js +203 -0
- package/dist/handoff/parent-handoff.js +48 -0
- package/dist/mcp/server.js +45 -1
- package/dist/routing/glm-routing.js +131 -0
- package/dist/runs/checkpoint.js +204 -0
- package/dist/runs/drain.js +165 -0
- package/dist/runs/heartbeat.js +45 -0
- package/dist/runs/registry.js +350 -0
- package/dist/runs/store.js +186 -0
- package/dist/runs/ulid.js +112 -0
- package/dist/runs/worker-run.js +672 -0
- package/dist/templates/agents-block.js +53 -44
- package/dist/templates/claude-block.js +56 -47
- package/dist/templates/glm-delegation-skill.js +76 -65
- package/dist/tui/command-ui.js +158 -0
- package/dist/tui/progress.js +338 -0
- package/dist/tui/render.js +144 -0
- package/package.json +1 -1
|
@@ -1,36 +1,13 @@
|
|
|
1
1
|
import { runDoctorChecks, doctorHasFailures } from "./doctor.js";
|
|
2
2
|
import { emitJson } from "./context.js";
|
|
3
3
|
import { logger } from "../core/logging.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
let currentSection = "";
|
|
12
|
-
for (const result of results) {
|
|
13
|
-
if (result.section !== currentSection) {
|
|
14
|
-
currentSection = result.section;
|
|
15
|
-
lines.push(currentSection);
|
|
16
|
-
}
|
|
17
|
-
lines.push(` ${SYMBOL[result.status]} ${result.name}`);
|
|
18
|
-
if (result.detail) {
|
|
19
|
-
lines.push(` ${result.detail}`);
|
|
20
|
-
}
|
|
21
|
-
if (result.note) {
|
|
22
|
-
lines.push(` ${result.note}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
if (networkResult) {
|
|
26
|
-
lines.push("Network");
|
|
27
|
-
lines.push(` ${networkResult === "reachable" ? "✓" : "⚠"} Z.ai endpoint ${networkResult}`);
|
|
28
|
-
}
|
|
29
|
-
const failures = doctorHasFailures(results);
|
|
30
|
-
lines.push("");
|
|
31
|
-
lines.push(`Status: ${failures ? "ISSUES DETECTED" : "HEALTHY"}`);
|
|
32
|
-
return lines.join("\n");
|
|
33
|
-
}
|
|
4
|
+
import { Errors } from "../core/errors.js";
|
|
5
|
+
import { inspectZaiKey } from "../core/key-inspector.js";
|
|
6
|
+
import { authenticateZaiKey, authenticationCheckStatus, deriveDoctorVerdict, } from "./doctor-auth.js";
|
|
7
|
+
import { describeKeyStore, detectUserEnvStore } from "../core/user-env.js";
|
|
8
|
+
import { createWriter } from "../tui/render.js";
|
|
9
|
+
import { createCommandUi } from "../tui/command-ui.js";
|
|
10
|
+
import { version } from "../core/version.js";
|
|
34
11
|
/** Lightweight endpoint reachability probe (spec §42). Never consumes coding quota. */
|
|
35
12
|
export async function probeEndpoint(baseUrl, fetchImpl = fetch) {
|
|
36
13
|
try {
|
|
@@ -45,24 +22,177 @@ export async function probeEndpoint(baseUrl, fetchImpl = fetch) {
|
|
|
45
22
|
return "not reachable";
|
|
46
23
|
}
|
|
47
24
|
}
|
|
25
|
+
function shellUnsetLines(platform) {
|
|
26
|
+
if (platform === "win32") {
|
|
27
|
+
return ["PowerShell: Remove-Item Env:ZAI_API_KEY", "CMD: set ZAI_API_KEY="];
|
|
28
|
+
}
|
|
29
|
+
return ["POSIX shell: unset ZAI_API_KEY"];
|
|
30
|
+
}
|
|
31
|
+
function describeComparisonRow(inspection) {
|
|
32
|
+
switch (inspection.comparison) {
|
|
33
|
+
case "match":
|
|
34
|
+
return { status: "ok", value: "Matches the saved key" };
|
|
35
|
+
case "different":
|
|
36
|
+
return { status: "warn", value: "Different from the saved key" };
|
|
37
|
+
case "unavailable":
|
|
38
|
+
return { status: "warn", value: `Could not read ${describeKeyStore(inspection.store)} to compare` };
|
|
39
|
+
case "not-comparable":
|
|
40
|
+
if (inspection.store === "none") {
|
|
41
|
+
return { status: "info", value: "No persistent store on this platform" };
|
|
42
|
+
}
|
|
43
|
+
if (inspection.effectiveSource === "process-env") {
|
|
44
|
+
return { status: "info", value: "No saved key to compare against" };
|
|
45
|
+
}
|
|
46
|
+
if (inspection.effectiveSource === "user-store") {
|
|
47
|
+
return { status: "info", value: "No process override" };
|
|
48
|
+
}
|
|
49
|
+
return { status: "info", value: "Not applicable" };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Ordered, actionable "next step" lines. Empty when the verdict is HEALTHY. */
|
|
53
|
+
function buildNextSteps(input) {
|
|
54
|
+
const { authentication, comparison, store, platform, networkProbeFailed, verdictStatus } = input;
|
|
55
|
+
if (verdictStatus === "HEALTHY") {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
const lines = [];
|
|
59
|
+
if (comparison === "different") {
|
|
60
|
+
lines.push("This terminal's key takes priority over the saved key (process environment wins).", "Restart the terminal's hosting app to refresh its environment,", "or if this is an intentional process override, update that override instead.", ...shellUnsetLines(platform));
|
|
61
|
+
}
|
|
62
|
+
else if (comparison === "unavailable") {
|
|
63
|
+
lines.push(`Could not read ${describeKeyStore(store)} to compare against the process key.`);
|
|
64
|
+
}
|
|
65
|
+
switch (authentication.reason) {
|
|
66
|
+
case "missing-key":
|
|
67
|
+
lines.push("No ZAI_API_KEY was found.", platform === "win32" ? "Run: glm-router key set" : 'Run: glm-router key set, or export ZAI_API_KEY="<your-key>"');
|
|
68
|
+
break;
|
|
69
|
+
case "http-401":
|
|
70
|
+
lines.push("The selected key was rejected. To replace it: glm-router key set");
|
|
71
|
+
break;
|
|
72
|
+
case "http-403":
|
|
73
|
+
lines.push("Access was denied for the selected key. Check the key's account permissions — not necessarily expiry.");
|
|
74
|
+
break;
|
|
75
|
+
case "rate-limited":
|
|
76
|
+
lines.push("The Z.ai monitor endpoint is rate-limiting this key. Retry in a moment.");
|
|
77
|
+
break;
|
|
78
|
+
case "http-error":
|
|
79
|
+
case "network-error":
|
|
80
|
+
case "timeout":
|
|
81
|
+
lines.push("Could not reach the Z.ai monitor endpoint. Check network/proxy connectivity and retry.");
|
|
82
|
+
break;
|
|
83
|
+
case "invalid-response":
|
|
84
|
+
case "provider-error":
|
|
85
|
+
lines.push("The Z.ai monitor endpoint returned an unexpected response — this is not proof the key is invalid.");
|
|
86
|
+
break;
|
|
87
|
+
case "offline":
|
|
88
|
+
lines.push("Authentication was not checked (--offline). Run without --offline to verify the key online.");
|
|
89
|
+
break;
|
|
90
|
+
case "accepted":
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (networkProbeFailed) {
|
|
94
|
+
lines.push("The configured Anthropic endpoint reachability probe did not succeed (separate from key authentication).");
|
|
95
|
+
}
|
|
96
|
+
if (lines.length > 0 && authentication.reason !== "offline") {
|
|
97
|
+
lines.push("Then run: glm-router doctor");
|
|
98
|
+
}
|
|
99
|
+
return lines;
|
|
100
|
+
}
|
|
48
101
|
export async function doctorCommand(options, deps = {}) {
|
|
49
|
-
|
|
102
|
+
if (options.offline && options.network) {
|
|
103
|
+
throw Errors.invalidArgs("--offline and --network cannot be used together.", [
|
|
104
|
+
"Use --offline for local-only checks, or --network for the extra reachability probe.",
|
|
105
|
+
]);
|
|
106
|
+
}
|
|
107
|
+
const env = deps.env ?? process.env;
|
|
108
|
+
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
109
|
+
const offline = options.offline ?? false;
|
|
110
|
+
const platform = deps.platform ?? process.platform;
|
|
111
|
+
const store = deps.store ?? detectUserEnvStore({ platform, env });
|
|
112
|
+
const report = runDoctorChecks({ home: deps.home, env, readUserEnv: deps.readUserEnv, store });
|
|
113
|
+
const inspection = inspectZaiKey({ env, readUserEnvDiagnostic: deps.readUserEnvDiagnostic, store });
|
|
114
|
+
const [authentication, networkResult] = await Promise.all([
|
|
115
|
+
authenticateZaiKey(inspection.effectiveKey, fetchImpl, offline),
|
|
116
|
+
options.network ? probeEndpoint(report.config.provider.anthropicBaseUrl, fetchImpl) : Promise.resolve(undefined),
|
|
117
|
+
]);
|
|
118
|
+
const localFailure = doctorHasFailures(report.results);
|
|
119
|
+
const networkProbeFailed = networkResult !== undefined && networkResult !== "reachable";
|
|
120
|
+
const verdict = deriveDoctorVerdict({
|
|
121
|
+
localFailure,
|
|
122
|
+
authentication,
|
|
123
|
+
keyComparison: inspection.comparison,
|
|
124
|
+
offline,
|
|
125
|
+
networkProbeFailed,
|
|
126
|
+
});
|
|
50
127
|
if (options.json) {
|
|
51
|
-
const networkResult = options.network
|
|
52
|
-
? await probeEndpoint(report.config.provider.anthropicBaseUrl, deps.fetchImpl)
|
|
53
|
-
: undefined;
|
|
54
128
|
emitJson({
|
|
55
|
-
status:
|
|
129
|
+
status: verdict.status,
|
|
56
130
|
checks: report.results,
|
|
57
131
|
keySource: report.keySource,
|
|
58
132
|
network: networkResult,
|
|
133
|
+
authentication,
|
|
134
|
+
keyComparison: inspection.comparison,
|
|
135
|
+
keyMismatch: inspection.keyMismatch,
|
|
59
136
|
});
|
|
60
|
-
return
|
|
137
|
+
return verdict.exitCode;
|
|
61
138
|
}
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
139
|
+
const stream = deps.stdout ?? process.stdout;
|
|
140
|
+
const writer = createWriter(stream);
|
|
141
|
+
const ui = createCommandUi(writer, { quiet: options.quiet });
|
|
142
|
+
writer.line(renderText(ui, report.results, inspection, authentication, networkResult, verdict, platform));
|
|
66
143
|
logger.debug(`anthropic base url: ${report.config.provider.anthropicBaseUrl}`);
|
|
67
|
-
return
|
|
144
|
+
return verdict.exitCode;
|
|
145
|
+
}
|
|
146
|
+
function renderText(ui, results, inspection, authentication, networkResult, verdict, platform) {
|
|
147
|
+
const blocks = [];
|
|
148
|
+
const header = ui.header(`GLM CODING ROUTER v${version} / DOCTOR`, "Runtime and credential diagnostics");
|
|
149
|
+
if (header)
|
|
150
|
+
blocks.push(header);
|
|
151
|
+
let currentSection = "";
|
|
152
|
+
const localRows = [];
|
|
153
|
+
for (const result of results) {
|
|
154
|
+
if (result.section !== currentSection) {
|
|
155
|
+
currentSection = result.section;
|
|
156
|
+
localRows.push(ui.section(currentSection.toUpperCase()));
|
|
157
|
+
}
|
|
158
|
+
const status = result.status === "ok" ? "ok" : result.status === "warn" ? "warn" : "fail";
|
|
159
|
+
localRows.push(ui.row(result.name, result.detail ?? "", status));
|
|
160
|
+
if (result.note)
|
|
161
|
+
localRows.push(ui.detail(result.note));
|
|
162
|
+
}
|
|
163
|
+
blocks.push(localRows.join("\n"));
|
|
164
|
+
const credentialRows = [ui.section("CREDENTIALS")];
|
|
165
|
+
credentialRows.push(ui.row("Selected source", inspection.effectiveSource === "process-env" ? "Process environment" : inspection.effectiveSource === "user-store" ? describeKeyStore(inspection.store) : "none", "info"));
|
|
166
|
+
const comparisonRow = describeComparisonRow(inspection);
|
|
167
|
+
credentialRows.push(ui.row("Saved key comparison", comparisonRow.value, comparisonRow.status));
|
|
168
|
+
credentialRows.push(ui.row("Monitor authentication", authentication.detail, authenticationCheckStatus(authentication.state)));
|
|
169
|
+
blocks.push(credentialRows.join("\n"));
|
|
170
|
+
if (networkResult !== undefined) {
|
|
171
|
+
const networkRows = [
|
|
172
|
+
ui.section("NETWORK"),
|
|
173
|
+
ui.row("Z.ai endpoint reachability", networkResult, networkResult === "reachable" ? "ok" : "warn"),
|
|
174
|
+
];
|
|
175
|
+
blocks.push(networkRows.join("\n"));
|
|
176
|
+
}
|
|
177
|
+
const nextSteps = buildNextSteps({
|
|
178
|
+
authentication,
|
|
179
|
+
comparison: inspection.comparison,
|
|
180
|
+
store: inspection.store,
|
|
181
|
+
platform,
|
|
182
|
+
networkProbeFailed: networkResult !== undefined && networkResult !== "reachable",
|
|
183
|
+
verdictStatus: verdict.status,
|
|
184
|
+
});
|
|
185
|
+
if (nextSteps.length > 0) {
|
|
186
|
+
const footer = ui.footer(nextSteps.join("\n"));
|
|
187
|
+
if (footer)
|
|
188
|
+
blocks.push([ui.section("NEXT STEP"), footer].join("\n"));
|
|
189
|
+
}
|
|
190
|
+
const resultLabel = {
|
|
191
|
+
HEALTHY: "HEALTHY",
|
|
192
|
+
ATTENTION: "ATTENTION",
|
|
193
|
+
UNVERIFIED: "UNVERIFIED",
|
|
194
|
+
ISSUES: "ISSUES DETECTED",
|
|
195
|
+
};
|
|
196
|
+
blocks.push(`RESULT ${resultLabel[verdict.status]}`);
|
|
197
|
+
return blocks.join("\n\n");
|
|
68
198
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `glm-router` with no subcommand (specs/terminal-ui-doctor.md §B.1). Purely
|
|
3
|
+
* static: no provider request, no key/config read, no side effects. Exists
|
|
4
|
+
* so a first-time user sees "what do I run" instead of nothing.
|
|
5
|
+
*/
|
|
6
|
+
import { Errors } from "../core/errors.js";
|
|
7
|
+
import { version } from "../core/version.js";
|
|
8
|
+
import { createCommandUi } from "../tui/command-ui.js";
|
|
9
|
+
import { createWriter } from "../tui/render.js";
|
|
10
|
+
export function landingCommand(options, deps = {}) {
|
|
11
|
+
if (options.json) {
|
|
12
|
+
// Root --json has no data to emit — a data-producing command must be named.
|
|
13
|
+
throw Errors.invalidArgs("--json requires a command that produces data.", [
|
|
14
|
+
"Try:", "", " glm-router status --json", " glm-router doctor --json", " glm-router usage --json",
|
|
15
|
+
]);
|
|
16
|
+
}
|
|
17
|
+
const stream = deps.stdout ?? process.stdout;
|
|
18
|
+
const writer = createWriter(stream);
|
|
19
|
+
const ui = createCommandUi(writer, { quiet: options.quiet });
|
|
20
|
+
const blocks = [];
|
|
21
|
+
const header = ui.header(`GLM CODING ROUTER v${version}`, "Coding Plan workers for Claude Code and Codex");
|
|
22
|
+
if (header)
|
|
23
|
+
blocks.push(header);
|
|
24
|
+
blocks.push([
|
|
25
|
+
ui.section("CHECK & MONITOR"),
|
|
26
|
+
ui.row("glm-router doctor", "Check setup and verify API key"),
|
|
27
|
+
ui.row("glm-router status", "Quick offline overview"),
|
|
28
|
+
ui.row("glm-router usage", "Coding Plan quota and reset times"),
|
|
29
|
+
ui.row("glm-router dashboard", "Live quota and worker activity"),
|
|
30
|
+
].join("\n"));
|
|
31
|
+
blocks.push([
|
|
32
|
+
ui.section("WORK"),
|
|
33
|
+
ui.row('glm-worker "<task>"', "Run an implementation task"),
|
|
34
|
+
ui.row('glm-review "<task>"', "Run a read-only review"),
|
|
35
|
+
ui.row("glm-router runs", "Inspect recorded runs"),
|
|
36
|
+
].join("\n"));
|
|
37
|
+
blocks.push([
|
|
38
|
+
ui.section("SETUP"),
|
|
39
|
+
ui.row("glm-router init", "Guided setup"),
|
|
40
|
+
ui.row("glm-router key set", "Save a replacement API key"),
|
|
41
|
+
].join("\n"));
|
|
42
|
+
const footer = ui.footer("Start with: glm-router doctor\nAll commands: glm-router --help");
|
|
43
|
+
if (footer)
|
|
44
|
+
blocks.push(footer);
|
|
45
|
+
writer.line(blocks.join("\n\n"));
|
|
46
|
+
return 0;
|
|
47
|
+
}
|