@hicaru/pi-rlm 0.2.2 → 0.3.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.
Files changed (47) hide show
  1. package/README.md +20 -16
  2. package/README.ru.md +2 -2
  3. package/README.zh-CN.md +2 -2
  4. package/package.json +22 -19
  5. package/src/bridge/add-context.ts +322 -0
  6. package/src/bridge/subcall-handlers.ts +1 -1
  7. package/src/config/defaults.ts +2 -1
  8. package/src/config/settings.ts +5 -2
  9. package/src/context/anydoc.ts +67 -0
  10. package/src/context/listing.ts +70 -0
  11. package/src/context/md-cache.ts +112 -0
  12. package/src/context/merge.ts +97 -0
  13. package/src/context/namespace.ts +180 -0
  14. package/src/context/resolve.ts +122 -0
  15. package/src/context/source-dir.ts +166 -0
  16. package/src/context/source-doc.ts +71 -0
  17. package/src/context/source-git.ts +51 -0
  18. package/src/context/source-text.ts +45 -0
  19. package/src/context/types.ts +88 -0
  20. package/src/context/walk.ts +250 -0
  21. package/src/core/engine.ts +15 -19
  22. package/src/core/types.ts +7 -2
  23. package/src/index.ts +69 -42
  24. package/src/mode/rlm-mode.ts +5 -4
  25. package/src/prompts/glossary.ts +31 -28
  26. package/src/prompts/native.ts +4 -4
  27. package/src/prompts/system.ts +2 -2
  28. package/src/sandbox/context-file.ts +4 -4
  29. package/src/sandbox/interrupts.ts +25 -10
  30. package/src/sandbox/protocol.ts +13 -7
  31. package/src/sandbox/py/__pycache__/guards.cpython-314.pyc +0 -0
  32. package/src/sandbox/py/__pycache__/retrieval.cpython-314.pyc +0 -0
  33. package/src/sandbox/py/__pycache__/tasks.cpython-314.pyc +0 -0
  34. package/src/sandbox/py/guards.py +1 -1
  35. package/src/sandbox/py/retrieval.py +1 -1
  36. package/src/sandbox/py/tasks.py +17 -4
  37. package/src/sandbox/py/worker.py +68 -48
  38. package/src/sandbox/sandbox-manager.ts +18 -16
  39. package/src/sandbox/sandbox.ts +1 -1
  40. package/src/text/tokens.ts +3 -3
  41. package/src/tool/repl-details.ts +1 -1
  42. package/src/tool/repl-tool.ts +31 -19
  43. package/src/tool/rlm-tool.ts +1 -1
  44. package/src/ui/config-panel.ts +8 -4
  45. package/src/bridge/library.ts +0 -190
  46. package/src/context/library-context.ts +0 -339
  47. package/src/context/repomix-context.ts +0 -204
@@ -1,190 +0,0 @@
1
- /**
2
- * Shared load_library handler for headless engine and native repl() mode.
3
- *
4
- * Host packs the source via resolveLibrarySource (namespaced under lib/<id>/) and returns the
5
- * payload for the worker to append into the single `context` list.
6
- *
7
- * Idempotency is host-side: re-loading a source that was already packed does not re-clone or
8
- * re-pack, and the prefix set is the only state that decides it.
9
- *
10
- * Late-bound deps (getCwd / getEmitter) keep a single handler closure correct
11
- * across native repl() calls — getOrCreate only installs handlers at spawn.
12
- */
13
-
14
- import type { RlmEmitter } from "../tool/rlm-events.ts";
15
- import type { SubLlmHandlers } from "../sandbox/sandbox.ts";
16
- import {
17
- libraryNamespace,
18
- resolveLibrarySource,
19
- } from "../context/library-context.ts";
20
- import { previewText } from "../text/preview.ts";
21
-
22
- export interface LibraryBridgeOpts {
23
- /** Fixed cwd (headless). Prefer getCwd when the sandbox outlives a single invocation. */
24
- readonly cwd?: string;
25
- /** Late-bound cwd (native mode — sandbox handlers outlive a single repl()). */
26
- readonly getCwd?: () => string;
27
- readonly emitter?: RlmEmitter;
28
- /** Native mode: read the live emitter each call. */
29
- readonly getEmitter?: () => RlmEmitter | null | undefined;
30
- readonly parentId?: string;
31
- readonly signal?: AbortSignal;
32
- /** Prefixes already present in context — seeds host-side idempotency after a sandbox restart. */
33
- readonly loadedPrefixes?: readonly string[];
34
- /**
35
- * The live context this sandbox holds. Read to refuse pre-flight exactly what the worker's
36
- * `_append_library` would reject, before any prefix is committed.
37
- */
38
- readonly getContext?: () => unknown;
39
- /**
40
- * Post-load hook. The engine grows its live context here; native mode grows
41
- * SandboxManager.contextPayload.
42
- */
43
- readonly onLoaded?: (payload: unknown) => void | Promise<void>;
44
- }
45
-
46
- export interface LibraryHandlerBundle {
47
- readonly handlers: Pick<SubLlmHandlers, "loadLibrary">;
48
- /**
49
- * Reset the loaded-prefix cache (call when the sandbox is
50
- * discarded and will re-spawn).
51
- *
52
- * `keep` re-seeds the cache from the payload that will be replayed into the fresh worker.
53
- * `loaded` is a CACHE of `libraryPrefixesIn(context)`, never independent state, so it may only
54
- * be cleared by re-deriving it — clearing it outright would make the host re-clone and re-pack
55
- * a library the recreated worker already holds.
56
- */
57
- readonly reset: (keep?: readonly string[]) => void;
58
- /** Prefixes loaded in this sandbox lifetime (for tests). */
59
- readonly loadedPrefixes: () => ReadonlySet<string>;
60
- }
61
-
62
- /**
63
- * JS runtime kind → the Python type name worker.py reports, so both sides emit exactly one
64
- * message for the same refusal. Covers every shape a context payload can take after JSON
65
- * transport; anything else is a plain object, which `json.load` materializes as a dict.
66
- */
67
- const PY_TYPE_NAME: Readonly<Record<string, string>> = Object.freeze({
68
- string: "str", boolean: "bool", number: "int", bigint: "int", undefined: "None",
69
- });
70
-
71
- function pythonKindOf(value: unknown): string {
72
- if (value === null) return "None"; // matches worker.py's `if ctx is not None else "None"`
73
- return PY_TYPE_NAME[typeof value] ?? "dict";
74
- }
75
-
76
- /**
77
- * Refusal messages shared with worker.py `_append_library`. The worker is the backstop; the host
78
- * pre-flights the same two conditions so it never commits a prefix for an append that
79
- * will be rejected. Keep the wording identical — a comment in worker.py points back here.
80
- */
81
- const LIST_CONTEXT_REQUIRED = (kind: string): string =>
82
- `load_library requires list context (file bundle); got ${kind}`;
83
- const NO_FILES_PRODUCED = "load_library produced no files";
84
-
85
- export function buildLibraryHandler(opts: LibraryBridgeOpts): LibraryHandlerBundle {
86
- /** Prefixes already loaded in this sandbox — mirrors the worker's context state. */
87
- const loaded = new Set<string>(opts.loadedPrefixes ?? []);
88
- return {
89
- reset: (keep) => {
90
- const seed = keep ?? opts.loadedPrefixes ?? [];
91
- loaded.clear();
92
- for (const prefix of seed) loaded.add(prefix);
93
- },
94
- loadedPrefixes: () => loaded,
95
- handlers: {
96
- async loadLibrary(source, depth) {
97
- const emitter = opts.getEmitter?.() ?? opts.emitter;
98
- const cwd = opts.getCwd?.() ?? opts.cwd;
99
- if (cwd === undefined || cwd === "") {
100
- throw new Error("load_library: no cwd configured");
101
- }
102
- const id = emitter?.emitSubcallCreated({
103
- kind: "tool", parentId: opts.parentId,
104
- label: "load_library",
105
- args: previewText(source, 80),
106
- depth,
107
- });
108
- try {
109
- // Pre-flight the worker's own refusal: a non-list context cannot be appended to, and
110
- // committing a prefix for it would make the NEXT load lie with already_loaded.
111
- const current = opts.getContext?.();
112
- if (current !== undefined && !Array.isArray(current)) {
113
- throw new Error(LIST_CONTEXT_REQUIRED(pythonKindOf(current)));
114
- }
115
-
116
- // Cheap pre-check BEFORE cloning/packing: same namespace ⇒ nothing to do.
117
- const { sourceId: preId, pathPrefix: prefix } = libraryNamespace(source, cwd);
118
- if (loaded.has(prefix)) {
119
- if (id) {
120
- emitter?.emitSubcallUpdated({
121
- id,
122
- status: "done",
123
- resultPreview: `already loaded (${prefix}*)`,
124
- });
125
- }
126
- return {
127
- payload: Object.freeze([]),
128
- files: 0,
129
- chars: 0,
130
- sourceId: preId,
131
- pathPrefix: prefix,
132
- alreadyLoaded: true,
133
- };
134
- }
135
-
136
- const resolved = await resolveLibrarySource(source, cwd, opts.signal);
137
- if (!resolved.ok) throw new Error(resolved.error);
138
- const { payload, files, chars, sourceId, pathPrefix } = resolved.value;
139
- // The worker's other refusal, pre-flighted for the same reason.
140
- if (payload.length === 0) throw new Error(NO_FILES_PRODUCED);
141
-
142
- // Race: another concurrent load of the same prefix finished while we packed.
143
- if (loaded.has(pathPrefix)) {
144
- if (id) {
145
- emitter?.emitSubcallUpdated({
146
- id,
147
- status: "done",
148
- resultPreview: `already loaded (${pathPrefix}*)`,
149
- });
150
- }
151
- return {
152
- payload: Object.freeze([]),
153
- files: 0,
154
- chars: 0,
155
- sourceId,
156
- pathPrefix,
157
- alreadyLoaded: true,
158
- };
159
- }
160
-
161
- // Mark loaded only after the host has grown its own copy of the context.
162
- if (opts.onLoaded) {
163
- await opts.onLoaded(payload);
164
- }
165
- loaded.add(pathPrefix);
166
-
167
- if (id) {
168
- emitter?.emitSubcallUpdated({
169
- id,
170
- status: "done",
171
- resultPreview:
172
- `+${files} file(s) → context (${pathPrefix}*, ${chars.toLocaleString()} chars)`,
173
- });
174
- }
175
- return {
176
- payload,
177
- files,
178
- chars,
179
- sourceId,
180
- pathPrefix,
181
- alreadyLoaded: false,
182
- };
183
- } catch (err) {
184
- if (id) emitter?.emitSubcallUpdated({ id, status: "error", detail: String(err) });
185
- throw err; // serviceInterrupt catch → {error} reply → "Error: …" in the REPL
186
- }
187
- },
188
- },
189
- };
190
- }
@@ -1,339 +0,0 @@
1
- /**
2
- * Resolve load_library(source) into a sandbox-ready payload.
3
- *
4
- * Sources: local directory (repomix-packed), single file (utf-8), or remote git URL
5
- * (shallow clone then pack). Host-side only — never runs in the sandbox.
6
- *
7
- * Every successful payload is a namespaced list of ContextFile under
8
- * `lib/<source_id>/…` so the worker can append into the single `context` variable.
9
- * Source ids include a short content fingerprint so two libraries that share a
10
- * basename never collide.
11
- */
12
-
13
- import { createHash } from "node:crypto";
14
- import { execFile } from "node:child_process";
15
- import { mkdtemp, readFile, rm, stat } from "node:fs/promises";
16
- import { basename, isAbsolute, join, resolve } from "node:path";
17
- import { tmpdir } from "node:os";
18
- import { promisify } from "node:util";
19
- import {
20
- packRepository,
21
- serializeForSandbox,
22
- type ContextBundle,
23
- type ContextFile,
24
- } from "./repomix-context.ts";
25
- import { estimateTokens } from "../text/tokens.ts";
26
- import type { Result } from "../util/errors.ts";
27
- import { errorMessage } from "../util/errors.ts";
28
-
29
- const execFileP = promisify(execFile);
30
-
31
- /** Single-file sources above this must use open() + llm_query_chunked in the REPL. */
32
- export const MAX_LIBRARY_FILE_BYTES = 8 * 1024 * 1024;
33
-
34
- /** Catch-all prefix for a raw string payload with no namespace — never an identity key. */
35
- const LEGACY_UNKNOWN_PREFIX = "lib/unknown/";
36
-
37
- export interface LibrarySource {
38
- /** Always a namespaced file list (dirs, single files, and git clones). */
39
- readonly payload: readonly ContextFile[];
40
- readonly files: number;
41
- /** Sum of raw content lengths — what the model should size batches against. */
42
- readonly chars: number;
43
- readonly sourceId: string;
44
- readonly pathPrefix: string;
45
- }
46
-
47
- export interface LibraryNamespace {
48
- readonly sourceId: string;
49
- readonly pathPrefix: string;
50
- }
51
-
52
- /** https://host/… or git@host:… — option-injection safe (never starts with "-"). */
53
- const GIT_URL = /^(https:\/\/|git@)[\w.-]+[:/]\S+$/;
54
-
55
- /** Short, stable discriminator so two sources never share a namespace. */
56
- function sourceFingerprint(canonical: string): string {
57
- return createHash("sha256").update(canonical).digest("hex").slice(0, 8);
58
- }
59
-
60
- /**
61
- * Sanitize a path/url basename into a stable, filesystem-safe source id.
62
- * `resolvedPath` (absolute path) or the git URL is fingerprinted into the id so
63
- * distinct sources sharing a basename get distinct namespaces
64
- * (`lib/utils-3f9a1c02/` vs `lib/utils-a1b2c3d4/`).
65
- */
66
- export function librarySourceId(source: string, resolvedPath?: string): string {
67
- const trimmed = source.trim();
68
- const isGit = GIT_URL.test(trimmed);
69
- let raw: string;
70
- if (isGit) {
71
- const m = trimmed.match(/(?:\/|:)([\w.-]+?)(?:\.git)?\/?\s*$/);
72
- raw = m?.[1] ?? "repo";
73
- } else {
74
- raw = basename(resolvedPath ?? trimmed);
75
- }
76
- const cleaned = raw
77
- .replace(/\.git$/i, "")
78
- .replace(/[^\w.-]+/g, "-")
79
- .replace(/^-+|-+$/g, "")
80
- .slice(0, 80);
81
- const canonical = isGit ? trimmed : (resolvedPath ?? trimmed);
82
- return `${cleaned.length > 0 ? cleaned : "lib"}-${sourceFingerprint(canonical)}`;
83
- }
84
-
85
- export function pathPrefixFor(sourceId: string): string {
86
- return `lib/${sourceId}/`;
87
- }
88
-
89
- /**
90
- * Derive the namespace for a source string without packing (host-side idempotency).
91
- * Returns both sourceId and pathPrefix so callers never un-parse the prefix.
92
- */
93
- export function libraryNamespace(source: string, cwd: string): LibraryNamespace {
94
- const trimmed = source.trim();
95
- const sourceId = GIT_URL.test(trimmed)
96
- ? librarySourceId(trimmed)
97
- : librarySourceId(trimmed, isAbsolute(trimmed) ? trimmed : resolve(cwd, trimmed));
98
- return Object.freeze({ sourceId, pathPrefix: pathPrefixFor(sourceId) });
99
- }
100
-
101
- /** Derive the path prefix for a source string without packing. */
102
- export function libraryPathPrefix(source: string, cwd: string): string {
103
- return libraryNamespace(source, cwd).pathPrefix;
104
- }
105
-
106
- /** Namespaced files plus summed content chars (one pass). */
107
- export function namespaceLibraryFilesWithChars(
108
- payload: unknown,
109
- sourceId: string,
110
- ): { readonly files: readonly ContextFile[]; readonly chars: number } {
111
- const prefix = pathPrefixFor(sourceId);
112
- if (typeof payload === "string") {
113
- return Object.freeze({
114
- files: Object.freeze([
115
- Object.freeze({
116
- path: `${prefix}content`,
117
- content: payload,
118
- tokens: Math.max(1, estimateTokens(payload.length)),
119
- }),
120
- ]),
121
- chars: payload.length,
122
- });
123
- }
124
- if (!Array.isArray(payload)) return Object.freeze({ files: Object.freeze([]), chars: 0 });
125
- const out = new Array<ContextFile>(payload.length);
126
- let n = 0;
127
- let chars = 0;
128
- for (const item of payload) {
129
- if (item === null || typeof item !== "object") continue;
130
- const rec = item as Record<string, unknown>;
131
- const content = typeof rec.content === "string" ? rec.content : String(rec.content ?? "");
132
- let path = typeof rec.path === "string" ? rec.path : "unknown";
133
- path = path.replace(/^\/+/, "");
134
- if (!path.startsWith(prefix)) path = `${prefix}${path}`;
135
- const tokens = typeof rec.tokens === "number" && Number.isFinite(rec.tokens)
136
- ? Math.max(0, Math.floor(rec.tokens))
137
- : Math.max(1, estimateTokens(content.length));
138
- out[n++] = Object.freeze({ path, content, tokens });
139
- chars += content.length;
140
- }
141
- out.length = n;
142
- return Object.freeze({ files: Object.freeze(out), chars });
143
- }
144
-
145
- /** Namespace file entries under `lib/<sourceId>/…` (single shared implementation). */
146
- export function namespaceLibraryFiles(
147
- payload: unknown,
148
- sourceId: string,
149
- ): readonly ContextFile[] {
150
- return namespaceLibraryFilesWithChars(payload, sourceId).files;
151
- }
152
-
153
- /** The one `lib/<id>/` matcher. Never re-declare this regex; use the helpers below. */
154
- const LIB_PREFIX_RE = /^(lib\/[^/]+\/)/;
155
-
156
- /** Narrow an unknown context entry to a ContextFile. Type guard, never a cast. */
157
- export function isContextFile(entry: unknown): entry is ContextFile {
158
- if (entry === null || typeof entry !== "object") return false;
159
- return "path" in entry && typeof entry.path === "string"
160
- && "content" in entry && typeof entry.content === "string";
161
- }
162
-
163
- /** `path` of a context entry, or undefined when the entry is not file-shaped. */
164
- export function contextEntryPath(entry: unknown): string | undefined {
165
- return isContextFile(entry) ? entry.path : undefined;
166
- }
167
-
168
- /** The `lib/<id>/` prefix owning this path, or undefined. Skips the legacy catch-all. */
169
- function libPrefixOf(path: string): string | undefined {
170
- const prefix = LIB_PREFIX_RE.exec(path)?.[1];
171
- // `lib/unknown/` is the legacy catch-all: never treat it as an identity.
172
- return prefix === undefined || prefix === LEGACY_UNKNOWN_PREFIX ? undefined : prefix;
173
- }
174
-
175
- /** First `lib/<id>/` prefix found in the payload, or undefined. Skips `lib/unknown/`. */
176
- export function payloadPrefix(payload: readonly unknown[]): string | undefined {
177
- for (let i = 0; i < payload.length; i++) {
178
- const path = contextEntryPath(payload[i]);
179
- if (path === undefined) continue;
180
- const prefix = libPrefixOf(path);
181
- if (prefix !== undefined) return prefix;
182
- }
183
- return undefined;
184
- }
185
-
186
- /**
187
- * Every distinct `lib/<id>/` prefix present in a context payload.
188
- *
189
- * The loaded-prefix set in bridge/library.ts is a CACHE of this — derived state, never
190
- * independent state, so it may only be cleared by re-deriving it from the live payload.
191
- */
192
- export function libraryPrefixesIn(context: unknown): readonly string[] {
193
- if (!Array.isArray(context)) return Object.freeze([]);
194
- const seen = new Set<string>();
195
- for (let i = 0; i < context.length; i++) {
196
- const path = contextEntryPath(context[i]);
197
- if (path === undefined) continue;
198
- const prefix = libPrefixOf(path);
199
- if (prefix !== undefined) seen.add(prefix);
200
- }
201
- return Object.freeze(Array.from(seen));
202
- }
203
-
204
- export interface FilteredContext {
205
- readonly files: readonly ContextFile[];
206
- /** Prefixes that selected zero files — the caller decides whether that is fatal. */
207
- readonly unmatched: readonly string[];
208
- }
209
-
210
- /**
211
- * Narrow a context payload to entries under any of `prefixes` (plain prefix match, no globs).
212
- *
213
- * Backs `rlm_query(prompt, paths=[…])`. Prefix-only is deliberate: the sandbox's own filters use
214
- * Python `fnmatch`, which has no host-side equivalent here, and a subtree prefix is what callers
215
- * actually want — the child can still `search()` inside the slice.
216
- */
217
- export function filterContextByPaths(context: unknown, prefixes: readonly string[]): FilteredContext {
218
- if (!Array.isArray(context) || prefixes.length === 0) {
219
- return Object.freeze({ files: Object.freeze([]), unmatched: Object.freeze(Array.from(prefixes)) });
220
- }
221
- const hit = new Array<boolean>(prefixes.length).fill(false);
222
- const out = new Array<ContextFile>(context.length); // pre-allocated, trimmed once below
223
- let n = 0;
224
- for (let i = 0; i < context.length; i++) {
225
- const entry: unknown = context[i];
226
- if (!isContextFile(entry)) continue;
227
- for (let p = 0; p < prefixes.length; p++) {
228
- if (!entry.path.startsWith(prefixes[p])) continue;
229
- hit[p] = true;
230
- out[n++] = entry;
231
- break;
232
- }
233
- }
234
- out.length = n;
235
- const unmatched = new Array<string>(prefixes.length); // pre-allocated, no .push()
236
- let u = 0;
237
- for (let p = 0; p < prefixes.length; p++) {
238
- if (!hit[p]) unmatched[u++] = prefixes[p];
239
- }
240
- unmatched.length = u;
241
- return Object.freeze({ files: Object.freeze(out), unmatched: Object.freeze(unmatched) });
242
- }
243
-
244
- /**
245
- * Append a library payload into an existing list context.
246
- * Skips a payload whose path prefix is already present, so a repeat load is a no-op.
247
- */
248
- export function mergeLibraryIntoContext(base: unknown, libraryPayload: unknown): unknown {
249
- if (!Array.isArray(base)) return base;
250
- if (Array.isArray(libraryPayload)) {
251
- if (libraryPayload.length === 0) return base;
252
- const prefix = payloadPrefix(libraryPayload);
253
- if (prefix !== undefined) {
254
- for (let i = 0; i < base.length; i++) {
255
- const path = contextEntryPath(base[i]);
256
- if (path !== undefined && path.startsWith(prefix)) return base; // already present
257
- }
258
- }
259
- const merged = new Array<unknown>(base.length + libraryPayload.length);
260
- for (let i = 0; i < base.length; i++) merged[i] = base[i];
261
- for (let i = 0; i < libraryPayload.length; i++) merged[base.length + i] = libraryPayload[i];
262
- return merged;
263
- }
264
- if (typeof libraryPayload === "string") {
265
- // Raw string payload: wrap once under the unknown prefix.
266
- return mergeLibraryIntoContext(base, namespaceLibraryFiles(libraryPayload, "unknown"));
267
- }
268
- return base;
269
- }
270
-
271
- function toLibrarySource(payload: unknown, sourceId: string): LibrarySource {
272
- const { files, chars } = namespaceLibraryFilesWithChars(payload, sourceId);
273
- return {
274
- payload: files,
275
- files: files.length,
276
- chars,
277
- sourceId,
278
- pathPrefix: pathPrefixFor(sourceId),
279
- };
280
- }
281
-
282
- export async function resolveLibrarySource(
283
- source: string,
284
- cwd: string,
285
- signal?: AbortSignal,
286
- ): Promise<Result<LibrarySource, string>> {
287
- const trimmed = source.trim();
288
- if (trimmed === "") return { ok: false, error: "load_library: empty source" };
289
- if (GIT_URL.test(trimmed)) return await cloneAndPack(trimmed, signal);
290
- if (/^[a-z][a-z0-9+.-]*:\/\//i.test(trimmed)) {
291
- return { ok: false, error: `unsupported URL scheme (only https:// and git@ are allowed): ${trimmed}` };
292
- }
293
- const path = isAbsolute(trimmed) ? trimmed : resolve(cwd, trimmed);
294
- let s: Awaited<ReturnType<typeof stat>>;
295
- try {
296
- s = await stat(path);
297
- } catch {
298
- return { ok: false, error: `load_library: path not found: ${path}` };
299
- }
300
- const sourceId = librarySourceId(trimmed, path);
301
- if (s.isDirectory()) return await packDir(path, sourceId, signal);
302
- if (s.size > MAX_LIBRARY_FILE_BYTES) {
303
- return {
304
- ok: false,
305
- error: `load_library: ${path} is ${s.size.toLocaleString()} bytes `
306
- + `(limit ${MAX_LIBRARY_FILE_BYTES.toLocaleString()}) — `
307
- + "open() it in the REPL and delegate with llm_query_chunked instead",
308
- };
309
- }
310
- const text = await readFile(path, "utf-8");
311
- return { ok: true, value: toLibrarySource(text, sourceId) };
312
- }
313
-
314
- async function packDir(
315
- dir: string,
316
- sourceId: string,
317
- signal?: AbortSignal,
318
- ): Promise<Result<LibrarySource, string>> {
319
- const packed = await packRepository(dir, signal);
320
- if (!packed.ok) return { ok: false, error: `pack failed for ${dir} — ${packed.error}` };
321
- return { ok: true, value: bundleToSource(packed.value, sourceId) };
322
- }
323
-
324
- function bundleToSource(bundle: ContextBundle, sourceId: string): LibrarySource {
325
- return toLibrarySource(serializeForSandbox(bundle), sourceId);
326
- }
327
-
328
- async function cloneAndPack(url: string, signal?: AbortSignal): Promise<Result<LibrarySource, string>> {
329
- const dir = await mkdtemp(join(tmpdir(), "rlm-lib-"));
330
- const sourceId = librarySourceId(url);
331
- try {
332
- await execFileP("git", ["clone", "--depth", "1", "--", url, dir], { signal, timeout: 120_000 });
333
- return await packDir(dir, sourceId, signal);
334
- } catch (err: unknown) {
335
- return { ok: false, error: `git clone failed for ${url} — ${errorMessage(err)}` };
336
- } finally {
337
- await rm(dir, { recursive: true, force: true }).catch(() => {});
338
- }
339
- }