@octocodeai/freellama-mcp-server 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -25057,6 +25057,324 @@ var responseStore = new ResponseStore(
25057
25057
  envNumber("FREELLAMA_RESPONSE_TTL_DAYS", 7)
25058
25058
  );
25059
25059
 
25060
+ // src/handoffs.ts
25061
+ init_config();
25062
+ import { lstatSync, mkdirSync, readdirSync, realpathSync as realpathSync3, rmSync, statSync as statSync2, writeFileSync } from "node:fs";
25063
+ import path5 from "node:path";
25064
+
25065
+ // src/task-files.ts
25066
+ import { globSync, realpathSync as realpathSync2, statSync, readFileSync } from "node:fs";
25067
+ import { homedir as homedir2, tmpdir } from "node:os";
25068
+ import path4 from "node:path";
25069
+ var MAX_TEXT_FILE_BYTES = 4 * 1024 * 1024;
25070
+ var MAX_IMAGE_FILE_BYTES = 8 * 1024 * 1024;
25071
+ var MAX_TOTAL_FILE_BYTES = 32 * 1024 * 1024;
25072
+ var MAX_FILES = 2e3;
25073
+ var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]);
25074
+ var AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([".wav"]);
25075
+ var EMBEDDING_CHUNK_BYTES = 1536;
25076
+ var CHUNK_OVERLAP_LINES = 8;
25077
+ function chunkLines(text3, limit = EMBEDDING_CHUNK_BYTES) {
25078
+ const lines = text3.split("\n");
25079
+ const chunks = [];
25080
+ let current = [];
25081
+ let bytes = 0;
25082
+ let start2 = 1;
25083
+ const flush = (end, overlap = 0) => {
25084
+ if (current.length > 0 && current.join("\n").trim()) chunks.push({ text: current.join("\n"), lines: [start2, end] });
25085
+ const carried = overlap > 0 ? current.slice(-overlap) : [];
25086
+ const carriedBytes = carried.reduce((sum, line2) => sum + Buffer.byteLength(line2) + 1, 0);
25087
+ current = carriedBytes < limit / 4 ? carried : [];
25088
+ bytes = current === carried ? carriedBytes : 0;
25089
+ start2 = end + 1 - current.length;
25090
+ };
25091
+ lines.forEach((line2, index) => {
25092
+ const size2 = Buffer.byteLength(line2) + 1;
25093
+ if (bytes + size2 > limit && current.length > 0) flush(index, CHUNK_OVERLAP_LINES);
25094
+ if (size2 > limit) {
25095
+ for (let at = 0; at < line2.length; at += Math.floor(limit / 2)) {
25096
+ chunks.push({ text: line2.slice(at, at + Math.floor(limit / 2)), lines: [index + 1, index + 1] });
25097
+ }
25098
+ start2 = index + 2;
25099
+ return;
25100
+ }
25101
+ current.push(line2);
25102
+ bytes += size2;
25103
+ });
25104
+ flush(lines.length);
25105
+ return chunks;
25106
+ }
25107
+ function fileRoots(env = process.env, cwd = process.cwd(), home = homedir2()) {
25108
+ const configured = (env.FREELLAMA_FILE_ROOTS ?? "").split(path4.delimiter).filter(Boolean);
25109
+ const broad = /* @__PURE__ */ new Set([path4.parse(path4.resolve(cwd)).root, resolvedRoot(home)]);
25110
+ const project = broad.has(resolvedRoot(cwd)) ? [] : [cwd];
25111
+ const handoffs = path4.join(env.FREELLAMA_HOME || path4.join(home, ".octocode", "freellama"), "handoffs");
25112
+ const outputs = path4.join(env.FREELLAMA_HOME || path4.join(home, ".octocode", "freellama"), "tmp");
25113
+ return [...project, tmpdir(), ...configured, handoffs, outputs].map(resolvedRoot);
25114
+ }
25115
+ function resolvedRoot(root) {
25116
+ try {
25117
+ return realpathSync2(root);
25118
+ } catch {
25119
+ const absolute = path4.resolve(root);
25120
+ const parent = path4.dirname(absolute);
25121
+ return parent === absolute ? absolute : path4.join(resolvedRoot(parent), path4.basename(absolute));
25122
+ }
25123
+ }
25124
+ function within(file, root) {
25125
+ const relative = path4.relative(root, file);
25126
+ return relative === "" || !relative.startsWith("..") && !path4.isAbsolute(relative);
25127
+ }
25128
+ function isWav(bytes) {
25129
+ return bytes.length >= 12 && bytes.subarray(0, 4).toString() === "RIFF" && bytes.subarray(8, 12).toString() === "WAVE";
25130
+ }
25131
+ function expandFileEntries(entries, cwd = process.cwd()) {
25132
+ const out = [];
25133
+ for (const entry of entries) {
25134
+ if (!/[*?[]/.test(entry)) {
25135
+ out.push(entry);
25136
+ continue;
25137
+ }
25138
+ const matches = globSync(entry, { cwd }).map((match) => path4.resolve(cwd, match)).filter((match) => statSync(match).isFile()).sort();
25139
+ if (matches.length === 0) throw new Error(`no files match ${entry} (globs resolve from ${cwd})`);
25140
+ out.push(...matches);
25141
+ if (out.length > MAX_FILES) break;
25142
+ }
25143
+ return out;
25144
+ }
25145
+ function readFiles(entries, givenRoots) {
25146
+ const roots = givenRoots.map(resolvedRoot);
25147
+ const paths = expandFileEntries(entries);
25148
+ if (paths.length > MAX_FILES) throw new Error(`files names ${paths.length} paths; at most ${MAX_FILES} per call`);
25149
+ let total = 0;
25150
+ return paths.map((name) => {
25151
+ const absolute = path4.resolve(name);
25152
+ let real;
25153
+ try {
25154
+ real = realpathSync2(absolute);
25155
+ } catch {
25156
+ throw new Error(`file not found: ${name}`);
25157
+ }
25158
+ if (!roots.some((root) => within(real, root))) {
25159
+ throw new Error(
25160
+ `file ${name} is outside the readable roots (${roots.join(", ")}); copy it into the project or temp directory, or add its directory to FREELLAMA_FILE_ROOTS`
25161
+ );
25162
+ }
25163
+ const stat3 = statSync(real);
25164
+ if (!stat3.isFile()) throw new Error(`not a regular file: ${name}`);
25165
+ const extension = path4.extname(real).toLowerCase();
25166
+ let media = AUDIO_EXTENSIONS.has(extension) ? "audio" : IMAGE_EXTENSIONS.has(extension) ? "image" : void 0;
25167
+ const limit = media ? MAX_IMAGE_FILE_BYTES : MAX_TEXT_FILE_BYTES;
25168
+ if (stat3.size > limit) {
25169
+ throw new Error(`file ${name} is ${stat3.size} bytes; the limit is ${limit} for ${media ?? "text"}; split it or send the part that matters`);
25170
+ }
25171
+ total += stat3.size;
25172
+ if (total > MAX_TOTAL_FILE_BYTES) throw new Error(`files total more than ${MAX_TOTAL_FILE_BYTES} bytes; split the call`);
25173
+ const bytes = readFileSync(real);
25174
+ if (media === "audio" && !isWav(bytes)) {
25175
+ throw new Error(`audio file ${name} must contain a WAV RIFF/WAVE header`);
25176
+ }
25177
+ if (isWav(bytes)) media = "audio";
25178
+ return { path: name, bytes, media };
25179
+ });
25180
+ }
25181
+ function shortPath(name) {
25182
+ const relative = path4.relative(process.cwd(), path4.resolve(name));
25183
+ return relative && !relative.startsWith("..") && !path4.isAbsolute(relative) ? relative : name;
25184
+ }
25185
+ function leadingComment(body) {
25186
+ const lines = [];
25187
+ for (const line2 of body.split("\n").slice(0, 20)) {
25188
+ const trimmed = line2.trim();
25189
+ if (!/^(\/\/|#(?!include)|\/\*|\*|"""|\s*$)/.test(trimmed) || trimmed.startsWith("#!")) break;
25190
+ if (trimmed) lines.push(trimmed.replace(/^(\/\/[!/]?|#+|\/\*+|\*\/?|""")\s?/, ""));
25191
+ }
25192
+ return lines.join(" ").slice(0, 400);
25193
+ }
25194
+ var MAX_AUDIO_FILE_BYTES = 256 * 1024 * 1024;
25195
+ function readAllowedFile(name, limit, roots = fileRoots()) {
25196
+ const allowed = roots.map(resolvedRoot);
25197
+ let real;
25198
+ try {
25199
+ real = realpathSync2(path4.resolve(name));
25200
+ } catch {
25201
+ throw new Error(`file not found: ${name}`);
25202
+ }
25203
+ if (!allowed.some((root) => within(real, root))) {
25204
+ throw new Error(`file ${name} is outside the readable roots (${allowed.join(", ")}); copy it into the project or temp directory, or add its directory to FREELLAMA_FILE_ROOTS`);
25205
+ }
25206
+ const stat3 = statSync(real);
25207
+ if (!stat3.isFile()) throw new Error(`not a regular file: ${name}`);
25208
+ if (stat3.size > limit) throw new Error(`file ${name} is ${stat3.size} bytes; the limit is ${limit}`);
25209
+ return readFileSync(real);
25210
+ }
25211
+ function text(file) {
25212
+ if (file.bytes.includes(0)) throw new Error(`file ${file.path} is binary; only images (${[...IMAGE_EXTENSIONS].join(", ")}) and text files can be read`);
25213
+ return file.bytes.toString("utf8");
25214
+ }
25215
+ function applyTaskFiles(payload, paths, roots = fileRoots()) {
25216
+ if (paths === void 0 || paths.length === 0) return { payload, files: [] };
25217
+ const read = readFiles(paths, roots);
25218
+ const receipts = [];
25219
+ const media = read.filter((file) => file.media !== void 0);
25220
+ const texts = read.filter((file) => file.media === void 0);
25221
+ const next = { ...payload };
25222
+ const task = payload.task ?? "completion";
25223
+ if (task === "embedding") {
25224
+ const given = payload.input === void 0 ? [] : Array.isArray(payload.input) ? payload.input : [payload.input];
25225
+ const prefix = payload.documentPrefix ?? "";
25226
+ const items = [...given];
25227
+ for (const file of read) {
25228
+ if (file.media) {
25229
+ const encoded = file.bytes.toString("base64");
25230
+ items.push(file.media === "audio" ? { audio: encoded } : { image: encoded });
25231
+ receipts.push({ path: file.path, bytes: file.bytes.length, role: "input" });
25232
+ continue;
25233
+ }
25234
+ const body = text(file);
25235
+ const header = `${shortPath(file.path)}
25236
+ ${leadingComment(body)}`.trimEnd();
25237
+ for (const chunk of chunkLines(body)) {
25238
+ items.push(`${prefix}${header}
25239
+ ${chunk.text}`);
25240
+ receipts.push({ path: file.path, bytes: Buffer.byteLength(chunk.text), role: "input", lines: chunk.lines });
25241
+ }
25242
+ }
25243
+ next.input = items;
25244
+ return { payload: next, files: receipts };
25245
+ }
25246
+ const audio = media.find((file) => file.media === "audio");
25247
+ if (audio && task !== "decision") throw new Error(`audio file ${audio.path} is read only by tasks "decision" and "embedding" (models with audio input)`);
25248
+ if (media.length > 0) {
25249
+ next.images = [...payload.images ?? [], ...media.map((file) => file.bytes.toString("base64"))];
25250
+ for (const file of media) receipts.push({ path: file.path, bytes: file.bytes.length, role: file.media });
25251
+ }
25252
+ if (task === "decision") {
25253
+ if (texts.length > 0 && payload.state !== void 0) throw new Error("give the decision state either as `state` or as text `files`, not both");
25254
+ if (texts.length === 1) {
25255
+ const body = text(texts[0]);
25256
+ next.state = jsonOrText(body);
25257
+ } else if (texts.length > 1) {
25258
+ next.state = Object.fromEntries(texts.map((file) => [file.path, text(file)]));
25259
+ }
25260
+ for (const file of texts) receipts.push({ path: file.path, bytes: file.bytes.length, role: "state" });
25261
+ return { payload: next, files: receipts };
25262
+ }
25263
+ if (texts.length > 0) {
25264
+ if (payload.prompt === void 0) throw new Error("text `files` are appended to `prompt`; give the instructions in `prompt`");
25265
+ const sections = texts.map((file) => `--- file: ${file.path} ---
25266
+ ${text(file)}`);
25267
+ next.prompt = [payload.prompt, ...sections].join("\n\n");
25268
+ for (const file of texts) receipts.push({ path: file.path, bytes: file.bytes.length, role: "prompt" });
25269
+ }
25270
+ return { payload: next, files: receipts };
25271
+ }
25272
+ function jsonOrText(body) {
25273
+ try {
25274
+ const value = JSON.parse(body);
25275
+ if (value !== null && typeof value === "object") return value;
25276
+ } catch {
25277
+ }
25278
+ return body;
25279
+ }
25280
+ function inputLabels(inputCount, files) {
25281
+ const fromFiles = files.filter((file) => file.role === "input").map((file) => file.lines ? `${shortPath(file.path)}:L${file.lines[0]}-${file.lines[1]}` : shortPath(file.path));
25282
+ const given = inputCount - fromFiles.length;
25283
+ return [...Array.from({ length: given }, (_, index) => `input[${index}]`), ...fromFiles];
25284
+ }
25285
+
25286
+ // src/handoffs.ts
25287
+ var HANDOFF_DIR = path5.join(FREELLAMA_HOME, "handoffs");
25288
+ var HANDOFF_NAME = /^[a-z0-9-]{1,60}$/;
25289
+ function summaryOf(text3, lines = 5) {
25290
+ const kept = text3.split("\n").filter((line2) => line2.trim()).slice(0, lines).join("\n");
25291
+ return kept.length > 600 ? `${kept.slice(0, 600)}\u2026` : kept;
25292
+ }
25293
+ function handoffBody(title, content) {
25294
+ return `# ${title}
25295
+
25296
+ ${content.source}, ${(/* @__PURE__ */ new Date()).toISOString()}.
25297
+
25298
+ ## Summary
25299
+
25300
+ ${content.summary.trim() || "(empty)"}
25301
+
25302
+ ## Result
25303
+
25304
+ ${content.result.trim() || "(empty)"}
25305
+ `;
25306
+ }
25307
+ function writeHandoff(name, content, dir = HANDOFF_DIR) {
25308
+ if (!HANDOFF_NAME.test(name)) throw new Error("saveAs is 1 to 60 characters of a-z, 0-9 and -");
25309
+ mkdirSync(dir, { recursive: true, mode: 448 });
25310
+ const file = path5.join(dir, `${name}.md`);
25311
+ writeFileSync(file, handoffBody(name, content), { mode: 384 });
25312
+ return file;
25313
+ }
25314
+ function resolveThroughSymlinks(file) {
25315
+ try {
25316
+ return realpathSync3(file);
25317
+ } catch {
25318
+ const parent = path5.dirname(file);
25319
+ return parent === file ? file : path5.join(resolveThroughSymlinks(parent), path5.basename(file));
25320
+ }
25321
+ }
25322
+ function checkOutputPath(name, roots = fileRoots()) {
25323
+ const given = path5.resolve(name);
25324
+ const file = resolveThroughSymlinks(given);
25325
+ if ([given, file].some((candidate) => candidate.split(path5.sep).includes(".git"))) {
25326
+ throw new Error(`outputPath ${name} is inside a .git directory; FreeLlama does not write there`);
25327
+ }
25328
+ if (!roots.some((root) => within(file, root))) {
25329
+ throw new Error(`outputPath ${name} is outside the writable roots (${roots.join(", ")}); use a path inside the project or temp directory, or add its directory to FREELLAMA_FILE_ROOTS`);
25330
+ }
25331
+ let exists2 = true;
25332
+ try {
25333
+ lstatSync(file);
25334
+ } catch {
25335
+ exists2 = false;
25336
+ }
25337
+ if (exists2 && !statSync2(file).isFile()) throw new Error(`outputPath ${name} exists and is not a regular file`);
25338
+ return file;
25339
+ }
25340
+ function writeOutputFile(name, content, roots = fileRoots()) {
25341
+ const file = checkOutputPath(name, roots);
25342
+ mkdirSync(path5.dirname(file), { recursive: true, mode: 448 });
25343
+ writeFileSync(file, handoffBody(path5.basename(file), content), { mode: 384 });
25344
+ return file;
25345
+ }
25346
+ var OUTPUT_TMP_DIR = path5.join(FREELLAMA_HOME, "tmp");
25347
+ var DAY_MS2 = 864e5;
25348
+ function outputTtlDays(env = process.env) {
25349
+ const configured = Number(env.FREELLAMA_RESPONSE_TTL_DAYS ?? 7);
25350
+ return Number.isFinite(configured) && configured >= 0 ? configured : 7;
25351
+ }
25352
+ function pruneOutputDir(dir, now, ttlDays) {
25353
+ if (ttlDays <= 0) return;
25354
+ let names;
25355
+ try {
25356
+ names = readdirSync(dir);
25357
+ } catch {
25358
+ return;
25359
+ }
25360
+ for (const name of names.filter((entry) => entry.endsWith(".md"))) {
25361
+ const file = path5.join(dir, name);
25362
+ try {
25363
+ if (statSync2(file).mtimeMs < now - ttlDays * DAY_MS2) rmSync(file);
25364
+ } catch {
25365
+ }
25366
+ }
25367
+ }
25368
+ var outputCounter = 0;
25369
+ function writeDefaultOutput(content, dir = OUTPUT_TMP_DIR, now = /* @__PURE__ */ new Date(), ttlDays = outputTtlDays()) {
25370
+ mkdirSync(dir, { recursive: true, mode: 448 });
25371
+ pruneOutputDir(dir, now.getTime(), ttlDays);
25372
+ outputCounter += 1;
25373
+ const file = path5.join(dir, `${now.toISOString().replace(/[:.]/g, "-")}-${process.pid}-${outputCounter}.md`);
25374
+ writeFileSync(file, handoffBody(path5.basename(file), content), { mode: 384, flag: "wx" });
25375
+ return file;
25376
+ }
25377
+
25060
25378
  // src/tool-results.ts
25061
25379
  var nsToMs = (value) => typeof value === "number" ? Math.round(value / 1e6) : void 0;
25062
25380
  var object4 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
@@ -25113,8 +25431,6 @@ function compactReceipt(receipt) {
25113
25431
  "selected_model",
25114
25432
  "response",
25115
25433
  "output_check",
25116
- "metrics",
25117
- "telemetry",
25118
25434
  "scope",
25119
25435
  "warm",
25120
25436
  "job",
@@ -25131,13 +25447,15 @@ function compactReceipt(receipt) {
25131
25447
  }
25132
25448
  const response = withoutThinking(object4(receipt.response));
25133
25449
  if (response) compact.response = response;
25450
+ const metrics = object4(receipt.metrics);
25451
+ if (metrics) compact.metrics = Object.fromEntries(Object.entries(metrics).filter(([, value]) => value === null || typeof value !== "object"));
25134
25452
  const route3 = pick2(object4(receipt.route), ["selected_model", "task", "confidence", "reasons", "output_reserve_tokens"]);
25135
25453
  if (route3) compact.route = route3;
25136
25454
  const admission = pick2(object4(receipt.admission), ["mode", "queue_wait_ms", "transition_wait_ms", "resource_wait_ms", "total_wait_ms"]);
25137
25455
  if (admission) compact.admission = admission;
25138
25456
  if (execution) {
25139
25457
  compact.execution = {
25140
- ...pick2(execution, ["placement", "backend", "reason", "observation", "lifecycle", "context_sizing", "keep_alive", "preference_satisfied"]),
25458
+ ...pick2(execution, ["placement", "backend", "reason", "observation", "keep_alive", "preference_satisfied"]),
25141
25459
  ...completionPlanning ? { completion_planning: completionPlanning } : {},
25142
25460
  ...reservation ? { memory_reservation: reservation } : {}
25143
25461
  };
@@ -25178,8 +25496,19 @@ function compactResearchResult(content) {
25178
25496
  return { shaped, sections };
25179
25497
  }
25180
25498
  var UNSAVED_TOOLS = /* @__PURE__ */ new Set(["read_response"]);
25499
+ function finishedJob(tool, args, content) {
25500
+ if (tool !== "task_jobs" || args?.action !== "get") return void 0;
25501
+ const job = object4(content.job);
25502
+ const result = object4(job?.result);
25503
+ return job && result ? { job, result } : void 0;
25504
+ }
25181
25505
  function shapeResult(tool, args, content, isError, saved) {
25182
25506
  const full = args?.receipt === "full" || args?.preview === true || !saved;
25507
+ const finished = finishedJob(tool, args, content);
25508
+ if (finished) {
25509
+ if (full || args?.returnEmbeddings === true) return { shaped: content, sections: [] };
25510
+ return { shaped: { ...content, job: { ...finished.job, result: compactTaskResult(finished.result) } }, sections: ["receipt"] };
25511
+ }
25183
25512
  if (tool === "delegate_research") return full ? { shaped: content, sections: [] } : compactResearchResult(content);
25184
25513
  if (tool !== "run_task" || isError || content.deferred === true) return { shaped: content, sections: [] };
25185
25514
  let shaped = full ? content : compactTaskResult(content);
@@ -25194,6 +25523,72 @@ function shapeResult(tool, args, content, isError, saved) {
25194
25523
  }
25195
25524
  return { shaped, sections };
25196
25525
  }
25526
+ var AUTO_INLINE_CHARS = 8e3;
25527
+ var outputInput = {
25528
+ output: _enum(["auto", "inline", "file"]).optional().describe("auto (default): inline up to 8,000 chars, else a summary plus check lines and where to read the rest; inline: always whole; file: always the summary view"),
25529
+ outputPath: string2().min(1).optional().describe("also write the answer as Markdown here, creating or replacing it (inside the readable roots); default when shortened: FreeLlama home tmp/<timestamp>.md")
25530
+ };
25531
+ var SAVED_TEXT_SECTION = "/result/content/0/text";
25532
+ var KEPT_LINE = /^(check|handoff_file|verification|cited): /;
25533
+ var LABEL = /^\[[^\]\n]*\] /;
25534
+ function shortView(text3, where) {
25535
+ const label = LABEL.exec(text3)?.[0].trim();
25536
+ const lines = text3.replace(LABEL, "").split("\n");
25537
+ const kept = lines.filter((line2) => KEPT_LINE.test(line2));
25538
+ const summary = summaryOf(lines.filter((line2) => !KEPT_LINE.test(line2)).join("\n"));
25539
+ return [label, summary, ...kept, where].filter((part) => part !== void 0 && part !== "").join("\n");
25540
+ }
25541
+ function viewText(args, text3, responseFile) {
25542
+ const mode = typeof args?.output === "string" ? args.output : "auto";
25543
+ const shorten = mode === "file" || mode === "auto" && text3.length > AUTO_INLINE_CHARS;
25544
+ const answer = text3.replace(LABEL, "");
25545
+ const content = { source: "From FreeLlama", summary: summaryOf(answer), result: answer };
25546
+ let written;
25547
+ const notes = [];
25548
+ try {
25549
+ if (typeof args?.outputPath === "string") written = writeOutputFile(args.outputPath, content);
25550
+ else if (shorten) written = writeDefaultOutput(content);
25551
+ } catch (error2) {
25552
+ notes.push(`output_file not written: ${error2 instanceof Error ? error2.message : String(error2)}`);
25553
+ }
25554
+ const file = written ?? (shorten ? responseFile : void 0);
25555
+ const tail = notes.length > 0 ? `
25556
+ ${notes.join("\n")}` : "";
25557
+ if (!shorten) {
25558
+ return { text: `${text3}${written ? `
25559
+ output_file: ${written}` : ""}${tail}`, ...written ? { output_file: written } : {} };
25560
+ }
25561
+ if (!file) return { text: `${text3}${tail}
25562
+ not shortened (${text3.length} chars): no output file could be written and the response store is off, so this text is the only copy` };
25563
+ const where = written ? `output_file: ${written} (the full answer, ${text3.length} chars)` : `full answer (${text3.length} chars): read_response{file:"${responseFile}", section:"${SAVED_TEXT_SECTION}"}`;
25564
+ return {
25565
+ text: `${shortView(text3, where)}${tail}`,
25566
+ output: { mode, chars: text3.length, file },
25567
+ ...written ? { output_file: written } : {},
25568
+ where,
25569
+ answer: text3
25570
+ };
25571
+ }
25572
+ var MIN_COPY_CHARS = 400;
25573
+ function withoutAnswerCopies(value, answer, pointer2) {
25574
+ if (typeof value === "string") return value.length >= MIN_COPY_CHARS && answer.includes(value.trim()) ? pointer2 : value;
25575
+ if (Array.isArray(value)) return value.map((item) => withoutAnswerCopies(item, answer, pointer2));
25576
+ const fields = object4(value);
25577
+ return fields ? Object.fromEntries(Object.entries(fields).map(([key, item]) => [key, withoutAnswerCopies(item, answer, pointer2)])) : value;
25578
+ }
25579
+ function reshapedContent(result, args, responseFile) {
25580
+ const parts = result.content;
25581
+ const first = Array.isArray(parts) ? object4(parts[0]) : void 0;
25582
+ if (!Array.isArray(parts) || first?.type !== "text" || typeof first.text !== "string") return void 0;
25583
+ const view = viewText(args, first.text, responseFile);
25584
+ return { view, content: [{ ...first, text: view.text }, ...parts.slice(1)] };
25585
+ }
25586
+ function hasAnswerText(tool, args, content, isError) {
25587
+ if (isError) return false;
25588
+ if (finishedJob(tool, args, content)) return true;
25589
+ if (tool !== "run_task" && tool !== "delegate_research") return false;
25590
+ return args?.preview !== true && args?.defer !== true && content.deferred !== true;
25591
+ }
25197
25592
  function resultModel(content) {
25198
25593
  const model = content?.selected_model ?? object4(content?.route)?.selected_model;
25199
25594
  return typeof model === "string" && model ? model : void 0;
@@ -25206,13 +25601,19 @@ async function finishResult(tool, args, result, elapsedMs, startedAt, store2 = r
25206
25601
  return file ? { ...timed, _meta: { ...timed._meta, "freellama/response_file": file } } : timed;
25207
25602
  }
25208
25603
  const { shaped, sections } = shapeResult(tool, object4(args), content, timed.isError === true, file !== void 0);
25604
+ const reshaped = hasAnswerText(tool, object4(args), content, timed.isError === true) ? reshapedContent(timed, object4(args), file) : void 0;
25605
+ const { where, answer } = reshaped?.view ?? {};
25606
+ const lean = where !== void 0 && answer !== void 0 ? withoutAnswerCopies(shaped, answer, `answer moved: ${where}`) : shaped;
25209
25607
  return {
25210
25608
  ...timed,
25609
+ ...reshaped ? { content: reshaped.content } : {},
25211
25610
  structuredContent: {
25212
- ...shaped,
25611
+ ...lean,
25213
25612
  timing: content.timing,
25214
25613
  ...file ? { response_file: file } : {},
25215
- ...file && sections.length > 0 ? { sections } : {}
25614
+ ...file && sections.length > 0 ? { sections } : {},
25615
+ ...reshaped?.view.output ? { output: reshaped.view.output } : {},
25616
+ ...reshaped?.view.output_file ? { output_file: reshaped.view.output_file } : {}
25216
25617
  }
25217
25618
  };
25218
25619
  }
@@ -25267,16 +25668,16 @@ function taskNotices(content) {
25267
25668
  }
25268
25669
 
25269
25670
  // src/docs-resources.ts
25270
- import { existsSync as existsSync3, readdirSync } from "node:fs";
25671
+ import { existsSync as existsSync3, readdirSync as readdirSync2 } from "node:fs";
25271
25672
  import { readFile as readFile2 } from "node:fs/promises";
25272
- import path4 from "node:path";
25673
+ import path6 from "node:path";
25273
25674
  import { fileURLToPath as fileURLToPath3 } from "node:url";
25274
25675
  function registerDocsResources(server2) {
25275
- const PACKAGED_DOCS_DIR = path4.resolve(path4.dirname(fileURLToPath3(import.meta.url)), "..", "docs");
25276
- if (!existsSync3(path4.join(PACKAGED_DOCS_DIR, "README.md"))) {
25676
+ const PACKAGED_DOCS_DIR = path6.resolve(path6.dirname(fileURLToPath3(import.meta.url)), "..", "docs");
25677
+ if (!existsSync3(path6.join(PACKAGED_DOCS_DIR, "README.md"))) {
25277
25678
  throw new Error(`FreeLlama MCP documentation index is missing at ${PACKAGED_DOCS_DIR}/README.md; run the package build.`);
25278
25679
  }
25279
- const packagedDocs = readdirSync(PACKAGED_DOCS_DIR, { recursive: true, encoding: "utf8" }).map((name) => name.split(path4.sep).join("/")).filter((name) => name.endsWith(".md")).sort();
25680
+ const packagedDocs = readdirSync2(PACKAGED_DOCS_DIR, { recursive: true, encoding: "utf8" }).map((name) => name.split(path6.sep).join("/")).filter((name) => name.endsWith(".md")).sort();
25280
25681
  for (const name of packagedDocs) {
25281
25682
  const isIndex = name === "README.md";
25282
25683
  const slug = isIndex ? "index" : name.replace(/\.md$/, "");
@@ -25289,14 +25690,14 @@ function registerDocsResources(server2) {
25289
25690
  description: isIndex ? "Index of packaged FreeLlama operator and agent documentation. Read this first, then fetch one relevant document." : `Packaged FreeLlama documentation: ${name}.`
25290
25691
  },
25291
25692
  async () => ({
25292
- contents: [{ uri, mimeType: "text/markdown", text: await readFile2(path4.join(PACKAGED_DOCS_DIR, name), "utf8") }]
25693
+ contents: [{ uri, mimeType: "text/markdown", text: await readFile2(path6.join(PACKAGED_DOCS_DIR, name), "utf8") }]
25293
25694
  })
25294
25695
  );
25295
25696
  }
25296
25697
  }
25297
25698
 
25298
25699
  // src/builtin-prompts.ts
25299
- import path6 from "node:path";
25700
+ import path7 from "node:path";
25300
25701
 
25301
25702
  // ../rust-core/src/model_catalog.json
25302
25703
  var model_catalog_default = {
@@ -25800,235 +26201,15 @@ Verify: local answers are candidates. Each run_task result ends with check: line
25800
26201
 
25801
26202
  Coding: the answer is SEARCH/REPLACE blocks. ${APPLY_EDITS}
25802
26203
 
25803
- Hand a result on: saveAs:"<name>" writes the answer to handoffs/<name>.md and returns handoff_file; a later run_task reads it with files:[handoff_file].
26204
+ Hand a result on: saveAs:"<name>" writes the answer to handoffs/<name>.md and returns handoff_file; a later run_task reads it with files:[handoff_file]. Answers over 8,000 characters come back as a summary with check: lines and an output_file holding the whole answer; output:"inline" returns the whole text, outputPath:"<path>" writes (or replaces) that file inside your project.
25804
26205
 
25805
26206
  Approval: pull a model only after the user approves one exact tag and its size (ollama_pull approvedSizeBytes). Unload only a model you warmed or the user named; delete only a tag the user names. A memory refusal names what holds memory: tell the user and never stop their processes.`;
25806
26207
 
25807
- // src/task-files.ts
25808
- import { globSync, realpathSync as realpathSync2, statSync, readFileSync } from "node:fs";
25809
- import { homedir as homedir2, tmpdir } from "node:os";
25810
- import path5 from "node:path";
25811
- var MAX_TEXT_FILE_BYTES = 4 * 1024 * 1024;
25812
- var MAX_IMAGE_FILE_BYTES = 8 * 1024 * 1024;
25813
- var MAX_TOTAL_FILE_BYTES = 32 * 1024 * 1024;
25814
- var MAX_FILES = 2e3;
25815
- var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]);
25816
- var AUDIO_EXTENSIONS = /* @__PURE__ */ new Set([".wav"]);
25817
- var EMBEDDING_CHUNK_BYTES = 1536;
25818
- var CHUNK_OVERLAP_LINES = 8;
25819
- function chunkLines(text3, limit = EMBEDDING_CHUNK_BYTES) {
25820
- const lines = text3.split("\n");
25821
- const chunks = [];
25822
- let current = [];
25823
- let bytes = 0;
25824
- let start2 = 1;
25825
- const flush = (end, overlap = 0) => {
25826
- if (current.length > 0 && current.join("\n").trim()) chunks.push({ text: current.join("\n"), lines: [start2, end] });
25827
- const carried = overlap > 0 ? current.slice(-overlap) : [];
25828
- const carriedBytes = carried.reduce((sum, line2) => sum + Buffer.byteLength(line2) + 1, 0);
25829
- current = carriedBytes < limit / 4 ? carried : [];
25830
- bytes = current === carried ? carriedBytes : 0;
25831
- start2 = end + 1 - current.length;
25832
- };
25833
- lines.forEach((line2, index) => {
25834
- const size2 = Buffer.byteLength(line2) + 1;
25835
- if (bytes + size2 > limit && current.length > 0) flush(index, CHUNK_OVERLAP_LINES);
25836
- if (size2 > limit) {
25837
- for (let at = 0; at < line2.length; at += Math.floor(limit / 2)) {
25838
- chunks.push({ text: line2.slice(at, at + Math.floor(limit / 2)), lines: [index + 1, index + 1] });
25839
- }
25840
- start2 = index + 2;
25841
- return;
25842
- }
25843
- current.push(line2);
25844
- bytes += size2;
25845
- });
25846
- flush(lines.length);
25847
- return chunks;
25848
- }
25849
- function fileRoots(env = process.env, cwd = process.cwd(), home = homedir2()) {
25850
- const configured = (env.FREELLAMA_FILE_ROOTS ?? "").split(path5.delimiter).filter(Boolean);
25851
- const broad = /* @__PURE__ */ new Set([path5.parse(path5.resolve(cwd)).root, resolvedRoot(home)]);
25852
- const project = broad.has(resolvedRoot(cwd)) ? [] : [cwd];
25853
- const handoffs = path5.join(env.FREELLAMA_HOME || path5.join(home, ".octocode", "freellama"), "handoffs");
25854
- return [...project, tmpdir(), ...configured, handoffs].map(resolvedRoot);
25855
- }
25856
- function resolvedRoot(root) {
25857
- try {
25858
- return realpathSync2(root);
25859
- } catch {
25860
- const absolute = path5.resolve(root);
25861
- const parent = path5.dirname(absolute);
25862
- return parent === absolute ? absolute : path5.join(resolvedRoot(parent), path5.basename(absolute));
25863
- }
25864
- }
25865
- function within(file, root) {
25866
- const relative = path5.relative(root, file);
25867
- return relative === "" || !relative.startsWith("..") && !path5.isAbsolute(relative);
25868
- }
25869
- function isWav(bytes) {
25870
- return bytes.length >= 12 && bytes.subarray(0, 4).toString() === "RIFF" && bytes.subarray(8, 12).toString() === "WAVE";
25871
- }
25872
- function expandFileEntries(entries, cwd = process.cwd()) {
25873
- const out = [];
25874
- for (const entry of entries) {
25875
- if (!/[*?[]/.test(entry)) {
25876
- out.push(entry);
25877
- continue;
25878
- }
25879
- const matches = globSync(entry, { cwd }).map((match) => path5.resolve(cwd, match)).filter((match) => statSync(match).isFile()).sort();
25880
- if (matches.length === 0) throw new Error(`no files match ${entry} (globs resolve from ${cwd})`);
25881
- out.push(...matches);
25882
- if (out.length > MAX_FILES) break;
25883
- }
25884
- return out;
25885
- }
25886
- function readFiles(entries, givenRoots) {
25887
- const roots = givenRoots.map(resolvedRoot);
25888
- const paths = expandFileEntries(entries);
25889
- if (paths.length > MAX_FILES) throw new Error(`files names ${paths.length} paths; at most ${MAX_FILES} per call`);
25890
- let total = 0;
25891
- return paths.map((name) => {
25892
- const absolute = path5.resolve(name);
25893
- let real;
25894
- try {
25895
- real = realpathSync2(absolute);
25896
- } catch {
25897
- throw new Error(`file not found: ${name}`);
25898
- }
25899
- if (!roots.some((root) => within(real, root))) {
25900
- throw new Error(
25901
- `file ${name} is outside the readable roots (${roots.join(", ")}); copy it into the project or temp directory, or add its directory to FREELLAMA_FILE_ROOTS`
25902
- );
25903
- }
25904
- const stat3 = statSync(real);
25905
- if (!stat3.isFile()) throw new Error(`not a regular file: ${name}`);
25906
- const extension = path5.extname(real).toLowerCase();
25907
- let media = AUDIO_EXTENSIONS.has(extension) ? "audio" : IMAGE_EXTENSIONS.has(extension) ? "image" : void 0;
25908
- const limit = media ? MAX_IMAGE_FILE_BYTES : MAX_TEXT_FILE_BYTES;
25909
- if (stat3.size > limit) {
25910
- throw new Error(`file ${name} is ${stat3.size} bytes; the limit is ${limit} for ${media ?? "text"}; split it or send the part that matters`);
25911
- }
25912
- total += stat3.size;
25913
- if (total > MAX_TOTAL_FILE_BYTES) throw new Error(`files total more than ${MAX_TOTAL_FILE_BYTES} bytes; split the call`);
25914
- const bytes = readFileSync(real);
25915
- if (media === "audio" && !isWav(bytes)) {
25916
- throw new Error(`audio file ${name} must contain a WAV RIFF/WAVE header`);
25917
- }
25918
- if (isWav(bytes)) media = "audio";
25919
- return { path: name, bytes, media };
25920
- });
25921
- }
25922
- function shortPath(name) {
25923
- const relative = path5.relative(process.cwd(), path5.resolve(name));
25924
- return relative && !relative.startsWith("..") && !path5.isAbsolute(relative) ? relative : name;
25925
- }
25926
- function leadingComment(body) {
25927
- const lines = [];
25928
- for (const line2 of body.split("\n").slice(0, 20)) {
25929
- const trimmed = line2.trim();
25930
- if (!/^(\/\/|#(?!include)|\/\*|\*|"""|\s*$)/.test(trimmed) || trimmed.startsWith("#!")) break;
25931
- if (trimmed) lines.push(trimmed.replace(/^(\/\/[!/]?|#+|\/\*+|\*\/?|""")\s?/, ""));
25932
- }
25933
- return lines.join(" ").slice(0, 400);
25934
- }
25935
- var MAX_AUDIO_FILE_BYTES = 256 * 1024 * 1024;
25936
- function readAllowedFile(name, limit, roots = fileRoots()) {
25937
- const allowed = roots.map(resolvedRoot);
25938
- let real;
25939
- try {
25940
- real = realpathSync2(path5.resolve(name));
25941
- } catch {
25942
- throw new Error(`file not found: ${name}`);
25943
- }
25944
- if (!allowed.some((root) => within(real, root))) {
25945
- throw new Error(`file ${name} is outside the readable roots (${allowed.join(", ")}); copy it into the project or temp directory, or add its directory to FREELLAMA_FILE_ROOTS`);
25946
- }
25947
- const stat3 = statSync(real);
25948
- if (!stat3.isFile()) throw new Error(`not a regular file: ${name}`);
25949
- if (stat3.size > limit) throw new Error(`file ${name} is ${stat3.size} bytes; the limit is ${limit}`);
25950
- return readFileSync(real);
25951
- }
25952
- function text(file) {
25953
- if (file.bytes.includes(0)) throw new Error(`file ${file.path} is binary; only images (${[...IMAGE_EXTENSIONS].join(", ")}) and text files can be read`);
25954
- return file.bytes.toString("utf8");
25955
- }
25956
- function applyTaskFiles(payload, paths, roots = fileRoots()) {
25957
- if (paths === void 0 || paths.length === 0) return { payload, files: [] };
25958
- const read = readFiles(paths, roots);
25959
- const receipts = [];
25960
- const media = read.filter((file) => file.media !== void 0);
25961
- const texts = read.filter((file) => file.media === void 0);
25962
- const next = { ...payload };
25963
- const task = payload.task ?? "completion";
25964
- if (task === "embedding") {
25965
- const given = payload.input === void 0 ? [] : Array.isArray(payload.input) ? payload.input : [payload.input];
25966
- const prefix = payload.documentPrefix ?? "";
25967
- const items = [...given];
25968
- for (const file of read) {
25969
- if (file.media) {
25970
- const encoded = file.bytes.toString("base64");
25971
- items.push(file.media === "audio" ? { audio: encoded } : { image: encoded });
25972
- receipts.push({ path: file.path, bytes: file.bytes.length, role: "input" });
25973
- continue;
25974
- }
25975
- const body = text(file);
25976
- const header = `${shortPath(file.path)}
25977
- ${leadingComment(body)}`.trimEnd();
25978
- for (const chunk of chunkLines(body)) {
25979
- items.push(`${prefix}${header}
25980
- ${chunk.text}`);
25981
- receipts.push({ path: file.path, bytes: Buffer.byteLength(chunk.text), role: "input", lines: chunk.lines });
25982
- }
25983
- }
25984
- next.input = items;
25985
- return { payload: next, files: receipts };
25986
- }
25987
- const audio = media.find((file) => file.media === "audio");
25988
- if (audio && task !== "decision") throw new Error(`audio file ${audio.path} is read only by tasks "decision" and "embedding" (models with audio input)`);
25989
- if (media.length > 0) {
25990
- next.images = [...payload.images ?? [], ...media.map((file) => file.bytes.toString("base64"))];
25991
- for (const file of media) receipts.push({ path: file.path, bytes: file.bytes.length, role: file.media });
25992
- }
25993
- if (task === "decision") {
25994
- if (texts.length > 0 && payload.state !== void 0) throw new Error("give the decision state either as `state` or as text `files`, not both");
25995
- if (texts.length === 1) {
25996
- const body = text(texts[0]);
25997
- next.state = jsonOrText(body);
25998
- } else if (texts.length > 1) {
25999
- next.state = Object.fromEntries(texts.map((file) => [file.path, text(file)]));
26000
- }
26001
- for (const file of texts) receipts.push({ path: file.path, bytes: file.bytes.length, role: "state" });
26002
- return { payload: next, files: receipts };
26003
- }
26004
- if (texts.length > 0) {
26005
- if (payload.prompt === void 0) throw new Error("text `files` are appended to `prompt`; give the instructions in `prompt`");
26006
- const sections = texts.map((file) => `--- file: ${file.path} ---
26007
- ${text(file)}`);
26008
- next.prompt = [payload.prompt, ...sections].join("\n\n");
26009
- for (const file of texts) receipts.push({ path: file.path, bytes: file.bytes.length, role: "prompt" });
26010
- }
26011
- return { payload: next, files: receipts };
26012
- }
26013
- function jsonOrText(body) {
26014
- try {
26015
- const value = JSON.parse(body);
26016
- if (value !== null && typeof value === "object") return value;
26017
- } catch {
26018
- }
26019
- return body;
26020
- }
26021
- function inputLabels(inputCount, files) {
26022
- const fromFiles = files.filter((file) => file.role === "input").map((file) => file.lines ? `${shortPath(file.path)}:L${file.lines[0]}-${file.lines[1]}` : shortPath(file.path));
26023
- const given = inputCount - fromFiles.length;
26024
- return [...Array.from({ length: given }, (_, index) => `input[${index}]`), ...fromFiles];
26025
- }
26026
-
26027
26208
  // src/builtin-prompts.ts
26028
26209
  var list = (value) => value.split(/[,\n]/).map((entry) => entry.trim()).filter(Boolean);
26029
26210
  function short(file) {
26030
- const relative = path6.relative(process.cwd(), path6.resolve(file));
26031
- return relative && !relative.startsWith("..") && !path6.isAbsolute(relative) ? relative : file;
26211
+ const relative = path7.relative(process.cwd(), path7.resolve(file));
26212
+ return relative && !relative.startsWith("..") && !path7.isAbsolute(relative) ? relative : file;
26032
26213
  }
26033
26214
  function perFile(files, shared, item = {}) {
26034
26215
  let paths = list(files);
@@ -26236,7 +26417,7 @@ function installedModelsText(data) {
26236
26417
  ].filter(Boolean).join(" ")
26237
26418
  );
26238
26419
  }
26239
- lines.push('Context, policy rank and benchmark per model: structuredContent; one model: models{view:"detail", model}.');
26420
+ lines.push('Context, policy rank and benchmark per model: models{view:"installed", includeVerbose:true}; one model: models{view:"detail", model}.');
26240
26421
  return lines.join("\n");
26241
26422
  }
26242
26423
  function residentModelsText(data) {
@@ -27154,6 +27335,32 @@ var modelsResultSchema = object2({
27154
27335
  knowledge: unknown().optional(),
27155
27336
  library: unknown().optional()
27156
27337
  }).loose();
27338
+ var SLIM_MODEL_KEYS = [
27339
+ "name",
27340
+ "model",
27341
+ "size",
27342
+ "capabilities",
27343
+ "resident",
27344
+ "resident_size",
27345
+ "resident_vram",
27346
+ "resident_context",
27347
+ "advertised_context",
27348
+ "placement"
27349
+ ];
27350
+ function slimModel(entry, includeKnowledge) {
27351
+ const slim = {};
27352
+ for (const key of SLIM_MODEL_KEYS) if (entry[key] !== void 0) slim[key] = entry[key];
27353
+ const execution = entry.execution;
27354
+ if (execution && typeof execution === "object") {
27355
+ slim.execution = {
27356
+ ...execution.placement !== void 0 ? { placement: execution.placement } : {},
27357
+ ...execution.upstream !== void 0 ? { upstream: execution.upstream } : {},
27358
+ ...execution.observation !== void 0 ? { observation: execution.observation } : {}
27359
+ };
27360
+ }
27361
+ if (includeKnowledge && entry.knowledge !== void 0) slim.knowledge = entry.knowledge;
27362
+ return slim;
27363
+ }
27157
27364
  var HOST_MEMORY_FIT_FRACTION = 0.6;
27158
27365
  var HUB_TASKS = /* @__PURE__ */ new Map([
27159
27366
  ["embedding", ["feature-extraction", "sentence-similarity"]],
@@ -27382,8 +27589,8 @@ function registerModelsTool(server2) {
27382
27589
  if (selectedView !== "library" && selectedView !== "raw" && [limit, cursor].some((value) => value !== void 0)) {
27383
27590
  return errorResult(new Error(`\`limit\` and \`cursor\` apply only to views "library" and "raw", not "${selectedView}".`));
27384
27591
  }
27385
- if (selectedView !== "detail" && includeVerbose !== void 0) {
27386
- return errorResult(new Error('`includeVerbose` is valid only for view "detail".'));
27592
+ if (selectedView !== "detail" && selectedView !== "installed" && selectedView !== "resident" && includeVerbose !== void 0) {
27593
+ return errorResult(new Error('`includeVerbose` is valid only for views "installed", "resident" and "detail".'));
27387
27594
  }
27388
27595
  if (selectedView !== "detail" && selectedView !== "library" && model !== void 0) {
27389
27596
  return errorResult(new Error('`model` is valid only for views "detail" and "library".'));
@@ -27417,7 +27624,8 @@ function registerModelsTool(server2) {
27417
27624
  }
27418
27625
  };
27419
27626
  });
27420
- return structuredResult({ ...data, models }, { text: residentModelsText({ models }) });
27627
+ const shown2 = includeVerbose ? models : models.map((entry) => slimModel(entry, false));
27628
+ return structuredResult({ ...data, models: shown2 }, { text: residentModelsText({ models }) });
27421
27629
  }
27422
27630
  case "detail": {
27423
27631
  if (!model) {
@@ -27463,7 +27671,8 @@ function registerModelsTool(server2) {
27463
27671
  includeLibrary ?? false,
27464
27672
  async (upstream) => (await ollamaFetch(upstream ?? ollamaEndpoint, "/api/tags")).models ?? []
27465
27673
  ) };
27466
- return structuredResult(installed, { text: installedModelsText(installed) });
27674
+ const shown2 = includeVerbose ? installed : { ...installed, models: installed.models.map((entry) => slimModel(entry, includeLibrary === true)) };
27675
+ return structuredResult(shown2, { text: installedModelsText(installed) });
27467
27676
  }
27468
27677
  }
27469
27678
  } catch (error2) {
@@ -27721,36 +27930,6 @@ function mergeProfiles(job, profiles) {
27721
27930
  return { job, model: models.length === 1 ? models[0] : models.join(", ") || void 0, applied: [...new Set(profiles.flatMap((profile) => profile.applied))] };
27722
27931
  }
27723
27932
 
27724
- // src/handoffs.ts
27725
- init_config();
27726
- import { mkdirSync, writeFileSync } from "node:fs";
27727
- import path7 from "node:path";
27728
- var HANDOFF_DIR = path7.join(FREELLAMA_HOME, "handoffs");
27729
- var HANDOFF_NAME = /^[a-z0-9-]{1,60}$/;
27730
- function summaryOf(text3, lines = 5) {
27731
- const kept = text3.split("\n").filter((line2) => line2.trim()).slice(0, lines).join("\n");
27732
- return kept.length > 600 ? `${kept.slice(0, 600)}\u2026` : kept;
27733
- }
27734
- function writeHandoff(name, content, dir = HANDOFF_DIR) {
27735
- if (!HANDOFF_NAME.test(name)) throw new Error("saveAs is 1 to 60 characters of a-z, 0-9 and -");
27736
- mkdirSync(dir, { recursive: true, mode: 448 });
27737
- const file = path7.join(dir, `${name}.md`);
27738
- const body = `# ${name}
27739
-
27740
- ${content.source}, ${(/* @__PURE__ */ new Date()).toISOString()}.
27741
-
27742
- ## Summary
27743
-
27744
- ${content.summary.trim() || "(empty)"}
27745
-
27746
- ## Result
27747
-
27748
- ${content.result.trim() || "(empty)"}
27749
- `;
27750
- writeFileSync(file, body, { mode: 384 });
27751
- return file;
27752
- }
27753
-
27754
27933
  // src/result-checks.ts
27755
27934
  import { readFileSync as readFileSync2 } from "node:fs";
27756
27935
  var object5 = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : void 0;
@@ -28469,6 +28648,7 @@ function registerRunTaskTool(server2) {
28469
28648
  receipt: _enum(["compact", "full"]).optional().describe("compact (default): full receipt at response_file; full: inline"),
28470
28649
  defer: boolean2().optional(),
28471
28650
  saveAs: string2().regex(HANDOFF_NAME).optional().describe("answer to handoffs/<name>.md, returned as handoff_file"),
28651
+ ...outputInput,
28472
28652
  returnEmbeddings: boolean2().optional(),
28473
28653
  nearest: object2({
28474
28654
  query: string2().min(1).optional(),
@@ -28491,6 +28671,7 @@ function registerRunTaskTool(server2) {
28491
28671
  job,
28492
28672
  workflow,
28493
28673
  saveAs,
28674
+ outputPath,
28494
28675
  objective,
28495
28676
  model,
28496
28677
  sessionId,
@@ -28528,6 +28709,10 @@ function registerRunTaskTool(server2) {
28528
28709
  maxParallelism
28529
28710
  }, extra) => {
28530
28711
  try {
28712
+ if (outputPath !== void 0) {
28713
+ if (preview || defer) return errorResult(new Error("`outputPath` saves a finished answer; it is not valid with `preview` or `defer`."));
28714
+ checkOutputPath(outputPath);
28715
+ }
28531
28716
  if (workflow !== void 0) {
28532
28717
  const flowJob = WORKFLOWS[workflow].job;
28533
28718
  if (job !== void 0 && job !== flowJob) return errorResult(new Error(`workflow "${workflow}" runs job "${flowJob}"; remove \`job\`.`));
@@ -28832,7 +29017,8 @@ function registerTaskJobsTool(server2) {
28832
29017
  endpoint: endpointParam,
28833
29018
  action: _enum(["list", "get", "cancel", "remove"]),
28834
29019
  jobId: string2().uuid().optional(),
28835
- returnEmbeddings: boolean2().optional()
29020
+ returnEmbeddings: boolean2().optional(),
29021
+ output: outputInput.output.describe("get only; as run_task")
28836
29022
  }).strict(),
28837
29023
  outputSchema: objectResultSchema,
28838
29024
  // One tool for reads and changes, so not read-only (the spec default, not restated). Not destructive either: cancel and remove
@@ -28840,13 +29026,13 @@ function registerTaskJobsTool(server2) {
28840
29026
  // outside FreeLlama, and a job's input can be submitted again.
28841
29027
  annotations: { destructiveHint: false }
28842
29028
  },
28843
- async ({ endpoint, action, jobId, returnEmbeddings }) => {
29029
+ async ({ endpoint, action, jobId, returnEmbeddings, output }) => {
28844
29030
  try {
28845
29031
  if (action === "list" ? jobId !== void 0 : jobId === void 0) {
28846
29032
  return errorResult(new Error("Use jobId only with get/cancel/remove; all require a jobId."));
28847
29033
  }
28848
- if (returnEmbeddings !== void 0 && action !== "get") {
28849
- return errorResult(new Error("returnEmbeddings is valid only with get."));
29034
+ if ((returnEmbeddings !== void 0 || output !== void 0) && action !== "get") {
29035
+ return errorResult(new Error("returnEmbeddings and output are valid only with get."));
28850
29036
  }
28851
29037
  const result = parsedResult(action === "list" ? await listTaskJobs2(endpoint) : action === "get" ? await getTaskJob2(endpoint, jobId) : action === "cancel" ? await cancelTaskJob2(endpoint, jobId) : await removeTaskJob2(endpoint, jobId));
28852
29038
  if (!returnEmbeddings && isStructuredSuccess(result)) {
@@ -29607,7 +29793,7 @@ function cancelOpenJobs(openJobs, cancel) {
29607
29793
  }
29608
29794
 
29609
29795
  // src/research/source-evidence.ts
29610
- import { realpathSync as realpathSync3, statSync as statSync2 } from "node:fs";
29796
+ import { realpathSync as realpathSync4, statSync as statSync3 } from "node:fs";
29611
29797
  import path9 from "node:path";
29612
29798
 
29613
29799
  // src/research/canonical-json.ts
@@ -29743,11 +29929,11 @@ var READS = /* @__PURE__ */ new Set(["cat", "head", "tail", "nl", "wc", "diff",
29743
29929
  function sourcePath(value, workspace) {
29744
29930
  if (!value || value.startsWith("-") || /[*?$`]/.test(value)) return false;
29745
29931
  try {
29746
- const root = realpathSync3(workspace);
29747
- const source = realpathSync3(path9.resolve(root, value));
29932
+ const root = realpathSync4(workspace);
29933
+ const source = realpathSync4(path9.resolve(root, value));
29748
29934
  const relative = path9.relative(root, source);
29749
29935
  if (relative === ".." || relative.startsWith(`..${path9.sep}`) || path9.isAbsolute(relative)) return false;
29750
- const stat3 = statSync2(source);
29936
+ const stat3 = statSync3(source);
29751
29937
  return stat3.isFile() || stat3.isDirectory();
29752
29938
  } catch {
29753
29939
  return false;
@@ -29826,7 +30012,7 @@ function searchHitCitations(steps, answer, workspace) {
29826
30012
  }
29827
30013
 
29828
30014
  // src/delegate.ts
29829
- import { existsSync as existsSync5, readFileSync as readFileSync3, realpathSync as realpathSync4, statSync as statSync3 } from "node:fs";
30015
+ import { existsSync as existsSync5, readFileSync as readFileSync3, realpathSync as realpathSync5, statSync as statSync4 } from "node:fs";
29830
30016
  import path11 from "node:path";
29831
30017
  init_config();
29832
30018
  function researchErrorResult(error2, diagnostic) {
@@ -29962,7 +30148,7 @@ function assessDelegatedAnswer(question, evidenceCount, model, verifiedDigest) {
29962
30148
  function extractExistingWorkspacePath(command, workspace) {
29963
30149
  let root;
29964
30150
  try {
29965
- root = realpathSync4(workspace);
30151
+ root = realpathSync5(workspace);
29966
30152
  } catch {
29967
30153
  return null;
29968
30154
  }
@@ -29976,10 +30162,10 @@ function extractExistingWorkspacePath(command, workspace) {
29976
30162
  const candidate = path11.isAbsolute(token) ? token : path11.resolve(root, token);
29977
30163
  if (!existsSync5(candidate)) continue;
29978
30164
  try {
29979
- const resolved = realpathSync4(candidate);
30165
+ const resolved = realpathSync5(candidate);
29980
30166
  const relative = path11.relative(root, resolved);
29981
30167
  const withinRoot = relative && relative !== ".." && !relative.startsWith(`..${path11.sep}`);
29982
- if (withinRoot && statSync3(resolved).isFile()) return relative;
30168
+ if (withinRoot && statSync4(resolved).isFile()) return relative;
29983
30169
  } catch {
29984
30170
  }
29985
30171
  }
@@ -30022,7 +30208,7 @@ function parseAdapterResult(raw) {
30022
30208
  }
30023
30209
 
30024
30210
  // src/tools/coder.ts
30025
- import { existsSync as existsSync7, rmSync as rmSync2 } from "node:fs";
30211
+ import { existsSync as existsSync7, rmSync as rmSync3 } from "node:fs";
30026
30212
  import { mkdtemp, realpath as realpath2 } from "node:fs/promises";
30027
30213
  import { tmpdir as tmpdir2 } from "node:os";
30028
30214
  import path13 from "node:path";
@@ -30032,13 +30218,13 @@ import { spawnSync } from "node:child_process";
30032
30218
  import {
30033
30219
  cpSync,
30034
30220
  existsSync as existsSync6,
30035
- lstatSync,
30221
+ lstatSync as lstatSync2,
30036
30222
  mkdirSync as mkdirSync2,
30037
30223
  mkdtempSync,
30038
30224
  readFileSync as readFileSync4,
30039
- readdirSync as readdirSync2,
30040
- realpathSync as realpathSync5,
30041
- rmSync,
30225
+ readdirSync as readdirSync3,
30226
+ realpathSync as realpathSync6,
30227
+ rmSync as rmSync2,
30042
30228
  symlinkSync,
30043
30229
  writeFileSync as writeFileSync2
30044
30230
  } from "node:fs";
@@ -30069,30 +30255,30 @@ function repositoryTop(dir) {
30069
30255
  const top = git(["rev-parse", "--show-toplevel"], dir);
30070
30256
  if (top.status !== 0) return null;
30071
30257
  if (git(["rev-parse", "--verify", "--quiet", "HEAD"], dir).status !== 0) return null;
30072
- return realpathSync5(line(top.stdout));
30258
+ return realpathSync6(line(top.stdout));
30073
30259
  }
30074
30260
  function commonDirOf(top) {
30075
30261
  const raw = line(gitOk(["rev-parse", "--git-common-dir"], top));
30076
- return realpathSync5(path12.resolve(top, raw));
30262
+ return realpathSync6(path12.resolve(top, raw));
30077
30263
  }
30078
30264
  function removeIsolatedCopy(workspace, container) {
30079
30265
  const work = path12.join(container, WORKTREE_NAME);
30080
30266
  let commonDir = null;
30081
30267
  try {
30082
- const top = repositoryTop(realpathSync5(workspace));
30268
+ const top = repositoryTop(realpathSync6(workspace));
30083
30269
  commonDir = top ? commonDirOf(top) : null;
30084
30270
  if (top && existsSync6(work)) git(["worktree", "remove", "--force", work], top);
30085
30271
  } catch {
30086
30272
  }
30087
30273
  try {
30088
- rmSync(container, { recursive: true, force: true });
30274
+ rmSync2(container, { recursive: true, force: true });
30089
30275
  } catch {
30090
30276
  }
30091
30277
  if (!commonDir) return;
30092
30278
  const entries = path12.join(commonDir, "worktrees");
30093
30279
  let names;
30094
30280
  try {
30095
- names = readdirSync2(entries);
30281
+ names = readdirSync3(entries);
30096
30282
  } catch {
30097
30283
  return;
30098
30284
  }
@@ -30100,7 +30286,7 @@ function removeIsolatedCopy(workspace, container) {
30100
30286
  for (const name of names) {
30101
30287
  try {
30102
30288
  if (ours.has(readFileSync4(path12.join(entries, name, "gitdir"), "utf8").trim())) {
30103
- rmSync(path12.join(entries, name), { recursive: true, force: true });
30289
+ rmSync2(path12.join(entries, name), { recursive: true, force: true });
30104
30290
  }
30105
30291
  } catch {
30106
30292
  }
@@ -30140,7 +30326,7 @@ async function prepareCoderRun(commands) {
30140
30326
  patchPath,
30141
30327
  cleanup: (workspace) => {
30142
30328
  removeIsolatedCopy(workspace, container);
30143
- if (!existsSync7(patchPath)) rmSync2(patchDir, { recursive: true, force: true });
30329
+ if (!existsSync7(patchPath)) rmSync3(patchDir, { recursive: true, force: true });
30144
30330
  }
30145
30331
  };
30146
30332
  }
@@ -30216,6 +30402,25 @@ var researchResultSchema = object2({
30216
30402
  usage: unknown().optional(),
30217
30403
  telemetry: unknown().optional()
30218
30404
  }).loose();
30405
+ var RESEARCH_ROLES = ["stronger", "default", "small"];
30406
+ var GITHUB_RESEARCH_ROLES = ["stronger", "default"];
30407
+ async function catalogResearchModel(endpoint, adapter) {
30408
+ try {
30409
+ const [installed, available] = await catalogLiveState(endpoint, void 0);
30410
+ const picks = catalogView("research-agent", installed, available).jobs[0]?.picks ?? [];
30411
+ const nameOf = (pick3) => "installed_as" in pick3 && typeof pick3.installed_as === "string" ? pick3.installed_as : pick3.tag;
30412
+ for (const role of adapter === "github" ? GITHUB_RESEARCH_ROLES : RESEARCH_ROLES) {
30413
+ const pick3 = picks.find((entry) => entry.role === role && entry.installed === true && entry.fits_now === true);
30414
+ if (pick3) return { model: nameOf(pick3), reason: `catalog ${role} pick, installed, fits now` };
30415
+ }
30416
+ for (const role of ["default", "stronger"]) {
30417
+ const pick3 = picks.find((entry) => entry.role === role && entry.installed === true);
30418
+ if (pick3) return { model: nameOf(pick3), reason: `catalog ${role} pick, installed; not fitting now, so serve's memory gate decides` };
30419
+ }
30420
+ } catch {
30421
+ }
30422
+ return void 0;
30423
+ }
30219
30424
  async function routedResearchModel(endpoint) {
30220
30425
  const decision = JSON.parse(
30221
30426
  await route2(endpoint, "coding", "balanced", void 0, void 0, void 0, [], void 0, void 0, void 0)
@@ -30231,7 +30436,7 @@ function registerDelegateResearchTool(server2) {
30231
30436
  server2.registerTool(
30232
30437
  "delegate_research",
30233
30438
  {
30234
- description: 'Use when: a read-only question about files under workspacePath (where is X, what calls Y) would take several searches; it needs a local model that calls tools well: tools metadata does not prove it, so take the catalog\'s research-agent pick. Do not use when: editing (adapter:"coder" edits an isolated copy, if the operator enabled it), facts outside the workspace (GitHub excepted: adapter:"github", which needs no `workspacePath`), final judgment, or a question one search answers; run_task embedding with `nearest` finds files faster. Up to 8 turns, killed after 180 s by default (longer for bigger models, or agent.timeoutSeconds); no defer (a host that cuts calls near 60 s may drop it). Returns: citations/answer; discard verification.recommendation=escalate, otherwise verify citations. coder: review diff, then run its applyCommand.',
30439
+ description: 'Use when: a read-only question about files under workspacePath (where is X, what calls Y) would take several searches; without `model` it uses the catalog\'s research-agent pick that is installed and fits now. Do not use when: editing (adapter:"coder" edits an isolated copy, if the operator enabled it), facts outside the workspace (GitHub excepted: adapter:"github", which needs no `workspacePath`), final judgment, or a question one search answers; run_task embedding with `nearest` finds files faster. Up to 8 turns, killed after 180 s by default (longer for bigger models, or agent.timeoutSeconds); no defer (a host that cuts calls near 60 s may drop it). Returns: citations/answer; discard verification.recommendation=escalate, otherwise verify citations. coder: review diff, then run its applyCommand.',
30235
30440
  inputSchema: object2({
30236
30441
  question: string2().min(1),
30237
30442
  workspacePath: string2().min(1).optional().describe(
@@ -30247,7 +30452,8 @@ function registerDelegateResearchTool(server2) {
30247
30452
  timeoutSeconds: number2().int().positive().max(MAX_DELEGATE_TIMEOUT_SECONDS).optional().describe("default scales with the model's size"),
30248
30453
  commands: coderCommandsSchema
30249
30454
  }).strict().optional(),
30250
- receipt: _enum(["compact", "full"]).optional().describe("compact (default): trail at response_file; full: inline")
30455
+ receipt: _enum(["compact", "full"]).optional().describe("compact (default): trail at response_file; full: inline"),
30456
+ ...outputInput
30251
30457
  }).strict(),
30252
30458
  outputSchema: researchResultSchema,
30253
30459
  // Read-only lookup; adapter "github" searches GitHub over the internet, so the tool is
@@ -30255,18 +30461,33 @@ function registerDelegateResearchTool(server2) {
30255
30461
  annotations: { destructiveHint: false }
30256
30462
  },
30257
30463
  // `receipt` only shapes the inline view (tool-results.ts); the tool always returns everything.
30258
- async ({ question, workspacePath, adapter, model, endpoint, agent }, extra) => {
30464
+ async ({ question, workspacePath, adapter, model, endpoint, agent, outputPath }, extra) => {
30259
30465
  const chosenAdapter = adapter ?? DEFAULT_RESEARCH_ADAPTER;
30260
30466
  let resolvedWorkspace;
30261
30467
  let chosenModel;
30468
+ let modelReason = "explicit model";
30262
30469
  try {
30470
+ if (outputPath !== void 0) checkOutputPath(outputPath);
30263
30471
  if (workspacePath === void 0 && chosenAdapter !== "github") {
30264
30472
  throw new Error(`workspacePath is required for adapter "${chosenAdapter}": the absolute directory to research (or one relative to the server's working directory). Only adapter "github" runs without one.`);
30265
30473
  }
30266
30474
  const refusal = coderRefusal(chosenAdapter, agent?.commands);
30267
30475
  if (refusal) throw refusal;
30268
30476
  resolvedWorkspace = workspacePath === void 0 ? void 0 : await assertAllowedWorkspace(workspacePath);
30269
- chosenModel = model ?? DEFAULT_DELEGATE_MODEL ?? await routedResearchModel(endpoint);
30477
+ if (model) chosenModel = model;
30478
+ else if (DEFAULT_DELEGATE_MODEL) {
30479
+ chosenModel = DEFAULT_DELEGATE_MODEL;
30480
+ modelReason = "FREELLAMA_MCP_DEFAULT_MODEL";
30481
+ } else {
30482
+ const picked = await catalogResearchModel(endpoint, chosenAdapter);
30483
+ if (picked) {
30484
+ chosenModel = picked.model;
30485
+ modelReason = picked.reason;
30486
+ } else {
30487
+ chosenModel = await routedResearchModel(endpoint);
30488
+ modelReason = "routing (no catalog research pick installed)";
30489
+ }
30490
+ }
30270
30491
  } catch (error2) {
30271
30492
  return researchErrorResult(error2);
30272
30493
  }
@@ -30422,9 +30643,10 @@ ${evidenceText}` : "");
30422
30643
  }
30423
30644
  const digest = verifiedResearchDigest(result.model_metadata?.execution_receipts, chosenModel);
30424
30645
  const verification = assessDelegatedAnswer(question, sources.length, chosenModel, digest);
30425
- const summary = `Delegated answer ready: ${result.tool_calls.length} tool call(s)` + (failed > 0 ? `, ${failed} of which did not succeed` : "") + `; ${result.usage.input_tokens ?? "?"} input / ${result.usage.output_tokens ?? "?"} output local tokens; verification=${verification.recommendation}. Read structuredContent.answer and citations.`;
30646
+ const summary = `Delegated answer ready: ${result.tool_calls.length} tool call(s)` + (failed > 0 ? `, ${failed} of which did not succeed` : "") + `; ${result.usage.input_tokens ?? "?"} input / ${result.usage.output_tokens ?? "?"} output local tokens; verification=${verification.recommendation}; research model: ${chosenModel} (${modelReason}). Read structuredContent.answer and citations.`;
30426
30647
  const payload = {
30427
30648
  adapter: chosenAdapter,
30649
+ researchModel: { model: chosenModel, reason: modelReason },
30428
30650
  verification,
30429
30651
  answer: result.final_answer,
30430
30652
  // recommendation + why live under `verification`; `citations` completes the triple so a
@@ -30452,7 +30674,7 @@ ${evidenceText}` : "");
30452
30674
 
30453
30675
  verification: ${verification.recommendation}` + (cited.length ? `
30454
30676
  cited: ${cited.join(", ")}` : "") + `
30455
- (${chosenModel}, ${result.tool_calls.length} tool call(s))`;
30677
+ (research model: ${chosenModel} (${modelReason}), ${result.tool_calls.length} tool call(s))`;
30456
30678
  return structuredResult(payload, { text: answerText });
30457
30679
  } catch (error2) {
30458
30680
  return researchErrorResult(error2);
@@ -30592,7 +30814,7 @@ function registerReadResponseTool(server2) {
30592
30814
  }
30593
30815
 
30594
30816
  // src/tools/audio.ts
30595
- import { existsSync as existsSync9, realpathSync as realpathSync6, writeFileSync as writeFileSync3 } from "node:fs";
30817
+ import { existsSync as existsSync9, realpathSync as realpathSync7, writeFileSync as writeFileSync3 } from "node:fs";
30596
30818
  import { tmpdir as tmpdir4 } from "node:os";
30597
30819
  import path16 from "node:path";
30598
30820
  init_config();
@@ -30663,7 +30885,7 @@ function registerAudioTool(server2) {
30663
30885
  const target = path16.resolve(outputFile ?? path16.join(tmpdir4(), `freellama-speech-${Date.now()}.wav`));
30664
30886
  let directory;
30665
30887
  try {
30666
- directory = realpathSync6(path16.dirname(target));
30888
+ directory = realpathSync7(path16.dirname(target));
30667
30889
  } catch {
30668
30890
  return errorResult(new Error(`outputFile directory does not exist: ${path16.dirname(target)}`));
30669
30891
  }
@@ -30923,10 +31145,18 @@ ${chunk.text}`
30923
31145
 
30924
31146
  // src/sweep/store.ts
30925
31147
  init_config();
30926
- import { appendFileSync, existsSync as existsSync10, mkdirSync as mkdirSync3, readdirSync as readdirSync3, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync4 } from "node:fs";
31148
+ import { appendFileSync, existsSync as existsSync10, mkdirSync as mkdirSync3, readdirSync as readdirSync4, readFileSync as readFileSync5, renameSync, writeFileSync as writeFileSync4 } from "node:fs";
30927
31149
  import path18 from "node:path";
30928
31150
  import { createHash as createHash2 } from "node:crypto";
30929
31151
  var SWEEP_ROOT = process.env.FREELLAMA_SWEEP_DIR || path18.join(FREELLAMA_HOME, "sweeps");
31152
+ function processAlive(pid) {
31153
+ try {
31154
+ process.kill(pid, 0);
31155
+ return true;
31156
+ } catch (error2) {
31157
+ return error2.code === "EPERM";
31158
+ }
31159
+ }
30930
31160
  var hashText = (text3) => createHash2("sha256").update(text3).digest("hex").slice(0, 32);
30931
31161
  var SweepStore = class {
30932
31162
  constructor(root = SWEEP_ROOT) {
@@ -30979,7 +31209,7 @@ var SweepStore = class {
30979
31209
  }
30980
31210
  list() {
30981
31211
  if (!existsSync10(this.root)) return [];
30982
- return readdirSync3(this.root).filter((name) => /^[0-9a-f-]{36}$/.test(name) && existsSync10(path18.join(this.root, name, "manifest.json")));
31212
+ return readdirSync4(this.root).filter((name) => /^[0-9a-f-]{36}$/.test(name) && existsSync10(path18.join(this.root, name, "manifest.json")));
30983
31213
  }
30984
31214
  };
30985
31215
  function jsonl(file) {
@@ -31088,6 +31318,8 @@ var SweepRunner = class {
31088
31318
  resume(id) {
31089
31319
  if (this.active.has(id)) return this.view(id);
31090
31320
  const manifest = this.store.manifest(id);
31321
+ const other = this.foreignOwner(manifest);
31322
+ if (other !== void 0) throw new Error(`sweep ${id} is still running in server process ${other}; wait for it or stop that process first`);
31091
31323
  if (manifest.phase === "collecting") throw new Error("this sweep was interrupted while collecting pages; start a new sweep");
31092
31324
  manifest.phase = "running";
31093
31325
  delete manifest.finishedAt;
@@ -31144,7 +31376,9 @@ var SweepRunner = class {
31144
31376
  if (result.change === "changed") counts.changed += 1;
31145
31377
  if (result.change === "new") counts.added += 1;
31146
31378
  }
31147
- const phase = (manifest.phase === "running" || manifest.phase === "collecting") && !this.active.has(id) ? "interrupted" : manifest.phase;
31379
+ const ownerPid = manifest.phase === "running" || manifest.phase === "collecting" ? this.foreignOwner(manifest) : void 0;
31380
+ const phase = (manifest.phase === "running" || manifest.phase === "collecting") && !this.active.has(id) && ownerPid === void 0 ? "interrupted" : manifest.phase;
31381
+ const resumable = phase === "interrupted" || phase === "cancelled" || phase === "completed" && counts.failed > 0 || phase === "failed" && counts.total > 0;
31148
31382
  const elapsedSeconds = Math.round(((manifest.finishedAt ? Date.parse(manifest.finishedAt) : Date.now()) - Date.parse(manifest.startedAt)) / 1e3);
31149
31383
  const pending = Math.max(0, counts.total - counts.done - counts.failed);
31150
31384
  const ms = this.active.get(id)?.ms ?? [];
@@ -31158,6 +31392,8 @@ var SweepRunner = class {
31158
31392
  pages: manifest.pages,
31159
31393
  skipped: manifest.skipped.length,
31160
31394
  error: manifest.error,
31395
+ ...resumable && !(manifest.phase === "collecting") ? { resume: { action: "resume", sweepId: id } } : {},
31396
+ ...ownerPid !== void 0 ? { ownerPid } : {},
31161
31397
  startedAt: manifest.startedAt,
31162
31398
  finishedAt: manifest.finishedAt,
31163
31399
  elapsedSeconds,
@@ -31165,10 +31401,18 @@ var SweepRunner = class {
31165
31401
  files: this.store.files(id)
31166
31402
  };
31167
31403
  }
31404
+ /** The pid of another live server process that owns this sweep's run, if any. */
31405
+ foreignOwner(manifest) {
31406
+ if (manifest.phase !== "running" && manifest.phase !== "collecting") return void 0;
31407
+ const pid = manifest.owner?.pid;
31408
+ return pid !== void 0 && pid !== process.pid && !this.active.has(manifest.config.id) && processAlive(pid) ? pid : void 0;
31409
+ }
31168
31410
  emit(id, message2) {
31169
31411
  for (const listener of this.listeners.get(id) ?? []) listener(message2);
31170
31412
  }
31171
31413
  launch(manifest, resuming = false) {
31414
+ manifest.owner = { pid: process.pid };
31415
+ this.store.saveManifest(manifest);
31172
31416
  const abort = new AbortController();
31173
31417
  const active = { abort, done: Promise.resolve(), ms: [] };
31174
31418
  this.active.set(manifest.config.id, active);
@@ -31382,11 +31626,11 @@ function runnerOf(id) {
31382
31626
  for (const runner of runners.values()) if (runner.isActive(id)) return runner;
31383
31627
  return runnerFor(store.manifest(id).config.endpoint);
31384
31628
  }
31385
- function viewText(view) {
31629
+ function viewText2(view) {
31386
31630
  const c = view.counts;
31387
31631
  const changes = c.reused + c.changed + c.added > 0 || c.removed > 0 ? `; since baseline: ${c.added} new, ${c.changed} changed, ${c.reused} unchanged (reused), ${c.removed} removed` : "";
31388
31632
  const eta = view.etaSeconds !== void 0 ? `, about ${Math.max(1, Math.round(view.etaSeconds / 60))} min left` : "";
31389
- const next = view.phase === "running" || view.phase === "collecting" ? `sweep{action:"wait", sweepId:"${view.id}"} to follow it` : view.phase === "interrupted" || view.phase === "cancelled" || c.failed > 0 ? `sweep{action:"resume", sweepId:"${view.id}"} continues it` : `sweep{action:"results", sweepId:"${view.id}"} lists flagged chunks`;
31633
+ const next = view.phase === "running" || view.phase === "collecting" ? `sweep{action:"wait", sweepId:"${view.id}"} to follow it` : view.resume ? `${view.phase === "interrupted" ? "its server process exited; " : ""}sweep{action:"resume", sweepId:"${view.id}"} continues it, reusing saved chunk results` : view.phase === "interrupted" ? "it stopped while collecting pages and cannot resume: start a new sweep" : c.failed > 0 ? `sweep{action:"resume", sweepId:"${view.id}"} retries the failed chunks` : `sweep{action:"results", sweepId:"${view.id}"} lists flagged chunks`;
31390
31634
  return `sweep ${view.id} ${view.phase}${view.error ? ` (${view.error})` : ""}: ${c.done + c.failed}/${c.total} chunks${view.pages !== void 0 ? ` from ${view.pages} pages` : ""}, ${c.flagged} flagged, ${c.unsure} unsure, ${c.clear} clear, ${c.failed} failed${changes}${eta}. Next: ${next}.`;
31391
31635
  }
31392
31636
  function answerLine(answers2) {
@@ -31447,7 +31691,7 @@ function registerSweepTool(server2) {
31447
31691
  if (action === "list") {
31448
31692
  const sweeps = store.list().map((id) => runnerOf(id).view(id)).sort((left, right) => right.startedAt.localeCompare(left.startedAt)).slice(0, 50);
31449
31693
  return structuredResult({ sweeps }, {
31450
- text: sweeps.length === 0 ? "no sweeps yet" : sweeps.map((view2) => `- ${view2.name ?? "(unnamed)"}: ${viewText(view2)}`).join("\n")
31694
+ text: sweeps.length === 0 ? "no sweeps yet" : sweeps.map((view2) => `- ${view2.name ?? "(unnamed)"}: ${viewText2(view2)}`).join("\n")
31451
31695
  });
31452
31696
  }
31453
31697
  if (action === "start") return await start(args);
@@ -31456,11 +31700,11 @@ function registerSweepTool(server2) {
31456
31700
  const runner = runnerOf(sweepId);
31457
31701
  if (action === "cancel") {
31458
31702
  const view2 = await runner.cancel(sweepId);
31459
- return structuredResult(view2, { text: viewText(view2) });
31703
+ return structuredResult(view2, { text: viewText2(view2) });
31460
31704
  }
31461
31705
  if (action === "resume") {
31462
31706
  const view2 = runner.resume(sweepId);
31463
- return structuredResult(view2, { text: viewText(view2) });
31707
+ return structuredResult(view2, { text: viewText2(view2) });
31464
31708
  }
31465
31709
  if (action === "wait") {
31466
31710
  const report = progressSink(extra);
@@ -31473,7 +31717,7 @@ function registerSweepTool(server2) {
31473
31717
  }
31474
31718
  if (action === "results") return results(sweepId, args.filter ?? "flagged", 50, args.cursor, args.saveAs ? { name: args.saveAs, view: runner.view(sweepId) } : void 0);
31475
31719
  const view = runner.view(sweepId);
31476
- return structuredResult(view, { text: viewText(view) });
31720
+ return structuredResult(view, { text: viewText2(view) });
31477
31721
  } catch (error2) {
31478
31722
  return errorResult(error2);
31479
31723
  }
@@ -31527,7 +31771,7 @@ async function start(args) {
31527
31771
  }
31528
31772
  };
31529
31773
  const view = await runnerFor(args.endpoint).start(config2);
31530
- return structuredResult(view, { text: viewText(view) });
31774
+ return structuredResult(view, { text: viewText2(view) });
31531
31775
  }
31532
31776
  function rowLine(row) {
31533
31777
  const verified = row.verified ? ` \u2192 ${row.verified.model}: ${row.verified.verdict} (${answerLine(row.verified.answers)})` : "";
@@ -31571,7 +31815,7 @@ function results(id, filter, limit, cursor, handoff2) {
31571
31815
  (${rows.length - offset - page2.length} more: cursor "${next}")` : "") + "\nThese are a small model's candidates: read 3 to 5 of the cited ranges yourself before relying on the rest.";
31572
31816
  const file = handoff2 ? writeHandoff(handoff2.name, {
31573
31817
  source: `From sweep ${id}${manifest.config.name ? ` (${manifest.config.name})` : ""}, filter ${filter}`,
31574
- summary: viewText(handoff2.view),
31818
+ summary: viewText2(handoff2.view),
31575
31819
  result: rows.length === 0 ? `no ${filter} chunks` : rows.map(rowLine).join("\n")
31576
31820
  }) : void 0;
31577
31821
  if (file) text3 += `
@@ -31590,7 +31834,7 @@ Name inputs with files (paths or globs under the server's working directory, the
31590
31834
  You own prompts, output format and verification: local answers are candidates, so check 3 to 5 (more where a mistake costs more) before relying on the rest, and treat low decision probabilities as unknown.
31591
31835
  Memory: a refusal names the programs holding memory; tell the user and never stop their processes, then retry once they free memory, or use a smaller installed model with the same capability.
31592
31836
  Approval: before ollama_pull, ask the user for one exact tag (name:tag) and its reported size, and pass that size as approvedSizeBytes; search results are not permission. Unload only a model you warmed (when its burst ends) or one the user named; delete only a tag the user names.
31593
- Results: check isError first, then read the text; open structuredContent or read_response{file:response_file, section} only for a receipt field your next step needs. Pass page.next_cursor (models) and next_offset (read_response) back unchanged. A server restart loses scopes, sessions and jobs. CPU/GPU placement is operator-configured; executionPreference only chooses among eligible backends. Docs: freellama://docs/index.`;
31837
+ Results: check isError first, then read the text; open structuredContent or read_response{file:response_file, section} only for a receipt field your next step needs. A long answer (run_task, delegate_research, task_jobs get) comes back as a summary plus the output_file holding it; output:"inline" returns it whole, outputPath picks the file. Pass page.next_cursor (models) and next_offset (read_response) back unchanged. A server restart loses scopes, sessions and jobs. CPU/GPU placement is operator-configured; executionPreference only chooses among eligible backends. Docs: freellama://docs/index.`;
31594
31838
  var server = withToolResults(new McpServer(
31595
31839
  { name: "freellama", version: SERVER_VERSION },
31596
31840
  { instructions: INSTRUCTIONS, capabilities: { logging: {} } }