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
|
@@ -67,7 +67,7 @@ registerTool({
|
|
|
67
67
|
description:
|
|
68
68
|
"Merge pending proposals into the genome. Without `model` (and without " +
|
|
69
69
|
"$ASHLR_LLM_URL) runs a deterministic sequential-apply; with an " +
|
|
70
|
-
"
|
|
70
|
+
"LLM configured by $ASHLR_LLM_URL it merges conflicting proposals " +
|
|
71
71
|
"for the same section.",
|
|
72
72
|
inputSchema: {
|
|
73
73
|
type: "object",
|
|
@@ -75,7 +75,7 @@ registerTool({
|
|
|
75
75
|
model: {
|
|
76
76
|
type: "string",
|
|
77
77
|
description:
|
|
78
|
-
"
|
|
78
|
+
"Pass 'none' to force offline sequential-apply, or any other value (for example 'llm') to merge with the LLM configured by $ASHLR_LLM_URL (default local LM Studio at 127.0.0.1:1234). The value is never used as an endpoint. If omitted, uses the LLM only when $ASHLR_LLM_URL or $ASHLR_LLM_KEY is set.",
|
|
79
79
|
},
|
|
80
80
|
cwd: {
|
|
81
81
|
type: "string",
|
package/servers/genome-server.ts
CHANGED
|
@@ -38,27 +38,26 @@ import type {
|
|
|
38
38
|
ProviderRequest,
|
|
39
39
|
StreamEvent,
|
|
40
40
|
} from "@ashlr/core-efficiency";
|
|
41
|
+
import { safeKeyedFetch } from "./_gateway-target.ts";
|
|
41
42
|
|
|
42
43
|
// ---------------------------------------------------------------------------
|
|
43
44
|
// LLM shim — OpenAI-compatible chat.completions streaming
|
|
44
45
|
// ---------------------------------------------------------------------------
|
|
45
46
|
|
|
47
|
+
/** Local LLM on loopback by IP literal; `localhost` by name is not accepted for a keyed call. */
|
|
48
|
+
export const DEFAULT_GENOME_LLM_URL = "http://127.0.0.1:1234/v1";
|
|
49
|
+
|
|
46
50
|
/**
|
|
47
51
|
* Build a minimal LLMSummarizer that hits an OpenAI-compatible endpoint.
|
|
48
52
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
* Returns null if no endpoint is configured AND no override given — the
|
|
55
|
-
* scribe falls back to sequential-apply merging in that case.
|
|
53
|
+
* The endpoint comes only from user config ($ASHLR_LLM_URL, else local LM
|
|
54
|
+
* Studio at http://127.0.0.1:1234/v1), never from a tool argument, so the
|
|
55
|
+
* model cannot choose where $ASHLR_LLM_KEY or genome content goes.
|
|
56
|
+
* safeKeyedFetch() applies the gateway rules (https, or http only to
|
|
57
|
+
* 127.0.0.1 / [::1]; no credentials in the URL; no redirects).
|
|
56
58
|
*/
|
|
57
|
-
export function buildLLMShim(
|
|
58
|
-
const base =
|
|
59
|
-
modelUrl ??
|
|
60
|
-
process.env.ASHLR_LLM_URL ??
|
|
61
|
-
"http://localhost:1234/v1";
|
|
59
|
+
export function buildLLMShim(): LLMSummarizer | undefined {
|
|
60
|
+
const base = (process.env.ASHLR_LLM_URL ?? "").trim() || DEFAULT_GENOME_LLM_URL;
|
|
62
61
|
const key = process.env.ASHLR_LLM_KEY ?? "";
|
|
63
62
|
const model = process.env.ASHLR_LLM_MODEL ?? "local";
|
|
64
63
|
|
|
@@ -88,7 +87,7 @@ export function buildLLMShim(modelUrl?: string): LLMSummarizer | undefined {
|
|
|
88
87
|
|
|
89
88
|
let res: Response;
|
|
90
89
|
try {
|
|
91
|
-
res = await
|
|
90
|
+
res = await safeKeyedFetch(endpoint, {
|
|
92
91
|
method: "POST",
|
|
93
92
|
headers: {
|
|
94
93
|
"Content-Type": "application/json",
|
|
@@ -226,12 +225,13 @@ export async function handleConsolidate(args: ConsolidateArgs): Promise<string>
|
|
|
226
225
|
}
|
|
227
226
|
// Only construct the LLM shim if caller explicitly requested one OR env is
|
|
228
227
|
// configured. When args.model is the literal string "none", skip the shim
|
|
229
|
-
// and fall through to sequential-apply.
|
|
228
|
+
// and fall through to sequential-apply. Any other `model` value only opts
|
|
229
|
+
// in to LLM merging; it is never used as an endpoint.
|
|
230
230
|
let router: LLMSummarizer | undefined;
|
|
231
231
|
if (args.model === "none") {
|
|
232
232
|
router = undefined;
|
|
233
233
|
} else if (args.model || process.env.ASHLR_LLM_URL || process.env.ASHLR_LLM_KEY) {
|
|
234
|
-
router = buildLLMShim(
|
|
234
|
+
router = buildLLMShim();
|
|
235
235
|
} else {
|
|
236
236
|
// No configured endpoint — sequential-apply keeps this deterministic and
|
|
237
237
|
// offline. Users opt into LLM merging by passing `model` or setting env.
|
|
@@ -330,14 +330,14 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
|
330
330
|
{
|
|
331
331
|
name: "ashlr__genome_consolidate",
|
|
332
332
|
description:
|
|
333
|
-
"Merge pending proposals into the genome. Without `model` (and without $ASHLR_LLM_URL) runs a deterministic sequential-apply; with
|
|
333
|
+
"Merge pending proposals into the genome. Without `model` (and without $ASHLR_LLM_URL) runs a deterministic sequential-apply; with the LLM configured by $ASHLR_LLM_URL it merges conflicting proposals for the same section.",
|
|
334
334
|
inputSchema: {
|
|
335
335
|
type: "object",
|
|
336
336
|
properties: {
|
|
337
337
|
model: {
|
|
338
338
|
type: "string",
|
|
339
339
|
description:
|
|
340
|
-
"
|
|
340
|
+
"Pass 'none' to force offline sequential-apply, or any other value (for example 'llm') to merge with the LLM configured by $ASHLR_LLM_URL (default local LM Studio at 127.0.0.1:1234). The value is never used as an endpoint. If omitted, uses the LLM only when $ASHLR_LLM_URL or $ASHLR_LLM_KEY is set.",
|
|
341
341
|
},
|
|
342
342
|
cwd: {
|
|
343
343
|
type: "string",
|
package/servers/grep-server.ts
CHANGED
|
@@ -26,6 +26,7 @@ import { getCalibrationMultiplier } from "../scripts/read-calibration";
|
|
|
26
26
|
import { recordSaving } from "./_stats";
|
|
27
27
|
import { logGenomeCompressionRatioEvent } from "./_telemetry";
|
|
28
28
|
import { clampToCwd, primaryProjectRoot } from "./_cwd-clamp";
|
|
29
|
+
import { rgStopReason, rgTextArgs, truncateRgText } from "./_rg-text";
|
|
29
30
|
import { getEmbeddingCache } from "./_tool-base";
|
|
30
31
|
import { embed, upsertCorpus } from "./_embedding-model";
|
|
31
32
|
import { populateGenomeEmbeddings } from "./_genome-embed-populator";
|
|
@@ -504,12 +505,15 @@ export async function ashlrGrep(input: {
|
|
|
504
505
|
|
|
505
506
|
const rgBin = resolveRg();
|
|
506
507
|
|
|
507
|
-
const res = spawnSync(rgBin,
|
|
508
|
+
const res = spawnSync(rgBin, rgTextArgs(input.pattern, cwd), {
|
|
508
509
|
encoding: "utf-8",
|
|
509
510
|
timeout: 15_000,
|
|
511
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
510
512
|
});
|
|
513
|
+
// `raw` is what a plain grep would have returned, so savings are counted
|
|
514
|
+
// against that, not against a verbose wire format.
|
|
511
515
|
const raw = res.stdout ?? "";
|
|
512
|
-
const truncated = raw
|
|
516
|
+
const truncated = truncateRgText(raw, rgStopReason(res));
|
|
513
517
|
const summarized = await summarizeIfLarge(truncated, {
|
|
514
518
|
toolName: "ashlr__grep",
|
|
515
519
|
systemPrompt: PROMPTS.grep,
|
|
@@ -40,12 +40,13 @@ registerTool({
|
|
|
40
40
|
required: ["query"],
|
|
41
41
|
},
|
|
42
42
|
handler: async (args: Record<string, unknown>, _ctx: ToolCallContext): Promise<ToolResult> => {
|
|
43
|
-
|
|
43
|
+
// Only the documented arguments are read. An extra `endpointOverride` (or
|
|
44
|
+
// any other key) is ignored: the LLM endpoint comes from user config only.
|
|
45
|
+
const a = args as Partial<OrientArgs>;
|
|
44
46
|
const out = await orient({
|
|
45
47
|
query: typeof a.query === "string" ? a.query : "",
|
|
46
48
|
dir: typeof a.dir === "string" ? a.dir : undefined,
|
|
47
49
|
depth: a.depth === "quick" || a.depth === "thorough" ? a.depth : undefined,
|
|
48
|
-
endpointOverride: typeof a.endpointOverride === "string" ? a.endpointOverride : undefined,
|
|
49
50
|
});
|
|
50
51
|
return { content: [{ type: "text", text: out.text }] };
|
|
51
52
|
},
|
package/servers/orient-server.ts
CHANGED
|
@@ -37,6 +37,7 @@ import { scan, formatBaseline, listFiles } from "../scripts/baseline-scan";
|
|
|
37
37
|
import { readFileSync } from "fs";
|
|
38
38
|
import { recordSaving as recordSavingCore } from "./_stats";
|
|
39
39
|
import { STOPWORDS, extractKeywords } from "./_text-helpers";
|
|
40
|
+
import { safeKeyedFetch } from "./_gateway-target.ts";
|
|
40
41
|
|
|
41
42
|
// Re-export for callers that historically imported `extractKeywords` from orient-server.
|
|
42
43
|
export { extractKeywords, STOPWORDS };
|
|
@@ -206,11 +207,23 @@ interface CallLLMOpts {
|
|
|
206
207
|
systemPrompt: string;
|
|
207
208
|
userContent: string;
|
|
208
209
|
timeoutMs?: number;
|
|
209
|
-
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** LM Studio on loopback by IP literal; `localhost` by name is not accepted for a keyed call. */
|
|
213
|
+
export const DEFAULT_ORIENT_LLM_URL = "http://127.0.0.1:1234/v1";
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* The orient LLM endpoint comes only from user config (ASHLR_LLM_URL), never
|
|
217
|
+
* from a tool argument, so the model cannot choose where ASHLR_LLM_KEY or
|
|
218
|
+
* project excerpts go. safeKeyedFetch() then applies the gateway rules.
|
|
219
|
+
*/
|
|
220
|
+
export function orientLlmBaseUrl(env: Record<string, string | undefined> = process.env): string {
|
|
221
|
+
const configured = (env.ASHLR_LLM_URL ?? "").trim().replace(/\/+$/, "");
|
|
222
|
+
return configured || DEFAULT_ORIENT_LLM_URL;
|
|
210
223
|
}
|
|
211
224
|
|
|
212
225
|
export async function callLocalLLM(opts: CallLLMOpts): Promise<string | null> {
|
|
213
|
-
const base =
|
|
226
|
+
const base = orientLlmBaseUrl();
|
|
214
227
|
const apiKey = process.env.ASHLR_LLM_KEY ?? "local-llm";
|
|
215
228
|
const model = process.env.ASHLR_LLM_MODEL ?? "qwen/qwen3-coder-30b@8bit";
|
|
216
229
|
const timeoutMs = opts.timeoutMs ?? LLM_TIMEOUT_MS;
|
|
@@ -218,7 +231,7 @@ export async function callLocalLLM(opts: CallLLMOpts): Promise<string | null> {
|
|
|
218
231
|
const ctl = new AbortController();
|
|
219
232
|
const t = setTimeout(() => ctl.abort(), timeoutMs);
|
|
220
233
|
try {
|
|
221
|
-
const res = await
|
|
234
|
+
const res = await safeKeyedFetch(`${base}/chat/completions`, {
|
|
222
235
|
method: "POST",
|
|
223
236
|
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
|
224
237
|
signal: ctl.signal,
|
|
@@ -252,8 +265,6 @@ export interface OrientArgs {
|
|
|
252
265
|
query: string;
|
|
253
266
|
dir?: string;
|
|
254
267
|
depth?: "quick" | "thorough";
|
|
255
|
-
/** Test hook: override LLM endpoint. */
|
|
256
|
-
endpointOverride?: string;
|
|
257
268
|
}
|
|
258
269
|
|
|
259
270
|
export interface OrientResult {
|
|
@@ -329,7 +340,6 @@ export async function orient(args: OrientArgs): Promise<OrientResult> {
|
|
|
329
340
|
systemPrompt: ORIENT_SYSTEM_PROMPT,
|
|
330
341
|
userContent,
|
|
331
342
|
timeoutMs: LLM_TIMEOUT_MS,
|
|
332
|
-
endpointOverride: args.endpointOverride,
|
|
333
343
|
});
|
|
334
344
|
|
|
335
345
|
let synthesis: string;
|
|
@@ -419,9 +429,6 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
419
429
|
query: typeof a.query === "string" ? a.query : "",
|
|
420
430
|
dir: typeof a.dir === "string" ? a.dir : undefined,
|
|
421
431
|
depth: a.depth === "quick" || a.depth === "thorough" ? a.depth : undefined,
|
|
422
|
-
endpointOverride: typeof (a as { endpointOverride?: string }).endpointOverride === "string"
|
|
423
|
-
? (a as { endpointOverride?: string }).endpointOverride
|
|
424
|
-
: undefined,
|
|
425
432
|
});
|
|
426
433
|
return { content: [{ type: "text", text: out.text }] };
|
|
427
434
|
} catch (err) {
|
package/servers/read-server.ts
CHANGED
|
@@ -26,7 +26,7 @@ import { isProSync } from "./_pro";
|
|
|
26
26
|
* Minimum content byte-length before AST skeleton rendering is attempted.
|
|
27
27
|
* Below this threshold the full snipCompact path is cheaper.
|
|
28
28
|
*/
|
|
29
|
-
const AST_THRESHOLD = 6144;
|
|
29
|
+
export const AST_THRESHOLD = 6144;
|
|
30
30
|
|
|
31
31
|
/**
|
|
32
32
|
* File extensions treated as code for the line-number-preservation path.
|
|
@@ -15,13 +15,21 @@ registerTool({
|
|
|
15
15
|
inputSchema: {
|
|
16
16
|
type: "object",
|
|
17
17
|
properties: {
|
|
18
|
-
filePath: { type: "string", description: "Absolute or cwd-relative path to write" },
|
|
18
|
+
filePath: { type: "string", description: "Absolute or cwd-relative path to write (`path` is accepted too)" },
|
|
19
19
|
content: { type: "string", description: "File content to write" },
|
|
20
20
|
},
|
|
21
21
|
required: ["filePath", "content"],
|
|
22
22
|
},
|
|
23
23
|
handler: async (args: Record<string, unknown>, _ctx: ToolCallContext): Promise<ToolResult> => {
|
|
24
|
-
|
|
24
|
+
// ashlr__read and ashlr__edit take `path`, so callers often pass it here too.
|
|
25
|
+
const filePath = args.filePath ?? args.path;
|
|
26
|
+
if (typeof filePath !== "string" || filePath === "") {
|
|
27
|
+
return { content: [{ type: "text", text: "ashlr__write: filePath must be a non-empty string" }], isError: true };
|
|
28
|
+
}
|
|
29
|
+
if (typeof args.content !== "string") {
|
|
30
|
+
return { content: [{ type: "text", text: "ashlr__write: content must be a string" }], isError: true };
|
|
31
|
+
}
|
|
32
|
+
const result = await ashlrWrite({ filePath, content: args.content } satisfies WriteArgs);
|
|
25
33
|
return { content: [{ type: "text", text: formatWriteResult(result) }] };
|
|
26
34
|
},
|
|
27
35
|
});
|