@keel_flow/cli 0.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/LICENSE +21 -0
- package/README.md +170 -0
- package/dist/build.d.ts +57 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +350 -0
- package/dist/build.js.map +1 -0
- package/dist/claude-auth.d.ts +16 -0
- package/dist/claude-auth.d.ts.map +1 -0
- package/dist/claude-auth.js +75 -0
- package/dist/claude-auth.js.map +1 -0
- package/dist/doctor.d.ts +18 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +547 -0
- package/dist/doctor.js.map +1 -0
- package/dist/git.d.ts +4 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +138 -0
- package/dist/git.js.map +1 -0
- package/dist/goals.d.ts +34 -0
- package/dist/goals.d.ts.map +1 -0
- package/dist/goals.js +218 -0
- package/dist/goals.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1015 -0
- package/dist/index.js.map +1 -0
- package/dist/kb-reindex.d.ts +31 -0
- package/dist/kb-reindex.d.ts.map +1 -0
- package/dist/kb-reindex.js +71 -0
- package/dist/kb-reindex.js.map +1 -0
- package/dist/keys.d.ts +26 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +209 -0
- package/dist/keys.js.map +1 -0
- package/dist/learn.d.ts +37 -0
- package/dist/learn.d.ts.map +1 -0
- package/dist/learn.js +274 -0
- package/dist/learn.js.map +1 -0
- package/dist/lifecycle.d.ts +27 -0
- package/dist/lifecycle.d.ts.map +1 -0
- package/dist/lifecycle.js +193 -0
- package/dist/lifecycle.js.map +1 -0
- package/dist/load-ts.d.ts +2 -0
- package/dist/load-ts.d.ts.map +1 -0
- package/dist/load-ts.js +20 -0
- package/dist/load-ts.js.map +1 -0
- package/dist/map-repo.d.ts +15 -0
- package/dist/map-repo.d.ts.map +1 -0
- package/dist/map-repo.js +36 -0
- package/dist/map-repo.js.map +1 -0
- package/dist/memory.d.ts +23 -0
- package/dist/memory.d.ts.map +1 -0
- package/dist/memory.js +84 -0
- package/dist/memory.js.map +1 -0
- package/dist/orchestrate.d.ts +61 -0
- package/dist/orchestrate.d.ts.map +1 -0
- package/dist/orchestrate.js +556 -0
- package/dist/orchestrate.js.map +1 -0
- package/dist/reflect.d.ts +12 -0
- package/dist/reflect.d.ts.map +1 -0
- package/dist/reflect.js +67 -0
- package/dist/reflect.js.map +1 -0
- package/dist/report.d.ts +3 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +56 -0
- package/dist/report.js.map +1 -0
- package/dist/rules.d.ts +16 -0
- package/dist/rules.d.ts.map +1 -0
- package/dist/rules.js +65 -0
- package/dist/rules.js.map +1 -0
- package/dist/telemetry-helper.d.ts +12 -0
- package/dist/telemetry-helper.d.ts.map +1 -0
- package/dist/telemetry-helper.js +40 -0
- package/dist/telemetry-helper.js.map +1 -0
- package/dist/up.d.ts +97 -0
- package/dist/up.d.ts.map +1 -0
- package/dist/up.js +656 -0
- package/dist/up.js.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// Claude subscription auth probe + login trigger for the keyless claude-bridge
|
|
2
|
+
// provider.
|
|
3
|
+
//
|
|
4
|
+
// claude-bridge (the default provider) routes through the Claude Agent SDK on
|
|
5
|
+
// SUBSCRIPTION auth — no API key. The SDK runs its own bundled native CLI, but
|
|
6
|
+
// the OAuth credentials it rides are minted by the external `claude` CLI's login.
|
|
7
|
+
// So: the `claude` binary is a LOGIN-TIME prerequisite (to sign in once), not a
|
|
8
|
+
// runtime one. This module probes the login state and triggers the sign-in.
|
|
9
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
// The default provider is claude-bridge (keyless, Claude subscription) when
|
|
12
|
+
// KEEL_PROVIDER_KIND is UNSET — the friendly "no API key" path. An explicit value
|
|
13
|
+
// passes through unchanged (even an unrecognized one, so a typo surfaces as a
|
|
14
|
+
// "not a recognized kind" warning rather than being silently swallowed).
|
|
15
|
+
// NOTE: the agentic CLI commands (orchestrate/build/learn) resolve their own
|
|
16
|
+
// default separately, because claude-bridge is single-shot only and throws on
|
|
17
|
+
// tool loops — defaulting THEM to claude-bridge would break them.
|
|
18
|
+
export function resolveProviderKind(env = process.env) {
|
|
19
|
+
const raw = env["KEEL_PROVIDER_KIND"];
|
|
20
|
+
return raw && raw.length > 0 ? raw : "claude-bridge";
|
|
21
|
+
}
|
|
22
|
+
const defaultRunner = (cmd, args) => {
|
|
23
|
+
try {
|
|
24
|
+
const stdout = execFileSync(cmd, args, { stdio: ["ignore", "pipe", "ignore"] }).toString();
|
|
25
|
+
return { status: 0, stdout };
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
const e = err;
|
|
29
|
+
if (e.code === "ENOENT")
|
|
30
|
+
return { status: 127, stdout: "" }; // binary not installed
|
|
31
|
+
return {
|
|
32
|
+
status: typeof e.status === "number" ? e.status : 1,
|
|
33
|
+
stdout: e.stdout != null ? String(e.stdout) : "",
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
// Probe the Claude subscription login state via `claude auth status --json`
|
|
38
|
+
// (returns {loggedIn, subscriptionType, email, ...}). This is the readiness
|
|
39
|
+
// signal for claude-bridge. cli-missing = the `claude` binary isn't installed;
|
|
40
|
+
// logged-out = installed but not signed in; authed = ready.
|
|
41
|
+
export function probeClaudeAuth(run = defaultRunner) {
|
|
42
|
+
const { status, stdout } = run("claude", ["auth", "status", "--json"]);
|
|
43
|
+
if (status === 127)
|
|
44
|
+
return { state: "cli-missing" };
|
|
45
|
+
try {
|
|
46
|
+
const parsed = JSON.parse(stdout);
|
|
47
|
+
if (parsed.loggedIn) {
|
|
48
|
+
return {
|
|
49
|
+
state: "authed",
|
|
50
|
+
...(parsed.subscriptionType ? { subscriptionType: parsed.subscriptionType } : {}),
|
|
51
|
+
...(parsed.email ? { email: parsed.email } : {}),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
return { state: "logged-out" };
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
// Non-JSON / unexpected output → safest to treat as logged-out and prompt.
|
|
58
|
+
return { state: "logged-out" };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export const CLAUDE_INSTALL_HINT = "Install the Claude CLI once to sign in (https://claude.ai/download), then run: keel login";
|
|
62
|
+
// Trigger the sign-in. "browser" runs `claude auth login` (interactive OAuth);
|
|
63
|
+
// "token" runs `claude setup-token` to mint a long-lived CLAUDE_CODE_OAUTH_TOKEN
|
|
64
|
+
// for headless/CI. Inherits stdio so the user sees the prompt/URL. Returns true
|
|
65
|
+
// on success, false if the binary is missing or login failed.
|
|
66
|
+
export function triggerClaudeLogin(mode = "browser") {
|
|
67
|
+
const args = mode === "token" ? ["setup-token"] : ["auth", "login"];
|
|
68
|
+
const res = spawnSync("claude", args, { stdio: "inherit" });
|
|
69
|
+
if (res.error && res.error.code === "ENOENT") {
|
|
70
|
+
process.stderr.write(pc.yellow(`\n${CLAUDE_INSTALL_HINT}\n`));
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return res.status === 0;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=claude-auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude-auth.js","sourceRoot":"","sources":["../src/claude-auth.ts"],"names":[],"mappings":"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"}
|
package/dist/doctor.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { probeClaudeAuth } from "./claude-auth.js";
|
|
2
|
+
export interface DoctorCheck {
|
|
3
|
+
name: string;
|
|
4
|
+
status: "ok" | "warn" | "fail";
|
|
5
|
+
detail: string;
|
|
6
|
+
}
|
|
7
|
+
export interface DoctorReport {
|
|
8
|
+
checks: DoctorCheck[];
|
|
9
|
+
exitCode: 0 | 1;
|
|
10
|
+
}
|
|
11
|
+
export declare function runDoctor(opts: {
|
|
12
|
+
cwd: string;
|
|
13
|
+
apiTimeoutMs?: number;
|
|
14
|
+
authProbe?: () => ReturnType<typeof probeClaudeAuth>;
|
|
15
|
+
}): Promise<DoctorReport>;
|
|
16
|
+
export declare function collectEnvPreflight(): string[];
|
|
17
|
+
export declare function printDoctorReport(report: DoctorReport): void;
|
|
18
|
+
//# sourceMappingURL=doctor.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../src/doctor.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,eAAe,EAAuB,MAAM,kBAAkB,CAAC;AAgBxE,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;IAC/B,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,QAAQ,EAAE,CAAC,GAAG,CAAC,CAAC;CACjB;AAMD,wBAAsB,SAAS,CAAC,IAAI,EAAE;IACpC,GAAG,EAAE,MAAM,CAAC;IACZ,YAAY,CAAC,EAAE,MAAM,CAAC;IAGtB,SAAS,CAAC,EAAE,MAAM,UAAU,CAAC,OAAO,eAAe,CAAC,CAAC;CACtD,GAAG,OAAO,CAAC,YAAY,CAAC,CA0exB;AAED,wBAAgB,mBAAmB,IAAI,MAAM,EAAE,CAc9C;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,YAAY,GAAG,IAAI,CAa5D"}
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
import { existsSync, statSync, readdirSync, readFileSync, writeFileSync, unlinkSync, mkdtempSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import pc from "picocolors";
|
|
5
|
+
import { loadPending } from "@keel_flow/learn";
|
|
6
|
+
import { cacheAgeMs, loadCache } from "@keel_flow/repo-map";
|
|
7
|
+
import { probeClaudeAuth, resolveProviderKind } from "./claude-auth.js";
|
|
8
|
+
const CI_NODE_MAJOR = 20;
|
|
9
|
+
function detectCaseInsensitiveFs() {
|
|
10
|
+
const dir = mkdtempSync(join(tmpdir(), "keel-fs-probe-"));
|
|
11
|
+
const lower = join(dir, "probe_keel.tmp");
|
|
12
|
+
const upper = join(dir, "PROBE_KEEL.TMP");
|
|
13
|
+
try {
|
|
14
|
+
writeFileSync(lower, "");
|
|
15
|
+
return existsSync(upper);
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
try {
|
|
19
|
+
unlinkSync(lower);
|
|
20
|
+
}
|
|
21
|
+
catch { }
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// Runs a battery of environmental sanity checks. Designed to fail
|
|
25
|
+
// loud-and-explanatory rather than silent — common newcomer pitfalls
|
|
26
|
+
// (Node too old, no provider, API unreachable) become a labelled list
|
|
27
|
+
// instead of a cryptic stack trace during `keel verify` or `keel adopt`.
|
|
28
|
+
export async function runDoctor(opts) {
|
|
29
|
+
const checks = [];
|
|
30
|
+
const cwd = opts.cwd;
|
|
31
|
+
const apiTimeoutMs = opts.apiTimeoutMs ?? 2000;
|
|
32
|
+
// 0. Environment preflight — surfaces local conditions that can mask CI divergence.
|
|
33
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
34
|
+
if (nodeMajor === CI_NODE_MAJOR) {
|
|
35
|
+
checks.push({
|
|
36
|
+
name: "Node version (env preflight)",
|
|
37
|
+
status: "ok",
|
|
38
|
+
detail: `node ${process.versions.node} — matches CI target (Node ${CI_NODE_MAJOR})`,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
else if (nodeMajor > CI_NODE_MAJOR) {
|
|
42
|
+
checks.push({
|
|
43
|
+
name: "Node version (env preflight)",
|
|
44
|
+
status: "warn",
|
|
45
|
+
detail: `node ${process.versions.node} — CI uses Node ${CI_NODE_MAJOR}; a local green here needs confirmation on the supported runtime (tsconfig lib / module resolution can differ between major versions)`,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
checks.push({
|
|
50
|
+
name: "Node version (env preflight)",
|
|
51
|
+
status: "fail",
|
|
52
|
+
detail: `node ${process.versions.node} — Keel requires >= ${CI_NODE_MAJOR}`,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const caseInsensitive = detectCaseInsensitiveFs();
|
|
56
|
+
if (caseInsensitive) {
|
|
57
|
+
checks.push({
|
|
58
|
+
name: "Filesystem case-sensitivity (env preflight)",
|
|
59
|
+
status: "warn",
|
|
60
|
+
detail: "case-insensitive filesystem detected — import paths that differ only in case will succeed locally but fail on the Linux CI runner; a local green is not a CI green",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
checks.push({
|
|
65
|
+
name: "Filesystem case-sensitivity (env preflight)",
|
|
66
|
+
status: "ok",
|
|
67
|
+
detail: "case-sensitive filesystem — import paths will behave the same as on Linux CI",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
// 1. Node version (legacy check kept for compatibility)
|
|
71
|
+
if (nodeMajor >= 20) {
|
|
72
|
+
checks.push({ name: "Node version", status: "ok", detail: `node ${process.versions.node}` });
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
checks.push({
|
|
76
|
+
name: "Node version",
|
|
77
|
+
status: "fail",
|
|
78
|
+
detail: `node ${process.versions.node} — Keel requires >= 20`,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
// 2. Architecture map
|
|
82
|
+
const archPath = join(cwd, "architecture/data.ts");
|
|
83
|
+
if (existsSync(archPath)) {
|
|
84
|
+
checks.push({ name: "architecture/data.ts", status: "ok", detail: archPath });
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
checks.push({
|
|
88
|
+
name: "architecture/data.ts",
|
|
89
|
+
status: "warn",
|
|
90
|
+
detail: `not found at ${archPath} — run from a Keel project root or use --cwd`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
// 3. Provider configuration. Default to claude-bridge (keyless Claude
|
|
94
|
+
// subscription) when KEEL_PROVIDER_KIND is unset — the friendly no-API-key path.
|
|
95
|
+
const providerKind = resolveProviderKind();
|
|
96
|
+
const anthKey = process.env["ANTHROPIC_API_KEY"];
|
|
97
|
+
const openaiKey = process.env["OPENAI_API_KEY"];
|
|
98
|
+
if (providerKind === "anthropic") {
|
|
99
|
+
if (anthKey) {
|
|
100
|
+
checks.push({
|
|
101
|
+
name: "Provider (anthropic)",
|
|
102
|
+
status: "ok",
|
|
103
|
+
detail: `ANTHROPIC_API_KEY set (last4: ${anthKey.slice(-4)})`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
checks.push({
|
|
108
|
+
name: "Provider (anthropic)",
|
|
109
|
+
status: "warn",
|
|
110
|
+
detail: "ANTHROPIC_API_KEY not set — spec compliance + adopt + extend will fail",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else if (providerKind === "openai-compatible") {
|
|
115
|
+
if (openaiKey) {
|
|
116
|
+
const baseURL = process.env["OPENAI_BASE_URL"] ?? "https://api.openai.com/v1";
|
|
117
|
+
checks.push({
|
|
118
|
+
name: "Provider (openai-compatible)",
|
|
119
|
+
status: "ok",
|
|
120
|
+
detail: `OPENAI_API_KEY set, baseURL=${baseURL}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
checks.push({
|
|
125
|
+
name: "Provider (openai-compatible)",
|
|
126
|
+
status: "warn",
|
|
127
|
+
detail: "OPENAI_API_KEY not set",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
else if (providerKind === "claude-bridge") {
|
|
132
|
+
// Probe the real subscription login state instead of assuming "ok" — a fresh
|
|
133
|
+
// user who never signed in would otherwise see green here and a dead agent.
|
|
134
|
+
if (process.env["CLAUDE_CODE_OAUTH_TOKEN"]) {
|
|
135
|
+
checks.push({
|
|
136
|
+
name: "Provider (claude-bridge)",
|
|
137
|
+
status: "ok",
|
|
138
|
+
detail: "CLAUDE_CODE_OAUTH_TOKEN set — keyless Claude subscription auth",
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
const auth = (opts.authProbe ?? probeClaudeAuth)();
|
|
143
|
+
if (auth.state === "authed") {
|
|
144
|
+
checks.push({
|
|
145
|
+
name: "Provider (claude-bridge)",
|
|
146
|
+
status: "ok",
|
|
147
|
+
detail: `signed in to Claude${auth.subscriptionType ? ` (${auth.subscriptionType})` : ""} — no API key required`,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
else if (auth.state === "logged-out") {
|
|
151
|
+
checks.push({
|
|
152
|
+
name: "Provider (claude-bridge)",
|
|
153
|
+
status: "warn",
|
|
154
|
+
detail: "not signed in to Claude — run `keel login` to enable the agent (keyless)",
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
checks.push({
|
|
159
|
+
name: "Provider (claude-bridge)",
|
|
160
|
+
status: "warn",
|
|
161
|
+
detail: "Claude CLI not found — install from https://claude.ai/download, then run `keel login`",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
checks.push({
|
|
168
|
+
name: `Provider (${providerKind})`,
|
|
169
|
+
status: "warn",
|
|
170
|
+
detail: `KEEL_PROVIDER_KIND=${providerKind} is not a recognized kind (expected anthropic, openai-compatible, or claude-bridge)`,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
// 4. Workspace API reachability
|
|
174
|
+
const apiUrl = process.env["KEEL_API_URL"] ?? process.env["KEEL_WORKSPACE_URL"];
|
|
175
|
+
if (apiUrl) {
|
|
176
|
+
const healthUrl = `${apiUrl.replace(/\/$/, "")}/health`;
|
|
177
|
+
try {
|
|
178
|
+
const controller = new AbortController();
|
|
179
|
+
const timer = setTimeout(() => controller.abort(), apiTimeoutMs);
|
|
180
|
+
const res = await fetch(healthUrl, { signal: controller.signal });
|
|
181
|
+
clearTimeout(timer);
|
|
182
|
+
if (res.ok) {
|
|
183
|
+
checks.push({ name: "Workspace API /health", status: "ok", detail: healthUrl });
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
checks.push({
|
|
187
|
+
name: "Workspace API /health",
|
|
188
|
+
status: "warn",
|
|
189
|
+
detail: `${healthUrl} returned ${res.status}`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch (err) {
|
|
194
|
+
checks.push({
|
|
195
|
+
name: "Workspace API /health",
|
|
196
|
+
status: "warn",
|
|
197
|
+
detail: `${healthUrl} unreachable: ${err instanceof Error ? err.message : String(err)}`,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
checks.push({
|
|
203
|
+
name: "Workspace API",
|
|
204
|
+
status: "warn",
|
|
205
|
+
detail: "KEEL_API_URL not set — usage events + verify-run history will be local-only",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
// 5. Single-user mode flag (informational; matters for the workspace + api)
|
|
209
|
+
const singleUser = process.env["KEEL_SINGLE_USER_MODE"];
|
|
210
|
+
checks.push({
|
|
211
|
+
name: "Single-user mode",
|
|
212
|
+
status: "ok",
|
|
213
|
+
detail: singleUser === "true" || singleUser === undefined
|
|
214
|
+
? "enabled (default; no auth required)"
|
|
215
|
+
: "disabled — multi-user OAuth must be wired before the workspace API will authenticate any request",
|
|
216
|
+
});
|
|
217
|
+
// 6. Telemetry
|
|
218
|
+
const telemetryOff = process.env["KEEL_TELEMETRY"] === "off";
|
|
219
|
+
if (telemetryOff) {
|
|
220
|
+
checks.push({
|
|
221
|
+
name: "Telemetry",
|
|
222
|
+
status: "ok",
|
|
223
|
+
detail: "off (KEEL_TELEMETRY=off — local + remote disabled)",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
const workspaceId = process.env["KEEL_WORKSPACE_ID"];
|
|
228
|
+
const telemetryApiUrl = process.env["KEEL_API_URL"];
|
|
229
|
+
if (telemetryApiUrl && workspaceId) {
|
|
230
|
+
checks.push({
|
|
231
|
+
name: "Telemetry",
|
|
232
|
+
status: "ok",
|
|
233
|
+
detail: `forwarding to ${telemetryApiUrl}`,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
else {
|
|
237
|
+
checks.push({ name: "Telemetry", status: "ok", detail: "local-only" });
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// 7a. Orchestrate readiness — at least one LLM provider key configured
|
|
241
|
+
const orchestrateKey = anthKey ?? openaiKey;
|
|
242
|
+
if (orchestrateKey) {
|
|
243
|
+
const which = anthKey ? "anthropic" : "openai-compatible";
|
|
244
|
+
checks.push({
|
|
245
|
+
name: "Orchestrate readiness",
|
|
246
|
+
status: "ok",
|
|
247
|
+
detail: `at least one provider key configured (${which})`,
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
else {
|
|
251
|
+
checks.push({
|
|
252
|
+
name: "Orchestrate readiness",
|
|
253
|
+
status: "warn",
|
|
254
|
+
detail: "no LLM provider key configured — keel orchestrate will refuse to run. Run `keel setup` to fix.",
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
// 7b. Active builds — informational. Lists .keel/builds/<slug>/state.json with state.
|
|
258
|
+
const buildsRoot = join(cwd, ".keel", "builds");
|
|
259
|
+
if (existsSync(buildsRoot) && statSync(buildsRoot).isDirectory()) {
|
|
260
|
+
const entries = readdirSync(buildsRoot, { withFileTypes: true }).filter((d) => d.isDirectory());
|
|
261
|
+
const lines = [];
|
|
262
|
+
for (const entry of entries) {
|
|
263
|
+
const statePath = join(buildsRoot, entry.name, "state.json");
|
|
264
|
+
if (!existsSync(statePath))
|
|
265
|
+
continue;
|
|
266
|
+
try {
|
|
267
|
+
const parsed = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
268
|
+
lines.push(`${entry.name}=${parsed.state ?? "?"}`);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
lines.push(`${entry.name}=unreadable`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
checks.push({
|
|
275
|
+
name: "Active builds",
|
|
276
|
+
status: "ok",
|
|
277
|
+
detail: lines.length === 0 ? "none" : lines.join(", "),
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
checks.push({
|
|
282
|
+
name: "Active builds",
|
|
283
|
+
status: "ok",
|
|
284
|
+
detail: "none (no .keel/builds directory)",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
// 7c. Pending reflections
|
|
288
|
+
const reflectionsRoot = join(cwd, ".keel", "reflections");
|
|
289
|
+
if (existsSync(reflectionsRoot) && statSync(reflectionsRoot).isDirectory()) {
|
|
290
|
+
const count = readdirSync(reflectionsRoot).filter((f) => f.endsWith(".md")).length;
|
|
291
|
+
checks.push({
|
|
292
|
+
name: "Pending reflections",
|
|
293
|
+
status: "ok",
|
|
294
|
+
detail: count === 0 ? "none" : `${count} on disk (run \`keel reflect ls\` to inspect)`,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
else {
|
|
298
|
+
checks.push({
|
|
299
|
+
name: "Pending reflections",
|
|
300
|
+
status: "ok",
|
|
301
|
+
detail: "none (no .keel/reflections directory)",
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
// 7d. Pending learnings
|
|
305
|
+
try {
|
|
306
|
+
const pending = await loadPending({ cwd });
|
|
307
|
+
checks.push({
|
|
308
|
+
name: "Pending learnings",
|
|
309
|
+
status: "ok",
|
|
310
|
+
detail: pending.length === 0
|
|
311
|
+
? "none"
|
|
312
|
+
: `${pending.length} pending (run \`keel learn\` to review)`,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
checks.push({
|
|
317
|
+
name: "Pending learnings",
|
|
318
|
+
status: "warn",
|
|
319
|
+
detail: `count failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
// 7e. Autonomous mode (Phase 3g)
|
|
323
|
+
const autonomous = process.env["KEEL_AUTONOMOUS"] === "true";
|
|
324
|
+
const apiUrlForGoals = process.env["KEEL_API_URL"];
|
|
325
|
+
const workspaceIdForGoals = process.env["KEEL_WORKSPACE_ID"];
|
|
326
|
+
if (autonomous) {
|
|
327
|
+
if (apiUrlForGoals && workspaceIdForGoals) {
|
|
328
|
+
try {
|
|
329
|
+
const url = `${apiUrlForGoals.replace(/\/$/, "")}/goals.list?input=${encodeURIComponent(JSON.stringify({ workspaceId: workspaceIdForGoals, status: "active" }))}`;
|
|
330
|
+
const res = await fetch(url, { signal: AbortSignal.timeout(apiTimeoutMs) });
|
|
331
|
+
if (res.ok) {
|
|
332
|
+
const body = (await res.json());
|
|
333
|
+
const active = body.result?.data?.length ?? 0;
|
|
334
|
+
let pending = 0;
|
|
335
|
+
for (const g of body.result?.data ?? []) {
|
|
336
|
+
const aUrl = `${apiUrlForGoals.replace(/\/$/, "")}/goals.actions.list?input=${encodeURIComponent(JSON.stringify({ goalId: g.id, status: "pending-approval", limit: 50 }))}`;
|
|
337
|
+
try {
|
|
338
|
+
const ar = await fetch(aUrl, { signal: AbortSignal.timeout(apiTimeoutMs) });
|
|
339
|
+
if (ar.ok) {
|
|
340
|
+
const ab = (await ar.json());
|
|
341
|
+
pending += ab.result?.data?.length ?? 0;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// tolerated; we report best-effort counts
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
checks.push({
|
|
349
|
+
name: "Autonomous mode",
|
|
350
|
+
status: "ok",
|
|
351
|
+
detail: `KEEL_AUTONOMOUS=true; ${active} active goal(s), ${pending} pending action(s)`,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
checks.push({
|
|
356
|
+
name: "Autonomous mode",
|
|
357
|
+
status: "warn",
|
|
358
|
+
detail: `KEEL_AUTONOMOUS=true but API returned ${res.status}`,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
catch (err) {
|
|
363
|
+
checks.push({
|
|
364
|
+
name: "Autonomous mode",
|
|
365
|
+
status: "warn",
|
|
366
|
+
detail: `KEEL_AUTONOMOUS=true but API unreachable: ${err instanceof Error ? err.message : String(err)}`,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
checks.push({
|
|
372
|
+
name: "Autonomous mode",
|
|
373
|
+
status: "warn",
|
|
374
|
+
detail: "KEEL_AUTONOMOUS=true but KEEL_API_URL or KEEL_WORKSPACE_ID is not set — scheduler cannot reach the goal store",
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
checks.push({
|
|
380
|
+
name: "Autonomous mode",
|
|
381
|
+
status: "ok",
|
|
382
|
+
detail: "off (default; set KEEL_AUTONOMOUS=true on the api process to enable)",
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
// 7f. KB pipeline — Phase 4. Shows active embedder + context-aware flag +
|
|
386
|
+
// rerank state without ever instantiating a heavy model (HF loaders are not
|
|
387
|
+
// touched here; we read env-var precedence the way createEmbedder does).
|
|
388
|
+
const kbModel = (() => {
|
|
389
|
+
const forced = process.env["KEEL_KB_EMBEDDER"];
|
|
390
|
+
if (forced)
|
|
391
|
+
return forced;
|
|
392
|
+
if (process.env["VOYAGE_API_KEY"])
|
|
393
|
+
return "voyage-context-3";
|
|
394
|
+
if (process.env["OPENAI_API_KEY"])
|
|
395
|
+
return "text-embedding-3-small";
|
|
396
|
+
return "Xenova/all-MiniLM-L6-v2";
|
|
397
|
+
})();
|
|
398
|
+
const kbDim = kbModel === "voyage-context-3"
|
|
399
|
+
? 1024
|
|
400
|
+
: kbModel === "text-embedding-3-small"
|
|
401
|
+
? 1536
|
|
402
|
+
: 384;
|
|
403
|
+
const kbContextAware = kbModel === "voyage-context-3";
|
|
404
|
+
const rerank = process.env["KEEL_KB_RERANK"] === "true";
|
|
405
|
+
checks.push({
|
|
406
|
+
name: "KB pipeline",
|
|
407
|
+
status: "ok",
|
|
408
|
+
detail: `embedder=${kbModel} (${kbDim}-dim, ${kbContextAware ? "context-aware" : "not context-aware"}); rerank=${rerank ? "enabled" : "disabled"}`,
|
|
409
|
+
});
|
|
410
|
+
// 7g. Repo map (Phase 5). Auto-derived companion to architecture/data.ts.
|
|
411
|
+
// Status: present and fresh (< 10min), present but stale, or absent.
|
|
412
|
+
const repoMap = loadCache(cwd);
|
|
413
|
+
if (repoMap) {
|
|
414
|
+
const ageMs = cacheAgeMs(cwd);
|
|
415
|
+
const ageMin = ageMs !== null ? Math.round(ageMs / 60_000) : null;
|
|
416
|
+
const fresh = ageMs !== null && ageMs < 10 * 60 * 1000;
|
|
417
|
+
checks.push({
|
|
418
|
+
name: "Repo map",
|
|
419
|
+
status: "ok",
|
|
420
|
+
detail: `${repoMap.entries.length} entries from ${repoMap.files.length} files; age=${ageMin === null ? "?" : `${ageMin}min`}${fresh ? "" : " (stale; will rebuild on next session)"}`,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
checks.push({
|
|
425
|
+
name: "Repo map",
|
|
426
|
+
status: "ok",
|
|
427
|
+
detail: "absent (run `keel map --repo` to generate .keel/repo-map.cache.json)",
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
// 7h. ESLint plugin (Phase 6). Heuristic scan of eslint.config.{js,mjs,cjs,ts}
|
|
431
|
+
// for @keel_flow/eslint-plugin import; same plugin powers the editor-time
|
|
432
|
+
// surface of the principle checks that keel verify runs at gate time.
|
|
433
|
+
const eslintConfigCandidates = [
|
|
434
|
+
"eslint.config.js",
|
|
435
|
+
"eslint.config.mjs",
|
|
436
|
+
"eslint.config.cjs",
|
|
437
|
+
"eslint.config.ts",
|
|
438
|
+
];
|
|
439
|
+
let eslintConfigPath = null;
|
|
440
|
+
for (const candidate of eslintConfigCandidates) {
|
|
441
|
+
const p = join(cwd, candidate);
|
|
442
|
+
if (existsSync(p)) {
|
|
443
|
+
eslintConfigPath = p;
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
if (!eslintConfigPath) {
|
|
448
|
+
checks.push({
|
|
449
|
+
name: "ESLint plugin",
|
|
450
|
+
status: "warn",
|
|
451
|
+
detail: "no eslint.config.{js,mjs,cjs,ts} found — add @keel_flow/eslint-plugin to enable editor-time principle checks",
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
else {
|
|
455
|
+
let configText = "";
|
|
456
|
+
try {
|
|
457
|
+
configText = readFileSync(eslintConfigPath, "utf-8");
|
|
458
|
+
}
|
|
459
|
+
catch {
|
|
460
|
+
configText = "";
|
|
461
|
+
}
|
|
462
|
+
if (configText.includes("@keel_flow/eslint-plugin")) {
|
|
463
|
+
checks.push({
|
|
464
|
+
name: "ESLint plugin",
|
|
465
|
+
status: "ok",
|
|
466
|
+
detail: `@keel_flow/eslint-plugin wired in ${eslintConfigPath}`,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
else {
|
|
470
|
+
checks.push({
|
|
471
|
+
name: "ESLint plugin",
|
|
472
|
+
status: "warn",
|
|
473
|
+
detail: `@keel_flow/eslint-plugin not referenced in ${eslintConfigPath} — add it to surface principle warnings in your editor`,
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// 7i. Keel template detection. Confirms the project was scaffolded
|
|
478
|
+
// from one of the known templates (framework or ai-app) by checking
|
|
479
|
+
// the canonical pair: eslint.config.* + architecture/data.ts. Both
|
|
480
|
+
// templates emit both; absence means the project was either created
|
|
481
|
+
// by hand or has drifted off-template.
|
|
482
|
+
const archPresent = existsSync(archPath);
|
|
483
|
+
const eslintPresent = eslintConfigPath !== null;
|
|
484
|
+
if (archPresent && eslintPresent) {
|
|
485
|
+
checks.push({
|
|
486
|
+
name: "Keel template",
|
|
487
|
+
status: "ok",
|
|
488
|
+
detail: "architecture/data.ts + eslint.config.* present (known Keel template shape)",
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
else if (!archPresent && !eslintPresent) {
|
|
492
|
+
checks.push({
|
|
493
|
+
name: "Keel template",
|
|
494
|
+
status: "warn",
|
|
495
|
+
detail: "neither architecture/data.ts nor eslint.config.* present — this directory does not look like a Keel project (run `create-keel-app` to scaffold one)",
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
else {
|
|
499
|
+
const missing = !archPresent ? "architecture/data.ts" : "eslint.config.*";
|
|
500
|
+
checks.push({
|
|
501
|
+
name: "Keel template",
|
|
502
|
+
status: "warn",
|
|
503
|
+
detail: `${missing} missing — project deviates from the canonical Keel template shape`,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
// 7. Principles file (Keel-shaped project)
|
|
507
|
+
const principlesPath = join(cwd, "principles/index.ts");
|
|
508
|
+
if (existsSync(principlesPath) && statSync(principlesPath).isFile()) {
|
|
509
|
+
checks.push({ name: "principles/index.ts", status: "ok", detail: principlesPath });
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
checks.push({
|
|
513
|
+
name: "principles/index.ts",
|
|
514
|
+
status: "warn",
|
|
515
|
+
detail: `not found at ${principlesPath} — verify will skip principle checks`,
|
|
516
|
+
});
|
|
517
|
+
}
|
|
518
|
+
const failed = checks.some((c) => c.status === "fail");
|
|
519
|
+
return { checks, exitCode: failed ? 1 : 0 };
|
|
520
|
+
}
|
|
521
|
+
export function collectEnvPreflight() {
|
|
522
|
+
const warnings = [];
|
|
523
|
+
const nodeMajor = parseInt(process.versions.node.split(".")[0] ?? "0", 10);
|
|
524
|
+
if (nodeMajor !== CI_NODE_MAJOR) {
|
|
525
|
+
warnings.push(`Node ${process.versions.node} detected; CI uses Node ${CI_NODE_MAJOR}. A local green may not hold on the supported runtime.`);
|
|
526
|
+
}
|
|
527
|
+
if (detectCaseInsensitiveFs()) {
|
|
528
|
+
warnings.push("Case-insensitive filesystem detected. Import paths that differ only in case pass locally but fail on Linux CI.");
|
|
529
|
+
}
|
|
530
|
+
return warnings;
|
|
531
|
+
}
|
|
532
|
+
export function printDoctorReport(report) {
|
|
533
|
+
process.stdout.write("\nKeel Doctor\n");
|
|
534
|
+
process.stdout.write("─".repeat(50) + "\n");
|
|
535
|
+
for (const c of report.checks) {
|
|
536
|
+
const icon = c.status === "ok" ? pc.green("✓") : c.status === "warn" ? pc.yellow("⚠") : pc.red("✗");
|
|
537
|
+
process.stdout.write(` ${icon} ${c.name}\n ${pc.dim(c.detail)}\n`);
|
|
538
|
+
}
|
|
539
|
+
process.stdout.write("\n");
|
|
540
|
+
if (report.exitCode === 0) {
|
|
541
|
+
process.stdout.write(pc.green("✓ All required checks passed.\n"));
|
|
542
|
+
}
|
|
543
|
+
else {
|
|
544
|
+
process.stdout.write(pc.red("✗ One or more critical checks failed.\n"));
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
//# sourceMappingURL=doctor.js.map
|