ashlr-plugin 1.36.4 → 1.37.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.
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/README.md +49 -8
- package/commands/ashlr-doctor.md +29 -0
- package/commands/ashlr-status.md +1 -1
- package/docs/PHANTOM-USAGE.md +58 -0
- package/docs/architecture.md +5 -5
- package/docs/ashlr-handshake.md +36 -0
- package/docs/assets/og.svg +1 -1
- package/docs/benchmarks-v2.json +226 -177
- package/docs/benchmarks.md +20 -10
- package/docs/competitive-wozcode.md +2 -2
- package/docs/embeddings-gateway.md +39 -0
- package/docs/index.html +3 -3
- package/docs/legal.md +1 -1
- package/docs/llm-providers.md +15 -1
- package/docs/multi-host-mcp.md +25 -3
- package/docs/phantom-preflight.md +23 -0
- package/docs/team-genome.md +2 -1
- package/hooks/_hook-timing-ledger.ts +2 -9
- package/hooks/hooks.json +3 -106
- package/hooks/posttooluse-stale-result.ts +3 -0
- package/hooks/pretooluse-common.ts +1 -1
- package/hooks/pretooluse-notebookedit.ts +1 -1
- package/hooks/pretooluse-write.ts +1 -1
- package/hooks/session-start.ts +48 -26
- package/package.json +9 -4
- package/ports/cursor/mcp.json +1 -1
- package/scripts/_read-thresholds.ts +119 -0
- package/scripts/ashlr-mcp.mjs +15 -0
- package/scripts/ashlr-mcp.ts +3 -0
- package/scripts/ashlr-plugin.mjs +59 -0
- package/scripts/bench-read-real.ts +90 -0
- package/scripts/bootstrap.mjs +14 -4
- package/scripts/cli.ts +35 -10
- package/scripts/context-status.ts +7 -2
- package/scripts/deploy-smoke.sh +8 -4
- package/scripts/doctor.ts +171 -42
- package/scripts/genome-auto-consolidate.ts +4 -2
- package/scripts/genome-init.ts +3 -3
- package/scripts/genome-key.ts +64 -7
- package/scripts/handshake.mjs +51 -0
- package/scripts/hook-bootstrap.mjs +170 -17
- package/scripts/mcp-entrypoint.ts +3 -0
- package/scripts/ollama-setup.ts +5 -4
- package/scripts/run-benchmark.ts +28 -18
- package/scripts/upgrade-flow.ts +9 -14
- package/servers/_base-urls.ts +48 -0
- package/servers/_companions.ts +258 -0
- package/servers/_cwd-clamp.ts +38 -1
- package/servers/_edit-match.ts +12 -2
- package/servers/_embedding-model.ts +77 -0
- package/servers/_gateway-target.ts +179 -0
- package/servers/_llm-providers/anthropic.ts +10 -4
- package/servers/_llm-providers/cloud.ts +9 -7
- package/servers/_llm-providers/index.ts +8 -6
- package/servers/_llm-providers/local.ts +9 -5
- package/servers/_mcp-host.ts +6 -0
- package/servers/_miss-tracker.ts +121 -0
- package/servers/_phantom-exec.ts +135 -0
- package/servers/_phantom-preflight.ts +130 -0
- package/servers/_pid-alive.ts +40 -0
- package/servers/_pro-token-keychain.ts +149 -0
- package/servers/_redact-keys.ts +75 -0
- package/servers/_rg-text.ts +111 -0
- package/servers/_router.ts +20 -2
- package/servers/_run-with-timeout.ts +56 -12
- package/servers/_test-watch.ts +1 -10
- package/servers/_token-measure.ts +3 -1
- package/servers/_tool-base.ts +6 -0
- package/servers/bash-server.ts +1 -10
- package/servers/edit-server.ts +24 -1
- package/servers/efficiency-server.ts +12 -2
- package/servers/genome-server-handlers.ts +2 -2
- package/servers/genome-server.ts +17 -17
- package/servers/grep-server.ts +6 -2
- package/servers/orient-server-handlers.ts +3 -2
- package/servers/orient-server.ts +16 -9
- package/servers/read-server.ts +1 -1
- package/servers/write-server-handlers.ts +10 -2
package/scripts/doctor.ts
CHANGED
|
@@ -10,11 +10,15 @@
|
|
|
10
10
|
* signatures stable.
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
|
+
import { describePhantomExec, detectPhantomExec } from "../servers/_phantom-exec.ts";
|
|
14
|
+
import { type CompanionResult, type CompanionRunner, detectCompanions } from "../servers/_companions.ts";
|
|
15
|
+
import { preflightMode, runPhantomPreflight, type PreflightRunner } from "../servers/_phantom-preflight.ts";
|
|
13
16
|
import { spawn } from "bun";
|
|
17
|
+
import { runWithTimeout } from "../servers/_run-with-timeout.ts";
|
|
14
18
|
import { existsSync, statSync } from "fs";
|
|
15
19
|
import { readFile } from "fs/promises";
|
|
16
20
|
import { devNull, homedir } from "os";
|
|
17
|
-
import { dirname, join, resolve } from "path";
|
|
21
|
+
import { basename, dirname, join, resolve } from "path";
|
|
18
22
|
import { fileURLToPath } from "url";
|
|
19
23
|
import { c, sym, box, isColorEnabled } from "./ui.ts";
|
|
20
24
|
import { bunBinaryOnDisk } from "./bun-resolve.mjs";
|
|
@@ -23,7 +27,8 @@ import { selectProvider } from "../servers/_llm-providers/index.ts";
|
|
|
23
27
|
import { proTokenCachePath, isProAssumeEnabled } from "../servers/_pro.ts";
|
|
24
28
|
import { getDoctorHookHealth } from "./hook-timings-report.ts";
|
|
25
29
|
|
|
26
|
-
|
|
30
|
+
/** `info` is shown with its fix hint but never counted as a warning or failure. */
|
|
31
|
+
export type Status = "ok" | "warn" | "fail" | "info";
|
|
27
32
|
export interface Line {
|
|
28
33
|
status: Status;
|
|
29
34
|
label: string;
|
|
@@ -37,11 +42,44 @@ export interface Section {
|
|
|
37
42
|
export interface Report {
|
|
38
43
|
header: string;
|
|
39
44
|
sections: Section[];
|
|
45
|
+
companions: CompanionResult[];
|
|
40
46
|
warnings: number;
|
|
41
47
|
failures: number;
|
|
42
48
|
}
|
|
43
49
|
|
|
44
|
-
|
|
50
|
+
/**
|
|
51
|
+
* The shared `ashlr.doctor` v1 wrapper (ashlr-stack docs/ashlr-handshake.md,
|
|
52
|
+
* "Doctor JSON") for `--json`. Additive: header, sections, companions,
|
|
53
|
+
* warnings and failures stay, and the exit code does not change. Mapping:
|
|
54
|
+
* failures > 0 is fail, else warnings > 0 is warn. `checks` flatten the
|
|
55
|
+
* section lines (`id` is the section and label in kebab case).
|
|
56
|
+
*/
|
|
57
|
+
export function withDoctorEnvelope(report: Report) {
|
|
58
|
+
const kebab = (s: string) =>
|
|
59
|
+
s
|
|
60
|
+
.toLowerCase()
|
|
61
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
62
|
+
.replace(/^-+|-+$/g, "") || "check";
|
|
63
|
+
const checks = report.sections.flatMap((sec) =>
|
|
64
|
+
sec.lines.map((l) => ({
|
|
65
|
+
id: kebab(`${sec.title}-${l.label}`),
|
|
66
|
+
status: l.status,
|
|
67
|
+
summary: l.detail ? `${l.label}: ${l.detail}` : l.label,
|
|
68
|
+
})),
|
|
69
|
+
);
|
|
70
|
+
const status = report.failures > 0 ? "fail" : report.warnings > 0 ? "warn" : "ok";
|
|
71
|
+
return {
|
|
72
|
+
...report,
|
|
73
|
+
kind: "ashlr.doctor" as const,
|
|
74
|
+
schema_version: 1 as const,
|
|
75
|
+
tool: "ashlr-plugin",
|
|
76
|
+
status,
|
|
77
|
+
native_status: report.failures > 0 ? "failures" : report.warnings > 0 ? "warnings" : "ok",
|
|
78
|
+
checks,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const GLYPH: Record<Status, string> = { ok: sym.check, warn: sym.warn, fail: sym.cross, info: sym.dot };
|
|
45
83
|
|
|
46
84
|
/** Colored glyph for TTY rendering; falls back to the plain glyph otherwise. */
|
|
47
85
|
function coloredGlyph(status: Status): string {
|
|
@@ -49,6 +87,7 @@ function coloredGlyph(status: Status): string {
|
|
|
49
87
|
if (!isColorEnabled()) return g;
|
|
50
88
|
if (status === "ok") return c.green(g);
|
|
51
89
|
if (status === "warn") return c.yellow(g);
|
|
90
|
+
if (status === "info") return c.dim(g);
|
|
52
91
|
return c.red(g);
|
|
53
92
|
}
|
|
54
93
|
|
|
@@ -108,10 +147,22 @@ export interface ProbeResult {
|
|
|
108
147
|
error?: string;
|
|
109
148
|
}
|
|
110
149
|
|
|
150
|
+
/**
|
|
151
|
+
* The bun binary to run probes with. Prefer the binary running doctor right
|
|
152
|
+
* now (`process.execPath`) over a PATH lookup: a PATH `bun` can be a shell
|
|
153
|
+
* shim that runs the real binary as a grandchild. Falls back to "bun" when
|
|
154
|
+
* doctor itself is not running under bun.
|
|
155
|
+
*/
|
|
156
|
+
export function realBunBinary(execPath: string = process.execPath): string {
|
|
157
|
+
const base = basename(execPath).toLowerCase();
|
|
158
|
+
return base === "bun" || base === "bun.exe" || base.startsWith("bun-") ? execPath : "bun";
|
|
159
|
+
}
|
|
160
|
+
|
|
111
161
|
export async function probeServer(
|
|
112
162
|
name: string,
|
|
113
163
|
scriptPath: string,
|
|
114
164
|
perServerTimeoutMs = 2000,
|
|
165
|
+
signal?: AbortSignal,
|
|
115
166
|
): Promise<ProbeResult> {
|
|
116
167
|
if (!existsSync(scriptPath)) {
|
|
117
168
|
return { server: name, ok: false, tools: [], error: "server script missing" };
|
|
@@ -125,34 +176,20 @@ export async function probeServer(
|
|
|
125
176
|
const listReq = { jsonrpc: "2.0", id: 2, method: "tools/list", params: {} };
|
|
126
177
|
const payload = JSON.stringify(initReq) + "\n" + JSON.stringify(listReq) + "\n";
|
|
127
178
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
179
|
+
// runWithTimeout spawns the probe in its own process group and kills the
|
|
180
|
+
// whole group on timeout or abort. `proc.kill()` on a Bun.spawn child only
|
|
181
|
+
// reached the direct child, so when `bun` on PATH is a shell shim (pnpm and
|
|
182
|
+
// npm global installs) the real bun ran as a grandchild and survived,
|
|
183
|
+
// spinning at full CPU long after doctor or the test runner exited.
|
|
184
|
+
const res = await runWithTimeout({
|
|
185
|
+
command: realBunBinary(),
|
|
186
|
+
args: ["run", scriptPath],
|
|
187
|
+
env: { ...process.env } as Record<string, string>,
|
|
188
|
+
input: payload,
|
|
189
|
+
timeoutMs: perServerTimeoutMs,
|
|
190
|
+
signal,
|
|
134
191
|
});
|
|
135
|
-
|
|
136
|
-
const timer = setTimeout(() => {
|
|
137
|
-
try { proc.kill(); } catch {}
|
|
138
|
-
}, perServerTimeoutMs);
|
|
139
|
-
|
|
140
|
-
try {
|
|
141
|
-
proc.stdin.write(payload);
|
|
142
|
-
await proc.stdin.end();
|
|
143
|
-
} catch {
|
|
144
|
-
// proc may already be dead
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
let out = "";
|
|
148
|
-
try {
|
|
149
|
-
out = await new Response(proc.stdout).text();
|
|
150
|
-
} catch {
|
|
151
|
-
// stream errored
|
|
152
|
-
}
|
|
153
|
-
// best-effort: we've already captured stdout and only need the process to settle; exit-code and signal reasons are not used in the probe.
|
|
154
|
-
await proc.exited.catch(() => {});
|
|
155
|
-
clearTimeout(timer);
|
|
192
|
+
const out = res.stdout;
|
|
156
193
|
|
|
157
194
|
const tools: string[] = [];
|
|
158
195
|
let sawListResult = false;
|
|
@@ -179,17 +216,26 @@ export async function probeAll(
|
|
|
179
216
|
totalCapMs = 5000,
|
|
180
217
|
perServerTimeoutMs = 2000,
|
|
181
218
|
): Promise<ProbeResult[]> {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
servers.map((s) => ({ server: s.name, ok: false, tools: [], error: "total probe cap exceeded" })),
|
|
188
|
-
),
|
|
189
|
-
totalCapMs,
|
|
190
|
-
),
|
|
219
|
+
// When the total cap wins the race, abort every probe still running so no
|
|
220
|
+
// server process (or its descendants) outlives this call.
|
|
221
|
+
const abort = new AbortController();
|
|
222
|
+
const work = Promise.all(
|
|
223
|
+
servers.map((s) => probeServer(s.name, s.script, perServerTimeoutMs, abort.signal)),
|
|
191
224
|
);
|
|
192
|
-
|
|
225
|
+
let capTimer: ReturnType<typeof setTimeout> | undefined;
|
|
226
|
+
const cap = new Promise<ProbeResult[]>((resolveP) => {
|
|
227
|
+
capTimer = setTimeout(() => {
|
|
228
|
+
abort.abort();
|
|
229
|
+
resolveP(
|
|
230
|
+
servers.map((s) => ({ server: s.name, ok: false, tools: [], error: "total probe cap exceeded" })),
|
|
231
|
+
);
|
|
232
|
+
}, totalCapMs);
|
|
233
|
+
});
|
|
234
|
+
try {
|
|
235
|
+
return await Promise.race([work, cap]);
|
|
236
|
+
} finally {
|
|
237
|
+
clearTimeout(capTimer);
|
|
238
|
+
}
|
|
193
239
|
}
|
|
194
240
|
|
|
195
241
|
// ---------- project hint check ----------
|
|
@@ -299,6 +345,25 @@ export interface CloudPingResult {
|
|
|
299
345
|
error?: string;
|
|
300
346
|
}
|
|
301
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Metadata-only probe for the optional Phantom CLI. Runs `phantom --version`
|
|
350
|
+
* and nothing else: it never runs reveal, never reads the vault or .env
|
|
351
|
+
* files, and never touches secret values. Returns the first line of output,
|
|
352
|
+
* or null when the binary is missing or fails.
|
|
353
|
+
*/
|
|
354
|
+
export async function getPhantomVersion(bin = "phantom"): Promise<string | null> {
|
|
355
|
+
try {
|
|
356
|
+
const proc = spawn([bin, "--version"], { stdout: "pipe", stderr: "ignore", stdin: "ignore" });
|
|
357
|
+
const out = await new Response(proc.stdout).text();
|
|
358
|
+
const code = await proc.exited;
|
|
359
|
+
if (code !== 0) return null;
|
|
360
|
+
const line = out.trim().split("\n")[0]?.trim();
|
|
361
|
+
return line ? line.slice(0, 80) : null;
|
|
362
|
+
} catch {
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
302
367
|
export interface BuildOpts {
|
|
303
368
|
root: string;
|
|
304
369
|
home?: string;
|
|
@@ -309,6 +374,14 @@ export interface BuildOpts {
|
|
|
309
374
|
probe?: (servers: Array<{ name: string; script: string }>) => Promise<ProbeResult[]>;
|
|
310
375
|
bunVersion?: () => Promise<string | null>;
|
|
311
376
|
bunOffPath?: () => Promise<string | null>;
|
|
377
|
+
/** Override for testing the phantom check (returns version string or null). */
|
|
378
|
+
phantomVersion?: () => Promise<string | null>;
|
|
379
|
+
/** Environment to inspect for a phantom exec session (defaults to process.env). */
|
|
380
|
+
env?: Record<string, string | undefined>;
|
|
381
|
+
/** Runner for the opt-in `phantom agent doctor --json --strict` preflight (tests). */
|
|
382
|
+
phantomPreflight?: PreflightRunner;
|
|
383
|
+
/** Runner for the `--ashlr-handshake` companion probes (tests). */
|
|
384
|
+
companionRunner?: CompanionRunner;
|
|
312
385
|
/** Override for testing cloud ping without real network calls */
|
|
313
386
|
pingCloud?: (url: string, timeoutMs: number) => Promise<CloudPingResult>;
|
|
314
387
|
}
|
|
@@ -376,6 +449,7 @@ export async function buildReport(opts: BuildOpts): Promise<Report> {
|
|
|
376
449
|
const probe = opts.probe ?? ((servers) => probeAll(servers));
|
|
377
450
|
const bunVersionFn = opts.bunVersion ?? getBunVersion;
|
|
378
451
|
const bunOffPathFn = opts.bunOffPath ?? getBunOffPath;
|
|
452
|
+
const phantomVersionFn = opts.phantomVersion ?? (() => getPhantomVersion());
|
|
379
453
|
|
|
380
454
|
const plugin = await readPluginJson(root);
|
|
381
455
|
const currentVersion = plugin?.version ?? "unknown";
|
|
@@ -445,8 +519,59 @@ export async function buildReport(opts: BuildOpts): Promise<Report> {
|
|
|
445
519
|
detail: `@modelcontextprotocol/sdk@${mcpSdkPkg.version ?? "?"} · @ashlr/core-efficiency@${corePkg.version ?? "?"}`,
|
|
446
520
|
});
|
|
447
521
|
}
|
|
522
|
+
// Optional Phantom CLI: presence and version only, never secret values.
|
|
523
|
+
const phantomVersion = await phantomVersionFn();
|
|
524
|
+
install.push(
|
|
525
|
+
phantomVersion
|
|
526
|
+
? { status: "ok", label: "phantom", detail: `${phantomVersion} (installed; run tools via \`phantom exec -- <cmd>\`)` }
|
|
527
|
+
: { status: "ok", label: "phantom", detail: "not installed (optional)" },
|
|
528
|
+
);
|
|
529
|
+
// Value-free: names, phm prefixes and proxy host/port only; never reveals.
|
|
530
|
+
{
|
|
531
|
+
const pe = detectPhantomExec(opts.env ?? process.env);
|
|
532
|
+
install.push({
|
|
533
|
+
status: pe.warnings.length ? "warn" : "ok",
|
|
534
|
+
label: "phantom exec",
|
|
535
|
+
detail: describePhantomExec(pe),
|
|
536
|
+
...(pe.warnings.length ? { fix: pe.warnings.join(" ") } : {}),
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
// Opt-in (ASHLR_PHANTOM_PREFLIGHT=1 or strict): Phantom's value-free agent
|
|
540
|
+
// preflight. Verdict and check ids only.
|
|
541
|
+
{
|
|
542
|
+
const pf = runPhantomPreflight(preflightMode(opts.env ?? process.env), opts.phantomPreflight);
|
|
543
|
+
if (pf) {
|
|
544
|
+
install.push({
|
|
545
|
+
status: pf.status,
|
|
546
|
+
label: "phantom preflight",
|
|
547
|
+
detail: pf.detail,
|
|
548
|
+
...(pf.status !== "ok" ? { fix: "run `phantom agent doctor` for details and fix commands" } : {}),
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
}
|
|
448
552
|
sections.push({ title: "install", lines: install });
|
|
449
553
|
|
|
554
|
+
// ----- ashlr companions (informational, never warn/fail) -----
|
|
555
|
+
const companions = await detectCompanions(opts.companionRunner);
|
|
556
|
+
sections.push({
|
|
557
|
+
title: "companions",
|
|
558
|
+
lines: companions.map((cmp): Line => {
|
|
559
|
+
const caps = cmp.capabilities.length ? ` [${cmp.capabilities.join(", ")}]` : "";
|
|
560
|
+
const detail =
|
|
561
|
+
cmp.state === "ready" ? `${cmp.version ?? "?"} ready${caps}`
|
|
562
|
+
: cmp.state === "degraded" ? `${cmp.version ?? "?"} health: ${cmp.health ?? "unknown"}${caps}`
|
|
563
|
+
: cmp.state === "outdated" ? "installed, too old for --ashlr-handshake"
|
|
564
|
+
: cmp.state === "unknown" ? "installed, handshake unreadable or timed out"
|
|
565
|
+
: "not installed";
|
|
566
|
+
return {
|
|
567
|
+
status: cmp.state === "ready" ? "ok" : "info",
|
|
568
|
+
label: cmp.name,
|
|
569
|
+
detail,
|
|
570
|
+
...(cmp.fix ? { fix: cmp.fix } : {}),
|
|
571
|
+
};
|
|
572
|
+
}),
|
|
573
|
+
});
|
|
574
|
+
|
|
450
575
|
// ----- mcp servers -----
|
|
451
576
|
const servers = [
|
|
452
577
|
{ name: "efficiency", script: join(root, "servers/efficiency-server.ts") },
|
|
@@ -818,7 +943,7 @@ export async function buildReport(opts: BuildOpts): Promise<Report> {
|
|
|
818
943
|
detail = `provider: onnx (offline summarizer)${overrideNote}`;
|
|
819
944
|
break;
|
|
820
945
|
case "local":
|
|
821
|
-
detail = `provider: local (${process.env.ASHLR_LLM_URL ?? "http://
|
|
946
|
+
detail = `provider: local (${process.env.ASHLR_LLM_URL ?? "http://127.0.0.1:1234/v1"})${overrideNote}`;
|
|
822
947
|
break;
|
|
823
948
|
case "none":
|
|
824
949
|
default:
|
|
@@ -917,7 +1042,7 @@ export async function buildReport(opts: BuildOpts): Promise<Report> {
|
|
|
917
1042
|
}
|
|
918
1043
|
}
|
|
919
1044
|
|
|
920
|
-
return { header, sections, warnings, failures };
|
|
1045
|
+
return { header, sections, companions, warnings, failures };
|
|
921
1046
|
}
|
|
922
1047
|
|
|
923
1048
|
// ---------- formatting ----------
|
|
@@ -983,6 +1108,10 @@ async function main(): Promise<number> {
|
|
|
983
1108
|
return 2;
|
|
984
1109
|
}
|
|
985
1110
|
const report = await buildReport({ root });
|
|
1111
|
+
if (process.argv.includes("--json")) {
|
|
1112
|
+
process.stdout.write(JSON.stringify(withDoctorEnvelope(report), null, 2) + "\n");
|
|
1113
|
+
return report.failures > 0 ? 1 : 0;
|
|
1114
|
+
}
|
|
986
1115
|
process.stdout.write(formatReport(report) + "\n");
|
|
987
1116
|
return report.failures > 0 ? 1 : 0;
|
|
988
1117
|
}
|
|
@@ -30,12 +30,14 @@ import {
|
|
|
30
30
|
writeFileSync,
|
|
31
31
|
} from "fs";
|
|
32
32
|
import { homedir } from "os";
|
|
33
|
+
import { anthropicBaseUrl, openaiBaseUrl } from "../servers/_base-urls.ts";
|
|
33
34
|
import { dirname, join, resolve } from "path";
|
|
34
35
|
// Imported for API-surface parity with the MCP server; the auto-consolidator
|
|
35
36
|
// uses the direct-fallback path so it stays offline and deterministic.
|
|
36
37
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
37
38
|
import { consolidateProposals } from "@ashlr/core-efficiency/genome";
|
|
38
39
|
import { summarizeIfLarge as _summarizeIfLargeImpl, PROMPTS, type SummarizeResult, type SummarizeOpts } from "../servers/_summarize";
|
|
40
|
+
import { safeKeyedFetch } from "../servers/_gateway-target.ts";
|
|
39
41
|
|
|
40
42
|
// Mutable hook container so tests can inject stubs:
|
|
41
43
|
// _hooks.summarizeIfLarge — controls LLM synthesis
|
|
@@ -77,7 +79,7 @@ async function isLlmReachable(): Promise<boolean> {
|
|
|
77
79
|
// ANTHROPIC_API_KEY — probe the models endpoint
|
|
78
80
|
if (process.env["ANTHROPIC_API_KEY"]) {
|
|
79
81
|
try {
|
|
80
|
-
const res = await
|
|
82
|
+
const res = await safeKeyedFetch(`${anthropicBaseUrl()}/v1/models`, {
|
|
81
83
|
method: "GET",
|
|
82
84
|
headers: { "x-api-key": process.env["ANTHROPIC_API_KEY"]!, "anthropic-version": "2023-06-01" },
|
|
83
85
|
signal: AbortSignal.timeout(2_000),
|
|
@@ -91,7 +93,7 @@ async function isLlmReachable(): Promise<boolean> {
|
|
|
91
93
|
// OPENAI_API_KEY — probe the models endpoint
|
|
92
94
|
if (process.env["OPENAI_API_KEY"]) {
|
|
93
95
|
try {
|
|
94
|
-
const res = await
|
|
96
|
+
const res = await safeKeyedFetch(`${openaiBaseUrl()}/models`, {
|
|
95
97
|
method: "GET",
|
|
96
98
|
headers: { "Authorization": `Bearer ${process.env["OPENAI_API_KEY"]!}` },
|
|
97
99
|
signal: AbortSignal.timeout(2_000),
|
package/scripts/genome-init.ts
CHANGED
|
@@ -24,6 +24,7 @@ import {
|
|
|
24
24
|
writeSection,
|
|
25
25
|
} from "@ashlr/core-efficiency/genome";
|
|
26
26
|
import { scan, type Baseline } from "./baseline-scan.ts";
|
|
27
|
+
import { ollamaBaseUrl } from "../servers/_base-urls.ts";
|
|
27
28
|
import { Spinner, ProgressBar, c, sym, isColorEnabled } from "./ui.ts";
|
|
28
29
|
|
|
29
30
|
// ---------------------------------------------------------------------------
|
|
@@ -214,7 +215,6 @@ export interface WorkspaceGraph {
|
|
|
214
215
|
// Local model summarization (Ollama)
|
|
215
216
|
// ---------------------------------------------------------------------------
|
|
216
217
|
|
|
217
|
-
const OLLAMA_BASE_URL = "http://localhost:11434";
|
|
218
218
|
const OLLAMA_TIMEOUT_MS = 2000;
|
|
219
219
|
const OLLAMA_GENERATE_TIMEOUT_MS = 30000;
|
|
220
220
|
/** Max chars of CLAUDE.md to pass to the LLM. Avoids loading huge files. */
|
|
@@ -234,7 +234,7 @@ async function resolveOllamaModel(): Promise<string | null> {
|
|
|
234
234
|
try {
|
|
235
235
|
const controller = new AbortController();
|
|
236
236
|
const timer = setTimeout(() => controller.abort(), OLLAMA_TIMEOUT_MS);
|
|
237
|
-
const res = await fetch(`${
|
|
237
|
+
const res = await fetch(`${ollamaBaseUrl()}/api/tags`, {
|
|
238
238
|
signal: controller.signal,
|
|
239
239
|
});
|
|
240
240
|
clearTimeout(timer);
|
|
@@ -269,7 +269,7 @@ async function summarizeWithOllama(
|
|
|
269
269
|
try {
|
|
270
270
|
const controller = new AbortController();
|
|
271
271
|
const timer = setTimeout(() => controller.abort(), OLLAMA_GENERATE_TIMEOUT_MS);
|
|
272
|
-
const res = await fetch(`${
|
|
272
|
+
const res = await fetch(`${ollamaBaseUrl()}/api/chat`, {
|
|
273
273
|
method: "POST",
|
|
274
274
|
headers: { "Content-Type": "application/json" },
|
|
275
275
|
body: JSON.stringify({
|
package/scripts/genome-key.ts
CHANGED
|
@@ -5,12 +5,14 @@
|
|
|
5
5
|
* Usage:
|
|
6
6
|
* bun run scripts/genome-key.ts generate <genomeId>
|
|
7
7
|
* bun run scripts/genome-key.ts export <genomeId>
|
|
8
|
-
* bun run scripts/genome-key.ts import <genomeId>
|
|
8
|
+
* bun run scripts/genome-key.ts import <genomeId> (key from stdin: paste, or pipe it)
|
|
9
9
|
* bun run scripts/genome-key.ts rotate <genomeId>
|
|
10
10
|
*
|
|
11
11
|
* Keys are stored at ~/.ashlr/team-keys/<genomeId>.key (mode 0600, 32 raw bytes).
|
|
12
12
|
* Export/import uses base32 (RFC 4648) for human-friendly sharing over Signal,
|
|
13
|
-
* 1Password, etc.
|
|
13
|
+
* 1Password, etc. `import` reads the key from stdin (hidden when typed or
|
|
14
|
+
* pasted at a terminal) so it never lands in argv or shell history. A key
|
|
15
|
+
* given as a third argument still works but prints a warning.
|
|
14
16
|
*
|
|
15
17
|
* Key rotation procedure:
|
|
16
18
|
* 1. Reads all sections from the remote genome via the API.
|
|
@@ -75,6 +77,50 @@ async function promptConfirm(question: string): Promise<boolean> {
|
|
|
75
77
|
});
|
|
76
78
|
}
|
|
77
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Read one key line from stdin. At a terminal the input is not echoed (raw
|
|
82
|
+
* mode); piped input is read to the first newline. The key never touches
|
|
83
|
+
* argv, the environment or any output.
|
|
84
|
+
*/
|
|
85
|
+
async function readKeyFromStdin(): Promise<string> {
|
|
86
|
+
const stdin = process.stdin;
|
|
87
|
+
const tty = stdin.isTTY === true && typeof stdin.setRawMode === "function";
|
|
88
|
+
if (tty) process.stderr.write("Paste the team genome key, then press Enter (input is hidden): ");
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
let buf = "";
|
|
91
|
+
const finish = () => {
|
|
92
|
+
stdin.removeListener("data", onData);
|
|
93
|
+
stdin.removeListener("end", finish);
|
|
94
|
+
if (tty) {
|
|
95
|
+
stdin.setRawMode(false);
|
|
96
|
+
process.stderr.write("\n");
|
|
97
|
+
}
|
|
98
|
+
stdin.pause();
|
|
99
|
+
resolve(buf.trim());
|
|
100
|
+
};
|
|
101
|
+
const onData = (chunk: Buffer | string) => {
|
|
102
|
+
for (const ch of String(chunk)) {
|
|
103
|
+
if (ch === "\n" || ch === "\r") return finish();
|
|
104
|
+
if (tty && ch === "\u0003") {
|
|
105
|
+
stdin.setRawMode(false);
|
|
106
|
+
process.stderr.write("\n");
|
|
107
|
+
process.exit(130);
|
|
108
|
+
}
|
|
109
|
+
if (tty && (ch === "\u007f" || ch === "\b")) {
|
|
110
|
+
buf = buf.slice(0, -1);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
buf += ch;
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
if (tty) stdin.setRawMode(true);
|
|
117
|
+
stdin.setEncoding("utf-8");
|
|
118
|
+
stdin.on("data", onData);
|
|
119
|
+
stdin.once("end", finish);
|
|
120
|
+
stdin.resume();
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
78
124
|
// ---------------------------------------------------------------------------
|
|
79
125
|
// Commands
|
|
80
126
|
// ---------------------------------------------------------------------------
|
|
@@ -109,7 +155,8 @@ async function cmdExport(genomeId: string): Promise<void> {
|
|
|
109
155
|
info("=====================================");
|
|
110
156
|
info("");
|
|
111
157
|
info("Share this with team members. They import it with:");
|
|
112
|
-
info(` bun run scripts/genome-key.ts import ${genomeId}
|
|
158
|
+
info(` bun run scripts/genome-key.ts import ${genomeId}`);
|
|
159
|
+
info(" (then paste the key; or pipe it in, e.g. from a password manager CLI)");
|
|
113
160
|
}
|
|
114
161
|
|
|
115
162
|
async function cmdImport(genomeId: string, base32Key: string): Promise<void> {
|
|
@@ -117,7 +164,9 @@ async function cmdImport(genomeId: string, base32Key: string): Promise<void> {
|
|
|
117
164
|
try {
|
|
118
165
|
key = decodeBase32(base32Key);
|
|
119
166
|
} catch (err) {
|
|
120
|
-
|
|
167
|
+
// The decoder's message quotes the offending character; never echo key material.
|
|
168
|
+
void err;
|
|
169
|
+
die("Invalid base32 key. Make sure you copied the full key.");
|
|
121
170
|
}
|
|
122
171
|
if (key.length !== 32) {
|
|
123
172
|
die(`Key must be 32 bytes when decoded (got ${key.length}). Make sure you copied the full key.`);
|
|
@@ -206,7 +255,7 @@ if (!command || !genomeId) {
|
|
|
206
255
|
"Usage:\n" +
|
|
207
256
|
" bun run scripts/genome-key.ts generate <genomeId>\n" +
|
|
208
257
|
" bun run scripts/genome-key.ts export <genomeId>\n" +
|
|
209
|
-
" bun run scripts/genome-key.ts import <genomeId>
|
|
258
|
+
" bun run scripts/genome-key.ts import <genomeId> (key from stdin)\n" +
|
|
210
259
|
" bun run scripts/genome-key.ts rotate <genomeId>\n",
|
|
211
260
|
);
|
|
212
261
|
process.exit(1);
|
|
@@ -220,8 +269,16 @@ switch (command) {
|
|
|
220
269
|
await cmdExport(genomeId);
|
|
221
270
|
break;
|
|
222
271
|
case "import": {
|
|
223
|
-
|
|
224
|
-
if (
|
|
272
|
+
let b32 = rest[0];
|
|
273
|
+
if (b32) {
|
|
274
|
+
process.stderr.write(
|
|
275
|
+
"warning: a key passed as an argument is visible to other local users (ps) and is kept in shell history. " +
|
|
276
|
+
`Next time run \`bun run scripts/genome-key.ts import ${genomeId}\` and paste or pipe the key on stdin.\n`,
|
|
277
|
+
);
|
|
278
|
+
} else {
|
|
279
|
+
b32 = await readKeyFromStdin();
|
|
280
|
+
}
|
|
281
|
+
if (!b32) die("No key given. Paste the key, or pipe it on stdin.");
|
|
225
282
|
await cmdImport(genomeId, b32);
|
|
226
283
|
break;
|
|
227
284
|
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ashlr companion handshake v1 (`ashlr-plugin --ashlr-handshake`): one JSON
|
|
3
|
+
* object naming the plugin, its protocols and capabilities, local health and
|
|
4
|
+
* the files it may edit, so `phm companions` / `phm doctor` stop guessing
|
|
5
|
+
* from `--help`. Contract: docs/ashlr-handshake.md (shared with ashlr-stack
|
|
6
|
+
* and locus).
|
|
7
|
+
*
|
|
8
|
+
* Answered by the node launcher before it looks for bun, so it works (and
|
|
9
|
+
* reports bun as missing) on a machine without bun. No network, no token
|
|
10
|
+
* files read, no secret values.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} version
|
|
15
|
+
* @param {{ bun: boolean, phantom: boolean }} found
|
|
16
|
+
*/
|
|
17
|
+
export function buildHandshake(version, found) {
|
|
18
|
+
return {
|
|
19
|
+
kind: "ashlr.handshake",
|
|
20
|
+
schema_version: 1,
|
|
21
|
+
name: "ashlr-plugin",
|
|
22
|
+
command: "ashlr-plugin",
|
|
23
|
+
version,
|
|
24
|
+
protocols: { "ashlr.handshake": 1, "phantom.agent.preflight": 1 },
|
|
25
|
+
capabilities: [
|
|
26
|
+
"token-efficient-tools",
|
|
27
|
+
"mcp-server",
|
|
28
|
+
"claude-code-plugin",
|
|
29
|
+
"codex-install",
|
|
30
|
+
"savings-stats",
|
|
31
|
+
"agent-preflight",
|
|
32
|
+
"doctor",
|
|
33
|
+
],
|
|
34
|
+
health: {
|
|
35
|
+
status: found.bun ? "ok" : "degraded",
|
|
36
|
+
checks: [
|
|
37
|
+
// The CLI, hooks and MCP server run under bun.
|
|
38
|
+
{ id: "bun-on-path", status: found.bun ? "ok" : "missing" },
|
|
39
|
+
// Only ASHLR_PHANTOM_PREFLIGHT needs it.
|
|
40
|
+
{ id: "phantom-on-path", status: found.phantom ? "ok" : "absent" },
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
edits: ["~/.ashlr/", "~/.claude/settings.json", "~/.codex/config.toml"],
|
|
44
|
+
receipts: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** True when argv (without node and the script) asks for the handshake. */
|
|
49
|
+
export function wantsHandshake(argv) {
|
|
50
|
+
return argv.includes("--ashlr-handshake");
|
|
51
|
+
}
|