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
|
@@ -27,11 +27,22 @@
|
|
|
27
27
|
* Usage (wired from hooks/hooks.json):
|
|
28
28
|
* node "${CODEX_PLUGIN_ROOT}/scripts/hook-bootstrap.mjs" \
|
|
29
29
|
* "${CODEX_PLUGIN_ROOT}/hooks/<hook>.ts" [args...]
|
|
30
|
+
*
|
|
31
|
+
* Group mode runs several hooks for one event from a single node process:
|
|
32
|
+
* node hook-bootstrap.mjs --group [--match <tools>] <hook>.ts [--match <tools>] <hook>.ts ...
|
|
33
|
+
* `--match` takes a Claude Code matcher (`Read|Grep`) and applies to the next
|
|
34
|
+
* hook only; a hook without one always runs. Each hook still runs as its own
|
|
35
|
+
* `bun run` process, in parallel, with the same stdin. Their outputs are
|
|
36
|
+
* merged: `additionalContext` strings are joined, plain-text output is kept,
|
|
37
|
+
* and exit code 2 from any hook wins. Starting node once per event instead of
|
|
38
|
+
* once per hook is what saves the time.
|
|
30
39
|
*/
|
|
31
40
|
|
|
32
|
-
import { spawnSync } from "node:child_process";
|
|
33
|
-
import {
|
|
41
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
42
|
+
import { readFileSync } from "node:fs";
|
|
43
|
+
import { prependBunToPath } from "./bun-resolve.mjs";
|
|
34
44
|
|
|
45
|
+
const groupMode = process.argv[2] === "--group";
|
|
35
46
|
const hookPath = process.argv[2];
|
|
36
47
|
if (!hookPath) {
|
|
37
48
|
process.stderr.write("[ashlr:hook-bootstrap] missing hook path argument\n");
|
|
@@ -42,6 +53,10 @@ const looksLikeCodexPluginHook =
|
|
|
42
53
|
Boolean(process.env.CODEX_PLUGIN_ROOT) ||
|
|
43
54
|
Boolean(process.env.ASHLR_CODEX_HOOK_MODE) ||
|
|
44
55
|
(Boolean(process.env.CLAUDE_PLUGIN_ROOT) &&
|
|
56
|
+
// Claude Code sets CLAUDECODE=1 for its child processes but neither of
|
|
57
|
+
// the two variables below, so without this check every Claude Code hook
|
|
58
|
+
// ran in Codex nudge mode and suggested bare `ashlr__*` tool names.
|
|
59
|
+
!process.env.CLAUDECODE &&
|
|
45
60
|
!process.env.CLAUDE_SESSION_ID &&
|
|
46
61
|
!process.env.CLAUDE_CODE_MODEL);
|
|
47
62
|
|
|
@@ -52,24 +67,162 @@ if (process.env.ASHLR_MCP_HOST === "codex-cli" && !process.env.ASHLR_HOOK_MODE)
|
|
|
52
67
|
process.env.ASHLR_HOOK_MODE = process.env.ASHLR_CODEX_HOOK_MODE || "nudge";
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
70
|
+
/** True when the error means the `bun` executable was not found. */
|
|
71
|
+
function bunMissing(error) {
|
|
72
|
+
return Boolean(error) && error.code === "ENOENT";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (groupMode) {
|
|
76
|
+
await runGroup(process.argv.slice(3));
|
|
77
|
+
} else {
|
|
78
|
+
runSingle();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function runSingle() {
|
|
82
|
+
const forwarded = process.argv.slice(3);
|
|
83
|
+
const run = () => spawnSync("bun", ["run", hookPath, ...forwarded], { stdio: "inherit", env: process.env });
|
|
84
|
+
let result = run();
|
|
85
|
+
if (bunMissing(result.error)) {
|
|
86
|
+
// Not on PATH: try ~/.bun/bin (a prior bootstrap.mjs install). If bun is
|
|
87
|
+
// still unreachable, exit 0. Hooks must never fail the harness; the MCP
|
|
88
|
+
// server's bootstrap.mjs installs bun on its next spawn.
|
|
89
|
+
prependBunToPath();
|
|
90
|
+
result = run();
|
|
91
|
+
if (bunMissing(result.error)) process.exit(0);
|
|
92
|
+
}
|
|
93
|
+
if (result.error) {
|
|
94
|
+
process.stderr.write(`[ashlr:hook-bootstrap] ${result.error.message}\n`);
|
|
61
95
|
process.exit(0);
|
|
62
96
|
}
|
|
97
|
+
process.exit(result.status ?? 0);
|
|
63
98
|
}
|
|
64
99
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
100
|
+
/** Claude Code matcher semantics: empty or `*` matches all, else a regex over the whole tool name. */
|
|
101
|
+
function matches(matcher, toolName) {
|
|
102
|
+
if (!matcher || matcher === "*") return true;
|
|
103
|
+
if (!toolName) return true;
|
|
104
|
+
try {
|
|
105
|
+
return new RegExp(`^(?:${matcher})$`).test(toolName);
|
|
106
|
+
} catch {
|
|
107
|
+
return matcher === toolName;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parseGroup(args) {
|
|
112
|
+
const hooks = [];
|
|
113
|
+
let matcher = null;
|
|
114
|
+
for (let i = 0; i < args.length; i++) {
|
|
115
|
+
if (args[i] === "--match") {
|
|
116
|
+
matcher = args[++i] ?? null;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
hooks.push({ path: args[i], matcher });
|
|
120
|
+
matcher = null;
|
|
121
|
+
}
|
|
122
|
+
return hooks;
|
|
123
|
+
}
|
|
70
124
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
125
|
+
function runHook(path, input) {
|
|
126
|
+
return new Promise((resolveRun) => {
|
|
127
|
+
let child;
|
|
128
|
+
try {
|
|
129
|
+
child = spawn("bun", ["run", path], { stdio: ["pipe", "pipe", "pipe"], env: process.env });
|
|
130
|
+
} catch (error) {
|
|
131
|
+
resolveRun({ error, status: 0, stdout: "", stderr: "" });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const out = [];
|
|
135
|
+
const err = [];
|
|
136
|
+
child.stdout.on("data", (c) => out.push(c));
|
|
137
|
+
child.stderr.on("data", (c) => err.push(c));
|
|
138
|
+
child.on("error", (error) => resolveRun({ error, status: 0, stdout: "", stderr: "" }));
|
|
139
|
+
child.on("close", (status) =>
|
|
140
|
+
resolveRun({
|
|
141
|
+
error: null,
|
|
142
|
+
status: status ?? 0,
|
|
143
|
+
stdout: Buffer.concat(out).toString("utf8"),
|
|
144
|
+
stderr: Buffer.concat(err).toString("utf8"),
|
|
145
|
+
}),
|
|
146
|
+
);
|
|
147
|
+
child.stdin.on("error", () => {}); // a hook that exits without reading stdin
|
|
148
|
+
child.stdin.end(input);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function runGroup(args) {
|
|
153
|
+
let input = Buffer.alloc(0);
|
|
154
|
+
try {
|
|
155
|
+
input = readFileSync(0);
|
|
156
|
+
} catch {
|
|
157
|
+
// No stdin (manual run): hooks get an empty payload.
|
|
158
|
+
}
|
|
159
|
+
let payload = {};
|
|
160
|
+
try {
|
|
161
|
+
payload = JSON.parse(input.toString("utf8") || "{}");
|
|
162
|
+
} catch {
|
|
163
|
+
// Unparseable payload: run every hook and let each decide.
|
|
164
|
+
}
|
|
165
|
+
const toolName = typeof payload.tool_name === "string" ? payload.tool_name : "";
|
|
166
|
+
const event = typeof payload.hook_event_name === "string" ? payload.hook_event_name : "";
|
|
167
|
+
const selected = parseGroup(args).filter((h) => matches(h.matcher, toolName));
|
|
168
|
+
if (selected.length === 0) process.exit(0);
|
|
169
|
+
|
|
170
|
+
let results = await Promise.all(selected.map((h) => runHook(h.path, input)));
|
|
171
|
+
if (results.some((r) => bunMissing(r.error))) {
|
|
172
|
+
prependBunToPath();
|
|
173
|
+
results = await Promise.all(selected.map((h) => runHook(h.path, input)));
|
|
174
|
+
if (results.some((r) => bunMissing(r.error))) process.exit(0);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const contexts = [];
|
|
178
|
+
const plain = [];
|
|
179
|
+
let merged = null;
|
|
180
|
+
let blocking = null;
|
|
181
|
+
let failure = 0;
|
|
182
|
+
for (const r of results) {
|
|
183
|
+
if (r.stderr) process.stderr.write(r.stderr);
|
|
184
|
+
if (r.error) {
|
|
185
|
+
process.stderr.write(`[ashlr:hook-bootstrap] ${r.error.message}\n`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (r.status === 2 && blocking === null) blocking = r;
|
|
189
|
+
else if (r.status !== 0 && failure === 0) failure = r.status;
|
|
190
|
+
const text = r.stdout.trim();
|
|
191
|
+
if (!text) continue;
|
|
192
|
+
let parsed = null;
|
|
193
|
+
try {
|
|
194
|
+
parsed = JSON.parse(text);
|
|
195
|
+
} catch {
|
|
196
|
+
// Plain-text output.
|
|
197
|
+
}
|
|
198
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
199
|
+
plain.push(text);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const { hookSpecificOutput, ...rest } = parsed;
|
|
203
|
+
merged = { ...(merged ?? {}), ...rest };
|
|
204
|
+
if (hookSpecificOutput && typeof hookSpecificOutput.additionalContext === "string" && hookSpecificOutput.additionalContext) {
|
|
205
|
+
contexts.push(hookSpecificOutput.additionalContext);
|
|
206
|
+
}
|
|
207
|
+
if (hookSpecificOutput && !event && typeof hookSpecificOutput.hookEventName === "string") {
|
|
208
|
+
merged.__event = hookSpecificOutput.hookEventName;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (blocking) process.exit(2);
|
|
213
|
+
|
|
214
|
+
if (merged === null && contexts.length === 0) {
|
|
215
|
+
// Every hook printed plain text (or nothing): keep it as plain text.
|
|
216
|
+
if (plain.length > 0) process.stdout.write(plain.join("\n\n") + "\n");
|
|
217
|
+
process.exit(failure);
|
|
218
|
+
}
|
|
219
|
+
const { __event, ...top } = merged ?? {};
|
|
220
|
+
// Mixed output: plain text joins the JSON's context so nothing is lost.
|
|
221
|
+
const allContext = [...contexts, ...plain];
|
|
222
|
+
const output = { ...top };
|
|
223
|
+
if (allContext.length > 0) {
|
|
224
|
+
output.hookSpecificOutput = { hookEventName: event || __event, additionalContext: allContext.join("\n\n") };
|
|
225
|
+
}
|
|
226
|
+
process.stdout.write(JSON.stringify(output));
|
|
227
|
+
process.exit(failure);
|
|
74
228
|
}
|
|
75
|
-
process.exit(result.status ?? 0);
|
|
@@ -27,6 +27,9 @@ import { fileURLToPath } from "node:url";
|
|
|
27
27
|
|
|
28
28
|
const __filename = fileURLToPath(import.meta.url);
|
|
29
29
|
const PLUGIN_ROOT = resolve(dirname(__filename), "..");
|
|
30
|
+
// Record the folder the host launched us in (the user's project) before
|
|
31
|
+
// moving to PLUGIN_ROOT; the router adopts it once dependencies resolve.
|
|
32
|
+
process.env.ASHLR_LAUNCH_CWD ||= process.cwd();
|
|
30
33
|
process.chdir(PLUGIN_ROOT);
|
|
31
34
|
|
|
32
35
|
// 1. Self-install deps if missing. Idempotent — `bun install` is a no-op once
|
package/scripts/ollama-setup.ts
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
/* eslint-disable no-console */
|
|
23
23
|
|
|
24
24
|
import { spawn } from "bun";
|
|
25
|
+
import { DEFAULT_OLLAMA_BASE_URL, ollamaBaseUrl } from "../servers/_base-urls.ts";
|
|
25
26
|
|
|
26
27
|
// ---------------------------------------------------------------------------
|
|
27
28
|
// Colored output (zero deps, respects NO_COLOR / FORCE_COLOR / !isTTY)
|
|
@@ -79,7 +80,7 @@ function printFix(cmd: string): void {
|
|
|
79
80
|
// Types + constants
|
|
80
81
|
// ---------------------------------------------------------------------------
|
|
81
82
|
|
|
82
|
-
export const OLLAMA_BASE_URL =
|
|
83
|
+
export const OLLAMA_BASE_URL = DEFAULT_OLLAMA_BASE_URL;
|
|
83
84
|
const TAGS_TIMEOUT_MS = 3000;
|
|
84
85
|
const SMOKE_TIMEOUT_MS = 30000;
|
|
85
86
|
|
|
@@ -192,7 +193,7 @@ export interface TagsResult {
|
|
|
192
193
|
|
|
193
194
|
/** Fetch /api/tags. Returns `running: false` if the daemon is unreachable. */
|
|
194
195
|
export async function fetchTags(
|
|
195
|
-
baseUrl =
|
|
196
|
+
baseUrl = ollamaBaseUrl(),
|
|
196
197
|
timeoutMs = TAGS_TIMEOUT_MS,
|
|
197
198
|
fetchImpl: typeof fetch = fetch,
|
|
198
199
|
): Promise<TagsResult> {
|
|
@@ -241,7 +242,7 @@ export interface SmokeTestResult {
|
|
|
241
242
|
*/
|
|
242
243
|
export async function smokeTest(
|
|
243
244
|
model: string,
|
|
244
|
-
baseUrl =
|
|
245
|
+
baseUrl = ollamaBaseUrl(),
|
|
245
246
|
timeoutMs = SMOKE_TIMEOUT_MS,
|
|
246
247
|
fetchImpl: typeof fetch = fetch,
|
|
247
248
|
): Promise<SmokeTestResult> {
|
|
@@ -368,7 +369,7 @@ export async function run(args: CliArgs): Promise<number> {
|
|
|
368
369
|
if (!tags.running) {
|
|
369
370
|
printOk("Ollama CLI found on PATH");
|
|
370
371
|
printFail(
|
|
371
|
-
`Ollama daemon not reachable at ${
|
|
372
|
+
`Ollama daemon not reachable at ${ollamaBaseUrl()}${tags.error ? ` (${tags.error})` : ""}`,
|
|
372
373
|
);
|
|
373
374
|
console.log("");
|
|
374
375
|
for (const l of startInstructions()) console.log(` ${l}`);
|
package/scripts/run-benchmark.ts
CHANGED
|
@@ -14,9 +14,11 @@
|
|
|
14
14
|
* ashlr__edit by calling the handler functions directly — no MCP layer.
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import { describePhantomExec, detectPhantomExec, providerKeyUsable } from "../servers/_phantom-exec.ts";
|
|
17
18
|
import { readFileSync, writeFileSync, existsSync } from "fs";
|
|
18
19
|
import { resolve, relative } from "path";
|
|
19
20
|
import { spawnSync } from "child_process";
|
|
21
|
+
import { rgTextArgs, truncateRgText } from "../servers/_rg-text";
|
|
20
22
|
|
|
21
23
|
import {
|
|
22
24
|
estimateTokensFromString,
|
|
@@ -345,10 +347,11 @@ function resolveRg(): string {
|
|
|
345
347
|
function measureGrep(pattern: string, repoPath: string): GrepSample | null {
|
|
346
348
|
try {
|
|
347
349
|
const rgBin = resolveRg();
|
|
348
|
-
|
|
350
|
+
// Baseline: what a plain grep returns (`path:line:text`, no column cap).
|
|
351
|
+
const res = spawnSync(rgBin, ["-n", "--no-heading", "--color", "never", "--", pattern, repoPath], {
|
|
349
352
|
encoding: "utf-8",
|
|
350
353
|
timeout: 15_000,
|
|
351
|
-
maxBuffer:
|
|
354
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
352
355
|
});
|
|
353
356
|
const raw = res.stdout ?? "";
|
|
354
357
|
if (!raw.trim()) return null;
|
|
@@ -356,11 +359,13 @@ function measureGrep(pattern: string, repoPath: string): GrepSample | null {
|
|
|
356
359
|
const rawBytes = Buffer.byteLength(raw, "utf-8");
|
|
357
360
|
const rawTokens = estimateTokensFromString(raw);
|
|
358
361
|
|
|
359
|
-
// Replicate rg-fallback path in ashlrGrep
|
|
360
|
-
const
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
362
|
+
// Replicate the rg-fallback path in ashlrGrep (servers/_rg-text.ts).
|
|
363
|
+
const ashlrRes = spawnSync(rgBin, rgTextArgs(pattern, repoPath), {
|
|
364
|
+
encoding: "utf-8",
|
|
365
|
+
timeout: 15_000,
|
|
366
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
367
|
+
});
|
|
368
|
+
const truncated = truncateRgText(ashlrRes.stdout ?? "");
|
|
364
369
|
|
|
365
370
|
const ashlrBytes = Buffer.byteLength(truncated, "utf-8");
|
|
366
371
|
const ashlrTokens = estimateTokensFromString(truncated);
|
|
@@ -402,13 +407,10 @@ function summarizeEditForBench(
|
|
|
402
407
|
replace: string,
|
|
403
408
|
fileSize: number,
|
|
404
409
|
): string {
|
|
405
|
-
//
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
` - removed (${estimateTokensFromString(search)} tok): ${first(search)}${search.length > 72 ? "…" : ""}`,
|
|
410
|
-
` + added (${estimateTokensFromString(replace)} tok): ${first(replace)}${replace.length > 72 ? "…" : ""}`,
|
|
411
|
-
].join("\n");
|
|
410
|
+
// Mirrors servers/edit-server.ts: an exact, unique match (every benchmark edit
|
|
411
|
+
// is one) returns the header only, because the caller already has the text it sent.
|
|
412
|
+
void search; void replace; void fileSize;
|
|
413
|
+
return `[ashlr__edit] file.ts · 1 of 1 hunks applied`;
|
|
412
414
|
}
|
|
413
415
|
|
|
414
416
|
function measureEdit(size: EditSize): EditSample {
|
|
@@ -548,9 +550,10 @@ size buckets (2–5 KB, 5–15 KB, 15–50 KB, 50+ KB). Re-running on the same
|
|
|
548
550
|
commit always picks the same files.
|
|
549
551
|
|
|
550
552
|
**ashlr__grep**: Five common patterns (function, import, TODO, class, interface)
|
|
551
|
-
are run via rg
|
|
552
|
-
|
|
553
|
-
|
|
553
|
+
are run via plain \`rg -n\` (path:line:text, the shape a native grep returns)
|
|
554
|
+
against the repo root; that output is the raw size. The ashlr__grep fallback
|
|
555
|
+
path (no genome) caps lines at 300 columns and, above 4 000 chars, keeps whole
|
|
556
|
+
lines from the head and tail. The ratio is ashlr/raw.
|
|
554
557
|
|
|
555
558
|
Note: when a .ashlrcode/genome/ index is present, real-world grep savings are
|
|
556
559
|
substantially higher. This benchmark measures only the conservative
|
|
@@ -705,7 +708,14 @@ export async function runBenchmark(opts: {
|
|
|
705
708
|
|
|
706
709
|
// --validate-tokenizer: sample 5% through measureTokens
|
|
707
710
|
let heuristicError: number | null = null;
|
|
708
|
-
|
|
711
|
+
const phantomExec = detectPhantomExec();
|
|
712
|
+
if (validateTokenizer && phantomExec.inSession) {
|
|
713
|
+
console.log(`[run-benchmark] ${describePhantomExec(phantomExec)}`);
|
|
714
|
+
}
|
|
715
|
+
if (validateTokenizer && process.env.ANTHROPIC_API_KEY && !providerKeyUsable("ANTHROPIC_API_KEY")) {
|
|
716
|
+
for (const w of phantomExec.warnings) console.log(`[run-benchmark] --validate-tokenizer: ${w}`);
|
|
717
|
+
console.log("[run-benchmark] --validate-tokenizer: ANTHROPIC_API_KEY is not usable here, skipping");
|
|
718
|
+
} else if (validateTokenizer && process.env.ANTHROPIC_API_KEY) {
|
|
709
719
|
console.log("[run-benchmark] --validate-tokenizer: sampling API token counts...");
|
|
710
720
|
const { measureTokens } = await import("../servers/_token-measure.ts");
|
|
711
721
|
const sampleN = Math.max(1, Math.ceil(readSamples.length * 0.05));
|
package/scripts/upgrade-flow.ts
CHANGED
|
@@ -29,6 +29,7 @@ import * as readline from "readline";
|
|
|
29
29
|
import { randomBytes } from "crypto";
|
|
30
30
|
import { recordNudgeClicked, maybeSyncToCloud as maybeSyncNudgeEvents } from "../servers/_nudge-events.ts";
|
|
31
31
|
import { validateProToken } from "../servers/_pro.ts";
|
|
32
|
+
import { PLAINTEXT_FALLBACK_WARNING, storeProToken } from "../servers/_pro-token-keychain.ts";
|
|
32
33
|
|
|
33
34
|
// ---------------------------------------------------------------------------
|
|
34
35
|
// Config
|
|
@@ -186,19 +187,10 @@ function saveToken(token: string): void {
|
|
|
186
187
|
try { chmodSync(TOKEN_FILE, 0o600); } catch { /* best-effort */ }
|
|
187
188
|
}
|
|
188
189
|
|
|
189
|
-
async function writeEnvFile(token: string): Promise<
|
|
190
|
+
async function writeEnvFile(token: string): Promise<"keychain" | "file"> {
|
|
190
191
|
ensureAshlrDir();
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// Remove any prior ASHLR_PRO_TOKEN line, then append the fresh one
|
|
195
|
-
const lines = existing
|
|
196
|
-
.split("\n")
|
|
197
|
-
.filter((l) => !l.startsWith("export ASHLR_PRO_TOKEN="));
|
|
198
|
-
lines.push(`export ASHLR_PRO_TOKEN=${token}`);
|
|
199
|
-
|
|
200
|
-
await writeFile(ENV_FILE, lines.join("\n").trimStart() + "\n", { encoding: "utf8", mode: 0o600 });
|
|
201
|
-
try { chmodSync(ENV_FILE, 0o600); } catch { /* best-effort */ }
|
|
192
|
+
// OS keychain first; the 0600 `export ASHLR_PRO_TOKEN=` line only as fallback.
|
|
193
|
+
return storeProToken(token, ENV_FILE);
|
|
202
194
|
}
|
|
203
195
|
|
|
204
196
|
function readStoredToken(): string | null {
|
|
@@ -690,9 +682,12 @@ async function main(): Promise<number> {
|
|
|
690
682
|
|
|
691
683
|
// Persist token
|
|
692
684
|
saveToken(token);
|
|
693
|
-
await writeEnvFile(token);
|
|
685
|
+
const where = await writeEnvFile(token);
|
|
694
686
|
process.env["ASHLR_PRO_TOKEN"] = token;
|
|
695
|
-
|
|
687
|
+
if (where === "file") warn(PLAINTEXT_FALLBACK_WARNING);
|
|
688
|
+
ok(where === "keychain"
|
|
689
|
+
? "Token saved to ~/.ashlr/pro-token and the OS keychain (auto-loaded on next session)."
|
|
690
|
+
: "Token saved to ~/.ashlr/pro-token and ~/.ashlr/env (auto-loaded on next session).");
|
|
696
691
|
} else {
|
|
697
692
|
step(2, totalSteps, "Sign in");
|
|
698
693
|
ok("Already authenticated — skipping sign-in.");
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base URLs for the provider hosts the plugin's scripts and servers call.
|
|
3
|
+
*
|
|
4
|
+
* Each helper reads the same environment variable the provider's own tooling
|
|
5
|
+
* uses and falls back to the host the plugin always used, so nothing changes
|
|
6
|
+
* unless a variable is set. A gateway (for example LiteLLM) or a local proxy
|
|
7
|
+
* can then sit in front by setting the variable.
|
|
8
|
+
*
|
|
9
|
+
* ANTHROPIC_BASE_URL root without /v1 (default https://api.anthropic.com)
|
|
10
|
+
* OPENAI_BASE_URL includes /v1 (default https://api.openai.com/v1)
|
|
11
|
+
* OLLAMA_HOST host, host:port, :port or a full URL (default http://localhost:11434)
|
|
12
|
+
*
|
|
13
|
+
* Empty or whitespace-only values count as unset. Trailing slashes are trimmed.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
export const DEFAULT_ANTHROPIC_BASE_URL = "https://api.anthropic.com";
|
|
17
|
+
export const DEFAULT_OPENAI_BASE_URL = "https://api.openai.com/v1";
|
|
18
|
+
export const DEFAULT_OLLAMA_BASE_URL = "http://localhost:11434";
|
|
19
|
+
|
|
20
|
+
type Env = Record<string, string | undefined>;
|
|
21
|
+
|
|
22
|
+
function trimmed(value: string | undefined): string {
|
|
23
|
+
return (value ?? "").trim().replace(/\/+$/, "");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Anthropic API root, no trailing slash. Append `/v1/...`. */
|
|
27
|
+
export function anthropicBaseUrl(env: Env = process.env): string {
|
|
28
|
+
return trimmed(env["ANTHROPIC_BASE_URL"]) || DEFAULT_ANTHROPIC_BASE_URL;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** OpenAI API root including `/v1`, no trailing slash. Append `/models` etc. */
|
|
32
|
+
export function openaiBaseUrl(env: Env = process.env): string {
|
|
33
|
+
return trimmed(env["OPENAI_BASE_URL"]) || DEFAULT_OPENAI_BASE_URL;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Ollama root, no trailing slash. OLLAMA_HOST may omit the scheme (`host:port`,
|
|
38
|
+
* `host`, `:port`); `http://` is assumed and port 11434 when none is given.
|
|
39
|
+
*/
|
|
40
|
+
export function ollamaBaseUrl(env: Env = process.env): string {
|
|
41
|
+
const raw = trimmed(env["OLLAMA_HOST"]);
|
|
42
|
+
if (!raw) return DEFAULT_OLLAMA_BASE_URL;
|
|
43
|
+
if (/^https?:\/\//i.test(raw)) return raw;
|
|
44
|
+
let hostPort = raw;
|
|
45
|
+
if (hostPort.startsWith(":")) hostPort = `localhost${hostPort}`;
|
|
46
|
+
if (!/:\d+$/.test(hostPort)) hostPort = `${hostPort}:11434`;
|
|
47
|
+
return `http://${hostPort}`;
|
|
48
|
+
}
|