agent-sh 0.14.9 → 0.14.11
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 +47 -20
- package/dist/agent/agent-loop.js +20 -15
- package/dist/agent/events.d.ts +2 -1
- package/dist/agent/index.js +44 -7
- package/dist/agent/live-view.d.ts +3 -3
- package/dist/agent/live-view.js +15 -7
- package/dist/agent/providers/ollama.d.ts +11 -0
- package/dist/agent/providers/ollama.js +72 -0
- package/dist/agent/providers/opencode.d.ts +10 -0
- package/dist/agent/providers/opencode.js +112 -0
- package/dist/agent/providers/openrouter.js +9 -0
- package/dist/agent/providers/zai-coding-plan.d.ts +5 -0
- package/dist/agent/providers/zai-coding-plan.js +26 -0
- package/dist/agent/subagent.js +1 -1
- package/dist/cli/args.js +2 -2
- package/dist/cli/install.js +10 -1
- package/dist/shell/events.d.ts +3 -0
- package/dist/shell/shell.js +3 -0
- package/dist/utils/diff-renderer.d.ts +4 -0
- package/dist/utils/diff-renderer.js +15 -20
- package/examples/extensions/ads/SKILL.md +170 -0
- package/examples/extensions/ads/index.ts +695 -0
- package/examples/extensions/ash-scheme/index.ts +339 -605
- package/examples/extensions/ash-scheme/package.json +1 -1
- package/examples/extensions/ashi/EXTENDING.md +116 -0
- package/examples/extensions/ashi/README.md +10 -54
- package/examples/extensions/ashi/package.json +6 -2
- package/examples/extensions/ashi/src/autocomplete-controller.ts +95 -0
- package/examples/extensions/ashi/src/autocomplete.ts +1 -23
- package/examples/extensions/ashi/src/capture.ts +9 -3
- package/examples/extensions/ashi/src/chat/assistant.ts +87 -0
- package/examples/extensions/ashi/src/chat/lines.ts +20 -0
- package/examples/extensions/ashi/src/chat/thinking.ts +42 -0
- package/examples/extensions/ashi/src/chat/tool-group.ts +84 -0
- package/examples/extensions/ashi/src/chat/user-message.ts +20 -0
- package/examples/extensions/ashi/src/cli.ts +58 -12
- package/examples/extensions/ashi/src/clipboard-image.ts +41 -0
- package/examples/extensions/ashi/src/commands.ts +11 -1
- package/examples/extensions/ashi/src/display-config.ts +9 -1
- package/examples/extensions/ashi/src/frontend.ts +340 -259
- package/examples/extensions/ashi/src/hooks.ts +33 -40
- package/examples/extensions/ashi/src/renderer.ts +222 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/app.ts +122 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/index.ts +23 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/nodes.ts +133 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/schema-mount.ts +193 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/theme-adapters.ts +48 -0
- package/examples/extensions/ashi/src/renderers/pi-tui/tool-group.ts +21 -0
- package/examples/extensions/ashi/src/schema.ts +43 -205
- package/examples/extensions/ashi/src/status-footer.ts +15 -23
- package/examples/extensions/ashi/src/terminal-mode.ts +9 -0
- package/examples/extensions/ashi/src/theme.ts +1 -47
- package/examples/extensions/ashi-ink/README.md +59 -0
- package/examples/extensions/ashi-ink/package.json +30 -0
- package/examples/extensions/ashi-ink/src/index.ts +6 -0
- package/examples/extensions/ashi-ink/src/ink-renderer.tsx +865 -0
- package/examples/extensions/ashi-ink/src/shims.d.ts +5 -0
- package/examples/extensions/ashi-ink/test/render.test.tsx +408 -0
- package/examples/extensions/ashi-ink/tsconfig.json +14 -0
- package/examples/extensions/ashi-scheme-render.ts +4 -10
- package/examples/extensions/ashi-shell-passthrough.ts +95 -0
- package/examples/extensions/latex-images.ts +22 -19
- package/examples/extensions/terminal-buffer.ts +4 -2
- package/package.json +3 -9
- package/examples/extensions/ashi/src/components.ts +0 -238
- package/examples/extensions/ollama.ts +0 -108
- package/examples/extensions/opencode-provider.ts +0 -251
- package/examples/extensions/zai-coding-plan.ts +0 -35
|
@@ -5,9 +5,13 @@
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as os from "node:os";
|
|
7
7
|
import * as path from "node:path";
|
|
8
|
+
import { createRequire } from "node:module";
|
|
8
9
|
import type { AgentContext } from "agent-sh/types";
|
|
9
10
|
import { getSettings } from "agent-sh/settings";
|
|
10
|
-
|
|
11
|
+
// LIPS 1.0's ESM build exposes named exports with no default; older builds (and
|
|
12
|
+
// the CJS interop path) put everything under `default`. Accept both.
|
|
13
|
+
import * as lipsNs from "@jcubic/lips";
|
|
14
|
+
const lips: any = (lipsNs as any).default ?? lipsNs;
|
|
11
15
|
|
|
12
16
|
type ToolResult = {
|
|
13
17
|
content: string; exitCode: number | null; isError: boolean;
|
|
@@ -26,6 +30,8 @@ async function withDisplay(
|
|
|
26
30
|
title: toolName, toolCallId, kind, rawInput, displayDetail,
|
|
27
31
|
});
|
|
28
32
|
const result = await run();
|
|
33
|
+
// Stream the result so the TUI's tracked `output` holds it, not just the summary.
|
|
34
|
+
if (result.content) bus.emit("agent:tool-output-chunk", { toolCallId, chunk: result.content });
|
|
29
35
|
bus.emit("agent:tool-completed", {
|
|
30
36
|
toolCallId,
|
|
31
37
|
exitCode: result.exitCode,
|
|
@@ -41,64 +47,16 @@ async function withDisplay(
|
|
|
41
47
|
// the TUI still renders diffs.
|
|
42
48
|
const HIDDEN_IN_SCHEME_ONLY = ["bash", "pwsh", "read_file", "write_file", "edit_file", "ls", "glob", "grep"];
|
|
43
49
|
|
|
44
|
-
const { Pair, nil, LSymbol, LNumber, Macro,
|
|
45
|
-
|
|
46
|
-
// LIPS' `define` discards the promise returned by async host bindings.
|
|
47
|
-
// With `(define x (read-file …)) x` the exec() advances before `env.set`
|
|
48
|
-
// fires, so `x` is reported unbound. Reinstall to return the promise.
|
|
49
|
-
function installFixedDefine(env: any): void {
|
|
50
|
-
const fixed = Macro.defmacro("define", function (this: any, code: any, eval_args: any) {
|
|
51
|
-
const target = this;
|
|
52
|
-
if (code.car instanceof Pair && code.car.car instanceof LSymbol) {
|
|
53
|
-
return new Pair(
|
|
54
|
-
new LSymbol("define"),
|
|
55
|
-
new Pair(
|
|
56
|
-
code.car.car,
|
|
57
|
-
new Pair(
|
|
58
|
-
new Pair(new LSymbol("lambda"), new Pair(code.car.cdr, code.cdr)),
|
|
59
|
-
nil,
|
|
60
|
-
),
|
|
61
|
-
),
|
|
62
|
-
);
|
|
63
|
-
} else if (eval_args.macro_expand) {
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
if (eval_args.dynamic_scope) eval_args.dynamic_scope = target;
|
|
67
|
-
eval_args.env = target;
|
|
68
|
-
let value = code.cdr.car;
|
|
69
|
-
if (value instanceof Pair) {
|
|
70
|
-
value = lipsEvaluate(value, eval_args);
|
|
71
|
-
} else if (value instanceof LSymbol) {
|
|
72
|
-
value = target.get(value);
|
|
73
|
-
}
|
|
74
|
-
if (code.car instanceof LSymbol) {
|
|
75
|
-
const name = code.car;
|
|
76
|
-
if (value && typeof value.then === "function") {
|
|
77
|
-
return value.then((v: any) => { target.set(name, v); });
|
|
78
|
-
}
|
|
79
|
-
target.set(name, value);
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
env.set("define", fixed);
|
|
83
|
-
}
|
|
50
|
+
const { Pair, nil, LSymbol, LNumber, Macro, bootstrap, LString } = lips as any;
|
|
84
51
|
|
|
85
|
-
// LIPS
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const branch = cond !== false ? code.cdr.car : code.cdr.cdr.car;
|
|
94
|
-
return lipsEvaluate(branch, { env: target, dynamic_scope: dynScope, error: opts.error });
|
|
95
|
-
};
|
|
96
|
-
const condVal = lipsEvaluate(code.car, { env: target, dynamic_scope: dynScope, error: opts.error });
|
|
97
|
-
if (condVal && typeof condVal.then === "function") return condVal.then(choose);
|
|
98
|
-
return choose(condVal);
|
|
99
|
-
});
|
|
100
|
-
env.set("if", lenient);
|
|
101
|
-
}
|
|
52
|
+
// LIPS 1.0 boxes string literals as LString; unbox to a JS primitive so the gap
|
|
53
|
+
// shims and host bridge (which assume JS strings) operate on them correctly.
|
|
54
|
+
const toJsStr = (x: any): any => (x instanceof LString ? x.toString() : x);
|
|
55
|
+
|
|
56
|
+
// LIPS 1.0 stores a symbol's name in `__name__`; `.name` is undefined (the 0.x
|
|
57
|
+
// build used `.name`). Read every symbol name through this so both builds work.
|
|
58
|
+
const symName = (x: any): string | undefined =>
|
|
59
|
+
x instanceof LSymbol ? ((x as any).__name__ ?? (x as any).name) : undefined;
|
|
102
60
|
|
|
103
61
|
const LOG_PATH = path.join(os.homedir(), ".agent-sh", "scheme-eval.log");
|
|
104
62
|
const SCHEME_DEFINE_DIR = path.join(os.homedir(), ".agent-sh", "scheme-define");
|
|
@@ -123,7 +81,7 @@ function installSchemeDefine(
|
|
|
123
81
|
throw new Error("scheme-define: expected (scheme-define name (args …) \"doc\" body …)");
|
|
124
82
|
}
|
|
125
83
|
const nameSym = code.car;
|
|
126
|
-
const name = nameSym
|
|
84
|
+
const name = symName(nameSym)!;
|
|
127
85
|
const argsForm = code.cdr instanceof Pair ? code.cdr.car : nil;
|
|
128
86
|
let rest = code.cdr instanceof Pair ? code.cdr.cdr : nil;
|
|
129
87
|
let doc = "";
|
|
@@ -175,7 +133,7 @@ async function loadPersistedDefines(
|
|
|
175
133
|
const fp = path.join(SCHEME_DEFINE_DIR, f);
|
|
176
134
|
try {
|
|
177
135
|
const src = fs.readFileSync(fp, "utf-8");
|
|
178
|
-
await (lips as any).exec(src, env);
|
|
136
|
+
await (lips as any).exec(src, { env });
|
|
179
137
|
} catch (e) {
|
|
180
138
|
logErr("scheme-define load", e, { file: fp });
|
|
181
139
|
}
|
|
@@ -266,7 +224,7 @@ function lookup(result: unknown, key: string): unknown {
|
|
|
266
224
|
let node: any = result;
|
|
267
225
|
while (node && node instanceof Pair) {
|
|
268
226
|
const entry = node.car;
|
|
269
|
-
if (entry && entry.car && entry.car
|
|
227
|
+
if (entry && entry.car && symName(entry.car) === key) return entry.cdr;
|
|
270
228
|
node = node.cdr;
|
|
271
229
|
}
|
|
272
230
|
return undefined;
|
|
@@ -310,8 +268,11 @@ function stripPagination(raw: string): string[] {
|
|
|
310
268
|
|
|
311
269
|
function format(v: unknown): string {
|
|
312
270
|
if (v === undefined || v === null) return "";
|
|
271
|
+
if (v instanceof LString) v = v.toString();
|
|
313
272
|
if (typeof v === "string") return JSON.stringify(v);
|
|
314
|
-
|
|
273
|
+
// Render JS booleans Scheme-style, matching how #t/#f print inside records.
|
|
274
|
+
if (typeof v === "boolean") return v ? "#t" : "#f";
|
|
275
|
+
if (typeof v === "number") return String(v);
|
|
315
276
|
if (v && typeof (v as any).toString === "function") {
|
|
316
277
|
try { return (v as any).toString(); } catch {}
|
|
317
278
|
}
|
|
@@ -319,10 +280,9 @@ function format(v: unknown): string {
|
|
|
319
280
|
}
|
|
320
281
|
|
|
321
282
|
// ── evaluator ─────────────────────────────────────────────────────
|
|
322
|
-
// LIPS
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
// invalid \X to \\X so LIPS parses it as a literal backslash + X.
|
|
283
|
+
// LIPS' string lexer accepts only JSON-style escapes (\" \\ \/ \b \f \n \r \t
|
|
284
|
+
// \uXXXX), but models routinely write \s \w \d etc. in regex strings. Promote
|
|
285
|
+
// any other \X to \\X so it parses as a literal backslash + X.
|
|
326
286
|
function preprocessSchemeSource(source: string): string {
|
|
327
287
|
const JSON_ESC = new Set(["\\", "/", '"', "b", "f", "n", "r", "t"]);
|
|
328
288
|
let out = "";
|
|
@@ -355,16 +315,16 @@ function preprocessSchemeSource(source: string): string {
|
|
|
355
315
|
i++;
|
|
356
316
|
continue;
|
|
357
317
|
}
|
|
358
|
-
// Any other \X — promote so
|
|
318
|
+
// Any other \X — promote to \\X so it parses as a literal backslash
|
|
359
319
|
out += "\\\\" + next;
|
|
360
320
|
i++;
|
|
361
321
|
}
|
|
362
322
|
return out;
|
|
363
323
|
}
|
|
364
324
|
|
|
365
|
-
// If LIPS
|
|
366
|
-
//
|
|
367
|
-
//
|
|
325
|
+
// If LIPS rejects a string literal, localize the invalid escapes so the agent
|
|
326
|
+
// gets actionable line/col info instead of a raw offset. Only triggers when
|
|
327
|
+
// preprocessing didn't catch everything.
|
|
368
328
|
function formatStringEscapeDiagnostic(source: string, baseMsg: string): string {
|
|
369
329
|
const JSON_ESC = new Set(["\\", "/", '"', "b", "f", "n", "r", "t"]);
|
|
370
330
|
let line = 1, col = 1;
|
|
@@ -474,30 +434,29 @@ function formatParenDiagnostic(source: string, baseMsg: string): string {
|
|
|
474
434
|
|
|
475
435
|
async function evaluate(env: any, source: string, timeoutMs: number) {
|
|
476
436
|
const preprocessed = preprocessSchemeSource(source);
|
|
477
|
-
//
|
|
478
|
-
//
|
|
479
|
-
//
|
|
480
|
-
|
|
481
|
-
const
|
|
482
|
-
const
|
|
437
|
+
// Capture output into the result instead of letting it vanish to console.log.
|
|
438
|
+
// LIPS 1.0's native display/write resolve the *functions* from the env (the
|
|
439
|
+
// stdout-port override alone misses them), so shadow each output procedure.
|
|
440
|
+
const OUTPUT_PROCS = ["stdout", "display", "write", "write-string", "write-char"];
|
|
441
|
+
const prev: Record<string, any> = {};
|
|
442
|
+
for (const name of OUTPUT_PROCS) prev[name] = (env as any).get(name, { throwError: false });
|
|
483
443
|
const buf: string[] = [];
|
|
484
|
-
(
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
return String(a);
|
|
495
|
-
}).join("");
|
|
496
|
-
buf.push(out);
|
|
444
|
+
const raw = (a: any): string => {
|
|
445
|
+
if (a === null || a === undefined) return "";
|
|
446
|
+
if (typeof a === "string") return a;
|
|
447
|
+
if (a && typeof (a as any).toString === "function") return (a as any).toString();
|
|
448
|
+
return String(a);
|
|
449
|
+
};
|
|
450
|
+
(env as any).set("stdout", { write: (...args: any[]) => { for (const a of args) buf.push(raw(a)); } });
|
|
451
|
+
(env as any).set("display", (...args: any[]) => { buf.push(args.map(raw).join("")); });
|
|
452
|
+
(env as any).set("write", (...args: any[]) => {
|
|
453
|
+
buf.push(args.map((a) => (typeof toJsStr(a) === "string" ? JSON.stringify(toJsStr(a)) : raw(a))).join(""));
|
|
497
454
|
});
|
|
455
|
+
(env as any).set("write-string", (...args: any[]) => { buf.push(raw(args[0])); });
|
|
456
|
+
(env as any).set("write-char", (...args: any[]) => { buf.push(raw(args[0])); });
|
|
498
457
|
try {
|
|
499
458
|
const results = await Promise.race<any>([
|
|
500
|
-
(lips as any).exec(preprocessed, env),
|
|
459
|
+
(lips as any).exec(preprocessed, { env }),
|
|
501
460
|
new Promise((_, reject) =>
|
|
502
461
|
setTimeout(() => reject(new Error(`scheme_eval timed out after ${timeoutMs}ms`)), timeoutMs),
|
|
503
462
|
),
|
|
@@ -516,22 +475,25 @@ async function evaluate(env: any, source: string, timeoutMs: number) {
|
|
|
516
475
|
let msg = e?.message ?? String(e);
|
|
517
476
|
if (/[Uu]nbalanced parenthes/.test(msg)) {
|
|
518
477
|
msg = formatParenDiagnostic(source, msg);
|
|
519
|
-
} else if (/Bad escaped character
|
|
478
|
+
} else if (/Invalid string literal|Bad escaped character|Unexpected.*JSON|JSON at position/.test(msg)) {
|
|
520
479
|
msg = formatStringEscapeDiagnostic(source, msg);
|
|
480
|
+
} else if (msg.includes("Unbound variable `#\\")) {
|
|
481
|
+
msg += "\n Unknown character literal. Supported: #\\newline #\\space #\\tab" +
|
|
482
|
+
" #\\return #\\null #\\delete #\\escape, #\\xNN, and #\\<char>.";
|
|
521
483
|
}
|
|
522
484
|
return { ok: false as const, error: msg };
|
|
523
485
|
} finally {
|
|
524
|
-
|
|
525
|
-
|
|
486
|
+
for (const name of OUTPUT_PROCS) {
|
|
487
|
+
if (prev[name] !== undefined) (env as any).set(name, prev[name]);
|
|
488
|
+
}
|
|
526
489
|
}
|
|
527
490
|
}
|
|
528
491
|
|
|
529
492
|
// ── standard-library shims ───────────────────────────────────────
|
|
530
|
-
//
|
|
531
|
-
//
|
|
532
|
-
//
|
|
533
|
-
//
|
|
534
|
-
// works regardless of which Scheme dialect it learned from.
|
|
493
|
+
// Fill the gaps the bootstrapped std library leaves: SRFI-1 helpers, Racket
|
|
494
|
+
// spellings, and cross-dialect aliases. std covers R7RS base, but a model
|
|
495
|
+
// trained on Racket/Chicken/Guile still reaches for names std doesn't bind.
|
|
496
|
+
// defineIfMissing so anything std already provides wins.
|
|
535
497
|
function installStdShims(env: any): void {
|
|
536
498
|
const defineIfMissing = (name: string, fn: any) => {
|
|
537
499
|
if ((env as any).get(name, { throwError: false }) === undefined) env.set(name, fn);
|
|
@@ -543,7 +505,6 @@ function installStdShims(env: any): void {
|
|
|
543
505
|
};
|
|
544
506
|
const truthy = (v: any) => v !== false;
|
|
545
507
|
|
|
546
|
-
// ── R7RS equality ─────────────────────────────────────────
|
|
547
508
|
// LIPS wraps numbers as LNumber instances, so `===` fails on equal-valued
|
|
548
509
|
// numbers from different sources. Handle the wrapper types before recursing.
|
|
549
510
|
const atomEqual = (a: any, b: any): boolean => {
|
|
@@ -551,7 +512,7 @@ function installStdShims(env: any): void {
|
|
|
551
512
|
if (a instanceof LNumber && b instanceof LNumber) return a.cmp(b) === 0;
|
|
552
513
|
if (typeof a === "number" && b instanceof LNumber) return LNumber(a).cmp(b) === 0;
|
|
553
514
|
if (typeof b === "number" && a instanceof LNumber) return LNumber(b).cmp(a) === 0;
|
|
554
|
-
if (a instanceof LSymbol && b instanceof LSymbol) return a
|
|
515
|
+
if (a instanceof LSymbol && b instanceof LSymbol) return symName(a) === symName(b);
|
|
555
516
|
return false;
|
|
556
517
|
};
|
|
557
518
|
const lipsEqual = (a: any, b: any): boolean => {
|
|
@@ -567,41 +528,13 @@ function installStdShims(env: any): void {
|
|
|
567
528
|
}
|
|
568
529
|
return false;
|
|
569
530
|
};
|
|
570
|
-
defineIfMissing("equal?", lipsEqual);
|
|
571
|
-
defineIfMissing("eqv?", atomEqual);
|
|
572
|
-
|
|
573
|
-
// ── R7RS list/member ─────────────────────────────────────
|
|
574
|
-
defineIfMissing("list?", (v: any) => v === nil || v instanceof Pair);
|
|
575
|
-
const memberLike = (eq: (a: any, b: any) => boolean) => (item: any, lst: any) => {
|
|
576
|
-
let p = lst;
|
|
577
|
-
while (p instanceof Pair) {
|
|
578
|
-
if (eq(p.car, item)) return p;
|
|
579
|
-
p = p.cdr;
|
|
580
|
-
}
|
|
581
|
-
return false;
|
|
582
|
-
};
|
|
583
|
-
defineIfMissing("member", memberLike(lipsEqual));
|
|
584
|
-
defineIfMissing("memq", memberLike((a, b) => a === b));
|
|
585
|
-
defineIfMissing("memv", memberLike((a, b) => a === b));
|
|
586
|
-
defineIfMissing("assv", (item: any, lst: any) => {
|
|
587
|
-
let p = lst;
|
|
588
|
-
while (p instanceof Pair) {
|
|
589
|
-
const e = p.car;
|
|
590
|
-
if (e instanceof Pair && e.car === item) return e;
|
|
591
|
-
p = p.cdr;
|
|
592
|
-
}
|
|
593
|
-
return false;
|
|
594
|
-
});
|
|
595
531
|
|
|
596
|
-
// ── SRFI-1 list helpers ──────────────────────────────────
|
|
597
532
|
defineIfMissing("first", (lst: any) => pairToArray(lst)[0]);
|
|
598
533
|
defineIfMissing("second", (lst: any) => pairToArray(lst)[1]);
|
|
599
534
|
defineIfMissing("third", (lst: any) => pairToArray(lst)[2]);
|
|
600
535
|
defineIfMissing("fourth", (lst: any) => pairToArray(lst)[3]);
|
|
601
536
|
defineIfMissing("fifth", (lst: any) => pairToArray(lst)[4]);
|
|
602
537
|
defineIfMissing("last", (lst: any) => { const a = pairToArray(lst); return a[a.length - 1]; });
|
|
603
|
-
defineIfMissing("take", (lst: any, n: any) => toSchemeList(pairToArray(lst).slice(0, Number(n))));
|
|
604
|
-
defineIfMissing("drop", (lst: any, n: any) => toSchemeList(pairToArray(lst).slice(Number(n))));
|
|
605
538
|
defineIfMissing("take-while", (pred: any, lst: any) => {
|
|
606
539
|
const out: any[] = [];
|
|
607
540
|
for (const x of pairToArray(lst)) { if (!truthy(pred(x))) break; out.push(x); }
|
|
@@ -619,7 +552,6 @@ function installStdShims(env: any): void {
|
|
|
619
552
|
return toSchemeList(Array.from({ length: n }, (_, i) => s + i * k));
|
|
620
553
|
});
|
|
621
554
|
defineIfMissing("any", (pred: any, lst: any) => pairToArray(lst).some((x) => truthy(pred(x))));
|
|
622
|
-
defineIfMissing("every", (pred: any, lst: any) => pairToArray(lst).every((x) => truthy(pred(x))));
|
|
623
555
|
defineIfMissing("count", (pred: any, lst: any) => pairToArray(lst).filter((x) => truthy(pred(x))).length);
|
|
624
556
|
defineIfMissing("filter-map", (f: any, lst: any) => {
|
|
625
557
|
const out: any[] = [];
|
|
@@ -645,44 +577,8 @@ function installStdShims(env: any): void {
|
|
|
645
577
|
for (const x of pairToArray(lst)) (truthy(pred(x)) ? t : f).push(x);
|
|
646
578
|
return new Pair(toSchemeList(t), toSchemeList(f));
|
|
647
579
|
});
|
|
648
|
-
|
|
649
|
-
const a = pairToArray(lst); let acc = init;
|
|
650
|
-
for (let i = a.length - 1; i >= 0; i--) acc = f(a[i], acc);
|
|
651
|
-
return acc;
|
|
652
|
-
});
|
|
653
|
-
defineIfMissing("zip", (a: any, b: any) => {
|
|
654
|
-
const xs = pairToArray(a); const ys = pairToArray(b);
|
|
655
|
-
const n = Math.min(xs.length, ys.length);
|
|
656
|
-
return toSchemeList(Array.from({ length: n }, (_, i) => toSchemeList([xs[i], ys[i]])));
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
// ── R7RS numeric predicates / ops ────────────────────────
|
|
660
|
-
defineIfMissing("zero?", (n: any) => Number(n) === 0);
|
|
661
|
-
defineIfMissing("positive?", (n: any) => Number(n) > 0);
|
|
662
|
-
defineIfMissing("negative?", (n: any) => Number(n) < 0);
|
|
663
|
-
defineIfMissing("odd?", (n: any) => Math.abs(Number(n)) % 2 === 1);
|
|
664
|
-
defineIfMissing("even?", (n: any) => Number(n) % 2 === 0);
|
|
665
|
-
defineIfMissing("modulo", (a: any, b: any) => { const m = Number(a) % Number(b); return (m < 0) === (Number(b) < 0) ? m : m + Number(b); });
|
|
666
|
-
defineIfMissing("quotient", (a: any, b: any) => Math.trunc(Number(a) / Number(b)));
|
|
667
|
-
defineIfMissing("remainder", (a: any, b: any) => Number(a) % Number(b));
|
|
668
|
-
defineIfMissing("expt", (a: any, b: any) => Math.pow(Number(a), Number(b)));
|
|
669
|
-
defineIfMissing("ceiling", (n: any) => Math.ceil(Number(n)));
|
|
670
|
-
|
|
671
|
-
// ── R7RS string ops ──────────────────────────────────────
|
|
672
|
-
defineIfMissing("string=?", (a: any, b: any) => String(a) === String(b));
|
|
673
|
-
defineIfMissing("string<?", (a: any, b: any) => String(a) < String(b));
|
|
674
|
-
defineIfMissing("string>?", (a: any, b: any) => String(a) > String(b));
|
|
675
|
-
defineIfMissing("string-upcase", (s: any) => String(s).toUpperCase());
|
|
676
|
-
defineIfMissing("string-downcase", (s: any) => String(s).toLowerCase());
|
|
677
|
-
defineIfMissing("string->list", (s: any) => toSchemeList(Array.from(String(s))));
|
|
678
|
-
defineIfMissing("list->string", (lst: any) => pairToArray(lst).map(String).join(""));
|
|
679
|
-
defineIfMissing("string-ref", (s: any, i: any) => {
|
|
680
|
-
const str = String(s);
|
|
681
|
-
const idx = Math.floor(Number(i));
|
|
682
|
-
return idx >= 0 && idx < str.length ? str[idx] : false;
|
|
683
|
-
});
|
|
580
|
+
|
|
684
581
|
defineIfMissing("string-trim-both", (s: any) => String(s).trim());
|
|
685
|
-
defineIfMissing("identity", (x: any) => x);
|
|
686
582
|
// Pattern can be string or (regexp "pat"). Racket (?i:…) / (?m:…) inline
|
|
687
583
|
// flag groups are translated to JS RegExp flags.
|
|
688
584
|
const compileRegex = (pat: any): RegExp => {
|
|
@@ -698,6 +594,7 @@ function installStdShims(env: any): void {
|
|
|
698
594
|
};
|
|
699
595
|
defineIfMissing("regexp", (pat: any) => compileRegex(pat));
|
|
700
596
|
const regexpMatch = (pat: any, s: any) => {
|
|
597
|
+
s = toJsStr(s);
|
|
701
598
|
if (typeof s !== "string") return false;
|
|
702
599
|
const m = s.match(compileRegex(pat));
|
|
703
600
|
return m ? toSchemeList(Array.from(m)) : false;
|
|
@@ -720,6 +617,7 @@ function installStdShims(env: any): void {
|
|
|
720
617
|
return false;
|
|
721
618
|
});
|
|
722
619
|
defineIfMissing("regexp-match-positions", (pat: any, s: any) => {
|
|
620
|
+
s = toJsStr(s);
|
|
723
621
|
if (typeof s !== "string") return false;
|
|
724
622
|
const m = s.match(compileRegex(pat));
|
|
725
623
|
if (!m) return false;
|
|
@@ -728,101 +626,25 @@ function installStdShims(env: any): void {
|
|
|
728
626
|
return new Pair(full, nil);
|
|
729
627
|
});
|
|
730
628
|
|
|
731
|
-
// ── Racket spellings ─────────────────────────────────────
|
|
732
|
-
defineIfMissing("string-split", (s: any, sep: any) => {
|
|
733
|
-
if (typeof s !== "string") return nil;
|
|
734
|
-
if (sep === undefined) return toSchemeList(s.split(/\s+/).filter(Boolean));
|
|
735
|
-
return toSchemeList(s.split(String(sep)));
|
|
736
|
-
});
|
|
737
|
-
defineIfMissing("string-join", (lst: any, sep: any) =>
|
|
738
|
-
pairToArray(lst).map(String).join(sep === undefined ? " " : String(sep)));
|
|
739
629
|
defineIfMissing("displayln", function (this: any, x: any) {
|
|
740
630
|
const display = (env as any).get("display", { throwError: false });
|
|
741
631
|
if (display) { display(x); display("\n"); }
|
|
742
632
|
});
|
|
743
633
|
|
|
744
|
-
// ── R7RS error/exit ──────────────────────────────────────
|
|
745
634
|
defineIfMissing("error", (...msgs: any[]) => {
|
|
746
635
|
throw new Error(msgs.map((m) => (typeof m === "string" ? m : String(m))).join(" "));
|
|
747
636
|
});
|
|
748
637
|
defineIfMissing("void", () => undefined);
|
|
749
638
|
|
|
750
|
-
// ── R7RS write (LIPS' display is good enough; write quotes strings) ──
|
|
751
639
|
defineIfMissing("write", function (this: any, x: any) {
|
|
752
640
|
const display = (env as any).get("display", { throwError: false });
|
|
753
641
|
if (display) display(typeof x === "string" ? JSON.stringify(x) : x);
|
|
754
642
|
});
|
|
755
643
|
|
|
756
|
-
// ── R7RS numbers (gaps) ────────────────────────────────────
|
|
757
|
-
defineIfMissing("abs", (n: any) => Math.abs(Number(n)));
|
|
758
|
-
defineIfMissing("floor", (n: any) => Math.floor(Number(n)));
|
|
759
|
-
defineIfMissing("round", (n: any) => Math.round(Number(n)));
|
|
760
|
-
defineIfMissing("truncate", (n: any) => Math.trunc(Number(n)));
|
|
761
|
-
defineIfMissing("sqrt", (n: any) => Math.sqrt(Number(n)));
|
|
762
|
-
defineIfMissing("log", (n: any, base: any) =>
|
|
763
|
-
base === undefined ? Math.log(Number(n)) : Math.log(Number(n)) / Math.log(Number(base)));
|
|
764
|
-
defineIfMissing("exp", (n: any) => Math.exp(Number(n)));
|
|
765
|
-
defineIfMissing("sin", (n: any) => Math.sin(Number(n)));
|
|
766
|
-
defineIfMissing("cos", (n: any) => Math.cos(Number(n)));
|
|
767
|
-
defineIfMissing("tan", (n: any) => Math.tan(Number(n)));
|
|
768
|
-
defineIfMissing("asin", (n: any) => Math.asin(Number(n)));
|
|
769
|
-
defineIfMissing("acos", (n: any) => Math.acos(Number(n)));
|
|
770
|
-
defineIfMissing("atan", (a: any, b: any) =>
|
|
771
|
-
b === undefined ? Math.atan(Number(a)) : Math.atan2(Number(a), Number(b)));
|
|
772
|
-
defineIfMissing("gcd", (...args: any[]) => {
|
|
773
|
-
const gcd2 = (a: number, b: number): number => b === 0 ? Math.abs(a) : gcd2(b, a % b);
|
|
774
|
-
return args.length === 0 ? 0 : args.map(Number).reduce(gcd2);
|
|
775
|
-
});
|
|
776
|
-
defineIfMissing("lcm", (...args: any[]) => {
|
|
777
|
-
const gcd2 = (a: number, b: number): number => b === 0 ? Math.abs(a) : gcd2(b, a % b);
|
|
778
|
-
const lcm2 = (a: number, b: number): number => a && b ? Math.abs(a * b) / gcd2(a, b) : 0;
|
|
779
|
-
return args.length === 0 ? 1 : args.map(Number).reduce(lcm2);
|
|
780
|
-
});
|
|
781
|
-
defineIfMissing("exact", (n: any) => Math.round(Number(n)));
|
|
782
|
-
defineIfMissing("inexact", (n: any) => Number(n));
|
|
783
|
-
defineIfMissing("exact->inexact", (n: any) => Number(n));
|
|
784
|
-
defineIfMissing("inexact->exact", (n: any) => Math.round(Number(n)));
|
|
785
|
-
defineIfMissing("exact-integer?", (n: any) => Number.isInteger(Number(n)));
|
|
786
|
-
defineIfMissing("exact?", (n: any) => Number.isInteger(Number(n)));
|
|
787
|
-
defineIfMissing("inexact?", (n: any) => !Number.isInteger(Number(n)));
|
|
788
|
-
defineIfMissing("=", (...args: any[]) => {
|
|
789
|
-
if (args.length < 2) return true;
|
|
790
|
-
const first = Number(args[0]);
|
|
791
|
-
for (let i = 1; i < args.length; i++) if (Number(args[i]) !== first) return false;
|
|
792
|
-
return true;
|
|
793
|
-
});
|
|
794
|
-
defineIfMissing("finite?", (n: any) => Number.isFinite(Number(n)));
|
|
795
|
-
defineIfMissing("infinite?", (n: any) => !Number.isFinite(Number(n)) && !Number.isNaN(Number(n)));
|
|
796
|
-
defineIfMissing("nan?", (n: any) => Number.isNaN(Number(n)));
|
|
797
644
|
defineIfMissing("add1", (n: any) => Number(n) + 1);
|
|
798
645
|
defineIfMissing("sub1", (n: any) => Number(n) - 1);
|
|
799
646
|
defineIfMissing("sqr", (n: any) => Number(n) * Number(n));
|
|
800
647
|
|
|
801
|
-
// ── R7RS predicates ────────────────────────────────────────
|
|
802
|
-
defineIfMissing("boolean?", (x: any) => x === true || x === false);
|
|
803
|
-
defineIfMissing("boolean=?", (a: any, b: any) => a === b && (a === true || a === false));
|
|
804
|
-
defineIfMissing("procedure?", (x: any) => typeof x === "function");
|
|
805
|
-
defineIfMissing("symbol?", (x: any) => x instanceof LSymbol);
|
|
806
|
-
defineIfMissing("symbol=?", (a: any, b: any) =>
|
|
807
|
-
a instanceof LSymbol && b instanceof LSymbol && (a as any).name === (b as any).name);
|
|
808
|
-
defineIfMissing("string->symbol", (s: any) => new LSymbol(String(s)));
|
|
809
|
-
defineIfMissing("integer?", (x: any) => typeof x === "number" ? Number.isInteger(x)
|
|
810
|
-
: (x && typeof x.valueOf === "function" && Number.isInteger(Number(x.valueOf()))));
|
|
811
|
-
|
|
812
|
-
// ── R7RS strings (gaps) ────────────────────────────────────
|
|
813
|
-
defineIfMissing("substring", (s: any, start: any, end: any) => {
|
|
814
|
-
const str = String(s);
|
|
815
|
-
const a = Math.max(0, Math.floor(Number(start) || 0));
|
|
816
|
-
const b = end === undefined ? str.length : Math.min(str.length, Math.floor(Number(end)));
|
|
817
|
-
return str.slice(a, b);
|
|
818
|
-
});
|
|
819
|
-
defineIfMissing("string-copy", (s: any) => String(s));
|
|
820
|
-
defineIfMissing("make-string", (n: any, ch?: any) => {
|
|
821
|
-
const len = Math.max(0, Math.floor(Number(n) || 0));
|
|
822
|
-
const c = ch === undefined ? " " : String(ch);
|
|
823
|
-
return c.length === 1 ? c.repeat(len) : (c + "").repeat(len).slice(0, len);
|
|
824
|
-
});
|
|
825
|
-
defineIfMissing("string-foldcase", (s: any) => String(s).toLowerCase());
|
|
826
648
|
defineIfMissing("string-trim", (s: any) => String(s).trim());
|
|
827
649
|
defineIfMissing("string-trim-left", (s: any) => String(s).replace(/^\s+/, ""));
|
|
828
650
|
defineIfMissing("string-trim-right", (s: any) => String(s).replace(/\s+$/, ""));
|
|
@@ -830,34 +652,15 @@ function installStdShims(env: any): void {
|
|
|
830
652
|
String(s).startsWith(String(prefix)));
|
|
831
653
|
defineIfMissing("string-suffix?", (suffix: any, s: any) =>
|
|
832
654
|
String(s).endsWith(String(suffix)));
|
|
833
|
-
defineIfMissing("non-empty-string?", (x: any) =>
|
|
834
|
-
|
|
655
|
+
defineIfMissing("non-empty-string?", (x: any) => {
|
|
656
|
+
x = toJsStr(x);
|
|
657
|
+
return typeof x === "string" && x.length > 0;
|
|
658
|
+
});
|
|
835
659
|
defineIfMissing("string-index", (s: any, needle: any) => {
|
|
836
660
|
const i = String(s).indexOf(String(needle));
|
|
837
661
|
return i < 0 ? false : i;
|
|
838
662
|
});
|
|
839
|
-
|
|
840
|
-
String(a).toLowerCase() === String(b).toLowerCase());
|
|
841
|
-
defineIfMissing("string-ci<?", (a: any, b: any) =>
|
|
842
|
-
String(a).toLowerCase() < String(b).toLowerCase());
|
|
843
|
-
defineIfMissing("string-ci>?", (a: any, b: any) =>
|
|
844
|
-
String(a).toLowerCase() > String(b).toLowerCase());
|
|
845
|
-
defineIfMissing("string<=?", (a: any, b: any) => String(a) <= String(b));
|
|
846
|
-
defineIfMissing("string>=?", (a: any, b: any) => String(a) >= String(b));
|
|
847
|
-
|
|
848
|
-
// ── R7RS / SRFI-1 list gaps ────────────────────────────────
|
|
849
|
-
defineIfMissing("list-tail", (lst: any, n: any) => {
|
|
850
|
-
let k = Math.floor(Number(n) || 0);
|
|
851
|
-
let cur: any = lst;
|
|
852
|
-
while (k-- > 0 && cur instanceof Pair) cur = cur.cdr;
|
|
853
|
-
return cur;
|
|
854
|
-
});
|
|
855
|
-
defineIfMissing("list-ref", (lst: any, n: any) => {
|
|
856
|
-
let k = Math.floor(Number(n) || 0);
|
|
857
|
-
let cur: any = lst;
|
|
858
|
-
while (k-- > 0 && cur instanceof Pair) cur = cur.cdr;
|
|
859
|
-
return cur instanceof Pair ? cur.car : false;
|
|
860
|
-
});
|
|
663
|
+
|
|
861
664
|
defineIfMissing("list-index", (pred: any, lst: any) => {
|
|
862
665
|
let i = 0, cur: any = lst;
|
|
863
666
|
while (cur instanceof Pair) {
|
|
@@ -897,13 +700,6 @@ function installStdShims(env: any): void {
|
|
|
897
700
|
for (let i = args.length - 2; i >= 0; i--) tail = new Pair(args[i], tail);
|
|
898
701
|
return tail;
|
|
899
702
|
});
|
|
900
|
-
defineIfMissing("list*", (...args: any[]) => {
|
|
901
|
-
if (args.length === 0) return nil;
|
|
902
|
-
if (args.length === 1) return args[0];
|
|
903
|
-
let tail: any = args[args.length - 1];
|
|
904
|
-
for (let i = args.length - 2; i >= 0; i--) tail = new Pair(args[i], tail);
|
|
905
|
-
return tail;
|
|
906
|
-
});
|
|
907
703
|
defineIfMissing("append-reverse", (a: any, b: any) => {
|
|
908
704
|
let cur: any = a, out: any = b;
|
|
909
705
|
while (cur instanceof Pair) { out = new Pair(cur.car, out); cur = cur.cdr; }
|
|
@@ -946,26 +742,6 @@ function installStdShims(env: any): void {
|
|
|
946
742
|
}
|
|
947
743
|
return toSchemeList(out);
|
|
948
744
|
});
|
|
949
|
-
// LIPS' `range` is single-arg only; override (not defineIfMissing) so the
|
|
950
|
-
// 1/2/3-arg Racket form wins.
|
|
951
|
-
env.set("range", (a: any, b: any, step: any) => {
|
|
952
|
-
let start: number, end: number, st: number;
|
|
953
|
-
if (b === undefined) { start = 0; end = Number(a); st = 1; }
|
|
954
|
-
else { start = Number(a); end = Number(b); st = step === undefined ? 1 : Number(step); }
|
|
955
|
-
const out: number[] = [];
|
|
956
|
-
if (st > 0) for (let i = start; i < end; i += st) out.push(i);
|
|
957
|
-
else if (st < 0) for (let i = start; i > end; i += st) out.push(i);
|
|
958
|
-
return toSchemeList(out);
|
|
959
|
-
});
|
|
960
|
-
defineIfMissing("flatten", (lst: any) => {
|
|
961
|
-
const out: any[] = [];
|
|
962
|
-
const walk = (x: any) => {
|
|
963
|
-
if (x instanceof Pair) { let c: any = x; while (c instanceof Pair) { walk(c.car); c = c.cdr; } }
|
|
964
|
-
else if (x !== nil) out.push(x);
|
|
965
|
-
};
|
|
966
|
-
walk(lst);
|
|
967
|
-
return toSchemeList(out);
|
|
968
|
-
});
|
|
969
745
|
defineIfMissing("index-of", (lst: any, x: any) => {
|
|
970
746
|
let i = 0, cur: any = lst;
|
|
971
747
|
while (cur instanceof Pair) {
|
|
@@ -1017,7 +793,6 @@ function installStdShims(env: any): void {
|
|
|
1017
793
|
return toSchemeList(groups.map((g) => toSchemeList(g.items)));
|
|
1018
794
|
});
|
|
1019
795
|
|
|
1020
|
-
// ── Regex (Racket) ─────────────────────────────────────────
|
|
1021
796
|
const reCompile = (pat: any): RegExp => {
|
|
1022
797
|
if (pat instanceof RegExp) return pat;
|
|
1023
798
|
let p = String(pat);
|
|
@@ -1031,10 +806,12 @@ function installStdShims(env: any): void {
|
|
|
1031
806
|
};
|
|
1032
807
|
defineIfMissing("regexp?", (x: any) => x instanceof RegExp);
|
|
1033
808
|
defineIfMissing("regexp-replace", (pat: any, s: any, repl: any) => {
|
|
809
|
+
s = toJsStr(s);
|
|
1034
810
|
if (typeof s !== "string") return s;
|
|
1035
811
|
return s.replace(reCompile(pat), String(repl));
|
|
1036
812
|
});
|
|
1037
813
|
defineIfMissing("regexp-replace*", (pat: any, s: any, repl: any) => {
|
|
814
|
+
s = toJsStr(s);
|
|
1038
815
|
if (typeof s !== "string") return s;
|
|
1039
816
|
const re = reCompile(pat);
|
|
1040
817
|
const global = re.flags.includes("g") ? re : new RegExp(re.source, re.flags + "g");
|
|
@@ -1042,10 +819,11 @@ function installStdShims(env: any): void {
|
|
|
1042
819
|
});
|
|
1043
820
|
defineIfMissing("regexp-quote", (s: any) =>
|
|
1044
821
|
String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
1045
|
-
defineIfMissing("regexp-split", (pat: any, s: any) =>
|
|
1046
|
-
|
|
822
|
+
defineIfMissing("regexp-split", (pat: any, s: any) => {
|
|
823
|
+
s = toJsStr(s);
|
|
824
|
+
return typeof s === "string" ? toSchemeList(s.split(reCompile(pat))) : nil;
|
|
825
|
+
});
|
|
1047
826
|
|
|
1048
|
-
// ── Format (Racket) ────────────────────────────────────────
|
|
1049
827
|
// format: simple ~a ~s ~v ~n support — covers most logging/inspection
|
|
1050
828
|
defineIfMissing("format", (fmt: any, ...rest: any[]) => {
|
|
1051
829
|
const f = String(fmt);
|
|
@@ -1074,7 +852,6 @@ function installStdShims(env: any): void {
|
|
|
1074
852
|
defineIfMissing("~v", (...xs: any[]) =>
|
|
1075
853
|
xs.map((x) => typeof x === "string" ? JSON.stringify(x) : (x === undefined ? "" : x.toString())).join(""));
|
|
1076
854
|
|
|
1077
|
-
// ── Hash tables (Racket) ───────────────────────────────────
|
|
1078
855
|
// Backed by JS Map. Stored as `LipsHash` symbol so we can pattern-match.
|
|
1079
856
|
class LipsHash {
|
|
1080
857
|
map: Map<any, any> = new Map();
|
|
@@ -1082,7 +859,7 @@ function installStdShims(env: any): void {
|
|
|
1082
859
|
if (entries) for (const [k, v] of entries) this.map.set(this._key(k), v);
|
|
1083
860
|
}
|
|
1084
861
|
_key(k: any): any {
|
|
1085
|
-
if (k instanceof LSymbol) return "::sym::" + (k
|
|
862
|
+
if (k instanceof LSymbol) return "::sym::" + symName(k);
|
|
1086
863
|
if (typeof k === "object" && k !== null) return JSON.stringify(k);
|
|
1087
864
|
return k;
|
|
1088
865
|
}
|
|
@@ -1139,20 +916,16 @@ function installStdShims(env: any): void {
|
|
|
1139
916
|
defineIfMissing("hash-values", (h: any) => h instanceof LipsHash ? toSchemeList(h.values()) : nil);
|
|
1140
917
|
defineIfMissing("hash-count", (h: any) => h instanceof LipsHash ? h.size() : 0);
|
|
1141
918
|
|
|
1142
|
-
// ── Sort (R7RS-large / SRFI-132 / Racket) ──────────────────
|
|
1143
919
|
// V8's Array.prototype.sort is stable (ES2019), so one impl serves all.
|
|
1144
920
|
const sortImpl = (lst: any, less: any) => {
|
|
1145
921
|
const arr = pairToArray(lst).slice();
|
|
1146
922
|
arr.sort((a, b) => (truthy(less(a, b)) ? -1 : truthy(less(b, a)) ? 1 : 0));
|
|
1147
923
|
return toSchemeList(arr);
|
|
1148
924
|
};
|
|
1149
|
-
defineIfMissing("sort", sortImpl);
|
|
1150
925
|
defineIfMissing("sort!", sortImpl);
|
|
1151
926
|
// SRFI-132 / R7RS-large flips the argument order.
|
|
1152
927
|
defineIfMissing("list-sort", (less: any, lst: any) => sortImpl(lst, less));
|
|
1153
928
|
|
|
1154
|
-
// ── Racket list aliases & gaps ─────────────────────────────
|
|
1155
|
-
defineIfMissing("empty?", (v: any) => v === nil);
|
|
1156
929
|
defineIfMissing("empty", nil);
|
|
1157
930
|
defineIfMissing("cons?", (v: any) => v instanceof Pair);
|
|
1158
931
|
defineIfMissing("andmap", (pred: any, lst: any) => pairToArray(lst).every((x) => truthy(pred(x))));
|
|
@@ -1170,11 +943,6 @@ function installStdShims(env: any): void {
|
|
|
1170
943
|
}
|
|
1171
944
|
return false;
|
|
1172
945
|
});
|
|
1173
|
-
defineIfMissing("make-list", (n: any, v: any) => {
|
|
1174
|
-
const count = Math.max(0, Math.floor(Number(n) || 0));
|
|
1175
|
-
const fill = v === undefined ? nil : v;
|
|
1176
|
-
return toSchemeList(Array(count).fill(fill));
|
|
1177
|
-
});
|
|
1178
946
|
defineIfMissing("build-list", (n: any, proc: any) => {
|
|
1179
947
|
const count = Math.max(0, Math.floor(Number(n) || 0));
|
|
1180
948
|
const out: any[] = [];
|
|
@@ -1196,14 +964,6 @@ function installStdShims(env: any): void {
|
|
|
1196
964
|
const k = Math.max(0, Math.floor(Number(n) || 0));
|
|
1197
965
|
return new Pair(toSchemeList(a.slice(0, k)), toSchemeList(a.slice(k)));
|
|
1198
966
|
});
|
|
1199
|
-
defineIfMissing("shuffle", (lst: any) => {
|
|
1200
|
-
const a = pairToArray(lst).slice();
|
|
1201
|
-
for (let i = a.length - 1; i > 0; i--) {
|
|
1202
|
-
const j = Math.floor(Math.random() * (i + 1));
|
|
1203
|
-
[a[i], a[j]] = [a[j], a[i]];
|
|
1204
|
-
}
|
|
1205
|
-
return toSchemeList(a);
|
|
1206
|
-
});
|
|
1207
967
|
defineIfMissing("add-between", (lst: any, sep: any) => {
|
|
1208
968
|
const a = pairToArray(lst);
|
|
1209
969
|
if (a.length < 2) return toSchemeList(a);
|
|
@@ -1325,16 +1085,8 @@ function installStdShims(env: any): void {
|
|
|
1325
1085
|
return toSchemeList(arr.filter((x) => !targets.some((t) => lipsEqual(t, x))));
|
|
1326
1086
|
});
|
|
1327
1087
|
|
|
1328
|
-
// ── Racket numbers (gaps) ──────────────────────────────────
|
|
1329
1088
|
defineIfMissing("pi", Math.PI);
|
|
1330
1089
|
// Racket overloads: (random) 0≤x<1, (random k) 0≤i<k, (random lo hi).
|
|
1331
|
-
defineIfMissing("random", (a: any, b: any) => {
|
|
1332
|
-
if (a === undefined) return Math.random();
|
|
1333
|
-
if (b === undefined) return Math.floor(Math.random() * Math.max(0, Math.floor(Number(a))));
|
|
1334
|
-
const lo = Math.floor(Number(a));
|
|
1335
|
-
const hi = Math.floor(Number(b));
|
|
1336
|
-
return lo + Math.floor(Math.random() * Math.max(0, hi - lo));
|
|
1337
|
-
});
|
|
1338
1090
|
defineIfMissing("exact-floor", (n: any) => Math.floor(Number(n)));
|
|
1339
1091
|
defineIfMissing("exact-ceiling", (n: any) => Math.ceil(Number(n)));
|
|
1340
1092
|
defineIfMissing("exact-round", (n: any) => Math.round(Number(n)));
|
|
@@ -1353,7 +1105,6 @@ function installStdShims(env: any): void {
|
|
|
1353
1105
|
return Number(n).toFixed(d);
|
|
1354
1106
|
});
|
|
1355
1107
|
|
|
1356
|
-
// ── Racket strings & chars (gaps) ──────────────────────────
|
|
1357
1108
|
defineIfMissing("string-titlecase", (s: any) =>
|
|
1358
1109
|
String(s).replace(/\b([a-z])/g, (_, c) => c.toUpperCase()));
|
|
1359
1110
|
defineIfMissing("string-pad", (s: any, width: any, ch?: any) => {
|
|
@@ -1368,8 +1119,6 @@ function installStdShims(env: any): void {
|
|
|
1368
1119
|
const c = ch === undefined ? " " : String(ch).charAt(0) || " ";
|
|
1369
1120
|
return str.length >= w ? str : str + c.repeat(w - str.length);
|
|
1370
1121
|
});
|
|
1371
|
-
defineIfMissing("char-upcase", (c: any) => String(c).toUpperCase());
|
|
1372
|
-
defineIfMissing("char-downcase", (c: any) => String(c).toLowerCase());
|
|
1373
1122
|
defineIfMissing("string-normalize-spaces", (s: any, sep?: any, repl?: any) => {
|
|
1374
1123
|
const str = String(s).trim();
|
|
1375
1124
|
const splitOn = sep === undefined ? /\s+/ : (sep instanceof RegExp ? sep : new RegExp(String(sep)));
|
|
@@ -1383,7 +1132,6 @@ function installStdShims(env: any): void {
|
|
|
1383
1132
|
return out;
|
|
1384
1133
|
});
|
|
1385
1134
|
|
|
1386
|
-
// ── Racket hash (gaps) ─────────────────────────────────────
|
|
1387
1135
|
defineIfMissing("hash-update!", (h: any, k: any, upd: any, dflt: any) => {
|
|
1388
1136
|
if (!(h instanceof LipsHash)) return h;
|
|
1389
1137
|
const cur = h.has(k) ? h.get(k) : (typeof dflt === "function" ? dflt() : dflt);
|
|
@@ -1462,14 +1210,12 @@ function installStdShims(env: any): void {
|
|
|
1462
1210
|
defineIfMissing("make-hasheqv", (alist?: any) => (env as any).get("make-hash")(alist));
|
|
1463
1211
|
defineIfMissing("make-immutable-hash", (alist?: any) => (env as any).get("make-hash")(alist));
|
|
1464
1212
|
|
|
1465
|
-
// ── Racket boxes (mutable cells) ───────────────────────────
|
|
1466
1213
|
class LipsBox { constructor(public v: any) {} }
|
|
1467
1214
|
defineIfMissing("box", (v: any) => new LipsBox(v));
|
|
1468
1215
|
defineIfMissing("box?", (x: any) => x instanceof LipsBox);
|
|
1469
1216
|
defineIfMissing("unbox", (b: any) => b instanceof LipsBox ? b.v : b);
|
|
1470
1217
|
defineIfMissing("set-box!", (b: any, v: any) => { if (b instanceof LipsBox) b.v = v; return undefined; });
|
|
1471
1218
|
|
|
1472
|
-
// ── Environment introspection ──────────────────────────────
|
|
1473
1219
|
const collectEnvNames = (): string[] => {
|
|
1474
1220
|
const seen = new Set<string>();
|
|
1475
1221
|
let cur: any = env;
|
|
@@ -1481,28 +1227,26 @@ function installStdShims(env: any): void {
|
|
|
1481
1227
|
return Array.from(seen).sort();
|
|
1482
1228
|
};
|
|
1483
1229
|
defineIfMissing("defined?", (sym: any) => {
|
|
1484
|
-
const name = sym instanceof LSymbol ? (sym
|
|
1230
|
+
const name = sym instanceof LSymbol ? symName(sym) : String(sym);
|
|
1485
1231
|
return (env as any).get(name, { throwError: false }) !== undefined;
|
|
1486
1232
|
});
|
|
1487
1233
|
const aproposImpl = (pat: any) => {
|
|
1488
|
-
const needle = pat instanceof LSymbol ? (pat
|
|
1234
|
+
const needle = pat instanceof LSymbol ? (symName(pat) ?? "") : String(pat ?? "");
|
|
1489
1235
|
const re = pat instanceof RegExp ? pat : null;
|
|
1490
1236
|
const match = (n: string) => re ? re.test(n) : n.includes(needle);
|
|
1491
1237
|
return toSchemeList(collectEnvNames().filter(match).map((n) => new LSymbol(n)));
|
|
1492
1238
|
};
|
|
1493
|
-
defineIfMissing("apropos", aproposImpl);
|
|
1494
1239
|
defineIfMissing("apropos-list", aproposImpl);
|
|
1495
1240
|
|
|
1496
|
-
// ── Misc Racket ────────────────────────────────────────────
|
|
1497
1241
|
defineIfMissing("current-seconds", () => Math.floor(Date.now() / 1000));
|
|
1498
1242
|
defineIfMissing("current-milliseconds", () => Date.now());
|
|
1499
1243
|
defineIfMissing("current-inexact-milliseconds", () => performance.now());
|
|
1500
1244
|
}
|
|
1501
1245
|
|
|
1502
1246
|
// Canonical names we claim coverage for. R = R7RS small base; S = SRFI-1;
|
|
1503
|
-
// K = Racket racket/base/list/string/format. Continuations, ports,
|
|
1504
|
-
//
|
|
1505
|
-
//
|
|
1247
|
+
// K = Racket racket/base/list/string/format. Continuations, ports, and
|
|
1248
|
+
// bytevectors are intentionally omitted — they'd be misleading "coverage"
|
|
1249
|
+
// without real functionality.
|
|
1506
1250
|
const COVERAGE_CHECKLIST: string[] = [
|
|
1507
1251
|
// R7RS § 6.1 equivalence
|
|
1508
1252
|
"eq?", "eqv?", "equal?",
|
|
@@ -1584,6 +1328,10 @@ const COVERAGE_CHECKLIST: string[] = [
|
|
|
1584
1328
|
// Racket strings & chars (gaps)
|
|
1585
1329
|
"string-titlecase", "string-pad", "string-pad-right",
|
|
1586
1330
|
"char-upcase", "char-downcase",
|
|
1331
|
+
"char->integer", "integer->char", "char?",
|
|
1332
|
+
"char=?", "char<?", "char>?", "char<=?", "char>=?", "char-ci=?",
|
|
1333
|
+
"char-alphabetic?", "char-numeric?", "char-whitespace?",
|
|
1334
|
+
"char-upper-case?", "char-lower-case?", "digit-value",
|
|
1587
1335
|
"string-normalize-spaces", "build-string",
|
|
1588
1336
|
// Racket hash
|
|
1589
1337
|
"make-hash", "hash", "hash?", "hash-ref", "hash-set!", "hash-set",
|
|
@@ -1619,7 +1367,7 @@ function auditShimCoverage(env: any): { defined: number; missing: string[] } {
|
|
|
1619
1367
|
function unwrapSchemeBool(v: any): any {
|
|
1620
1368
|
if (v === true || v === false) return v;
|
|
1621
1369
|
if (v instanceof LSymbol) {
|
|
1622
|
-
const n = (v
|
|
1370
|
+
const n = symName(v);
|
|
1623
1371
|
if (n === "#t") return true;
|
|
1624
1372
|
if (n === "#f") return false;
|
|
1625
1373
|
}
|
|
@@ -1628,32 +1376,82 @@ function unwrapSchemeBool(v: any): any {
|
|
|
1628
1376
|
return v;
|
|
1629
1377
|
}
|
|
1630
1378
|
|
|
1631
|
-
//
|
|
1632
|
-
//
|
|
1633
|
-
//
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1379
|
+
// Single source of truth for the host primitive surface: drives both the tool
|
|
1380
|
+
// description and `(help …)`, so what the model reads matches what it can
|
|
1381
|
+
// introspect at runtime. Each binding returns the natural Scheme value for its
|
|
1382
|
+
// job; bash returns a record because the exit code has nowhere else to live (a
|
|
1383
|
+
// plain bash tool call drops it before the model ever sees it).
|
|
1384
|
+
type HostSig = { name: string; sig: string; ret: string; doc: string };
|
|
1385
|
+
const HOST_SIGS: HostSig[] = [
|
|
1386
|
+
{ name: "bash", sig: '(bash "cmd" [timeout-sec])',
|
|
1387
|
+
ret: "((output . str) (exit-code . n) (error . bool))",
|
|
1388
|
+
doc: "run a shell command; full result. Accessors: output-of exit-code-of ok? error?" },
|
|
1389
|
+
{ name: "sh", sig: '(sh "cmd" [timeout-sec])', ret: "str",
|
|
1390
|
+
doc: "run a shell command, return stdout only (stderr text on failure)" },
|
|
1391
|
+
{ name: "read-file", sig: '(read-file "path" [offset] [limit])', ret: "str | #f",
|
|
1392
|
+
doc: "file contents, or #f on error. offset is 1-indexed; limit caps lines" },
|
|
1393
|
+
{ name: "write-file", sig: '(write-file "path" "content")', ret: "#t | err-str",
|
|
1394
|
+
doc: "overwrite a file" },
|
|
1395
|
+
{ name: "edit-file", sig: '(edit-file "path" "old" "new" [replace-all])', ret: "#t | err-str",
|
|
1396
|
+
doc: "replace exact text; pass #t to replace every occurrence" },
|
|
1397
|
+
{ name: "grep", sig: '(grep "pat" ["dir"] [:opt val …])',
|
|
1398
|
+
ret: "(listof ((file . str) (line . n) (text . str)))",
|
|
1399
|
+
doc: "ripgrep search. options: :include :case-insensitive :context-before :context-after :limit :offset" },
|
|
1400
|
+
{ name: "grep-files", sig: '(grep-files "pat" ["dir"] [:opt val …])', ret: "(listof str)",
|
|
1401
|
+
doc: "files containing a match. options: :include :case-insensitive :limit :offset" },
|
|
1402
|
+
{ name: "glob", sig: '(glob "pat" ["dir"])', ret: "(listof str)",
|
|
1403
|
+
doc: "paths matching a glob, mtime-sorted" },
|
|
1404
|
+
];
|
|
1405
|
+
const sigLine = (h: HostSig): string => `${h.sig} → ${h.ret}`;
|
|
1406
|
+
const sigForName = (name: string): string => {
|
|
1407
|
+
const h = HOST_SIGS.find((s) => s.name === name);
|
|
1408
|
+
return h ? sigLine(h) : name;
|
|
1409
|
+
};
|
|
1410
|
+
|
|
1411
|
+
// Append a primitive's signature to any exception it throws, so a malformed
|
|
1412
|
+
// call teaches the right shape in one round-trip instead of a bare stack.
|
|
1413
|
+
function withSig(name: string, fn: (...a: any[]) => Promise<any>) {
|
|
1414
|
+
return async (...a: any[]) => {
|
|
1415
|
+
try {
|
|
1416
|
+
return await fn(...a);
|
|
1417
|
+
} catch (e: any) {
|
|
1418
|
+
const base = e?.message ?? String(e);
|
|
1419
|
+
if (!String(base).includes("signature:")) {
|
|
1420
|
+
try { e.message = `${base}\n signature: ${sigForName(name)}`; } catch { /* frozen */ }
|
|
1652
1421
|
}
|
|
1422
|
+
throw e;
|
|
1653
1423
|
}
|
|
1654
|
-
|
|
1424
|
+
};
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const isKwSym = (x: any): boolean => {
|
|
1428
|
+
const n = symName(x);
|
|
1429
|
+
return typeof n === "string" && n.startsWith(":");
|
|
1430
|
+
};
|
|
1431
|
+
|
|
1432
|
+
// Split a host primitive's evaluated arg list into leading positionals and a
|
|
1433
|
+
// trailing :key value option map. LIPS has no keyword args, so the grep macros
|
|
1434
|
+
// quote leading-colon symbols (otherwise they'd be looked up as unbound
|
|
1435
|
+
// variables); they arrive here as plain LSymbols named ":foo".
|
|
1436
|
+
function splitArgs(
|
|
1437
|
+
args: any[], keyMap: Record<string, string>, numericKeys?: Set<string>,
|
|
1438
|
+
): { positionals: any[]; opts: Record<string, unknown> } {
|
|
1439
|
+
const positionals: any[] = [];
|
|
1440
|
+
let i = 0;
|
|
1441
|
+
while (i < args.length && !isKwSym(args[i])) { positionals.push(args[i]); i++; }
|
|
1442
|
+
const opts: Record<string, unknown> = {};
|
|
1443
|
+
while (i < args.length) {
|
|
1444
|
+
if (!isKwSym(args[i])) { i++; continue; }
|
|
1445
|
+
const tgt = keyMap[symName(args[i])!.slice(1)];
|
|
1446
|
+
if (tgt !== undefined) {
|
|
1447
|
+
const raw = args[i + 1];
|
|
1448
|
+
opts[tgt] = numericKeys && numericKeys.has(tgt)
|
|
1449
|
+
? Number(raw)
|
|
1450
|
+
: unwrapSchemeBool(toJsStr(raw));
|
|
1451
|
+
}
|
|
1452
|
+
i += 2;
|
|
1655
1453
|
}
|
|
1656
|
-
return
|
|
1454
|
+
return { positionals, opts };
|
|
1657
1455
|
}
|
|
1658
1456
|
|
|
1659
1457
|
function resolveExecutor(ctx: AgentContext, name: string): ToolExecutor {
|
|
@@ -1673,54 +1471,44 @@ function installBindings(
|
|
|
1673
1471
|
glob: ToolExecutor | null,
|
|
1674
1472
|
): void {
|
|
1675
1473
|
const runBash = async (command: string, timeoutSec?: number) => {
|
|
1676
|
-
const args: Record<string, unknown> = { command };
|
|
1474
|
+
const args: Record<string, unknown> = { command: toJsStr(command) };
|
|
1677
1475
|
if (typeof timeoutSec === "number") args.timeout = timeoutSec;
|
|
1678
1476
|
const result = await bash(args);
|
|
1679
|
-
let
|
|
1477
|
+
let output = typeof result.content === "string" ? result.content : String(result.content ?? "");
|
|
1680
1478
|
// Undo bash.ts's "(no output)" sentinel so `(eq? out "")` works.
|
|
1681
|
-
if (
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
};
|
|
1479
|
+
if (output === "(no output)") output = "";
|
|
1480
|
+
// stdout and stderr are merged upstream — the bash tool surfaces only a
|
|
1481
|
+
// combined `content` — so the record exposes one `output`, not a fabricated
|
|
1482
|
+
// split. `exit-code` is the real shell code and the only channel that
|
|
1483
|
+
// carries it: a plain bash tool call drops it before the model.
|
|
1484
|
+
return { exitCode: result.exitCode ?? (result.isError ? 1 : 0), output, error: result.isError };
|
|
1688
1485
|
};
|
|
1689
|
-
env.set("bash", async (command: string, timeoutSec?: number) => {
|
|
1486
|
+
env.set("bash", withSig("bash", async (command: string, timeoutSec?: number) => {
|
|
1690
1487
|
try {
|
|
1691
1488
|
const r = await runBash(command, timeoutSec);
|
|
1692
1489
|
return alist([
|
|
1490
|
+
["output", r.output],
|
|
1693
1491
|
["exit-code", r.exitCode],
|
|
1694
|
-
["
|
|
1695
|
-
["stderr", r.stderr],
|
|
1696
|
-
["success", r.success],
|
|
1492
|
+
["error", r.error],
|
|
1697
1493
|
]);
|
|
1698
1494
|
} catch (e: any) {
|
|
1699
1495
|
logErr("bash", e, { command, typeofCommand: typeof command });
|
|
1700
1496
|
throw e;
|
|
1701
1497
|
}
|
|
1702
|
-
});
|
|
1703
|
-
// Shortcut:
|
|
1704
|
-
|
|
1498
|
+
}));
|
|
1499
|
+
// Shortcut: stdout as a string. Use `bash` when you need the exit code, or
|
|
1500
|
+
// `(ok? (bash "…"))` for a success predicate.
|
|
1501
|
+
env.set("sh", withSig("sh", async (command: string, timeoutSec?: number) => {
|
|
1705
1502
|
try {
|
|
1706
|
-
|
|
1707
|
-
return r.stdout;
|
|
1503
|
+
return (await runBash(command, timeoutSec)).output;
|
|
1708
1504
|
} catch (e: any) {
|
|
1709
1505
|
logErr("sh", e, { command });
|
|
1710
1506
|
return "";
|
|
1711
1507
|
}
|
|
1712
|
-
});
|
|
1713
|
-
env.set("sh-ok?", async (command: string, timeoutSec?: number) => {
|
|
1714
|
-
try {
|
|
1715
|
-
const r = await runBash(command, timeoutSec);
|
|
1716
|
-
return r.success;
|
|
1717
|
-
} catch {
|
|
1718
|
-
return false;
|
|
1719
|
-
}
|
|
1720
|
-
});
|
|
1508
|
+
}));
|
|
1721
1509
|
|
|
1722
|
-
env.set("read-file", async (filePath: string, offset?: any, limit?: any) => {
|
|
1723
|
-
const args: Record<string, unknown> = { path: filePath, bypass_cache: true };
|
|
1510
|
+
env.set("read-file", withSig("read-file", async (filePath: string, offset?: any, limit?: any) => {
|
|
1511
|
+
const args: Record<string, unknown> = { path: toJsStr(filePath), bypass_cache: true };
|
|
1724
1512
|
if (offset !== undefined && offset !== null) {
|
|
1725
1513
|
const n = Number(offset);
|
|
1726
1514
|
if (!isNaN(n)) args.offset = n;
|
|
@@ -1731,19 +1519,21 @@ function installBindings(
|
|
|
1731
1519
|
}
|
|
1732
1520
|
const result = await readFile(args);
|
|
1733
1521
|
return result.isError ? false : result.content;
|
|
1734
|
-
});
|
|
1522
|
+
}));
|
|
1735
1523
|
|
|
1736
|
-
env.set("write-file", async (filePath: string, content: string) => {
|
|
1524
|
+
env.set("write-file", withSig("write-file", async (filePath: string, content: string) => {
|
|
1525
|
+
filePath = toJsStr(filePath); content = toJsStr(content);
|
|
1737
1526
|
// Re-emit tool lifecycle events so the TUI shows diffs.
|
|
1738
1527
|
const result = await withDisplay(
|
|
1739
1528
|
bus, "write_file", "write", { path: filePath, content }, filePath,
|
|
1740
1529
|
() => writeFile({ path: filePath, content }),
|
|
1741
1530
|
);
|
|
1742
1531
|
return result.isError ? result.content : true;
|
|
1743
|
-
});
|
|
1532
|
+
}));
|
|
1744
1533
|
|
|
1745
1534
|
if (editFile) {
|
|
1746
|
-
env.set("edit-file", async (filePath: string, oldStr: string, newStr: string, replaceAll?: any) => {
|
|
1535
|
+
env.set("edit-file", withSig("edit-file", async (filePath: string, oldStr: string, newStr: string, replaceAll?: any) => {
|
|
1536
|
+
filePath = toJsStr(filePath); oldStr = toJsStr(oldStr); newStr = toJsStr(newStr);
|
|
1747
1537
|
const toolArgs: Record<string, unknown> = { path: filePath, old_text: oldStr, new_text: newStr };
|
|
1748
1538
|
if (unwrapSchemeBool(replaceAll) === true) toolArgs.replace_all = true;
|
|
1749
1539
|
const result = await withDisplay(
|
|
@@ -1751,18 +1541,18 @@ function installBindings(
|
|
|
1751
1541
|
() => editFile(toolArgs),
|
|
1752
1542
|
);
|
|
1753
1543
|
return result.isError ? result.content : true;
|
|
1754
|
-
});
|
|
1544
|
+
}));
|
|
1755
1545
|
}
|
|
1756
1546
|
|
|
1757
1547
|
if (grep) {
|
|
1758
|
-
const GREP_KEYMAP = {
|
|
1548
|
+
const GREP_KEYMAP: Record<string, string> = {
|
|
1759
1549
|
"include": "include",
|
|
1760
1550
|
"case-insensitive": "case_insensitive",
|
|
1761
1551
|
"context-before": "context_before",
|
|
1762
1552
|
"context-after": "context_after",
|
|
1763
1553
|
"limit": "head_limit",
|
|
1764
1554
|
"offset": "offset",
|
|
1765
|
-
}
|
|
1555
|
+
};
|
|
1766
1556
|
const GREP_NUMERIC = new Set([
|
|
1767
1557
|
"context_before", "context_after", "head_limit", "offset",
|
|
1768
1558
|
]);
|
|
@@ -1779,93 +1569,96 @@ function installBindings(
|
|
|
1779
1569
|
.replace(/\\\+/g, "+").replace(/\\\?/g, "?");
|
|
1780
1570
|
};
|
|
1781
1571
|
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
}
|
|
1572
|
+
// pattern [path] are positional; everything after is :key value options,
|
|
1573
|
+
// split out by splitArgs (the grep macro quotes the colon symbols).
|
|
1574
|
+
env.set("%grep", withSig("grep", async (...rest: any[]) => {
|
|
1575
|
+
const { positionals, opts } = splitArgs(rest, GREP_KEYMAP, GREP_NUMERIC);
|
|
1576
|
+
const pStr = toJsStr(positionals[1]);
|
|
1577
|
+
const args: Record<string, unknown> = {
|
|
1578
|
+
pattern: normalizePattern(String(positionals[0] ?? "")), output_mode: "content", ...opts,
|
|
1579
|
+
};
|
|
1580
|
+
if (typeof pStr === "string") args.path = pStr;
|
|
1792
1581
|
const result = await grep(args);
|
|
1793
1582
|
if (result.isError) return nil;
|
|
1794
1583
|
if (result.content === "No matches found.") return nil;
|
|
1795
1584
|
const rows: unknown[] = [];
|
|
1796
|
-
for (const line of stripPagination(result.content)) {
|
|
1797
|
-
const parsed = parseGrepLine(line, typeof
|
|
1585
|
+
for (const line of stripPagination(result.content as string)) {
|
|
1586
|
+
const parsed = parseGrepLine(line, typeof pStr === "string" ? pStr : undefined);
|
|
1798
1587
|
if (parsed) rows.push(parsed);
|
|
1799
1588
|
}
|
|
1800
1589
|
return toSchemeList(rows);
|
|
1801
|
-
});
|
|
1590
|
+
}));
|
|
1802
1591
|
|
|
1803
|
-
env.set("%grep-files", async (
|
|
1804
|
-
const
|
|
1805
|
-
|
|
1806
|
-
|
|
1592
|
+
env.set("%grep-files", withSig("grep-files", async (...rest: any[]) => {
|
|
1593
|
+
const { positionals, opts } = splitArgs(rest, GREP_KEYMAP, GREP_NUMERIC);
|
|
1594
|
+
const pStr = toJsStr(positionals[1]);
|
|
1595
|
+
const args: Record<string, unknown> = {
|
|
1596
|
+
pattern: normalizePattern(String(positionals[0] ?? "")), output_mode: "files_with_matches", ...opts,
|
|
1597
|
+
};
|
|
1598
|
+
if (typeof pStr === "string") args.path = pStr;
|
|
1807
1599
|
const result = await grep(args);
|
|
1808
1600
|
if (result.isError || result.content === "No matches found.") return nil;
|
|
1809
|
-
return toSchemeList(stripPagination(result.content));
|
|
1810
|
-
});
|
|
1601
|
+
return toSchemeList(stripPagination(result.content as string));
|
|
1602
|
+
}));
|
|
1811
1603
|
}
|
|
1812
1604
|
|
|
1813
1605
|
if (glob) {
|
|
1814
1606
|
// Strip leading "./" so glob paths match grep's — otherwise eq? on the
|
|
1815
1607
|
// file field fails across the two.
|
|
1816
|
-
env.set("glob", async (pattern: string, p?: string) => {
|
|
1817
|
-
const
|
|
1818
|
-
|
|
1608
|
+
env.set("glob", withSig("glob", async (pattern: string, p?: string) => {
|
|
1609
|
+
const pStr = toJsStr(p);
|
|
1610
|
+
const args: Record<string, unknown> = { pattern: toJsStr(pattern) };
|
|
1611
|
+
if (typeof pStr === "string") args.path = pStr;
|
|
1819
1612
|
const result = await glob(args);
|
|
1820
1613
|
if (result.isError || result.content === "No files matched.") return nil;
|
|
1821
|
-
const paths = stripPagination(result.content).map((l) =>
|
|
1614
|
+
const paths = stripPagination(result.content as string).map((l) =>
|
|
1822
1615
|
l.startsWith("./") ? l.slice(2) : l,
|
|
1823
1616
|
);
|
|
1824
1617
|
return toSchemeList(paths);
|
|
1825
|
-
});
|
|
1618
|
+
}));
|
|
1826
1619
|
}
|
|
1827
1620
|
|
|
1828
|
-
//
|
|
1621
|
+
// Accessors on a bash result — JS-side so they're never missing.
|
|
1622
|
+
env.set("output-of", (r: unknown) => lookup(r, "output"));
|
|
1829
1623
|
env.set("exit-code-of", (r: unknown) => lookup(r, "exit-code"));
|
|
1830
|
-
env.set("
|
|
1831
|
-
env.set("
|
|
1832
|
-
|
|
1624
|
+
env.set("error?", (r: unknown) => lookup(r, "error") === true);
|
|
1625
|
+
env.set("ok?", (r: unknown) => lookup(r, "error") === false);
|
|
1626
|
+
|
|
1627
|
+
// Runtime discovery: (help) lists available host primitives; (help 'grep)
|
|
1628
|
+
// shows one. Filtered to what actually got bound this session.
|
|
1629
|
+
const availableNames = new Set<string>(["bash", "sh", "read-file", "write-file"]);
|
|
1630
|
+
if (editFile) availableNames.add("edit-file");
|
|
1631
|
+
if (grep) { availableNames.add("grep"); availableNames.add("grep-files"); }
|
|
1632
|
+
if (glob) availableNames.add("glob");
|
|
1633
|
+
const availableSigs = HOST_SIGS.filter((h) => availableNames.has(h.name));
|
|
1634
|
+
env.set("help", (name?: any) => {
|
|
1635
|
+
if (name === undefined || name === null) {
|
|
1636
|
+
return availableSigs.map(sigLine).join("\n");
|
|
1637
|
+
}
|
|
1638
|
+
const key = String(name instanceof LSymbol ? symName(name) : toJsStr(name)).replace(/^:/, "");
|
|
1639
|
+
const h = availableSigs.find((s) => s.name === key);
|
|
1640
|
+
return h ? `${sigLine(h)}\n ${h.doc}` : `no host primitive named ${key}; try (help) for the list`;
|
|
1641
|
+
});
|
|
1833
1642
|
|
|
1834
1643
|
// R7RS / string helpers LIPS doesn't ship.
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
typeof s === "string" && typeof needle === "string" && s.includes(needle);
|
|
1644
|
+
const stringContains = (s: unknown, needle: unknown) => {
|
|
1645
|
+
s = toJsStr(s); needle = toJsStr(needle);
|
|
1646
|
+
return typeof s === "string" && typeof needle === "string" && s.includes(needle);
|
|
1647
|
+
};
|
|
1838
1648
|
env.set("string-contains?", stringContains);
|
|
1839
1649
|
// Racket spells it without the `?`. Bind both so the model isn't punished
|
|
1840
1650
|
// for guessing dialect.
|
|
1841
1651
|
env.set("string-contains", stringContains);
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
env.set("number->string", (n: unknown) => String(n));
|
|
1845
|
-
env.set("string->number", (s: unknown) => {
|
|
1846
|
-
if (typeof s !== "string") return false;
|
|
1847
|
-
const n = Number(s);
|
|
1848
|
-
return Number.isNaN(n) ? false : n;
|
|
1849
|
-
});
|
|
1850
|
-
env.set("symbol->string", (sym: any) => (sym && sym.name) ? sym.name : String(sym));
|
|
1851
|
-
// LIPS doesn't ship max/min — useful enough that not having them breaks
|
|
1852
|
-
// common idioms like `(max 1 (- n 5))` for line-bound clamping.
|
|
1853
|
-
env.set("max", (...args: any[]) => args.reduce((a, b) => (Number(a) >= Number(b) ? a : b)));
|
|
1854
|
-
env.set("min", (...args: any[]) => args.reduce((a, b) => (Number(a) <= Number(b) ? a : b)));
|
|
1855
|
-
installStdShims(env);
|
|
1856
|
-
// Global string substitution — LIPS' built-in `replace` with a string
|
|
1857
|
-
// pattern only replaces the first match (it calls JS String.replace).
|
|
1858
|
-
// This binding replaces every occurrence, sed-style.
|
|
1652
|
+
// Global string substitution: `replace` with a string pattern replaces only
|
|
1653
|
+
// the first match; this binding replaces every occurrence, sed-style.
|
|
1859
1654
|
env.set("string-replace", (oldStr: unknown, newStr: unknown, s: unknown) => {
|
|
1655
|
+
s = toJsStr(s);
|
|
1860
1656
|
if (typeof s !== "string") return s;
|
|
1861
1657
|
return s.split(String(oldStr ?? "")).join(String(newStr ?? ""));
|
|
1862
1658
|
});
|
|
1863
1659
|
|
|
1864
|
-
// LIPS' tokenizer doesn't recognize R7RS `#t`/`#f` — they parse as
|
|
1865
|
-
// unbound symbols. Bind them so the natural R7RS reflex works.
|
|
1866
|
-
env.set("#t", true);
|
|
1867
|
-
env.set("#f", false);
|
|
1868
1660
|
env.set("lines", (s: unknown) => {
|
|
1661
|
+
s = toJsStr(s);
|
|
1869
1662
|
if (typeof s !== "string" || s.length === 0) return nil;
|
|
1870
1663
|
const parts = s.split("\n");
|
|
1871
1664
|
if (parts.length > 0 && parts[parts.length - 1] === "") parts.pop();
|
|
@@ -1876,98 +1669,63 @@ function installBindings(
|
|
|
1876
1669
|
}
|
|
1877
1670
|
|
|
1878
1671
|
// ── tool registration ─────────────────────────────────────────────
|
|
1672
|
+
// Generated from HOST_SIGS so the catalog the model reads matches what
|
|
1673
|
+
// `(help …)` reports at runtime.
|
|
1674
|
+
const HOST_BINDINGS_BLOCK = HOST_SIGS.map(
|
|
1675
|
+
(h) => ` ${h.sig.padEnd(44)} → ${h.ret}\n ${h.doc}`,
|
|
1676
|
+
).join("\n");
|
|
1677
|
+
|
|
1879
1678
|
const DESCRIPTION = [
|
|
1880
1679
|
"Evaluate a Scheme expression (R7RS-compatible).",
|
|
1881
1680
|
"",
|
|
1882
|
-
"A Scheme runtime with host bindings to the shell, filesystem, search
|
|
1883
|
-
"
|
|
1884
|
-
"
|
|
1885
|
-
"in one submission are available in the next.",
|
|
1681
|
+
"A Scheme runtime with host bindings to the shell, filesystem, and search.",
|
|
1682
|
+
"The environment persists across calls within a session — `define`s in one",
|
|
1683
|
+
"submission are visible in the next.",
|
|
1886
1684
|
"",
|
|
1887
|
-
"
|
|
1888
|
-
"
|
|
1889
|
-
"
|
|
1890
|
-
"
|
|
1891
|
-
"
|
|
1892
|
-
"
|
|
1893
|
-
"
|
|
1894
|
-
"
|
|
1895
|
-
"
|
|
1896
|
-
"
|
|
1897
|
-
" - The env persists across submissions, so binding intermediate",
|
|
1898
|
-
" results once (e.g. `(define files (glob …))`) avoids recomputing",
|
|
1899
|
-
" them in later calls.",
|
|
1900
|
-
" - `(bash …)` calls a real shell — natural for shell-shaped work",
|
|
1901
|
-
" (tests, builds, git, system commands). Three variants of the shell",
|
|
1902
|
-
" binding return different shapes:",
|
|
1903
|
-
" `(sh \"cmd\")` → just the output as a string. Fits \"run this,",
|
|
1904
|
-
" show me the result\" without unwrapping.",
|
|
1905
|
-
" `(sh-ok? \"cmd\")` → just a boolean. Fits `(if (sh-ok? \"…\") …)`",
|
|
1906
|
-
" branches and existence checks.",
|
|
1907
|
-
" `(bash \"cmd\")` → full alist when you need stdout *and* exit",
|
|
1908
|
-
" code *and* stderr separately (e.g. capture",
|
|
1909
|
-
" stderr while letting stdout flow on).",
|
|
1910
|
-
" For file content work, the host bindings (`grep`, `read-file`,",
|
|
1911
|
-
" `glob`) avoid shell-quoting entirely and return structured data.",
|
|
1912
|
-
" - `scheme-define` saves a procedure to disk so it auto-loads next",
|
|
1913
|
-
" session — useful when you've worked out something reusable.",
|
|
1685
|
+
"You already know the language: assume the full R7RS / SRFI-1 / Racket stdlib",
|
|
1686
|
+
"is present (map filter fold assoc, string-* and list ops, cond/when/unless,",
|
|
1687
|
+
"char and hash-table ops, …). The only novel surface is the host bindings.",
|
|
1688
|
+
"",
|
|
1689
|
+
"Calling convention:",
|
|
1690
|
+
" - Required arguments are positional: (read-file \"x\"), (grep \"pat\" \"src/\").",
|
|
1691
|
+
" - grep / grep-files options are :key value pairs:",
|
|
1692
|
+
" (grep \"TODO\" \"src/\" :include \"*.ts\" :context-after 2)",
|
|
1693
|
+
" - Each binding returns the natural Scheme value for its job (a string, a",
|
|
1694
|
+
" list, a boolean); bash returns a record because you usually want the code.",
|
|
1914
1695
|
"",
|
|
1915
1696
|
"Host bindings:",
|
|
1916
|
-
|
|
1917
|
-
" cmd is run via `bash -c`. Pipes/redirects/$VARS/&&/||/here-docs work",
|
|
1918
|
-
" inside the string; there's no piping between separate bash calls.",
|
|
1919
|
-
" (sh cmd [timeout-sec]) → stdout string (stderr text on failure)",
|
|
1920
|
-
" (sh-ok? cmd [timeout-sec]) → #t if exit code 0, else #f",
|
|
1921
|
-
" (read-file path) → string, or #f on error",
|
|
1922
|
-
" (read-file path offset) → from line offset (1-indexed) to end",
|
|
1923
|
-
" (read-file path offset limit) → offset + N lines",
|
|
1924
|
-
" (write-file path content) → #t on success, error string on failure",
|
|
1925
|
-
" (edit-file path old new) → #t on success, error string on failure",
|
|
1926
|
-
" (edit-file path old new #t) → replace every occurrence (not just one)",
|
|
1927
|
-
" (grep pattern [path] [limit|opts]) → list of ((file . S) (line . N) (text . S))",
|
|
1928
|
-
" (grep-files pattern [path] [opts]) → list of file paths",
|
|
1929
|
-
" Patterns are ripgrep regex (Rust). Both POSIX BRE escapes (`\\|`,",
|
|
1930
|
-
" `\\(`, `\\)`, `\\{`, `\\}`, `\\+`, `\\?`) and bare ERE-style metacharacters",
|
|
1931
|
-
" (`|`, `(`, `)`, `{`, `}`, `+`, `?`) work — the bridge translates BRE to",
|
|
1932
|
-
" ERE before invoking ripgrep. `.` is any char, `\\b` is a word boundary.",
|
|
1933
|
-
" opts: ((include . \"*.ts\") ; filename glob filter",
|
|
1934
|
-
" (case-insensitive . #t)",
|
|
1935
|
-
" (context-before . N) (context-after . N) ; grep only",
|
|
1936
|
-
" (limit . N) (offset . N))",
|
|
1937
|
-
" The opts alist is auto-quoted, so `((k . v) …)` and `'((k . v) …)` both work.",
|
|
1938
|
-
" (glob pattern [base-dir]) → list of file paths (mtime-sorted)",
|
|
1939
|
-
"Accessors on bash result: (stdout-of r) (stderr-of r) (exit-code-of r) (success? r)",
|
|
1940
|
-
"Strings: (string-length s) (string-contains? s n) (string-append . parts)",
|
|
1941
|
-
" (string-replace old new s) (number->string n) (string->number s)",
|
|
1942
|
-
" (lines s) (split sep s) (replace pat repl s) (max …) (min …)",
|
|
1697
|
+
HOST_BINDINGS_BLOCK,
|
|
1943
1698
|
"",
|
|
1944
|
-
"
|
|
1945
|
-
"
|
|
1946
|
-
"
|
|
1699
|
+
" Discover at runtime: (help) lists these, (help 'grep) shows one.",
|
|
1700
|
+
" bash runs via `bash -c` — pipes/redirects/$VARS/&&/here-docs work inside the",
|
|
1701
|
+
" string; stdout+stderr are merged into `output`, and `exit-code` is the real",
|
|
1702
|
+
" shell code. grep/grep-files patterns are ripgrep regex (Rust); POSIX BRE",
|
|
1703
|
+
" escapes (\\|, \\(, \\), \\{, \\}, \\+, \\?) and bare ERE metacharacters both work.",
|
|
1947
1704
|
"",
|
|
1948
|
-
"
|
|
1949
|
-
"
|
|
1950
|
-
"
|
|
1951
|
-
"
|
|
1952
|
-
"
|
|
1953
|
-
"
|
|
1954
|
-
" `find`, `filter-map`, `append-map`, `concatenate`, `partition`, `remove`,",
|
|
1955
|
-
" `delete`, `zip`, `take-while`, `drop-while`, `fold-right` are all bound.",
|
|
1956
|
-
" - R7RS extras: `string-upcase`/`-downcase`, `string-split`/`-join`,",
|
|
1957
|
-
" `zero?`/`positive?`/`negative?`/`odd?`/`even?`, `modulo`, `quotient`,",
|
|
1958
|
-
" `remainder`, `expt`, `ceiling`, `error`, `newline`, `displayln`.",
|
|
1705
|
+
"Composition is the point: chain read-only bindings in one submission so",
|
|
1706
|
+
"intermediate results stay in the Scheme heap instead of the conversation.",
|
|
1707
|
+
" (map (lambda (m) (read-file (cdr (assoc 'file m)) (cdr (assoc 'line m)) 3))",
|
|
1708
|
+
" (grep \"TODO\" \"src/\"))",
|
|
1709
|
+
"Side-effecting calls (write-file, edit-file, mutating bash) are clearer one",
|
|
1710
|
+
"at a time so you can react to each result.",
|
|
1959
1711
|
"",
|
|
1712
|
+
"scheme-define saves a procedure to ~/.agent-sh/scheme-define/{name}.scm so it",
|
|
1713
|
+
"auto-loads next session:",
|
|
1960
1714
|
" (scheme-define name (args …) \"docstring\" body …)",
|
|
1961
|
-
"
|
|
1962
|
-
"
|
|
1715
|
+
"",
|
|
1716
|
+
"Dialect notes:",
|
|
1717
|
+
" - R7RS truthy semantics: only `#f` is false. `(if 0 …)`, `(if '() …)`,",
|
|
1718
|
+
" `(if \"\" …)` all take the then-branch.",
|
|
1719
|
+
" - Characters are a real type: `#\\A` `#\\newline` `#\\space` `#\\tab` `#\\xNN`.",
|
|
1720
|
+
" - String escapes are JSON-style (`\\\\` `\\\"` `\\n` `\\r` `\\t` `\\uXXXX`);",
|
|
1721
|
+
" for a literal backslash write `\\\\`.",
|
|
1963
1722
|
"",
|
|
1964
1723
|
"Default timeout 15s; pass timeout_ms to override (max 60s).",
|
|
1965
1724
|
].join("\n");
|
|
1966
1725
|
|
|
1967
|
-
// Scheme prelude
|
|
1968
|
-
//
|
|
1969
|
-
//
|
|
1970
|
-
// available either.
|
|
1726
|
+
// Scheme prelude (Lisp `define-macro`s), run after std is bootstrapped.
|
|
1727
|
+
// cond/when/unless/newline/assq for convenience; the grep/grep-files macros
|
|
1728
|
+
// quote :key option symbols so the bridge can read them as an option map.
|
|
1971
1729
|
const PRELUDE = `
|
|
1972
1730
|
(define-macro (cond . clauses)
|
|
1973
1731
|
(if (null? clauses)
|
|
@@ -1989,36 +1747,24 @@ const PRELUDE = `
|
|
|
1989
1747
|
(define (newline) (display "\n"))
|
|
1990
1748
|
(define assq assoc)
|
|
1991
1749
|
|
|
1992
|
-
;; grep / grep-files:
|
|
1993
|
-
;;
|
|
1994
|
-
;;
|
|
1995
|
-
|
|
1996
|
-
(
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
(
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
(cons '%grep args)))
|
|
2007
|
-
|
|
2008
|
-
(define-macro (grep-files . args)
|
|
2009
|
-
(if (and (>= (length args) 3) (%alist-literal? (car (cdr (cdr args)))))
|
|
2010
|
-
(cons '%grep-files
|
|
2011
|
-
(cons (car args)
|
|
2012
|
-
(cons (car (cdr args))
|
|
2013
|
-
(cons (list 'quote (car (cdr (cdr args))))
|
|
2014
|
-
(cdr (cdr (cdr args)))))))
|
|
2015
|
-
(cons '%grep-files args)))
|
|
1750
|
+
;; grep / grep-files take :key value options. LIPS has no keyword args, so a
|
|
1751
|
+
;; bare :include would be evaluated as an unbound variable; the macro quotes
|
|
1752
|
+
;; leading-colon symbols and the %grep bridge splits them from the positionals.
|
|
1753
|
+
(define (%kw-symbol? s)
|
|
1754
|
+
(and (symbol? s)
|
|
1755
|
+
(let ((str (symbol->string s)))
|
|
1756
|
+
(and (> (string-length str) 0)
|
|
1757
|
+
(string=? (substring str 0 1) ":")))))
|
|
1758
|
+
|
|
1759
|
+
(define (%quote-kw args)
|
|
1760
|
+
(map (lambda (a) (if (%kw-symbol? a) (list 'quote a) a)) args))
|
|
1761
|
+
|
|
1762
|
+
(define-macro (grep . args) (cons '%grep (%quote-kw args)))
|
|
1763
|
+
(define-macro (grep-files . args) (cons '%grep-files (%quote-kw args)))
|
|
2016
1764
|
`;
|
|
2017
1765
|
|
|
2018
1766
|
export default function activate(ctx: AgentContext): void {
|
|
2019
1767
|
const env = (lips as any).env.inherit("scheme-ext");
|
|
2020
|
-
installFixedDefine(env);
|
|
2021
|
-
installLenientIf(env);
|
|
2022
1768
|
const defineRegistry: DefineRegistry = new Map();
|
|
2023
1769
|
const defineLoading = { active: false };
|
|
2024
1770
|
// Forward decl: assigned after baseInstruction is computed below.
|
|
@@ -2038,21 +1784,32 @@ export default function activate(ctx: AgentContext): void {
|
|
|
2038
1784
|
try { glob = resolveExecutor(ctx, "glob"); } catch { /* optional */ }
|
|
2039
1785
|
installBindings(env, ctx.bus, bash, readFile, writeFile, editFile, grep, glob);
|
|
2040
1786
|
|
|
2041
|
-
//
|
|
2042
|
-
//
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
}
|
|
2055
|
-
|
|
1787
|
+
// Bootstrap LIPS' compiled std library into the global env, then install the
|
|
1788
|
+
// gap-filling shims (skipped where std already defines a name, so native
|
|
1789
|
+
// R7RS wins), the prelude macros, and any persisted scheme-defines. Bootstrap
|
|
1790
|
+
// must precede installStdShims so native bindings take priority.
|
|
1791
|
+
void (async () => {
|
|
1792
|
+
try {
|
|
1793
|
+
const stdXcb = path.join(
|
|
1794
|
+
path.dirname(createRequire(import.meta.url).resolve("@jcubic/lips")),
|
|
1795
|
+
"std.xcb",
|
|
1796
|
+
);
|
|
1797
|
+
await bootstrap(stdXcb);
|
|
1798
|
+
} catch (e) {
|
|
1799
|
+
logErr("bootstrap", e);
|
|
1800
|
+
}
|
|
1801
|
+
installStdShims(env);
|
|
1802
|
+
await (lips as any).exec(PRELUDE, { env });
|
|
1803
|
+
await loadPersistedDefines(env, defineRegistry, defineLoading);
|
|
1804
|
+
const audit = auditShimCoverage(env);
|
|
1805
|
+
if (audit.missing.length > 0) {
|
|
1806
|
+
logErr("shim-audit", new Error("missing canonical names"), {
|
|
1807
|
+
defined: audit.defined,
|
|
1808
|
+
total: audit.defined + audit.missing.length,
|
|
1809
|
+
missing: audit.missing,
|
|
1810
|
+
});
|
|
1811
|
+
}
|
|
1812
|
+
})().catch((e: any) => logErr("init", e));
|
|
2056
1813
|
|
|
2057
1814
|
if (schemeOnly) {
|
|
2058
1815
|
for (const name of HIDDEN_IN_SCHEME_ONLY) {
|
|
@@ -2085,11 +1842,8 @@ export default function activate(ctx: AgentContext): void {
|
|
|
2085
1842
|
},
|
|
2086
1843
|
getDisplayInfo: () => ({ kind: "execute", icon: "λ", sourceLanguage: "scheme" }),
|
|
2087
1844
|
formatResult: (args, result) => {
|
|
2088
|
-
//
|
|
2089
|
-
//
|
|
2090
|
-
// reveals what ran rather than what came back. scheme-render.ts (if
|
|
2091
|
-
// loaded) honors this; ashi's default renderer currently ignores
|
|
2092
|
-
// body.kind === "lines", which is fine — summary still carries the gist.
|
|
1845
|
+
// TUI body shows the SOURCE, not the result (Ctrl+O reveals what ran);
|
|
1846
|
+
// the LLM still gets full content via tool_result.
|
|
2093
1847
|
const sourceLines = String(args.source ?? "").split("\n");
|
|
2094
1848
|
if (!result.isError) {
|
|
2095
1849
|
return {
|
|
@@ -2120,44 +1874,24 @@ export default function activate(ctx: AgentContext): void {
|
|
|
2120
1874
|
},
|
|
2121
1875
|
});
|
|
2122
1876
|
|
|
2123
|
-
//
|
|
2124
|
-
//
|
|
2125
|
-
//
|
|
2126
|
-
// add behavioral framing here — specifically the context-preservation
|
|
2127
|
-
// nudge that would be lost in the long tool description.
|
|
1877
|
+
// Behavioral framing in the system prompt (the API + examples live in the
|
|
1878
|
+
// tool description). Keep it short and non-duplicative: just when to reach
|
|
1879
|
+
// for scheme_eval over a direct tool call.
|
|
2128
1880
|
const baseInstruction = schemeOnly
|
|
2129
1881
|
? [
|
|
2130
1882
|
"# Scheme runtime",
|
|
2131
|
-
"scheme_eval is your only tool; see its description for the API.",
|
|
2132
|
-
"",
|
|
2133
|
-
"
|
|
2134
|
-
"
|
|
2135
|
-
"multi-step operations into a single scheme_eval call so intermediate",
|
|
2136
|
-
"results stay in the Scheme heap instead of the conversation. Example:",
|
|
2137
|
-
" (let ((files (glob \"src/**/*.ts\"))",
|
|
2138
|
-
" (matches (grep \"TODO\" \"src/\")))",
|
|
2139
|
-
" (filter (lambda (f) (member f (map (lambda (m) (cdr (assoc 'file m))) matches)))",
|
|
2140
|
-
" files))",
|
|
2141
|
-
"This does glob + grep + filter in one round-trip. Use `define` to",
|
|
2142
|
-
"cache results across calls: `(define files (glob …))` once, reuse later.",
|
|
1883
|
+
"scheme_eval is your only tool; see its description for the API. Each tool",
|
|
1884
|
+
"round-trip consumes context permanently, so compose multi-step work into a",
|
|
1885
|
+
"single call — intermediate results stay in the Scheme heap, not the",
|
|
1886
|
+
"conversation. `define` caches across calls.",
|
|
2143
1887
|
].join("\n")
|
|
2144
1888
|
: [
|
|
2145
1889
|
"# Scheme runtime",
|
|
2146
|
-
"scheme_eval evaluates Scheme with host bindings
|
|
2147
|
-
"
|
|
2148
|
-
"",
|
|
2149
|
-
"
|
|
2150
|
-
"
|
|
2151
|
-
"single operations. scheme_eval becomes valuable when you'd chain 2+ read-only",
|
|
2152
|
-
"tool calls that don't need inspection between steps — composing them inside",
|
|
2153
|
-
"Scheme keeps intermediate results in the Scheme heap instead of the",
|
|
2154
|
-
"conversation, saving context. Example:",
|
|
2155
|
-
" (let ((matches (grep \"pattern\" \"src/\")))",
|
|
2156
|
-
" (map (lambda (m) (list (cdr (assoc 'file m))",
|
|
2157
|
-
" (read-file (cdr (assoc 'file m)) (cdr (assoc 'line m)) 3)))",
|
|
2158
|
-
" (take matches 5)))",
|
|
2159
|
-
"does grep + read-file × 5 in one round-trip. `define` caches across",
|
|
2160
|
-
"calls: `(define files (glob …))` once, reuse in later submissions.",
|
|
1890
|
+
"scheme_eval evaluates Scheme with host bindings (bash, read-file, grep, glob,",
|
|
1891
|
+
"…); see its description for the API. Direct tools are the right default for",
|
|
1892
|
+
"single operations; reach for scheme_eval to chain 2+ read-only calls that",
|
|
1893
|
+
"don't need inspection between steps — composing them keeps intermediate",
|
|
1894
|
+
"results in the Scheme heap instead of the conversation.",
|
|
2161
1895
|
].join("\n");
|
|
2162
1896
|
// Re-register when the scheme-define registry changes so the index stays
|
|
2163
1897
|
// current within a session.
|