@openlines/opl-pi-sht 0.1.21 → 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/README.md +8 -4
- package/configs/opl-modes.json.sample +24 -28
- package/extensions/opl-browser/browser.ts +4 -4
- package/extensions/opl-footer/README.md +9 -2
- package/extensions/opl-footer/git-status.ts +42 -0
- package/extensions/opl-footer/index.ts +26 -8
- package/extensions/opl-footer/segments/cost.ts +2 -2
- package/extensions/opl-footer/segments/helpers.ts +0 -2
- package/extensions/opl-footer/segments/mode-switcher.ts +4 -2
- package/extensions/opl-footer/theme.ts +24 -5
- package/extensions/opl-init/README.md +23 -9
- package/extensions/opl-init/index.ts +38 -124
- package/extensions/opl-input/README.md +5 -3
- package/extensions/opl-input/index.ts +14 -7
- package/extensions/opl-input/mode-style.ts +1 -1
- package/extensions/opl-input/utils.ts +16 -5
- package/extensions/opl-modes/README.md +19 -13
- package/extensions/opl-modes/config.ts +64 -16
- package/extensions/opl-modes/index.ts +156 -70
- package/extensions/opl-modes/state.ts +40 -18
- package/extensions/opl-modes/types.ts +2 -0
- package/extensions/opl-modes/utils.ts +123 -12
- package/extensions/opl-simplebench/benchmark.ts +24 -7
- package/extensions/opl-simplebench/index.ts +8 -5
- package/extensions/opl-simplebench/research.ts +4 -1
- package/extensions/opl-simplebench/util/providers.ts +6 -1
- package/extensions/opl-todo/index.ts +2 -2
- package/package.json +2 -2
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
/** Pure utilities:
|
|
1
|
+
/** Pure utilities: read-only Bash gate, plan file I/O, color helpers */
|
|
2
2
|
|
|
3
3
|
import type { Theme, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
4
4
|
import { existsSync, readFileSync, readdirSync, mkdirSync } from "node:fs";
|
|
5
5
|
import { join } from "node:path";
|
|
6
6
|
|
|
7
|
-
import {
|
|
7
|
+
import { PLAN_DIR, PLAN_FILE_PREFIX } from "./config.js";
|
|
8
8
|
import type { PlanFileSummary } from "./types.js";
|
|
9
9
|
|
|
10
10
|
/** Serialize async model changes so the final requested model wins. */
|
|
11
11
|
export function createLatestModelQueue() {
|
|
12
12
|
let latest = 0;
|
|
13
|
-
let tail = Promise.resolve();
|
|
13
|
+
let tail: Promise<unknown> = Promise.resolve();
|
|
14
14
|
|
|
15
15
|
return <T>(change: () => Promise<T>): Promise<T | undefined> => {
|
|
16
16
|
const request = ++latest;
|
|
@@ -19,17 +19,124 @@ export function createLatestModelQueue() {
|
|
|
19
19
|
};
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
const stripQuotes = (text: string): string => text.replace(/["'\\]/g, "");
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Blind split on every operator, quotes ignored — used only for the blocklist scan, where
|
|
26
|
+
* over-splitting is fail-safe. A lone `&` counts as a separator unless it belongs to `&&`,
|
|
27
|
+
* `||`, or an `>&`/`<&` descriptor duplication (`echo hi 2>&1` stays one command).
|
|
28
|
+
*/
|
|
29
|
+
const BLIND_SPLIT = /\$\(|`|[<>]\(|&&|\|\||(?<![<>&])&(?![>&])|[;|\n\r]/;
|
|
30
|
+
|
|
31
|
+
/** Quote/substitution nesting level. `quote` is the string quote open inside this level. */
|
|
32
|
+
interface Frame { kind: "root" | "sub" | "tick"; quote: "" | "'" | '"'; parens: number }
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Split a command into the pieces the shell actually executes, so each one can be required
|
|
36
|
+
* to clear the allowlist: chaining (`&&`, `||`), backgrounding (`&`), pipes, `;`, newlines,
|
|
37
|
+
* process substitution, and the payload of `$(...)`/backticks. Quote-aware, so `grep "a|b"`
|
|
38
|
+
* stays one command, while a substitution inside double quotes still expands —
|
|
39
|
+
* `echo "$(node -e ...)"` yields the `node` payload as its own segment instead of blanking
|
|
40
|
+
* the whole string away. Quote characters are dropped from the segments (matching is
|
|
41
|
+
* prefix-anchored, so a leading `"` must not defeat a safe pattern).
|
|
42
|
+
*/
|
|
43
|
+
function commandSegments(command: string): string[] {
|
|
44
|
+
const out: string[] = [];
|
|
45
|
+
const stack: Frame[] = [{ kind: "root", quote: "", parens: 0 }];
|
|
46
|
+
let buf = "";
|
|
47
|
+
const flush = () => {
|
|
48
|
+
const s = buf.trim();
|
|
49
|
+
if (s) out.push(s);
|
|
50
|
+
buf = "";
|
|
51
|
+
};
|
|
52
|
+
for (let i = 0; i < command.length; i++) {
|
|
53
|
+
const ch = command[i];
|
|
54
|
+
const top = stack[stack.length - 1];
|
|
55
|
+
// Single quotes are literal: nothing expands, only the closing ' matters.
|
|
56
|
+
if (top.quote === "'") {
|
|
57
|
+
if (ch === "'") top.quote = "";
|
|
58
|
+
else buf += ch;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
// Double quotes keep their text literal, except for `$(...)` and backticks, which run.
|
|
62
|
+
if (top.quote === '"') {
|
|
63
|
+
if (ch === '"') { top.quote = ""; continue; }
|
|
64
|
+
if (ch === "\\") { i++; continue; } // \X is a literal X, so `\$(x)` does not expand
|
|
65
|
+
if (ch === "$" && command[i + 1] === "(") { stack.push({ kind: "sub", quote: "", parens: 0 }); flush(); i++; continue; }
|
|
66
|
+
if (ch === "`") { stack.push({ kind: "tick", quote: "", parens: 0 }); flush(); continue; }
|
|
67
|
+
buf += ch;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (ch === "'") { top.quote = "'"; continue; }
|
|
71
|
+
if (ch === '"') { top.quote = '"'; continue; }
|
|
72
|
+
if (ch === "\\") { buf += ch + (command[++i] ?? ""); continue; }
|
|
73
|
+
if (ch === "$" && command[i + 1] === "(") { stack.push({ kind: "sub", quote: "", parens: 0 }); flush(); i++; continue; }
|
|
74
|
+
if (ch === "`") {
|
|
75
|
+
if (top.kind === "tick") stack.pop();
|
|
76
|
+
else stack.push({ kind: "tick", quote: "", parens: 0 });
|
|
77
|
+
flush();
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (top.kind === "sub") {
|
|
81
|
+
if (ch === "(") top.parens++;
|
|
82
|
+
else if (ch === ")") {
|
|
83
|
+
if (top.parens === 0) { stack.pop(); flush(); continue; }
|
|
84
|
+
top.parens--;
|
|
85
|
+
}
|
|
86
|
+
} else if ((ch === "<" || ch === ">") && command[i + 1] === "(") {
|
|
87
|
+
stack.push({ kind: "sub", quote: "", parens: 0 });
|
|
88
|
+
flush();
|
|
89
|
+
i++;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (ch === ";" || ch === "|" || ch === "\n" || ch === "\r") { flush(); continue; }
|
|
93
|
+
if (ch === "&") {
|
|
94
|
+
if (command[i + 1] === "&") { i++; flush(); continue; } // &&
|
|
95
|
+
// A lone & backgrounds the command before it. `>&`/`<&` is descriptor duplication and
|
|
96
|
+
// `&>` is a redirect, so neither starts a new command here (the blocklist judges those).
|
|
97
|
+
if (!/[<>&|]/.test(command[i - 1] ?? "") && command[i + 1] !== ">") { flush(); continue; }
|
|
98
|
+
}
|
|
99
|
+
buf += ch;
|
|
100
|
+
}
|
|
101
|
+
flush();
|
|
102
|
+
return out;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Quote-blind segments for the blocklist, on the raw text and its quote-stripped skeleton. */
|
|
106
|
+
function blindSegments(text: string): string[] {
|
|
107
|
+
return text.split(BLIND_SPLIT).map((s) => s.trim()).filter((s) => s.length > 0);
|
|
26
108
|
}
|
|
27
109
|
|
|
28
|
-
/** Whether a command,
|
|
29
|
-
*
|
|
110
|
+
/** Whether a destructive pattern matches the command, any of its segments, or either
|
|
111
|
+
* with quotes/backslashes stripped, so `r"m"` and `-del"ete"` cannot dodge the list. */
|
|
30
112
|
export function isDestructive(command: string, patterns: RegExp[]): boolean {
|
|
31
|
-
const
|
|
32
|
-
|
|
113
|
+
const haystacks = [
|
|
114
|
+
command,
|
|
115
|
+
stripQuotes(command),
|
|
116
|
+
...blindSegments(command).flatMap((s) => [s, stripQuotes(s)]),
|
|
117
|
+
];
|
|
118
|
+
return patterns.some((p) => haystacks.some((h) => p.test(h)));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Why a Bash command must be blocked in a read-only mode, or null when it is allowed.
|
|
123
|
+
* Every shell segment has to match a safe pattern (so `cat f && node -e '...'` cannot
|
|
124
|
+
* ride the first command's allowance), and no destructive pattern may match anywhere.
|
|
125
|
+
* An undefined or empty safe list means "no allowlist gate".
|
|
126
|
+
*/
|
|
127
|
+
export function bashBlockReason(
|
|
128
|
+
command: string,
|
|
129
|
+
safePatterns?: RegExp[],
|
|
130
|
+
destructivePatterns?: RegExp[],
|
|
131
|
+
): string | null {
|
|
132
|
+
if (safePatterns && safePatterns.length > 0) {
|
|
133
|
+
const offender = commandSegments(command).find((s) => !safePatterns.some((p) => p.test(s)));
|
|
134
|
+
if (offender !== undefined) return `not in safe pattern list: ${offender}`;
|
|
135
|
+
}
|
|
136
|
+
if (destructivePatterns && destructivePatterns.length > 0 && isDestructive(command, destructivePatterns)) {
|
|
137
|
+
return `destructive pattern in: ${command}`;
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
33
140
|
}
|
|
34
141
|
|
|
35
142
|
const PLAN_ACTION_VERB = /^\s*\d+\.\s+(?:\*{1,2})?(?:add|creat|updat|fix|remov|refactor|implement|modif|chang|edit|writ|build|run|install|configur|set\s+up|delet|mov|renam|inject|migrat|replac|extract|test|deploy|integrat|convert)/i;
|
|
@@ -90,6 +197,8 @@ export function sanitizePlanName(name: string): string | null {
|
|
|
90
197
|
if (!trimmed) return null;
|
|
91
198
|
if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("..")) return null;
|
|
92
199
|
if (!/^[\w\s.-]+$/.test(trimmed)) return null;
|
|
200
|
+
// A name with no letter or digit (".", "--", ". ") would create plan-.md and an empty title.
|
|
201
|
+
if (!/\w/.test(trimmed)) return null;
|
|
93
202
|
return trimmed.replace(/\s+/g, "-");
|
|
94
203
|
}
|
|
95
204
|
|
|
@@ -134,7 +243,9 @@ export function isHexColor(color: string): boolean {
|
|
|
134
243
|
|
|
135
244
|
/** Convert hex color string to ANSI truecolor escape. */
|
|
136
245
|
function hexToAnsi(hex: string): string {
|
|
137
|
-
|
|
246
|
+
// Accept the #abc shorthand so a three-digit value renders the same as #aabbcc.
|
|
247
|
+
let h = hex.replace("#", "");
|
|
248
|
+
if (/^[0-9a-fA-F]{3}$/.test(h)) h = h.split("").map((c) => c + c).join("");
|
|
138
249
|
if (!/^[0-9a-fA-F]{6}$/.test(h)) return "";
|
|
139
250
|
const r = parseInt(h.slice(0, 2), 16);
|
|
140
251
|
const g = parseInt(h.slice(2, 4), 16);
|
|
@@ -312,7 +312,9 @@ function makePiBedrockThinkingChatFn(modelConfig: BenchmarkModel): ChatFn {
|
|
|
312
312
|
...(systemPrompt ? { systemPrompt } : {}),
|
|
313
313
|
messages: messages.filter(m => m.role !== "system") as any,
|
|
314
314
|
...(tools?.length ? { tools } : {}),
|
|
315
|
-
|
|
315
|
+
// pi-ai's TranscriptContext carries a private brand, so a literal cannot satisfy it
|
|
316
|
+
// structurally; the fields above are the shape streamSimple actually reads.
|
|
317
|
+
} as any, { reasoning: "max" });
|
|
316
318
|
const message: any = await stream.result();
|
|
317
319
|
const content = (message.content || []).filter((block: any) => block.type === "text").map((block: any) => block.text).join("");
|
|
318
320
|
const toolCalls = (message.content || []).filter((block: any) => block.type === "toolCall").map((block: any) => ({
|
|
@@ -387,13 +389,22 @@ function makeOllamaToolChatFn(): ChatFn {
|
|
|
387
389
|
|
|
388
390
|
// ── Ollama Chat Functions ─────────────────────────────────────────────
|
|
389
391
|
|
|
392
|
+
/** Both Ollama chat helpers return this shape; the unused fields stay undefined per mode. */
|
|
393
|
+
interface OllamaChatResult {
|
|
394
|
+
response: any;
|
|
395
|
+
elapsedMs: number;
|
|
396
|
+
requestCount?: number;
|
|
397
|
+
retryCount?: number;
|
|
398
|
+
timeToFirstTokenMs?: number | null;
|
|
399
|
+
}
|
|
400
|
+
|
|
390
401
|
async function ollamaChat(
|
|
391
402
|
model: string,
|
|
392
403
|
messages: Array<{ role: string; content: string }>,
|
|
393
404
|
options: Record<string, unknown> = {},
|
|
394
405
|
timeoutMs = CONFIG.DEFAULT_TIMEOUT_MS,
|
|
395
406
|
retries = CONFIG.MAX_RETRIES
|
|
396
|
-
): Promise<
|
|
407
|
+
): Promise<OllamaChatResult> {
|
|
397
408
|
const { tools, ...generationOptions } = options as any;
|
|
398
409
|
const body: any = { model, messages, stream: false, ...(tools ? { tools } : {}), ...(Object.keys(generationOptions).length ? { options: generationOptions } : {}) };
|
|
399
410
|
const url = `${ollamaBase()}/api/chat`;
|
|
@@ -425,7 +436,9 @@ async function ollamaChat(
|
|
|
425
436
|
throw new Error(`Empty response from Ollama after ${attempt + 1} attempt(s)`);
|
|
426
437
|
}
|
|
427
438
|
const parsed = JSON.parse(text);
|
|
428
|
-
|
|
439
|
+
// Surface the retry loop so artifacts record real request/retry counts (metricsFromChat
|
|
440
|
+
// used to receive undefined and silently report 1 request, 0 retries).
|
|
441
|
+
return { response: parsed, elapsedMs, requestCount: attempt + 1, retryCount: attempt };
|
|
429
442
|
} catch (e: unknown) {
|
|
430
443
|
const msg = e instanceof Error ? e.message : String(e);
|
|
431
444
|
if (e instanceof Error && e.name === "AbortError") {
|
|
@@ -455,7 +468,7 @@ async function ollamaChatStream(
|
|
|
455
468
|
messages: Array<{ role: string; content: string }>,
|
|
456
469
|
options: Record<string, unknown> = {},
|
|
457
470
|
timeoutMs = CONFIG.DEFAULT_TIMEOUT_MS,
|
|
458
|
-
): Promise<
|
|
471
|
+
): Promise<OllamaChatResult> {
|
|
459
472
|
const { tools, ...generationOptions } = options as any;
|
|
460
473
|
const body: any = { model, messages, stream: true, ...(tools ? { tools } : {}), ...(Object.keys(generationOptions).length ? { options: generationOptions } : {}) };
|
|
461
474
|
const url = `${ollamaBase()}/api/chat`;
|
|
@@ -484,6 +497,7 @@ async function ollamaChatStream(
|
|
|
484
497
|
let thinkingContent = "";
|
|
485
498
|
const toolCalls: any[] = [];
|
|
486
499
|
let done = false;
|
|
500
|
+
let timeToFirstTokenMs: number | null = null;
|
|
487
501
|
|
|
488
502
|
const reader = res.body.getReader();
|
|
489
503
|
const decoder = new TextDecoder();
|
|
@@ -497,6 +511,9 @@ async function ollamaChatStream(
|
|
|
497
511
|
for (const line of lines) {
|
|
498
512
|
try {
|
|
499
513
|
const parsed = JSON.parse(line);
|
|
514
|
+
if (timeToFirstTokenMs === null && (parsed.message?.content || parsed.message?.thinking || parsed.message?.tool_calls)) {
|
|
515
|
+
timeToFirstTokenMs = Date.now() - start;
|
|
516
|
+
}
|
|
500
517
|
if (parsed.message?.content) messageContent += parsed.message.content;
|
|
501
518
|
if (parsed.message?.thinking) thinkingContent += parsed.message.thinking;
|
|
502
519
|
if (parsed.message?.tool_calls) toolCalls.push(...parsed.message.tool_calls);
|
|
@@ -521,7 +538,7 @@ async function ollamaChatStream(
|
|
|
521
538
|
done: true,
|
|
522
539
|
};
|
|
523
540
|
|
|
524
|
-
return { response, elapsedMs };
|
|
541
|
+
return { response, elapsedMs, timeToFirstTokenMs };
|
|
525
542
|
} catch (e: unknown) {
|
|
526
543
|
if (e instanceof Error && e.name === "AbortError") {
|
|
527
544
|
throw new Error(`Ollama API timed out after ${msHuman(timeoutMs)}`);
|
|
@@ -562,7 +579,7 @@ async function testReasoningExtended(chatFn: ChatFn, model: string, onProgress?:
|
|
|
562
579
|
const msg = result.content.trim();
|
|
563
580
|
const scored = scoreReasoning(msg, test.expectedAnswer);
|
|
564
581
|
const answer = scored.answer;
|
|
565
|
-
results.push({ name: test.name, category: test.category, prompt: test.prompt, response: msg, elapsedMs: result.elapsedMs, metrics: metricsFromChat({ ...result, startedAt: result.startedAt ?? requestedAt, finishedAt: result.finishedAt ?? new Date().toISOString() }), score: scored.score, answer, expectedAnswer: test.expectedAnswer, pass: scored.pass
|
|
582
|
+
results.push({ name: test.name, category: test.category, prompt: test.prompt, response: msg, elapsedMs: result.elapsedMs, metrics: metricsFromChat({ ...result, startedAt: result.startedAt ?? requestedAt, finishedAt: result.finishedAt ?? new Date().toISOString() }), score: scored.score, answer, expectedAnswer: test.expectedAnswer, pass: scored.pass });
|
|
566
583
|
onProgress?.(`[1/3] Reasoning ${i + 1}/${total}: ${test.name} → ${scored.score}`);
|
|
567
584
|
} catch (e: any) {
|
|
568
585
|
results.push({ name: test.name, category: test.category, prompt: test.prompt, response: null, error: e?.message || String(e), elapsedMs: 0, metrics: emptyMetrics(), score: "ERROR", answer: "?", expectedAnswer: test.expectedAnswer, pass: false });
|
|
@@ -683,7 +700,7 @@ async function testModelExtended(model: string, ctx?: any, options: SimplebenchO
|
|
|
683
700
|
const providerInfo = ctx ? detectProvider(ctx) : { kind: "ollama" as const, name: "ollama" };
|
|
684
701
|
const resolvedModel = resolveBenchmarkModel(ctx, model);
|
|
685
702
|
const thinking = resolveThinkingMode(providerInfo, resolvedModel.model, options.thinkingMax === true);
|
|
686
|
-
const suite = options.testAll ? "test-all" : options.codingLite ? "coding-lite" : "baseline";
|
|
703
|
+
const suite: "baseline" | "coding-lite" | "test-all" = options.testAll ? "test-all" : options.codingLite ? "coding-lite" : "baseline";
|
|
687
704
|
|
|
688
705
|
// Direct llama-server probes are separate from the provider route. This is
|
|
689
706
|
// intentional: --llamagputop also works when inference goes through LiteLLM.
|
|
@@ -65,11 +65,10 @@ export default function (pi: ExtensionAPI) {
|
|
|
65
65
|
|
|
66
66
|
pi.registerCommand("simplebench", {
|
|
67
67
|
description: "Benchmark a model with auditable closed-answer, instruction, and tool-use tests.",
|
|
68
|
-
detailedHelp: "\n\n🔍 Simplebench Extension\n\nThis extension tests AI models across multiple dimensions:\n• Closed-answer contract: 20 deterministic final-line answers (logic, math, spatial, commonsense)\n• Tool Usage: Ability to use available tools effectively\n• Instruction Following: How well the model follows complex JSON instructions\n• Coding Lite: Six isolated, execution-backed coding tasks\n\n📋 Usage Examples:\n /simplebench # Test current model\n /simplebench <model> # Test a specific model\n /simplebench --all # Test all Ollama models\n /simplebench <model> --3ptest # Run the default 3ptest baseline explicitly\n /simplebench <model> --coding-lite # Run only coding tasks\n /simplebench <model> --test-all # Run baseline, coding, and grounded research\n /simplebench <model> --research-live # Add live search integration smoke test\n /simplebench --all --test-all # Run complete suite for every Ollama model\n /simplebench <model> --sequence[=<name>] # Run a configured runSequence profile\n /simplebench <model> --thinking-max # Request max reasoning\n /simplebench <model> --llama-server # Capture configured /props and /metrics\n /simplebench <model> --llamagputop # Capture configured llama.cpp stats\n /simplebench <model> --tag=coldrun # Label the run: benchmark.tag plus artifact filename prefix\n /simplebench --help # Show this help\n /simplebench --clear-cache # Clear tool support cache\n\nCoding tasks run in disposable directories and never access the user repository.\n",
|
|
69
68
|
getArgumentCompletions: async (prefix) => {
|
|
70
69
|
try {
|
|
71
70
|
const models = await getOllamaModels();
|
|
72
|
-
return models.map(m => ({ label: m, description: `Test ${m}` }))
|
|
71
|
+
return models.map(m => ({ value: m, label: m, description: `Test ${m}` }))
|
|
73
72
|
.filter(m => m.label.startsWith(prefix));
|
|
74
73
|
} catch (err) { debugLog("simplebench", "failed to get model completions", err); return []; }
|
|
75
74
|
},
|
|
@@ -151,7 +150,7 @@ pi.registerCommand("simplebench", {
|
|
|
151
150
|
pi.sendMessage({
|
|
152
151
|
customType: "simplebench-report",
|
|
153
152
|
content: report,
|
|
154
|
-
display:
|
|
153
|
+
display: true,
|
|
155
154
|
details: { model, timestamp: new Date().toISOString() },
|
|
156
155
|
});
|
|
157
156
|
} catch (e: any) {
|
|
@@ -191,7 +190,7 @@ pi.registerCommand("simplebench", {
|
|
|
191
190
|
pi.sendMessage({
|
|
192
191
|
customType: "simplebench-report",
|
|
193
192
|
content: report,
|
|
194
|
-
display:
|
|
193
|
+
display: true,
|
|
195
194
|
details: { model, timestamp: new Date().toISOString() },
|
|
196
195
|
});
|
|
197
196
|
} catch (e: any) {
|
|
@@ -214,7 +213,7 @@ pi.registerCommand("simplebench", {
|
|
|
214
213
|
pi.sendMessage({
|
|
215
214
|
customType: "simplebench-report",
|
|
216
215
|
content: report,
|
|
217
|
-
display:
|
|
216
|
+
display: true,
|
|
218
217
|
details: { model, timestamp: new Date().toISOString() },
|
|
219
218
|
});
|
|
220
219
|
} catch (e: any) {
|
|
@@ -256,6 +255,7 @@ pi.registerTool({
|
|
|
256
255
|
if (params?.tag !== undefined && !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(String(params.tag))) {
|
|
257
256
|
return {
|
|
258
257
|
content: [{ type: "text", text: "--tag must be a single word (letters, digits, dot, dash, underscore)" }],
|
|
258
|
+
details: undefined,
|
|
259
259
|
isError: true,
|
|
260
260
|
} as AgentToolResult;
|
|
261
261
|
}
|
|
@@ -264,6 +264,7 @@ pi.registerTool({
|
|
|
264
264
|
if (!model) {
|
|
265
265
|
return {
|
|
266
266
|
content: [{ type: "text", text: "No model currently selected to test." }],
|
|
267
|
+
details: undefined,
|
|
267
268
|
isError: true,
|
|
268
269
|
} as AgentToolResult;
|
|
269
270
|
}
|
|
@@ -271,6 +272,7 @@ pi.registerTool({
|
|
|
271
272
|
const report = await testModel(model, ctx, options);
|
|
272
273
|
return {
|
|
273
274
|
content: [{ type: "text", text: report }],
|
|
275
|
+
details: undefined,
|
|
274
276
|
isError: false,
|
|
275
277
|
} as AgentToolResult;
|
|
276
278
|
} catch (e: any) {
|
|
@@ -281,6 +283,7 @@ pi.registerTool({
|
|
|
281
283
|
|
|
282
284
|
return {
|
|
283
285
|
content: [{ type: "text", text: errorMessage }],
|
|
286
|
+
details: undefined,
|
|
284
287
|
isError: true,
|
|
285
288
|
} as AgentToolResult;
|
|
286
289
|
}
|
|
@@ -88,7 +88,10 @@ function normalizeClaim(value: string): string {
|
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
export function verifyGroundedResearch(markdown: string, fixture: GroundedResearchFixture): { passed: boolean; missingClaims: string[]; wrongCitations: string[]; unknownCitations: string[] } {
|
|
91
|
-
|
|
91
|
+
// Key is string: citation ids arrive from a regex capture, so a "S1"|"S2"|"S3" key would reject them.
|
|
92
|
+
const knownSources = new Map<string, GroundedResearchFixture["sources"][number]>(
|
|
93
|
+
fixture.sources.map(source => [source.id, source]),
|
|
94
|
+
);
|
|
92
95
|
const findings = markdown.match(/##\s+Findings\s*\n([\s\S]*?)(?=\n##\s|$)/i)?.[1] ?? "";
|
|
93
96
|
const sourceSection = markdown.match(/##\s+Sources\s*\n([\s\S]*?)(?=\n##\s|$)/i)?.[1] ?? "";
|
|
94
97
|
const citedClaims = [...findings.matchAll(/^\s*[-*]\s+(.+?)\s+\[([A-Za-z0-9]+)\]\s*$/gm)];
|
|
@@ -9,7 +9,12 @@ import * as fs from "node:fs";
|
|
|
9
9
|
import * as path from "node:path";
|
|
10
10
|
import os from "node:os";
|
|
11
11
|
import { debugLog } from "./debug";
|
|
12
|
-
|
|
12
|
+
|
|
13
|
+
/** Minimal shape of Pi's extension context that provider detection reads. Keeps this module
|
|
14
|
+
* standalone instead of importing a Pi type the extension host resolves at load time. */
|
|
15
|
+
interface PiExtensionContext {
|
|
16
|
+
model?: { provider?: string; id?: string };
|
|
17
|
+
}
|
|
13
18
|
|
|
14
19
|
// ============================================================================
|
|
15
20
|
// Constants
|
|
@@ -401,8 +401,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
401
401
|
}
|
|
402
402
|
// Fallback: switch above must never fall through. Returning undefined here
|
|
403
403
|
// makes pi wrap it in a MouseRegion with an undefined child and crashes the TUI.
|
|
404
|
-
|
|
405
|
-
return new Text(theme.fg("error",
|
|
404
|
+
// The action is known here, so report it instead of digging through content blocks.
|
|
405
|
+
return new Text(theme.fg("error", `Unhandled todo result for action "${String(details.action ?? "")}"`), 0, 0);
|
|
406
406
|
},
|
|
407
407
|
});
|
|
408
408
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openlines/opl-pi-sht",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "opl-pi-sht (shouldHaveThat) Pi coding-agent extension kit: token-efficient context, safe modes, fingerprinted /init, model benchmarking, web access, and browser automation.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"test:opl-footer": "bun test tests/opl-footer-helpers.test.mjs && OPL_EXTENSION=opl-footer bun test tests/extension-smoke.test.mjs",
|
|
46
46
|
"test:opl-init": "bun test tests/opl-init-crawl.test.mjs && OPL_EXTENSION=opl-init bun test tests/extension-smoke.test.mjs",
|
|
47
47
|
"test:opl-input": "bun test tests/opl-input-style.test.mjs && OPL_EXTENSION=opl-input bun test tests/extension-smoke.test.mjs",
|
|
48
|
-
"test:opl-modes": "bun test tests/opl-modes-helpers.test.mjs && OPL_EXTENSION=opl-modes bun test tests/extension-smoke.test.mjs",
|
|
48
|
+
"test:opl-modes": "bun test tests/opl-modes-helpers.test.mjs tests/opl-modes-lifecycle.test.mjs && OPL_EXTENSION=opl-modes bun test tests/extension-smoke.test.mjs",
|
|
49
49
|
"test:opl-questionnaire": "OPL_EXTENSION=opl-questionnaire bun test tests/extension-smoke.test.mjs",
|
|
50
50
|
"test:opl-todo": "bun test tests/opl-todo-config.test.mjs tests/opl-todo-render.test.mjs && OPL_EXTENSION=opl-todo bun test tests/extension-smoke.test.mjs",
|
|
51
51
|
"test:opl-webaccess": "bun test tests/opl-webaccess-helpers.test.mjs && OPL_EXTENSION=opl-webaccess bun test tests/extension-smoke.test.mjs",
|