loki-mode 10.11.0 → 10.11.2
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/SKILL.md +2 -2
- package/VERSION +1 -1
- package/dashboard/__init__.py +1 -1
- package/docs/CLI-REFERENCE.md +1 -1
- package/docs/INSTALLATION.md +1 -1
- package/docs/v10/BOARD.md +3 -3
- package/docs/v10/DEPS.md +3 -0
- package/docs/v10/FAILURE-CLASSES.md +17 -0
- package/docs/v10/PROGRESS.md +6 -0
- package/loki-ts/dist/loki.js +640 -639
- package/mcp/__init__.py +1 -1
- package/package.json +1 -1
- package/packages/control-plane/dist/server.js +214 -209
- package/packages/control-plane/package.json +3 -1
- package/packages/control-plane/src/ask/tools.ts +111 -0
- package/packages/control-plane/src/ask/tools_server.ts +46 -0
- package/packages/control-plane/src/server/routes/artifacts.ts +3 -3
- package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
- package/web-app/dist/assets/{AdminPage-qTfCHm93.js → AdminPage-CmgIgBFY.js} +1 -1
- package/web-app/dist/assets/{Avatar-V5Wfzf34.js → Avatar-Dm3ypJzh.js} +1 -1
- package/web-app/dist/assets/{Badge-B7fNko8Z.js → Badge-0siYUwAA.js} +1 -1
- package/web-app/dist/assets/{Button-C_3eInAe.js → Button-EMKQXvZe.js} +1 -1
- package/web-app/dist/assets/{CockpitPage-Ir2wag2q.js → CockpitPage-Z60eDvJ9.js} +1 -1
- package/web-app/dist/assets/{ComparePage-qbC63b0U.js → ComparePage-D4L7nViy.js} +1 -1
- package/web-app/dist/assets/{ErrorBoundary-BTEqFTCi.js → ErrorBoundary--v9DugrJ.js} +1 -1
- package/web-app/dist/assets/{EvidenceReceiptPanel-CcICm1gj.js → EvidenceReceiptPanel-CA866XsM.js} +1 -1
- package/web-app/dist/assets/{GitHubIssuesPanel-CXCWYQqX.js → GitHubIssuesPanel-Bi6FJmR2.js} +1 -1
- package/web-app/dist/assets/{GitHubPRsPanel-DmzhZYpS.js → GitHubPRsPanel-CgSfcWOg.js} +1 -1
- package/web-app/dist/assets/{HomePage-BRPspBy7.js → HomePage-BxQtk9Ec.js} +2 -2
- package/web-app/dist/assets/{LoginPage-C1Nb7Vf9.js → LoginPage-CCSxWlFA.js} +1 -1
- package/web-app/dist/assets/{MagicPage-CeXc60VN.js → MagicPage-B4eYm0jJ.js} +1 -1
- package/web-app/dist/assets/{MetricsPage-MuDDdqNu.js → MetricsPage-Bp4Hh0p2.js} +1 -1
- package/web-app/dist/assets/{NotFoundPage-8iOaj85A.js → NotFoundPage-DTXk_JK4.js} +1 -1
- package/web-app/dist/assets/{ProjectPage-DOKcPhxr.js → ProjectPage-6S1v0GHS.js} +3 -3
- package/web-app/dist/assets/{ProjectsPage-CNKaqvuO.js → ProjectsPage-9foqEwG2.js} +1 -1
- package/web-app/dist/assets/{SettingsPage-uZT6dOT1.js → SettingsPage-CWOG_hhD.js} +1 -1
- package/web-app/dist/assets/{ShowcasePage-CHzrwMZ3.js → ShowcasePage-Du9LYlao.js} +1 -1
- package/web-app/dist/assets/{SystemSettingsPage-DMHj5Xub.js → SystemSettingsPage-BQconaDX.js} +1 -1
- package/web-app/dist/assets/{TeamsPage-IGY1oZ2o.js → TeamsPage-_NsqtVmC.js} +1 -1
- package/web-app/dist/assets/{TemplatesPage-Byxc8XyZ.js → TemplatesPage-DyTEGBpR.js} +1 -1
- package/web-app/dist/assets/{TerminalOutput-CRamR4sR.js → TerminalOutput-XHOYTpky.js} +1 -1
- package/web-app/dist/assets/{activity-DQfL1H8b.js → activity-B9KiMdEs.js} +1 -1
- package/web-app/dist/assets/{bell-CVksERF2.js → bell-Oe4lG6kK.js} +1 -1
- package/web-app/dist/assets/{bot-jSx_C-35.js → bot-D86Qmxfy.js} +1 -1
- package/web-app/dist/assets/{check-o3bOhMvE.js → check-BvW6_ni0.js} +1 -1
- package/web-app/dist/assets/{chevron-left-DBhM8Y2E.js → chevron-left-DYA62-3g.js} +1 -1
- package/web-app/dist/assets/{circle-alert-vCzO_mB8.js → circle-alert-CHW8ye4p.js} +1 -1
- package/web-app/dist/assets/{clock-Bj125ZaJ.js → clock-QbQ2XfbM.js} +1 -1
- package/web-app/dist/assets/{cloud-DpLr6uQJ.js → cloud-ylzrY7eL.js} +1 -1
- package/web-app/dist/assets/{code-xml-VuYrHYvT.js → code-xml-DdKKw2Y9.js} +1 -1
- package/web-app/dist/assets/{database-DBmPbwaG.js → database-BLY_OWs3.js} +1 -1
- package/web-app/dist/assets/{dollar-sign-DEajFwH1.js → dollar-sign-CsFIa1e2.js} +1 -1
- package/web-app/dist/assets/{file-code-corner-BidTyjV_.js → file-code-corner-Df4nyrCW.js} +1 -1
- package/web-app/dist/assets/{file-plus-DJ4dSvxw.js → file-plus-_MWEY2WY.js} +1 -1
- package/web-app/dist/assets/{globe-BnGEwsOq.js → globe-Dpc7QU9m.js} +1 -1
- package/web-app/dist/assets/{hammer-KCPviXRE.js → hammer-B-PldT0c.js} +1 -1
- package/web-app/dist/assets/{index-CdK6NQG3.js → index-gq0R3j-o.js} +3 -3
- package/web-app/dist/assets/{layers-7G4hsaDk.js → layers-BjcNwHLZ.js} +1 -1
- package/web-app/dist/assets/{loader-circle-CWVxvRmJ.js → loader-circle-CoTgcqkV.js} +1 -1
- package/web-app/dist/assets/{lock-BEH3dNM0.js → lock-JEI8O6TS.js} +1 -1
- package/web-app/dist/assets/{package-DvijfmPC.js → package-CXWkNS40.js} +1 -1
- package/web-app/dist/assets/{plus-CEwJzXGx.js → plus-CRK4U5b0.js} +1 -1
- package/web-app/dist/assets/{refresh-cw-Ct7rMsNS.js → refresh-cw-CrzWr4cw.js} +1 -1
- package/web-app/dist/assets/{rotate-ccw-CoTyya_B.js → rotate-ccw-DV7UZyUw.js} +1 -1
- package/web-app/dist/assets/{scroll-text-CbsOnRXr.js → scroll-text-CXEqRrXU.js} +1 -1
- package/web-app/dist/assets/{server-MUPVBbag.js → server-CDFfFP10.js} +1 -1
- package/web-app/dist/assets/{shield-alert-CWwdqRUv.js → shield-alert-B5EC_m6_.js} +1 -1
- package/web-app/dist/assets/{thumbs-up-CPVKKyLS.js → thumbs-up-BeLDemd2.js} +1 -1
- package/web-app/dist/assets/{trash-2-BqzCzljk.js → trash-2-B7kS86-0.js} +1 -1
- package/web-app/dist/assets/{trending-down-DAo5VuPc.js → trending-down-BGvuLayo.js} +1 -1
- package/web-app/dist/assets/{trending-up-BCUggli9.js → trending-up-BkPM2tiK.js} +1 -1
- package/web-app/dist/assets/{upload-iQijlCY2.js → upload-CXVI0GRU.js} +1 -1
- package/web-app/dist/assets/{usePolling-B6kojZR_.js → usePolling-CMGMVsMA.js} +1 -1
- package/web-app/dist/assets/{user-DR3XkyHZ.js → user-DPcXNtfj.js} +1 -1
- package/web-app/dist/index.html +1 -1
package/mcp/__init__.py
CHANGED
package/package.json
CHANGED
|
@@ -16933,204 +16933,11 @@ function dropSet(repoDir, staged, preexistingDirty, env = process.env) {
|
|
|
16933
16933
|
import { basename as basename9, dirname as dirname7 } from "path";
|
|
16934
16934
|
|
|
16935
16935
|
// ../../loki-ts/src/engine10/testmap.ts
|
|
16936
|
-
import { basename as basename8, extname, join as
|
|
16937
|
-
var SKIP_DIRS = new Set([
|
|
16938
|
-
".git",
|
|
16939
|
-
"node_modules",
|
|
16940
|
-
"dist",
|
|
16941
|
-
"build",
|
|
16942
|
-
"target",
|
|
16943
|
-
"coverage",
|
|
16944
|
-
".venv",
|
|
16945
|
-
"venv",
|
|
16946
|
-
"__pycache__",
|
|
16947
|
-
".tox",
|
|
16948
|
-
".pytest_cache",
|
|
16949
|
-
".loki",
|
|
16950
|
-
".next"
|
|
16951
|
-
]);
|
|
16952
|
-
var JS_TEST_RE = /\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
16953
|
-
var PY_TEST_RE = /^(test_.+|.+_test)\.py$/;
|
|
16954
|
-
var GO_TEST_RE = /_test\.go$/;
|
|
16955
|
-
function isTestFile(rel) {
|
|
16956
|
-
const name = basename8(rel);
|
|
16957
|
-
return JS_TEST_RE.test(name) || PY_TEST_RE.test(name) || GO_TEST_RE.test(name);
|
|
16958
|
-
}
|
|
16959
|
-
|
|
16960
|
-
// ../../loki-ts/src/features/speed/unit_mode.ts
|
|
16961
|
-
import { closeSync as closeSync6, constants, fstatSync as fstatSync4, lstatSync as lstatSync4, openSync as openSync6, readSync as readSync4, realpathSync as realpathSync12 } from "fs";
|
|
16962
|
-
var MAX_SPEC_BYTES = 1024 * 1024;
|
|
16963
|
-
var MAX_TOKEN_BUDGET = 1e9;
|
|
16964
|
-
var norm2 = (p) => p.replace(/^\.\//, "");
|
|
16965
|
-
var CTRL = /[\x00-\x1f\x7f]/;
|
|
16966
|
-
var strs = (v) => Array.isArray(v) && v.every((x) => typeof x === "string" && !CTRL.test(x)) ? v.map(norm2) : null;
|
|
16967
|
-
function readBounded(path) {
|
|
16968
|
-
const l = lstatSync4(path);
|
|
16969
|
-
if (l.isSymbolicLink() || !l.isFile() || l.size > MAX_SPEC_BYTES)
|
|
16970
|
-
return null;
|
|
16971
|
-
const fd = openSync6(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
16972
|
-
try {
|
|
16973
|
-
const st = fstatSync4(fd);
|
|
16974
|
-
if (!st.isFile() || st.size > MAX_SPEC_BYTES)
|
|
16975
|
-
return null;
|
|
16976
|
-
const buf = Buffer.alloc(st.size + 1);
|
|
16977
|
-
let n2 = 0;
|
|
16978
|
-
for (;; ) {
|
|
16979
|
-
const r = readSync4(fd, buf, n2, buf.length - n2, null);
|
|
16980
|
-
if (r <= 0)
|
|
16981
|
-
break;
|
|
16982
|
-
n2 += r;
|
|
16983
|
-
if (n2 >= buf.length)
|
|
16984
|
-
return null;
|
|
16985
|
-
}
|
|
16986
|
-
return buf.subarray(0, n2).toString("utf8");
|
|
16987
|
-
} finally {
|
|
16988
|
-
closeSync6(fd);
|
|
16989
|
-
}
|
|
16990
|
-
}
|
|
16991
|
-
function parseText(txt) {
|
|
16992
|
-
try {
|
|
16993
|
-
const j = JSON.parse(txt), ws = strs(j.writeSet), pack = strs(j.pack), tb = j.tokenBudget;
|
|
16994
|
-
if (typeof j.id !== "string" || !j.id || !ws || ws.length === 0 || !pack || typeof tb !== "number" || !Number.isFinite(tb) || !(tb > 0) || tb > MAX_TOKEN_BUDGET)
|
|
16995
|
-
return null;
|
|
16996
|
-
return { id: j.id, writeSet: ws, pack, tokenBudget: tb };
|
|
16997
|
-
} catch {
|
|
16998
|
-
return null;
|
|
16999
|
-
}
|
|
17000
|
-
}
|
|
17001
|
-
function parseSpec(path) {
|
|
17002
|
-
try {
|
|
17003
|
-
const txt = readBounded(path);
|
|
17004
|
-
return txt === null ? null : parseText(txt);
|
|
17005
|
-
} catch {
|
|
17006
|
-
return null;
|
|
17007
|
-
}
|
|
17008
|
-
}
|
|
17009
|
-
var memo = new Map;
|
|
17010
|
-
function unitSpec(env = process.env) {
|
|
17011
|
-
const path = env["LOKI_UNIT_SPEC"], frozen = env["LOKI_UNIT_SPEC_FROZEN"];
|
|
17012
|
-
if (env["LOKI_SPEED"] === "0" || !path)
|
|
17013
|
-
return null;
|
|
17014
|
-
if (frozen !== undefined)
|
|
17015
|
-
return frozen.length <= MAX_SPEC_BYTES ? parseText(frozen) : null;
|
|
17016
|
-
if (!memo.has(path))
|
|
17017
|
-
memo.set(path, parseSpec(path));
|
|
17018
|
-
return memo.get(path) ?? null;
|
|
17019
|
-
}
|
|
17020
|
-
var inWriteSet = (spec, f) => spec.writeSet.some((w) => w.endsWith("/") ? f.startsWith(w) : f === w);
|
|
17021
|
-
function unitFlags(staged, env = process.env, pre = {}) {
|
|
17022
|
-
const s = unitSpec(env);
|
|
17023
|
-
return s ? staged.filter(({ f }) => !inWriteSet(s, f) && !Object.hasOwn(pre, f)).map(({ f }) => f) : null;
|
|
17024
|
-
}
|
|
17025
|
-
|
|
17026
|
-
// ../../loki-ts/src/e10ext/scope.ts
|
|
17027
|
-
var SCOPE_UNDETERMINED = "scope not determined; all edits committed";
|
|
17028
|
-
var outsideNote = (f) => `outside stated scope: ${f}`;
|
|
17029
|
-
var esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17030
|
-
var mentions = (text2, name) => new RegExp(`(?<![\\w./-])${esc(name)}(?![\\w-]|\\.\\w)`).test(text2);
|
|
17031
|
-
var STOP = new Set(["the", "and", "for", "with", "that", "this", "all", "any", "src", "from", "into", "each", "across", "apply", "add", "use", "make"]);
|
|
17032
|
-
var stem = (w) => w.length > 3 && w.endsWith("s") ? w.slice(0, -1) : w;
|
|
17033
|
-
var surfaceTokens = (text2) => new Set((text2.toLowerCase().match(/[a-z][a-z0-9_-]{2,}/g) ?? []).filter((w) => !STOP.has(w)).map(stem));
|
|
17034
|
-
var dirSegs = (f) => f.split("/").slice(0, -1).map((d) => stem(d.toLowerCase()));
|
|
17035
|
-
function contractText(o) {
|
|
17036
|
-
const c = o.intake?.contract_snapshot?.contract;
|
|
17037
|
-
return Array.isArray(c?.criteria) ? c.criteria.map((x) => typeof x?.text === "string" ? x.text : "").join(`
|
|
17038
|
-
`) : "";
|
|
17039
|
-
}
|
|
17040
|
-
function outsideScope(o, staged) {
|
|
17041
|
-
const rel = Array.isArray(o.plan?.relevant_files) ? o.plan.relevant_files.filter((x) => typeof x === "string") : [];
|
|
17042
|
-
const plan = typeof o.plan?.plan === "string" ? o.plan.plan : "";
|
|
17043
|
-
if (rel.length === 0 && plan.trim() === "")
|
|
17044
|
-
return null;
|
|
17045
|
-
const text2 = `${plan}
|
|
17046
|
-
${typeof o.intake?.task === "string" ? o.intake.task : ""}
|
|
17047
|
-
${contractText(o)}`, pre = o.intake?.preexisting_dirty ?? {};
|
|
17048
|
-
const toks = surfaceTokens(text2), plannedDirs = new Set(rel.map((r) => dirname7(r)).filter((d) => d !== "."));
|
|
17049
|
-
return staged.filter(({ st, f }) => st !== "A" && !isTestFile(f) && !Object.hasOwn(pre, f) && !rel.includes(f) && !mentions(text2, f) && !mentions(text2, basename9(f)) && !plannedDirs.has(dirname7(f)) && !dirSegs(f).some((d) => toks.has(d))).map(({ f }) => f);
|
|
17050
|
-
}
|
|
17051
|
-
function flagOutsideScope(o, staged) {
|
|
17052
|
-
const unit = unitFlags(staged, process.env, o.intake?.preexisting_dirty ?? {});
|
|
17053
|
-
const un = outsideScope(o, staged), seen = new Set((unit ?? []).map(outsideNote));
|
|
17054
|
-
return [...(unit ?? []).map(outsideNote), ...un ? un.map(outsideNote).filter((n2) => !seen.has(n2)) : staged.length > 0 ? [SCOPE_UNDETERMINED] : []];
|
|
17055
|
-
}
|
|
17056
|
-
|
|
17057
|
-
// ../../loki-ts/src/util/receipt_signer.ts
|
|
17058
|
-
var RECEIPT_SIGNER_BASENAME = "receipt-ed25519.pem";
|
|
17059
|
-
|
|
17060
|
-
// ../../loki-ts/src/util/shell.ts
|
|
17061
|
-
var MAX_STDOUT_BYTES = 16 * 1024 * 1024;
|
|
17062
|
-
async function readStreamCapped(stream, maxBytes = MAX_STDOUT_BYTES) {
|
|
17063
|
-
const reader = stream.getReader();
|
|
17064
|
-
const decoder = new TextDecoder;
|
|
17065
|
-
let out2 = "";
|
|
17066
|
-
let total = 0;
|
|
17067
|
-
try {
|
|
17068
|
-
while (total < maxBytes) {
|
|
17069
|
-
const { done, value } = await reader.read();
|
|
17070
|
-
if (done)
|
|
17071
|
-
break;
|
|
17072
|
-
if (!value)
|
|
17073
|
-
continue;
|
|
17074
|
-
total += value.byteLength;
|
|
17075
|
-
if (total > maxBytes) {
|
|
17076
|
-
const keep = value.byteLength - (total - maxBytes);
|
|
17077
|
-
out2 += decoder.decode(value.subarray(0, keep), { stream: true });
|
|
17078
|
-
break;
|
|
17079
|
-
}
|
|
17080
|
-
out2 += decoder.decode(value, { stream: true });
|
|
17081
|
-
}
|
|
17082
|
-
out2 += decoder.decode();
|
|
17083
|
-
} finally {
|
|
17084
|
-
try {
|
|
17085
|
-
await reader.cancel();
|
|
17086
|
-
} catch {}
|
|
17087
|
-
reader.releaseLock();
|
|
17088
|
-
}
|
|
17089
|
-
return out2;
|
|
17090
|
-
}
|
|
17091
|
-
async function run(argv, opts = {}) {
|
|
17092
|
-
const proc = Bun.spawn({
|
|
17093
|
-
cmd: [...argv],
|
|
17094
|
-
stdout: "pipe",
|
|
17095
|
-
stderr: "pipe",
|
|
17096
|
-
env: opts.env ? { ...process.env, ...opts.env } : process.env,
|
|
17097
|
-
cwd: opts.cwd
|
|
17098
|
-
});
|
|
17099
|
-
let timer;
|
|
17100
|
-
let killTimer;
|
|
17101
|
-
if (opts.timeoutMs && opts.timeoutMs > 0) {
|
|
17102
|
-
timer = setTimeout(() => {
|
|
17103
|
-
try {
|
|
17104
|
-
proc.kill("SIGTERM");
|
|
17105
|
-
} catch {}
|
|
17106
|
-
killTimer = setTimeout(() => {
|
|
17107
|
-
try {
|
|
17108
|
-
proc.kill("SIGKILL");
|
|
17109
|
-
} catch {}
|
|
17110
|
-
}, 2000);
|
|
17111
|
-
}, opts.timeoutMs);
|
|
17112
|
-
}
|
|
17113
|
-
try {
|
|
17114
|
-
const [stdout, stderr, exitCode] = await Promise.all([
|
|
17115
|
-
readStreamCapped(proc.stdout),
|
|
17116
|
-
new Response(proc.stderr).text(),
|
|
17117
|
-
proc.exited
|
|
17118
|
-
]);
|
|
17119
|
-
return { stdout, stderr, exitCode };
|
|
17120
|
-
} finally {
|
|
17121
|
-
if (timer)
|
|
17122
|
-
clearTimeout(timer);
|
|
17123
|
-
if (killTimer)
|
|
17124
|
-
clearTimeout(killTimer);
|
|
17125
|
-
}
|
|
17126
|
-
}
|
|
17127
|
-
|
|
17128
|
-
// ../../loki-ts/src/features/visual_evidence.ts
|
|
17129
|
-
import { spawn as spawn5 } from "child_process";
|
|
16936
|
+
import { basename as basename8, extname, join as join26, relative as relative2, sep as sep10 } from "path";
|
|
17130
16937
|
|
|
17131
16938
|
// ../../loki-ts/src/project_model/discover.ts
|
|
17132
16939
|
import { existsSync as existsSync14, mkdirSync as mkdirSync6, readFileSync as readFileSync19, rmSync as rmSync4, writeFileSync as writeFileSync7 } from "fs";
|
|
17133
|
-
import { join as
|
|
16940
|
+
import { join as join25 } from "path";
|
|
17134
16941
|
|
|
17135
16942
|
// ../../loki-ts/src/engine10/types.ts
|
|
17136
16943
|
var STAGE_BUDGETS = {
|
|
@@ -17147,23 +16954,23 @@ var STAGE_BUDGETS = {
|
|
|
17147
16954
|
};
|
|
17148
16955
|
|
|
17149
16956
|
// ../../loki-ts/src/project_model/schema.ts
|
|
17150
|
-
import { existsSync as existsSync13, lstatSync as
|
|
17151
|
-
import { isAbsolute, join as
|
|
16957
|
+
import { existsSync as existsSync13, lstatSync as lstatSync4, realpathSync as realpathSync12, statSync as statSync10 } from "fs";
|
|
16958
|
+
import { isAbsolute, join as join24, normalize, sep as sep9 } from "path";
|
|
17152
16959
|
var PROJECT_MODEL_SCHEMA = "loki.v10.project/1";
|
|
17153
16960
|
var COMMAND_KINDS = ["test", "lint", "build", "start"];
|
|
17154
16961
|
var isRec = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
17155
16962
|
var isStr = (v) => typeof v === "string" && v.trim() !== "";
|
|
17156
16963
|
function safeRel(p) {
|
|
17157
|
-
const n2 = normalize(p.trim()).split(
|
|
16964
|
+
const n2 = normalize(p.trim()).split(sep9).join("/").replace(/\/$/, "");
|
|
17158
16965
|
if (n2 === "" || isAbsolute(n2) || n2 === ".." || n2.startsWith("../"))
|
|
17159
16966
|
return null;
|
|
17160
16967
|
return n2;
|
|
17161
16968
|
}
|
|
17162
16969
|
function insideRepo(repoDir, rel) {
|
|
17163
16970
|
try {
|
|
17164
|
-
const real2 =
|
|
17165
|
-
const base =
|
|
17166
|
-
return real2 === base || real2.startsWith(base +
|
|
16971
|
+
const real2 = realpathSync12(join24(repoDir, rel));
|
|
16972
|
+
const base = realpathSync12(repoDir);
|
|
16973
|
+
return real2 === base || real2.startsWith(base + sep9);
|
|
17167
16974
|
} catch {
|
|
17168
16975
|
return false;
|
|
17169
16976
|
}
|
|
@@ -17174,7 +16981,7 @@ function checkCitation(repoDir, raw2) {
|
|
|
17174
16981
|
const rel = safeRel(raw2.replace(/:\d+(?:-\d+)?$/, ""));
|
|
17175
16982
|
if (rel === null)
|
|
17176
16983
|
return `citation "${raw2}" is not a repo-relative path`;
|
|
17177
|
-
const abs =
|
|
16984
|
+
const abs = join24(repoDir, rel);
|
|
17178
16985
|
if (!existsSync13(abs) || !statSync10(abs).isFile() || !insideRepo(repoDir, rel))
|
|
17179
16986
|
return `citation "${raw2}" names a file that does not exist`;
|
|
17180
16987
|
return null;
|
|
@@ -17186,7 +16993,7 @@ function checkFingerprint(repoDir, raw2) {
|
|
|
17186
16993
|
if (rel === null)
|
|
17187
16994
|
return `fingerprint "${raw2}" is not a repo-relative path`;
|
|
17188
16995
|
try {
|
|
17189
|
-
if (!
|
|
16996
|
+
if (!lstatSync4(join24(repoDir, rel)).isFile() || !insideRepo(repoDir, rel))
|
|
17190
16997
|
return `fingerprint "${raw2}" is not a regular file inside the repo`;
|
|
17191
16998
|
} catch {
|
|
17192
16999
|
return `fingerprint "${raw2}" names a file that does not exist`;
|
|
@@ -17214,7 +17021,7 @@ function checkDir(repoDir, v, where, errs) {
|
|
|
17214
17021
|
errs.push(`${where}: must be a repo-relative directory ("." for the repo root)`);
|
|
17215
17022
|
return ".";
|
|
17216
17023
|
}
|
|
17217
|
-
const abs =
|
|
17024
|
+
const abs = join24(repoDir, rel);
|
|
17218
17025
|
if (!existsSync13(abs) || !statSync10(abs).isDirectory() || !insideRepo(repoDir, rel))
|
|
17219
17026
|
errs.push(`${where}: directory "${rel}" does not exist`);
|
|
17220
17027
|
return rel;
|
|
@@ -17331,7 +17138,7 @@ var PROJECT_FILE = ".loki/project.json";
|
|
|
17331
17138
|
var UNKNOWN_TTL_MS = 60 * 60 * 1000;
|
|
17332
17139
|
function loadCached(repoDir, now = Date.now()) {
|
|
17333
17140
|
try {
|
|
17334
|
-
return parseCached(repoDir, JSON.parse(readFileSync19(
|
|
17141
|
+
return parseCached(repoDir, JSON.parse(readFileSync19(join25(repoDir, PROJECT_FILE), "utf8")), now);
|
|
17335
17142
|
} catch {
|
|
17336
17143
|
return null;
|
|
17337
17144
|
}
|
|
@@ -17403,7 +17210,205 @@ function isMultiRoot(api) {
|
|
|
17403
17210
|
return !!api && api.known() && api.packages().some((p) => p.root !== ".");
|
|
17404
17211
|
}
|
|
17405
17212
|
|
|
17213
|
+
// ../../loki-ts/src/project_model/scope.ts
|
|
17214
|
+
var apis = new WeakMap;
|
|
17215
|
+
var imports = new WeakMap;
|
|
17216
|
+
var dropped = new WeakMap;
|
|
17217
|
+
|
|
17218
|
+
// ../../loki-ts/src/engine10/testmap.ts
|
|
17219
|
+
var SKIP_DIRS = new Set([
|
|
17220
|
+
".git",
|
|
17221
|
+
"node_modules",
|
|
17222
|
+
"dist",
|
|
17223
|
+
"build",
|
|
17224
|
+
"target",
|
|
17225
|
+
"coverage",
|
|
17226
|
+
".venv",
|
|
17227
|
+
"venv",
|
|
17228
|
+
"__pycache__",
|
|
17229
|
+
".tox",
|
|
17230
|
+
".pytest_cache",
|
|
17231
|
+
".loki",
|
|
17232
|
+
".next"
|
|
17233
|
+
]);
|
|
17234
|
+
var JS_TEST_RE = /\.(test|spec)\.[cm]?[jt]sx?$/;
|
|
17235
|
+
var PY_TEST_RE = /^(test_.+|.+_test)\.py$/;
|
|
17236
|
+
var GO_TEST_RE = /_test\.go$/;
|
|
17237
|
+
function isTestFile(rel) {
|
|
17238
|
+
const name = basename8(rel);
|
|
17239
|
+
return JS_TEST_RE.test(name) || PY_TEST_RE.test(name) || GO_TEST_RE.test(name);
|
|
17240
|
+
}
|
|
17241
|
+
|
|
17242
|
+
// ../../loki-ts/src/features/speed/unit_mode.ts
|
|
17243
|
+
import { closeSync as closeSync6, constants, fstatSync as fstatSync4, lstatSync as lstatSync5, openSync as openSync6, readSync as readSync4, realpathSync as realpathSync13 } from "fs";
|
|
17244
|
+
var MAX_SPEC_BYTES = 1024 * 1024;
|
|
17245
|
+
var MAX_TOKEN_BUDGET = 1e9;
|
|
17246
|
+
var norm2 = (p) => p.replace(/^\.\//, "");
|
|
17247
|
+
var CTRL = /[\x00-\x1f\x7f]/;
|
|
17248
|
+
var strs = (v) => Array.isArray(v) && v.every((x) => typeof x === "string" && !CTRL.test(x)) ? v.map(norm2) : null;
|
|
17249
|
+
function readBounded(path) {
|
|
17250
|
+
const l = lstatSync5(path);
|
|
17251
|
+
if (l.isSymbolicLink() || !l.isFile() || l.size > MAX_SPEC_BYTES)
|
|
17252
|
+
return null;
|
|
17253
|
+
const fd = openSync6(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
|
|
17254
|
+
try {
|
|
17255
|
+
const st = fstatSync4(fd);
|
|
17256
|
+
if (!st.isFile() || st.size > MAX_SPEC_BYTES)
|
|
17257
|
+
return null;
|
|
17258
|
+
const buf = Buffer.alloc(st.size + 1);
|
|
17259
|
+
let n2 = 0;
|
|
17260
|
+
for (;; ) {
|
|
17261
|
+
const r = readSync4(fd, buf, n2, buf.length - n2, null);
|
|
17262
|
+
if (r <= 0)
|
|
17263
|
+
break;
|
|
17264
|
+
n2 += r;
|
|
17265
|
+
if (n2 >= buf.length)
|
|
17266
|
+
return null;
|
|
17267
|
+
}
|
|
17268
|
+
return buf.subarray(0, n2).toString("utf8");
|
|
17269
|
+
} finally {
|
|
17270
|
+
closeSync6(fd);
|
|
17271
|
+
}
|
|
17272
|
+
}
|
|
17273
|
+
function parseText(txt) {
|
|
17274
|
+
try {
|
|
17275
|
+
const j = JSON.parse(txt), ws = strs(j.writeSet), pack = strs(j.pack), tb = j.tokenBudget;
|
|
17276
|
+
if (typeof j.id !== "string" || !j.id || !ws || ws.length === 0 || !pack || typeof tb !== "number" || !Number.isFinite(tb) || !(tb > 0) || tb > MAX_TOKEN_BUDGET)
|
|
17277
|
+
return null;
|
|
17278
|
+
return { id: j.id, writeSet: ws, pack, tokenBudget: tb };
|
|
17279
|
+
} catch {
|
|
17280
|
+
return null;
|
|
17281
|
+
}
|
|
17282
|
+
}
|
|
17283
|
+
function parseSpec(path) {
|
|
17284
|
+
try {
|
|
17285
|
+
const txt = readBounded(path);
|
|
17286
|
+
return txt === null ? null : parseText(txt);
|
|
17287
|
+
} catch {
|
|
17288
|
+
return null;
|
|
17289
|
+
}
|
|
17290
|
+
}
|
|
17291
|
+
var memo = new Map;
|
|
17292
|
+
function unitSpec(env = process.env) {
|
|
17293
|
+
const path = env["LOKI_UNIT_SPEC"], frozen = env["LOKI_UNIT_SPEC_FROZEN"];
|
|
17294
|
+
if (env["LOKI_SPEED"] === "0" || !path)
|
|
17295
|
+
return null;
|
|
17296
|
+
if (frozen !== undefined)
|
|
17297
|
+
return frozen.length <= MAX_SPEC_BYTES ? parseText(frozen) : null;
|
|
17298
|
+
if (!memo.has(path))
|
|
17299
|
+
memo.set(path, parseSpec(path));
|
|
17300
|
+
return memo.get(path) ?? null;
|
|
17301
|
+
}
|
|
17302
|
+
var inWriteSet = (spec, f) => spec.writeSet.some((w) => w.endsWith("/") ? f.startsWith(w) : f === w);
|
|
17303
|
+
function unitFlags(staged, env = process.env, pre = {}) {
|
|
17304
|
+
const s = unitSpec(env);
|
|
17305
|
+
return s ? staged.filter(({ f }) => !inWriteSet(s, f) && !Object.hasOwn(pre, f)).map(({ f }) => f) : null;
|
|
17306
|
+
}
|
|
17307
|
+
|
|
17308
|
+
// ../../loki-ts/src/e10ext/scope.ts
|
|
17309
|
+
var SCOPE_UNDETERMINED = "scope not determined; all edits committed";
|
|
17310
|
+
var outsideNote = (f) => `outside stated scope: ${f}`;
|
|
17311
|
+
var esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
17312
|
+
var mentions = (text2, name) => new RegExp(`(?<![\\w./-])${esc(name)}(?![\\w-]|\\.\\w)`).test(text2);
|
|
17313
|
+
var STOP = new Set(["the", "and", "for", "with", "that", "this", "all", "any", "src", "from", "into", "each", "across", "apply", "add", "use", "make"]);
|
|
17314
|
+
var stem = (w) => w.length > 3 && w.endsWith("s") ? w.slice(0, -1) : w;
|
|
17315
|
+
var surfaceTokens = (text2) => new Set((text2.toLowerCase().match(/[a-z][a-z0-9_-]{2,}/g) ?? []).filter((w) => !STOP.has(w)).map(stem));
|
|
17316
|
+
var dirSegs = (f) => f.split("/").slice(0, -1).map((d) => stem(d.toLowerCase()));
|
|
17317
|
+
function contractText(o) {
|
|
17318
|
+
const c = o.intake?.contract_snapshot?.contract;
|
|
17319
|
+
return Array.isArray(c?.criteria) ? c.criteria.map((x) => typeof x?.text === "string" ? x.text : "").join(`
|
|
17320
|
+
`) : "";
|
|
17321
|
+
}
|
|
17322
|
+
function outsideScope(o, staged) {
|
|
17323
|
+
const rel = Array.isArray(o.plan?.relevant_files) ? o.plan.relevant_files.filter((x) => typeof x === "string") : [];
|
|
17324
|
+
const plan = typeof o.plan?.plan === "string" ? o.plan.plan : "";
|
|
17325
|
+
if (rel.length === 0 && plan.trim() === "")
|
|
17326
|
+
return null;
|
|
17327
|
+
const text2 = `${plan}
|
|
17328
|
+
${typeof o.intake?.task === "string" ? o.intake.task : ""}
|
|
17329
|
+
${contractText(o)}`, pre = o.intake?.preexisting_dirty ?? {};
|
|
17330
|
+
const toks = surfaceTokens(text2), plannedDirs = new Set(rel.map((r) => dirname7(r)).filter((d) => d !== "."));
|
|
17331
|
+
return staged.filter(({ st, f }) => st !== "A" && !isTestFile(f) && !Object.hasOwn(pre, f) && !rel.includes(f) && !mentions(text2, f) && !mentions(text2, basename9(f)) && !plannedDirs.has(dirname7(f)) && !dirSegs(f).some((d) => toks.has(d))).map(({ f }) => f);
|
|
17332
|
+
}
|
|
17333
|
+
function flagOutsideScope(o, staged) {
|
|
17334
|
+
const unit = unitFlags(staged, process.env, o.intake?.preexisting_dirty ?? {});
|
|
17335
|
+
const un = outsideScope(o, staged), seen = new Set((unit ?? []).map(outsideNote));
|
|
17336
|
+
return [...(unit ?? []).map(outsideNote), ...un ? un.map(outsideNote).filter((n2) => !seen.has(n2)) : staged.length > 0 ? [SCOPE_UNDETERMINED] : []];
|
|
17337
|
+
}
|
|
17338
|
+
|
|
17339
|
+
// ../../loki-ts/src/util/receipt_signer.ts
|
|
17340
|
+
var RECEIPT_SIGNER_BASENAME = "receipt-ed25519.pem";
|
|
17341
|
+
|
|
17342
|
+
// ../../loki-ts/src/util/shell.ts
|
|
17343
|
+
var MAX_STDOUT_BYTES = 16 * 1024 * 1024;
|
|
17344
|
+
async function readStreamCapped(stream, maxBytes = MAX_STDOUT_BYTES) {
|
|
17345
|
+
const reader = stream.getReader();
|
|
17346
|
+
const decoder = new TextDecoder;
|
|
17347
|
+
let out2 = "";
|
|
17348
|
+
let total = 0;
|
|
17349
|
+
try {
|
|
17350
|
+
while (total < maxBytes) {
|
|
17351
|
+
const { done, value } = await reader.read();
|
|
17352
|
+
if (done)
|
|
17353
|
+
break;
|
|
17354
|
+
if (!value)
|
|
17355
|
+
continue;
|
|
17356
|
+
total += value.byteLength;
|
|
17357
|
+
if (total > maxBytes) {
|
|
17358
|
+
const keep = value.byteLength - (total - maxBytes);
|
|
17359
|
+
out2 += decoder.decode(value.subarray(0, keep), { stream: true });
|
|
17360
|
+
break;
|
|
17361
|
+
}
|
|
17362
|
+
out2 += decoder.decode(value, { stream: true });
|
|
17363
|
+
}
|
|
17364
|
+
out2 += decoder.decode();
|
|
17365
|
+
} finally {
|
|
17366
|
+
try {
|
|
17367
|
+
await reader.cancel();
|
|
17368
|
+
} catch {}
|
|
17369
|
+
reader.releaseLock();
|
|
17370
|
+
}
|
|
17371
|
+
return out2;
|
|
17372
|
+
}
|
|
17373
|
+
async function run(argv, opts = {}) {
|
|
17374
|
+
const proc = Bun.spawn({
|
|
17375
|
+
cmd: [...argv],
|
|
17376
|
+
stdout: "pipe",
|
|
17377
|
+
stderr: "pipe",
|
|
17378
|
+
env: opts.env ? { ...process.env, ...opts.env } : process.env,
|
|
17379
|
+
cwd: opts.cwd
|
|
17380
|
+
});
|
|
17381
|
+
let timer;
|
|
17382
|
+
let killTimer;
|
|
17383
|
+
if (opts.timeoutMs && opts.timeoutMs > 0) {
|
|
17384
|
+
timer = setTimeout(() => {
|
|
17385
|
+
try {
|
|
17386
|
+
proc.kill("SIGTERM");
|
|
17387
|
+
} catch {}
|
|
17388
|
+
killTimer = setTimeout(() => {
|
|
17389
|
+
try {
|
|
17390
|
+
proc.kill("SIGKILL");
|
|
17391
|
+
} catch {}
|
|
17392
|
+
}, 2000);
|
|
17393
|
+
}, opts.timeoutMs);
|
|
17394
|
+
}
|
|
17395
|
+
try {
|
|
17396
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
17397
|
+
readStreamCapped(proc.stdout),
|
|
17398
|
+
new Response(proc.stderr).text(),
|
|
17399
|
+
proc.exited
|
|
17400
|
+
]);
|
|
17401
|
+
return { stdout, stderr, exitCode };
|
|
17402
|
+
} finally {
|
|
17403
|
+
if (timer)
|
|
17404
|
+
clearTimeout(timer);
|
|
17405
|
+
if (killTimer)
|
|
17406
|
+
clearTimeout(killTimer);
|
|
17407
|
+
}
|
|
17408
|
+
}
|
|
17409
|
+
|
|
17406
17410
|
// ../../loki-ts/src/features/visual_evidence.ts
|
|
17411
|
+
import { spawn as spawn5 } from "child_process";
|
|
17407
17412
|
import { createHash as createHash6, randomBytes as randomBytes2 } from "crypto";
|
|
17408
17413
|
import { existsSync as existsSync15, lstatSync as lstatSync6, mkdirSync as mkdirSync7, readdirSync as readdirSync9, readFileSync as readFileSync20, writeFileSync as writeFileSync8 } from "fs";
|
|
17409
17414
|
import { createServer } from "net";
|
|
@@ -18087,7 +18092,7 @@ var commitStage = {
|
|
|
18087
18092
|
return sat;
|
|
18088
18093
|
if (drop.length > 0 && (await git(ctx, ["--literal-pathspecs", "reset", "-q", ctx.baseSha, "--", ...drop.map(({ f }) => f)])).code !== 0)
|
|
18089
18094
|
return { status: "failed", data: {}, reason: "git reset failed" };
|
|
18090
|
-
const
|
|
18095
|
+
const dropped2 = new Set(drop.map(({ f }) => f)), notes = flagOutsideScope(ctx.outputs(), staged.filter(({ f }) => !dropped2.has(f)));
|
|
18091
18096
|
if ((await git(ctx, ["diff", "--cached", "--quiet"])).code === 0)
|
|
18092
18097
|
return { status: "completed", data: { committed: false, scope_notes: notes } };
|
|
18093
18098
|
const title = (str3(ctx.outputs().intake?.title) ?? `run ${ctx.runId}`).split(`
|
|
@@ -18189,7 +18194,7 @@ var sealStage = {
|
|
|
18189
18194
|
}
|
|
18190
18195
|
if (!diffOk)
|
|
18191
18196
|
notProven.add("diff not computed (git diff-tree failed)");
|
|
18192
|
-
const
|
|
18197
|
+
const dropped2 = new Set;
|
|
18193
18198
|
const tcs = o.verify?.["test_counts"];
|
|
18194
18199
|
const cnt = (x) => {
|
|
18195
18200
|
const c = x;
|
|
@@ -18204,7 +18209,7 @@ var sealStage = {
|
|
|
18204
18209
|
const mine = assertDeltaNotes(ctx.repoDir, ctx.baseSha, head, t, str3(o.intake?.task) ?? "", cnt(tc?.b), cnt(tc?.h));
|
|
18205
18210
|
if (!mine || mine.length !== vl.length || mine.some((n2) => !vl.includes(n2)))
|
|
18206
18211
|
for (const v of vl)
|
|
18207
|
-
|
|
18212
|
+
dropped2.add(v);
|
|
18208
18213
|
}
|
|
18209
18214
|
for (const c of checks)
|
|
18210
18215
|
if (c.result === "not_run")
|
|
@@ -18212,7 +18217,7 @@ var sealStage = {
|
|
|
18212
18217
|
for (const f of strs2(o.verify?.flaky))
|
|
18213
18218
|
notProven.add(`flaky test: ${f}`);
|
|
18214
18219
|
for (const n2 of verifyNotProven)
|
|
18215
|
-
if (!
|
|
18220
|
+
if (!dropped2.has(n2))
|
|
18216
18221
|
notProven.add(n2);
|
|
18217
18222
|
for (const n2 of [...strs2(o.commit?.not_proven), ...strs2(o.commit?.scope_notes)])
|
|
18218
18223
|
notProven.add(n2);
|
|
@@ -11,9 +11,11 @@
|
|
|
11
11
|
"db:generate": "drizzle-kit generate --dialect sqlite --schema ./src/db/schema.ts --out ./drizzle"
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
14
15
|
"drizzle-orm": "^0.45.3",
|
|
15
16
|
"hono": "^4.13.12",
|
|
16
|
-
"yaml": "^2.9.1"
|
|
17
|
+
"yaml": "^2.9.1",
|
|
18
|
+
"zod": "^4.4.3"
|
|
17
19
|
},
|
|
18
20
|
"devDependencies": {
|
|
19
21
|
"@playwright/test": "1.58.1",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// CP-ASK slice 2: read-only data tools for Ask Loki, as pure functions over the CP database.
|
|
2
|
+
// Nothing here writes, spawns a build, or returns a path, URL or token. Run, event and artifact text is untrusted data.
|
|
3
|
+
// Cost and events reuse the route handlers through an in-process Hono (no HTTP, no duplicated logic); the artifact
|
|
4
|
+
// allowlist and read cap come from routes/artifacts.ts.
|
|
5
|
+
import { realpathSync } from "node:fs";
|
|
6
|
+
import { join, sep } from "node:path";
|
|
7
|
+
import { eq } from "drizzle-orm";
|
|
8
|
+
import { Hono } from "hono";
|
|
9
|
+
import type { Db } from "../db/migrate.ts";
|
|
10
|
+
import { localRepos } from "../db/schema.ts";
|
|
11
|
+
import { mount as mountArtifacts, NAME, readCapped, safeId } from "../server/routes/artifacts.ts";
|
|
12
|
+
import { mount as mountCost } from "../server/routes/cost.ts";
|
|
13
|
+
import { computeStats } from "../server/routes/stats.ts";
|
|
14
|
+
import type { RouteCtx } from "../server/routes/index.ts";
|
|
15
|
+
import { repoNames } from "../server/repos.ts";
|
|
16
|
+
import { listRuns, runDetail } from "../server/runs.ts";
|
|
17
|
+
|
|
18
|
+
export type ToolResult = Record<string, unknown>;
|
|
19
|
+
export const TOOL_NAMES = ["runs_search", "run_get", "run_events", "run_artifact", "runs_compare", "stats", "cost", "repos_list"] as const;
|
|
20
|
+
|
|
21
|
+
const err = (error: string): ToolResult => ({ error });
|
|
22
|
+
const isoOk = (s: string | undefined) => s === undefined || (!Number.isNaN(Date.parse(s)) && /^\d{4}-\d{2}-\d{2}/.test(s));
|
|
23
|
+
|
|
24
|
+
function internal(db: Db): Hono {
|
|
25
|
+
const app = new Hono();
|
|
26
|
+
const ctx = { app, act: new Hono(), db } as unknown as RouteCtx;
|
|
27
|
+
mountCost(ctx);
|
|
28
|
+
mountArtifacts(ctx);
|
|
29
|
+
return app;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function internalGet(db: Db, path: string): Promise<ToolResult> {
|
|
33
|
+
const r = await internal(db).request(path);
|
|
34
|
+
const body = (await r.json().catch(() => ({ error: "unreadable response" }))) as ToolResult;
|
|
35
|
+
return r.status === 200 ? body : err(typeof body.error === "string" ? body.error : `request failed (${r.status})`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function runsSearch(db: Db, a: { verdict?: string; repo?: string; since?: string; until?: string; group_id?: string; limit?: number; cursor?: string }): ToolResult {
|
|
39
|
+
if (!isoOk(a.since) || !isoOk(a.until)) return err("since and until must be ISO dates");
|
|
40
|
+
if (a.limit !== undefined && (!Number.isInteger(a.limit) || a.limit < 1 || a.limit > 200)) return err("limit must be 1..200");
|
|
41
|
+
return listRuns(db, a);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function runGet(db: Db, a: { source_id: string; run_id: string }): ToolResult {
|
|
45
|
+
if (!safeId(a.source_id) || !safeId(a.run_id)) return err("invalid source_id or run_id");
|
|
46
|
+
return runDetail(db, a.source_id, a.run_id) ?? err("run not found");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function runEvents(db: Db, a: { source_id: string; run_id: string; after?: number; limit?: number }): Promise<ToolResult> {
|
|
50
|
+
if (!safeId(a.source_id) || !safeId(a.run_id)) return err("invalid source_id or run_id");
|
|
51
|
+
if (a.after !== undefined && (!Number.isInteger(a.after) || a.after < 0)) return err("after must be a non-negative integer");
|
|
52
|
+
if (a.limit !== undefined && (!Number.isInteger(a.limit) || a.limit < 1)) return err("limit must be a positive integer");
|
|
53
|
+
const q = new URLSearchParams();
|
|
54
|
+
if (a.after !== undefined) q.set("after", String(a.after));
|
|
55
|
+
if (a.limit !== undefined) q.set("limit", String(a.limit));
|
|
56
|
+
return internalGet(db, `/v1/runs/${a.source_id}/${a.run_id}/events?${q}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One allowlisted artifact of a run, read through the same containment the HTTP route uses. Binary (png) is refused. */
|
|
60
|
+
export function runArtifact(db: Db, a: { source_id: string; run_id: string; name: string }): ToolResult {
|
|
61
|
+
if (!safeId(a.source_id) || !safeId(a.run_id)) return err("invalid source_id or run_id");
|
|
62
|
+
if (typeof a.name !== "string" || !NAME.test(a.name)) return err("artifact is not on the allowlist");
|
|
63
|
+
if (a.name.endsWith(".png")) return err("binary artifacts are not readable through this tool");
|
|
64
|
+
if (!runDetail(db, a.source_id, a.run_id)) return err("run not found");
|
|
65
|
+
const repo = db.select({ p: localRepos.realpath }).from(localRepos).where(eq(localRepos.sourceId, a.source_id)).get();
|
|
66
|
+
if (!repo) return err("artifact not available");
|
|
67
|
+
try {
|
|
68
|
+
const runsRoot = realpathSync(join(repo.p, ".loki", "runs"));
|
|
69
|
+
const runDir = realpathSync(join(runsRoot, a.run_id));
|
|
70
|
+
if (runDir !== join(runsRoot, a.run_id)) return err("artifact not found");
|
|
71
|
+
const file = realpathSync(join(runDir, a.name));
|
|
72
|
+
if (!file.startsWith(runDir + sep)) return err("artifact not found");
|
|
73
|
+
const r = readCapped(file);
|
|
74
|
+
if (r.status !== 200) return err(r.status === 413 ? "artifact too large" : "artifact not found");
|
|
75
|
+
return { name: a.name, content: r.body.toString("utf8") };
|
|
76
|
+
} catch { return err("artifact not found"); }
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Both runs side by side: ids, verdicts, costs, tokens and the cost delta (b minus a). */
|
|
80
|
+
export function runsCompare(db: Db, a: { source_a: string; run_a: string; source_b: string; run_b: string }): ToolResult {
|
|
81
|
+
const out: Record<string, unknown>[] = [];
|
|
82
|
+
for (const [s, r] of [[a.source_a, a.run_a], [a.source_b, a.run_b]] as const) {
|
|
83
|
+
const d = runGet(db, { source_id: s, run_id: r }) as Record<string, any>;
|
|
84
|
+
if (d.error) return err(`${s}/${r}: ${d.error}`);
|
|
85
|
+
out.push({
|
|
86
|
+
source_id: d.source_id, run_id: d.run_id, origin_repo: d.origin_repo, verdict: d.verdict, effective_verdict: d.effective_verdict,
|
|
87
|
+
status: d.status, provider: d.provider, model: d.model, cost_usd: d.cost_usd, partial_usd: d.partial_usd,
|
|
88
|
+
input_tokens: d.input_tokens, output_tokens: d.output_tokens, wall_s: d.wall_s, pr_url: d.pr_url, tampered: d.tampered,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const ca = out[0]?.cost_usd, cb = out[1]?.cost_usd;
|
|
92
|
+
const delta = typeof ca === "number" && typeof cb === "number" ? Math.round((cb - ca) * 1e6) / 1e6 : null;
|
|
93
|
+
return { runs: out, cost_delta_usd: delta };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function stats(db: Db, a: { since?: string }): ToolResult {
|
|
97
|
+
if (!isoOk(a.since)) return err("since must be an ISO date");
|
|
98
|
+
return computeStats(db, a.since === undefined ? null : new Date(a.since).toISOString()) as unknown as ToolResult;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function cost(db: Db, a: { group?: string; since?: string }): Promise<ToolResult> | ToolResult {
|
|
102
|
+
if (!isoOk(a.since)) return err("since must be an ISO date");
|
|
103
|
+
const q = new URLSearchParams({ group: a.group ?? "day" });
|
|
104
|
+
if (a.since) q.set("since", a.since);
|
|
105
|
+
return internalGet(db, `/v1/stats/cost?${q}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Display names only, never paths. */
|
|
109
|
+
export function reposList(db: Db): ToolResult {
|
|
110
|
+
return { repos: repoNames(db) };
|
|
111
|
+
}
|