toolroll 0.9.40 → 0.9.42
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/CHANGELOG.md +28 -0
- package/dist/cli.js +3 -1
- package/dist/coding-types.d.ts +2 -24
- package/dist/coding-workspace.js +12 -11
- package/dist/contracts/cli.d.ts +736 -0
- package/dist/contracts/cli.js +223 -0
- package/dist/contracts/coding-activity.d.ts +64 -0
- package/dist/contracts/coding-activity.js +58 -0
- package/dist/contracts/console-api.d.ts +483 -0
- package/dist/contracts/console-api.js +273 -0
- package/dist/contracts/diff-stat.d.ts +58 -0
- package/dist/contracts/diff-stat.js +59 -0
- package/dist/contracts/integration-metadata.d.ts +99 -0
- package/dist/contracts/integration-metadata.js +101 -0
- package/dist/contracts/model-catalog.d.ts +21 -0
- package/dist/contracts/model-catalog.js +14 -0
- package/dist/contracts/native-census.d.ts +42 -0
- package/dist/contracts/native-census.js +36 -0
- package/dist/contracts/observation-request.d.ts +37 -0
- package/dist/contracts/observation-request.js +25 -0
- package/dist/contracts/provider.d.ts +30 -0
- package/dist/contracts/provider.js +15 -0
- package/dist/contracts/recipes.d.ts +166 -0
- package/dist/contracts/recipes.js +52 -0
- package/dist/contracts/retention.d.ts +42 -0
- package/dist/contracts/retention.js +24 -0
- package/dist/contracts/storage.d.ts +23 -0
- package/dist/contracts/storage.js +10 -0
- package/dist/contracts/store-json.d.ts +315 -0
- package/dist/contracts/store-json.js +196 -0
- package/dist/contracts/trigger-payloads.d.ts +369 -0
- package/dist/contracts/trigger-payloads.js +144 -0
- package/dist/contracts/update-journal.d.ts +254 -0
- package/dist/contracts/update-journal.js +155 -0
- package/dist/contracts/verification-view.d.ts +93 -0
- package/dist/contracts/verification-view.js +43 -0
- package/dist/control-setup.d.ts +1 -1
- package/dist/desktop-update.d.ts +7 -40
- package/dist/desktop-update.js +36 -8
- package/dist/evidence.d.ts +3 -19
- package/dist/flow-triggers.js +61 -60
- package/dist/mcp-connect.d.ts +0 -1
- package/dist/mcp-connect.js +41 -23
- package/dist/model-catalog.js +8 -6
- package/dist/observations.d.ts +2 -6
- package/dist/observations.js +13 -9
- package/dist/operate.js +2 -1
- package/dist/process-recovery-native.d.ts +2 -13
- package/dist/process-recovery-native.js +8 -5
- package/dist/project-tools.d.ts +6 -24
- package/dist/project-tools.js +22 -18
- package/dist/provider.d.ts +3 -8
- package/dist/provider.js +15 -11
- package/dist/recipes.d.ts +3 -28
- package/dist/recipes.js +79 -52
- package/dist/retention.d.ts +4 -5
- package/dist/retention.js +5 -4
- package/dist/serve.js +142 -28
- package/dist/storage.d.ts +2 -1
- package/dist/storage.js +12 -9
- package/dist/store.d.ts +3 -15
- package/dist/store.js +80 -158
- package/dist/surface.d.ts +6 -54
- package/dist/surface.js +6 -1
- package/dist/toolroll-update.d.ts +8 -99
- package/dist/toolroll-update.js +28 -18
- package/dist/verification-evidence.js +27 -12
- package/package.json +1 -1
package/dist/desktop-update.js
CHANGED
|
@@ -12,6 +12,7 @@ import { activeUpdateWork, freezeUpdateGate, installUpdateGate, removeUpdateGate
|
|
|
12
12
|
import { currentDesktopAccess } from "./desktop-access.js";
|
|
13
13
|
import { assertCodingUpdateStopped, backupCodingCatalog, codingCatalogExists, releaseStaleCodingOwner } from "./coding-update.js";
|
|
14
14
|
import { processMayBeAlive } from "./process-liveness.js";
|
|
15
|
+
import { readDesktopRecoveryView, readDesktopUpdateJournal, supervisorPidsOf, updateRequestIs } from "./contracts/update-journal.js";
|
|
15
16
|
/** Loaded on first use (as backup.ts and store.ts do), so modules that only import this one (the console,
|
|
16
17
|
* and tests that load it in a browser-like environment) never need `node:sqlite` itself. */
|
|
17
18
|
function sqlite() {
|
|
@@ -60,13 +61,40 @@ function privateFile(file) {
|
|
|
60
61
|
if (!stat.isFile() || stat.isSymbolicLink() || (process.platform !== "win32" && ((stat.mode & 0o077) !== 0 || stat.uid !== process.getuid?.())))
|
|
61
62
|
throw Error("The update record must be an owner-only regular file.");
|
|
62
63
|
}
|
|
64
|
+
/** The refusal the hand-written reader gave for the field a saved journal first gets wrong, so callers and people read
|
|
65
|
+
* the same words; fields it never checked read as an invalid record. */
|
|
66
|
+
function journalRefusal(issue) {
|
|
67
|
+
const [field, inner] = issue.path.split(/[.[]/);
|
|
68
|
+
switch (field) {
|
|
69
|
+
case "old":
|
|
70
|
+
if (inner === undefined || inner === "path")
|
|
71
|
+
return "The saved update record is invalid. Preserve it and the app backups; nothing was changed.";
|
|
72
|
+
return "The saved update paths or build identities are invalid. Nothing was changed.";
|
|
73
|
+
case "next":
|
|
74
|
+
case "databaseFile":
|
|
75
|
+
case "configHash":
|
|
76
|
+
case "wasRunning":
|
|
77
|
+
case "backupPath":
|
|
78
|
+
case "backupHash":
|
|
79
|
+
return "The saved update paths or build identities are invalid. Nothing was changed.";
|
|
80
|
+
case "replacementOccurred": return "The recorded replacement state is invalid. Nothing was changed.";
|
|
81
|
+
case "codingCatalogExpected": return "The recorded coding catalog presence is invalid. Nothing was changed.";
|
|
82
|
+
case "stoppedPids": return "The recorded service processes are invalid. Nothing was changed.";
|
|
83
|
+
case "codingBackupPath":
|
|
84
|
+
case "codingBackupHash": return "The retained coding backup paths or identity are invalid. Nothing was changed.";
|
|
85
|
+
default: return "The saved update record is invalid. Preserve it and the app backups; nothing was changed.";
|
|
86
|
+
}
|
|
87
|
+
}
|
|
63
88
|
export function readUpdateJournal(stateDir, retainedReceipt) {
|
|
64
89
|
const file = retainedReceipt ?? journalPath(stateDir);
|
|
65
90
|
if (!existsSync(file))
|
|
66
91
|
return null;
|
|
67
92
|
privateFile(file);
|
|
68
|
-
const
|
|
69
|
-
if (
|
|
93
|
+
const read = readDesktopUpdateJournal(JSON.parse(readFileSync(file, "utf8")));
|
|
94
|
+
if (!read.ok)
|
|
95
|
+
throw Error(`${journalRefusal(read.issues[0])} (${read.issues.map(issue => issue.line).join("; ")})`);
|
|
96
|
+
const j = read.value;
|
|
97
|
+
if (!/^[a-f0-9-]{36}$/.test(j.id) || j.stateDir !== resolve(stateDir) || !isAbsolute(j.old?.path ?? "") || j.workDir !== join(dirname(j.old.path), ".standing-orders-updates", j.id) || !/^[a-zA-Z0-9.-]+$/.test(j.label) || !["install", "restore"].includes(j.intended) || !["prepared", "draining", "backing-up", "stopping", "installing", "verifying", "rolling-back", "releasing", "complete", "restored", "cancelled", "needs-attention"].includes(j.phase))
|
|
70
98
|
throw Error("The saved update record is invalid. Preserve it and the app backups; nothing was changed.");
|
|
71
99
|
if (retainedReceipt && retainedReceipt !== join(j.workDir, "receipt.json"))
|
|
72
100
|
throw Error("The retained recovery receipt is at the wrong path.");
|
|
@@ -374,7 +402,7 @@ export function desktopUpdateStatus(stateDir) {
|
|
|
374
402
|
let recovery = null;
|
|
375
403
|
try {
|
|
376
404
|
privateFile(join(j.workDir, "recovery.json"));
|
|
377
|
-
recovery = JSON.parse(readFileSync(join(j.workDir, "recovery.json"), "utf8"));
|
|
405
|
+
recovery = readDesktopRecoveryView(JSON.parse(readFileSync(join(j.workDir, "recovery.json"), "utf8")));
|
|
378
406
|
}
|
|
379
407
|
catch { /* A legacy/manual update has no guardian. */ }
|
|
380
408
|
const wantsAutomatic = recovery?.id === j.id && ["armed", "running", "backoff"].includes(recovery.state ?? "") && !terminal(j.phase);
|
|
@@ -389,8 +417,7 @@ export function updateStopRequested(j) {
|
|
|
389
417
|
if (!existsSync(file))
|
|
390
418
|
return false;
|
|
391
419
|
privateFile(file);
|
|
392
|
-
|
|
393
|
-
if (request.id !== j.id || request.action !== "stop")
|
|
420
|
+
if (!updateRequestIs(JSON.parse(readFileSync(file, "utf8")), j.id, "stop"))
|
|
394
421
|
throw Error("The saved stop request is invalid. Automatic starts are refused.");
|
|
395
422
|
return true;
|
|
396
423
|
}
|
|
@@ -410,7 +437,9 @@ export function requestUpdateRestore(stateDir) {
|
|
|
410
437
|
function restoreRequested(j) {
|
|
411
438
|
try {
|
|
412
439
|
const request = JSON.parse(readFileSync(join(j.workDir, "request.json"), "utf8"));
|
|
413
|
-
|
|
440
|
+
if (request === null)
|
|
441
|
+
throw Error("The saved restore request is invalid.");
|
|
442
|
+
return updateRequestIs(request, j.id, "restore");
|
|
414
443
|
}
|
|
415
444
|
catch (error) {
|
|
416
445
|
if (error.code === "ENOENT")
|
|
@@ -543,8 +572,7 @@ function assertDesktopCodingStopped(j, db) {
|
|
|
543
572
|
/** The service's own processes, from its supervisor's status file: read before the stop, never after. */
|
|
544
573
|
function servicePids(stateDir) {
|
|
545
574
|
try {
|
|
546
|
-
|
|
547
|
-
return [status.supervisorPid, status.controllerPid].filter((pid) => Number.isSafeInteger(pid) && Number(pid) > 1);
|
|
575
|
+
return supervisorPidsOf(JSON.parse(readFileSync(join(stateDir, "controller-supervisor.json"), "utf8")));
|
|
548
576
|
}
|
|
549
577
|
catch {
|
|
550
578
|
return [];
|
package/dist/evidence.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import { type ParsedReport, type ReportImage } from "./scout-report.js";
|
|
17
17
|
import { type ParsedProof } from "./proof.js";
|
|
18
18
|
import { type HandoffArtifact } from "./contracts/handoff.js";
|
|
19
|
+
import type { DiffStat } from "./contracts/diff-stat.js";
|
|
19
20
|
export type { HandoffArtifact } from "./contracts/handoff.js";
|
|
20
21
|
import type { Artifact, Store } from "./store.js";
|
|
21
22
|
import type { ExecResult } from "./exec.js";
|
|
@@ -229,25 +230,8 @@ export declare function quarantineMailboxes(worktree: string, root: string | nul
|
|
|
229
230
|
export declare function captureParkEvidence(store: Store, git: (file: string, args: readonly string[], options?: {
|
|
230
231
|
cwd?: string;
|
|
231
232
|
}) => Promise<ExecResult>, worktree: string, baseRevision: string | null, root: string, runId: number, now: Date): Promise<number[]>;
|
|
232
|
-
/**
|
|
233
|
-
export type DiffStatFile
|
|
234
|
-
path: string;
|
|
235
|
-
additions: number | null;
|
|
236
|
-
deletions: number | null;
|
|
237
|
-
renamedFrom?: string;
|
|
238
|
-
};
|
|
239
|
-
export type DiffStat = {
|
|
240
|
-
schema: 1;
|
|
241
|
-
base: string;
|
|
242
|
-
head: string;
|
|
243
|
-
fileCount: number;
|
|
244
|
-
additions: number;
|
|
245
|
-
deletions: number;
|
|
246
|
-
binaryCount: number;
|
|
247
|
-
files: DiffStatFile[];
|
|
248
|
-
/** True when the file list was cut to fit the cap — counts stay complete. */
|
|
249
|
-
filesTruncated: boolean;
|
|
250
|
-
};
|
|
233
|
+
/** The terminal diff-stat and its file rows: src/contracts/diff-stat.ts. */
|
|
234
|
+
export type { DiffStat, DiffStatFile } from "./contracts/diff-stat.js";
|
|
251
235
|
/**
|
|
252
236
|
* Parse `git diff --numstat -z` output. NUL-delimited, so filenames are never
|
|
253
237
|
* guessed out of patch headers (Codex roadmap review, scope cut A.3): plain
|
package/dist/flow-triggers.js
CHANGED
|
@@ -42,6 +42,7 @@ import { mailboxAccess, mailCursorOf, mailCursorText, readThroughImap } from "./
|
|
|
42
42
|
import { readFlowSecrets } from "./flow-secrets.js";
|
|
43
43
|
import { tmpdir } from "node:os";
|
|
44
44
|
import { envValue } from "./names.js";
|
|
45
|
+
import { linearLabelNames, readFormSubmission, readGithubEvent, readGithubIssue, readGithubIssueEventList, readGithubIssueList, readGithubRun, readGithubRunList, readInboundMail, readLinearAnswer, readLinearEvent, readLinearIssue, readWebhookPayload } from "./contracts/trigger-payloads.js";
|
|
45
46
|
export { FLOW_TRIGGER_KINDS };
|
|
46
47
|
export const CHAT_APP_NAMES = { slack: "Slack", discord: "Discord", teams: "Teams", telegram: "Telegram" };
|
|
47
48
|
/** What each kind is called on the canvas. */
|
|
@@ -771,7 +772,7 @@ async function fetchMail(store, config, trigger, io, now) {
|
|
|
771
772
|
if (!read.ok)
|
|
772
773
|
return { ok: false, problem: read.said };
|
|
773
774
|
const own = signed.address.toLowerCase();
|
|
774
|
-
const items = read.mails.filter(mail => mailMatches(config, mail)).map(mail => {
|
|
775
|
+
const items = read.mails.map(readInboundMail).filter(mail => mailMatches(config, mail)).map(mail => {
|
|
775
776
|
const key = `mail:${read.at.validity}:${mail.uid}`, at = mailCursorText({ validity: read.at.validity, uid: mail.uid });
|
|
776
777
|
const label = `Email from ${mail.from || "an unknown sender"}`.slice(0, 120);
|
|
777
778
|
// Machines and this account itself never start cards: a flow that answers mail mustn't answer them.
|
|
@@ -793,44 +794,42 @@ async function fetchMail(store, config, trigger, io, now) {
|
|
|
793
794
|
: read.renumbered ? "The mail server renumbered this folder; watching it from now." : undefined;
|
|
794
795
|
return { ok: true, items, cursor: mailCursorText(read.at), more: read.more, ...(said === undefined ? {} : { said }) };
|
|
795
796
|
}
|
|
796
|
-
const text = (value) => typeof value === "string" ? value : "";
|
|
797
|
-
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
798
797
|
/** A GitHub issue or pull request as a card, or why it is left out; null when it doesn't match. */
|
|
799
798
|
export function githubItem(config, raw, what) {
|
|
800
|
-
const item =
|
|
801
|
-
const number = Number(item
|
|
799
|
+
const item = readGithubIssue(raw);
|
|
800
|
+
const number = Number(item.number);
|
|
802
801
|
if (!Number.isSafeInteger(number))
|
|
803
802
|
return null;
|
|
804
|
-
if (what === "issue" && item
|
|
803
|
+
if (what === "issue" && item.pull_request !== undefined)
|
|
805
804
|
return null;
|
|
806
|
-
if (what === "pull" && item
|
|
805
|
+
if (what === "pull" && item.draft === true)
|
|
807
806
|
return null;
|
|
808
|
-
const labels =
|
|
807
|
+
const labels = (item.labels ?? []).map(one => (one.name ?? "").toLowerCase());
|
|
809
808
|
if (config.label !== null && !labels.includes(config.label.toLowerCase()))
|
|
810
809
|
return null;
|
|
811
810
|
const label = what === "issue" ? `GitHub issue #${number}` : `Pull request #${number}`;
|
|
812
|
-
const key = `${what}:${number}`, at =
|
|
813
|
-
if (config.from === "team" && !TEAM.has(
|
|
811
|
+
const key = `${what}:${number}`, at = item.updated_at || null;
|
|
812
|
+
if (config.from === "team" && !TEAM.has(item.author_association ?? ""))
|
|
814
813
|
return { key, skip: "opened by someone without write access", label, at };
|
|
815
|
-
const login =
|
|
816
|
-
const branches = what === "pull" ? ` (${
|
|
817
|
-
return { key, at, title:
|
|
818
|
-
description: `From ${what === "issue" ? "GitHub issue" : "pull request"} #${number} by @${login}${branches}:\n\n${
|
|
819
|
-
source: { kind: "github", label, url:
|
|
814
|
+
const login = item.user?.login || "someone";
|
|
815
|
+
const branches = what === "pull" ? ` (${item.head?.ref ?? ""} → ${item.base?.ref ?? ""})` : "";
|
|
816
|
+
return { key, at, title: item.title || label,
|
|
817
|
+
description: `From ${what === "issue" ? "GitHub issue" : "pull request"} #${number} by @${login}${branches}:\n\n${item.body ?? ""}`.trim().slice(0, 4000),
|
|
818
|
+
source: { kind: "github", label, url: item.html_url || null } };
|
|
820
819
|
}
|
|
821
820
|
/** A failed workflow run as a card: one card per failing commit on the watched branch. */
|
|
822
821
|
export function githubRunItem(config, raw) {
|
|
823
|
-
const run =
|
|
824
|
-
if (
|
|
822
|
+
const run = readGithubRun(raw);
|
|
823
|
+
if (run.conclusion !== "failure" || (config.branch !== null && (run.head_branch ?? "") !== config.branch))
|
|
825
824
|
return null;
|
|
826
|
-
const sha =
|
|
825
|
+
const sha = run.head_sha ?? "";
|
|
827
826
|
if (sha === "")
|
|
828
827
|
return null;
|
|
829
|
-
const name =
|
|
830
|
-
const commit =
|
|
831
|
-
return { key: `checks:${sha}`, at:
|
|
832
|
-
description: `From GitHub: “${name}” failed on ${config.branch} at ${sha.slice(0, 7)}${commit === "" ? "" : ` (${commit})`}.\nRun: ${
|
|
833
|
-
source: { kind: "github", label: `Failed check on ${config.branch}`, url:
|
|
828
|
+
const name = run.name || "Checks";
|
|
829
|
+
const commit = run.display_title ?? "";
|
|
830
|
+
return { key: `checks:${sha}`, at: run.created_at || null, title: `Checks failed on ${config.branch}: ${name}`,
|
|
831
|
+
description: `From GitHub: “${name}” failed on ${config.branch} at ${sha.slice(0, 7)}${commit === "" ? "" : ` (${commit})`}.\nRun: ${run.html_url ?? ""}`,
|
|
832
|
+
source: { kind: "github", label: `Failed check on ${config.branch}`, url: run.html_url || null } };
|
|
834
833
|
}
|
|
835
834
|
async function fetchGitHub(config, trigger, gh) {
|
|
836
835
|
const since = trigger.cursor ?? trigger.createdAt;
|
|
@@ -861,20 +860,19 @@ async function fetchGitHub(config, trigger, gh) {
|
|
|
861
860
|
}
|
|
862
861
|
const created = Date.parse(trigger.createdAt);
|
|
863
862
|
if (config.watch === "checks") {
|
|
864
|
-
const runs = (
|
|
863
|
+
const runs = (readGithubRunList(body).workflow_runs ?? []).filter(one => Date.parse(one.created_at ?? "") > created);
|
|
865
864
|
const items = runs.reverse().map(one => githubRunItem(config, one)).filter((one) => one !== null);
|
|
866
865
|
return { ok: true, items: unique(items), cursor: trigger.cursor };
|
|
867
866
|
}
|
|
868
|
-
const list = Array.isArray(body) ? body : [];
|
|
869
867
|
if (labelEvents) {
|
|
870
|
-
const labeled =
|
|
871
|
-
&& Date.parse(
|
|
872
|
-
const items = labeled.map(one => { const item = githubItem(config,
|
|
873
|
-
return { ok: true, items: unique(items), cursor: labeled.map(one =>
|
|
868
|
+
const labeled = readGithubIssueEventList(body).filter(one => one.event === "labeled" && (one.label?.name ?? "").toLowerCase() === config.label.toLowerCase()
|
|
869
|
+
&& Date.parse(one.created_at ?? "") > Date.parse(since)).reverse();
|
|
870
|
+
const items = labeled.map(one => { const item = githubItem(config, one.issue, "issue"); return item === null ? null : { ...item, at: one.created_at || null }; }).filter((one) => one !== null);
|
|
871
|
+
return { ok: true, items: unique(items), cursor: labeled.map(one => one.created_at ?? "").at(-1) ?? trigger.cursor };
|
|
874
872
|
}
|
|
875
873
|
// New issues and pull requests: opened after the trigger was added, oldest first; the key keeps each to one card.
|
|
876
874
|
const what = config.watch === "pulls" ? "pull" : "issue";
|
|
877
|
-
const items =
|
|
875
|
+
const items = readGithubIssueList(body).filter(one => Date.parse(one.created_at ?? "") > created).reverse().map(one => githubItem(config, one, what)).filter((one) => one !== null);
|
|
878
876
|
return { ok: true, items, cursor: trigger.cursor };
|
|
879
877
|
}
|
|
880
878
|
const unique = (items) => items.filter((item, index) => items.findIndex(one => one.key === item.key) === index);
|
|
@@ -882,21 +880,20 @@ export const LINEAR_URL = "https://api.linear.app/graphql";
|
|
|
882
880
|
export const LINEAR_QUERY = "query FlowTrigger($filter: IssueFilter) { issues(filter: $filter, first: 50, orderBy: updatedAt) { nodes { identifier title description url updatedAt state { name } team { key } labels { nodes { name } } } } }";
|
|
883
881
|
/** A Linear issue as a card; null when it doesn't match the trigger. */
|
|
884
882
|
export function linearItem(config, raw) {
|
|
885
|
-
const issue =
|
|
886
|
-
const identifier =
|
|
883
|
+
const issue = readLinearIssue(raw);
|
|
884
|
+
const identifier = issue.identifier ?? "";
|
|
887
885
|
if (identifier === "")
|
|
888
886
|
return null;
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
if (config.team !== null && text(record(issue["team"])["key"]).toUpperCase() !== config.team)
|
|
887
|
+
const labels = linearLabelNames(issue).map(one => one.toLowerCase());
|
|
888
|
+
if (config.team !== null && (issue.team?.key ?? "").toUpperCase() !== config.team)
|
|
892
889
|
return null;
|
|
893
|
-
if (config.state !== null &&
|
|
890
|
+
if (config.state !== null && (issue.state?.name ?? "").toLowerCase() !== config.state.toLowerCase())
|
|
894
891
|
return null;
|
|
895
892
|
if (config.label !== null && !labels.includes(config.label.toLowerCase()))
|
|
896
893
|
return null;
|
|
897
|
-
return { key: `linear:${identifier}`, at:
|
|
898
|
-
description: `From Linear ${identifier}:\n\n${
|
|
899
|
-
source: { kind: "linear", label: `Linear ${identifier}`, url:
|
|
894
|
+
return { key: `linear:${identifier}`, at: issue.updatedAt || null, title: issue.title || identifier,
|
|
895
|
+
description: `From Linear ${identifier}:\n\n${issue.description ?? ""}`.trim().slice(0, 4000),
|
|
896
|
+
source: { kind: "linear", label: `Linear ${identifier}`, url: issue.url || null } };
|
|
900
897
|
}
|
|
901
898
|
async function fetchLinear(config, trigger, io) {
|
|
902
899
|
const key = readLinearKey(io.dir);
|
|
@@ -917,22 +914,22 @@ async function fetchLinear(config, trigger, io) {
|
|
|
917
914
|
catch {
|
|
918
915
|
return { ok: false, problem: "Couldn't reach Linear." };
|
|
919
916
|
}
|
|
920
|
-
let
|
|
917
|
+
let raw;
|
|
921
918
|
try {
|
|
922
|
-
|
|
919
|
+
raw = await response.json();
|
|
923
920
|
}
|
|
924
921
|
catch {
|
|
925
922
|
return { ok: false, problem: `Linear answered ${response.status} and it couldn't be read.` };
|
|
926
923
|
}
|
|
927
|
-
const
|
|
924
|
+
const body = readLinearAnswer(raw);
|
|
925
|
+
const errors = (body.errors ?? []).map(one => one.message ?? "");
|
|
928
926
|
if (response.status === 401 || errors.some(one => /authenticat|api key/i.test(one)))
|
|
929
927
|
return { ok: false, problem: "Linear didn't accept the key. Add a new one on this flow's Triggers panel." };
|
|
930
928
|
if (!response.ok || errors.length > 0)
|
|
931
929
|
return { ok: false, problem: `Linear said: ${(errors[0] ?? `status ${response.status}`).slice(0, 160)}` };
|
|
932
|
-
const
|
|
933
|
-
const issues = (Array.isArray(nodes) ? nodes : []).sort((a, b) => text(record(a)["updatedAt"]).localeCompare(text(record(b)["updatedAt"])));
|
|
930
|
+
const issues = (body.data?.issues?.nodes ?? []).sort((a, b) => (a.updatedAt ?? "").localeCompare(b.updatedAt ?? ""));
|
|
934
931
|
const items = issues.map(one => linearItem(config, one)).filter((one) => one !== null);
|
|
935
|
-
return { ok: true, items, cursor: issues.map(one =>
|
|
932
|
+
return { ok: true, items, cursor: issues.map(one => one.updatedAt ?? "").filter(one => one !== "").at(-1) ?? trigger.cursor };
|
|
936
933
|
}
|
|
937
934
|
const header = (delivery, name) => { const value = delivery.headers[name]; return Array.isArray(value) ? value[0] ?? "" : value ?? ""; };
|
|
938
935
|
function sameHex(expected, given) {
|
|
@@ -963,8 +960,8 @@ export function receiveFlowHook(store, token, delivery, dir, now) {
|
|
|
963
960
|
const signature = header(delivery, "x-hub-signature-256");
|
|
964
961
|
if (secret === null || !sameHex(`sha256=${createHmac("sha256", secret).update(delivery.body).digest("hex")}`, signature))
|
|
965
962
|
return { status: 401, said: "Signature doesn't match." };
|
|
966
|
-
const event = header(delivery, "x-github-event"), body =
|
|
967
|
-
const repository =
|
|
963
|
+
const event = header(delivery, "x-github-event"), body = readGithubEvent(payload), action = body.action ?? "";
|
|
964
|
+
const repository = body.repository?.full_name ?? "";
|
|
968
965
|
if (event === "ping") {
|
|
969
966
|
note("GitHub connected.");
|
|
970
967
|
return { status: 200, said: "Connected." };
|
|
@@ -972,10 +969,10 @@ export function receiveFlowHook(store, token, delivery, dir, now) {
|
|
|
972
969
|
if (repository.toLowerCase() !== config.repo.toLowerCase())
|
|
973
970
|
return { status: 202, said: "Not this repository." };
|
|
974
971
|
// A label trigger takes the moment its label is added (or an issue opened with it); a plain one takes new issues.
|
|
975
|
-
const labeledNow = action === "labeled" && config.label !== null &&
|
|
976
|
-
const found = config.watch === "issues" && event === "issues" && (action === "opened" || labeledNow) ? githubItem(config, body
|
|
977
|
-
: config.watch === "pulls" && event === "pull_request" && ["opened", "ready_for_review", "labeled", "reopened"].includes(action) ? githubItem(config, body
|
|
978
|
-
: config.watch === "checks" && event === "workflow_run" && action === "completed" ? githubRunItem(config, body
|
|
972
|
+
const labeledNow = action === "labeled" && config.label !== null && (body.label?.name ?? "").toLowerCase() === config.label.toLowerCase();
|
|
973
|
+
const found = config.watch === "issues" && event === "issues" && (action === "opened" || labeledNow) ? githubItem(config, body.issue, "issue")
|
|
974
|
+
: config.watch === "pulls" && event === "pull_request" && ["opened", "ready_for_review", "labeled", "reopened"].includes(action) ? githubItem(config, body.pull_request, "pull")
|
|
975
|
+
: config.watch === "checks" && event === "workflow_run" && action === "completed" ? githubRunItem(config, body.workflow_run) : null;
|
|
979
976
|
items = found === null ? [] : [found];
|
|
980
977
|
}
|
|
981
978
|
else if (config.kind === "linear") {
|
|
@@ -984,23 +981,26 @@ export function receiveFlowHook(store, token, delivery, dir, now) {
|
|
|
984
981
|
return { status: 401, said: "Paste Linear's signing secret on the flow's Triggers panel first." };
|
|
985
982
|
if (!sameHex(createHmac("sha256", secret).update(delivery.body).digest("hex"), header(delivery, "linear-signature")))
|
|
986
983
|
return { status: 401, said: "Signature doesn't match." };
|
|
987
|
-
const body =
|
|
984
|
+
const body = readLinearEvent(payload), sent = Number(body.webhookTimestamp);
|
|
988
985
|
if (!Number.isFinite(sent) || Math.abs(now.getTime() - sent) > 5 * 60_000)
|
|
989
986
|
return { status: 401, said: "Too old." };
|
|
990
|
-
if (
|
|
987
|
+
if (body.type !== "Issue" || !["create", "update"].includes(body.action ?? ""))
|
|
991
988
|
return { status: 202, said: "Ignored." };
|
|
992
989
|
// A state trigger fires when the issue arrives in that state, not on every later edit there.
|
|
993
|
-
const moved =
|
|
994
|
-
const found = moved ? linearItem(config, body
|
|
990
|
+
const moved = body.action === "create" || body.updatedFrom?.stateId !== undefined || config.state === null;
|
|
991
|
+
const found = moved ? linearItem(config, body.data) : null;
|
|
995
992
|
items = found === null ? [] : [found];
|
|
996
993
|
}
|
|
997
994
|
else if (config.kind === "webhook") {
|
|
998
|
-
|
|
995
|
+
// Any JSON, kept as sent: the trigger's own paths are picked from it, and it is shown whole when they find no text.
|
|
996
|
+
const body = readWebhookPayload(payload);
|
|
997
|
+
const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
|
|
998
|
+
const pick = (path) => path === null ? null : path.split(".").reduce((node, part) => record(node)[part], body);
|
|
999
999
|
const title = pick(config.titleField ?? "title") ?? pick("summary") ?? pick("message");
|
|
1000
1000
|
const bodyText = pick(config.bodyField ?? "description") ?? pick("body") ?? pick("text");
|
|
1001
1001
|
const id = header(delivery, "x-request-id") || header(delivery, "x-delivery-id") || header(delivery, "idempotency-key") || createHash("sha256").update(delivery.body).digest("hex");
|
|
1002
1002
|
items = [{ key: `hook:${id.slice(0, 120)}`, at: null, title: typeof title === "string" && title.trim() !== "" ? title : config.title,
|
|
1003
|
-
description: `From a webhook:\n\n${typeof bodyText === "string" ? bodyText : JSON.stringify(
|
|
1003
|
+
description: `From a webhook:\n\n${typeof bodyText === "string" ? bodyText : JSON.stringify(body, null, 2)}`.slice(0, 4000),
|
|
1004
1004
|
source: { kind: "webhook", label: "Webhook", url: null } }];
|
|
1005
1005
|
}
|
|
1006
1006
|
if (items.length === 0)
|
|
@@ -1081,13 +1081,14 @@ export function receiveFlowForm(store, token, fields, now) {
|
|
|
1081
1081
|
const again = `<p><a href="${esc(FORM_PATH + token)}">Send another</a></p>`;
|
|
1082
1082
|
const thanks = formHtml(config.label, `<p>Thanks — it's been sent.</p>${again}`);
|
|
1083
1083
|
// A filled trap, or a page posted faster than a person could, gets thanks and makes nothing.
|
|
1084
|
-
const
|
|
1085
|
-
|
|
1084
|
+
const form = readFormSubmission(fields, config.questions.length);
|
|
1085
|
+
const shown = Number(form.t);
|
|
1086
|
+
if ((form.website ?? "") !== "" || !Number.isFinite(shown) || now.getTime() - shown < 2000 || now.getTime() - shown > 86_400_000)
|
|
1086
1087
|
return { status: 200, html: thanks };
|
|
1087
1088
|
const recent = store.handle.prepare("SELECT COUNT(*) AS n FROM flow_trigger_event WHERE trigger = ? AND key LIKE 'form:%' AND at > ?").get(trigger.id, new Date(now.getTime() - 3_600_000).toISOString());
|
|
1088
1089
|
if (Number(recent?.["n"] ?? 0) >= FORM_CARDS_PER_HOUR)
|
|
1089
1090
|
return { status: 429, html: formHtml(config.label, `<p>Too many sent in the last hour. Try again later.</p>`) };
|
|
1090
|
-
const answers =
|
|
1091
|
+
const answers = form.answers.map((answer, index) => (answer ?? "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim().slice(0, index === 0 ? 200 : 3000));
|
|
1091
1092
|
if (answers[0] === "")
|
|
1092
1093
|
return { status: 400, html: formHtml(config.label, `<p>Answer “${esc(config.questions[0])}” first.</p>${again}`) };
|
|
1093
1094
|
const details = config.questions.slice(1).map((question, index) => answers[index + 1] === "" ? null : `${question}\n${answers[index + 1]}`).filter(one => one !== null).join("\n\n");
|
package/dist/mcp-connect.d.ts
CHANGED
|
@@ -64,7 +64,6 @@ export declare function connectionsOf(store: Store, repo: string, home?: string)
|
|
|
64
64
|
*/
|
|
65
65
|
export declare function readsOnly(action: string, subjects: readonly string[]): boolean;
|
|
66
66
|
export declare const readScope: (scope: string) => boolean;
|
|
67
|
-
/** Whether a saved grant only reads: it names at least one scope and every one is a read or identity scope. Unknown (null) never is. */
|
|
68
67
|
export declare const readOnlyGrant: (grant: readonly string[] | null) => boolean;
|
|
69
68
|
/** An OAuth `scope` value as scope tokens (RFC 6749), sorted and deduplicated; null when absent, empty or malformed. */
|
|
70
69
|
export declare function scopeList(value: unknown): string[] | null;
|
package/dist/mcp-connect.js
CHANGED
|
@@ -16,6 +16,7 @@ import { homedir } from "node:os";
|
|
|
16
16
|
import { basename } from "node:path";
|
|
17
17
|
import { LOCAL_APPS, addToolTo, catalogTool, localAppOf, projectToolsOf, readToolSecrets, setToolSecrets, testToolOf } from "./project-tools.js";
|
|
18
18
|
import { envValue } from "./names.js";
|
|
19
|
+
import { SCOPE_TOKEN, readAuthorizationServer, readProtectedResource, readRegistration, readTokenResponse } from "./contracts/integration-metadata.js";
|
|
19
20
|
/**
|
|
20
21
|
* Services that connect this way: their servers speak streamable HTTP and
|
|
21
22
|
* advertise registering clients on the spot. Advertising isn't accepting:
|
|
@@ -133,7 +134,10 @@ const IDENTITY_SCOPES = new Set(["openid", "profile", "email"]);
|
|
|
133
134
|
const READ_SCOPE = /^[a-z][a-z0-9_]{0,63}:read$/;
|
|
134
135
|
export const readScope = (scope) => IDENTITY_SCOPES.has(scope) || READ_SCOPE.test(scope);
|
|
135
136
|
/** Whether a saved grant only reads: it names at least one scope and every one is a read or identity scope. Unknown (null) never is. */
|
|
136
|
-
|
|
137
|
+
// A scope a server adds to every token without being asked, that grants no access to data. PostHog adds
|
|
138
|
+
// `introspection` (its ALWAYS_ALLOWED_SCOPES): it lets the token be checked, and has no write form. Never requested.
|
|
139
|
+
const GRANTED_ANYWAY = new Set(["introspection"]);
|
|
140
|
+
export const readOnlyGrant = (grant) => grant !== null && grant.some(readScope) && grant.every(scope => readScope(scope) || GRANTED_ANYWAY.has(scope));
|
|
137
141
|
/** An OAuth `scope` value as scope tokens (RFC 6749), sorted and deduplicated; null when absent, empty or malformed. */
|
|
138
142
|
export function scopeList(value) {
|
|
139
143
|
if (typeof value !== "string")
|
|
@@ -199,8 +203,7 @@ const GRANTED = "OAUTH_GRANTED_SCOPE";
|
|
|
199
203
|
export const CONNECT_CALLBACK = "/settings/tools/connected";
|
|
200
204
|
const VISIT_MS = 15 * 60_000, RENEW_RETRY_MS = 5 * 60_000;
|
|
201
205
|
const renewTried = new Map();
|
|
202
|
-
/**
|
|
203
|
-
const SCOPE_TOKEN = /^[\x21\x23-\x5b\x5d-\x7e]{1,128}$/;
|
|
206
|
+
/** The most scope text a sign-in address carries (addresses stay well under 8 KB). */
|
|
204
207
|
const SCOPE_PARAM_LIMIT = 6_000;
|
|
205
208
|
const json = async (fetcher, url) => {
|
|
206
209
|
try {
|
|
@@ -235,25 +238,29 @@ export async function discoverSignIn(mcpUrl, fetcher = fetch) {
|
|
|
235
238
|
catch {
|
|
236
239
|
pointer = null;
|
|
237
240
|
}
|
|
238
|
-
const
|
|
241
|
+
const found = (pointer !== null && https(pointer) ? await json(fetcher, pointer) : null)
|
|
239
242
|
?? await json(fetcher, `${mcp.origin}/.well-known/oauth-protected-resource${mcp.pathname === "/" ? "" : mcp.pathname}`)
|
|
240
243
|
?? await json(fetcher, `${mcp.origin}/.well-known/oauth-protected-resource`);
|
|
241
|
-
const
|
|
244
|
+
const resource = found === null ? {} : readProtectedResource(found);
|
|
245
|
+
const issuers = (resource.authorization_servers ?? []).filter(https);
|
|
242
246
|
const issuer = new URL(issuers[0] ?? mcp.origin);
|
|
243
247
|
const path = issuer.pathname === "/" ? "" : issuer.pathname.replace(/\/$/, "");
|
|
244
|
-
const
|
|
248
|
+
const answered = await json(fetcher, `${issuer.origin}/.well-known/oauth-authorization-server${path}`)
|
|
245
249
|
?? await json(fetcher, `${issuer.origin}/.well-known/oauth-authorization-server`)
|
|
246
250
|
?? (path === "" ? null : await json(fetcher, `${issuer.origin}${path}/.well-known/openid-configuration`))
|
|
247
251
|
?? await json(fetcher, `${issuer.origin}/.well-known/openid-configuration`);
|
|
248
|
-
if (
|
|
252
|
+
if (answered === null)
|
|
249
253
|
return null;
|
|
250
|
-
const
|
|
251
|
-
|
|
254
|
+
const meta = readAuthorizationServer(answered);
|
|
255
|
+
const { authorization_endpoint: authorize, token_endpoint: token, registration_endpoint: register } = meta;
|
|
256
|
+
if (!https(authorize) || !https(token) || !https(register))
|
|
257
|
+
return null;
|
|
258
|
+
if (!(meta.code_challenge_methods_supported ?? ["S256"]).includes("S256"))
|
|
252
259
|
return null;
|
|
253
260
|
// Ask for every scope the server lists for this resource: a token missing one the server itself requires is
|
|
254
261
|
// refused after a successful sign-in (PostHog lists 155 and needs user:read, the 140th). Only well-formed scope
|
|
255
262
|
// tokens, and at most what fits a sign-in address.
|
|
256
|
-
const listed =
|
|
263
|
+
const listed = resource.scopes_supported ?? [];
|
|
257
264
|
const scopes = [];
|
|
258
265
|
for (const one of listed) {
|
|
259
266
|
if (scopes.join(" ").length + one.length + 1 > SCOPE_PARAM_LIMIT)
|
|
@@ -261,8 +268,8 @@ export async function discoverSignIn(mcpUrl, fetcher = fetch) {
|
|
|
261
268
|
scopes.push(one);
|
|
262
269
|
}
|
|
263
270
|
// A token is for the resource the server names (Stripe's has no trailing slash), when it names one.
|
|
264
|
-
const named = resource
|
|
265
|
-
return { authorize
|
|
271
|
+
const named = resource.resource;
|
|
272
|
+
return { authorize, token, register, scopes, resource: https(named) ? named : null };
|
|
266
273
|
}
|
|
267
274
|
/**
|
|
268
275
|
* Start connecting a service: find its sign-in, register this console as a
|
|
@@ -284,11 +291,14 @@ export async function startConnect(input, fetcher = fetch, now = Date.now()) {
|
|
|
284
291
|
// Figma's answer (Oct 2026): it registers only apps it has approved. Said before reading a body that may not be JSON.
|
|
285
292
|
if (registered.status === 401 || registered.status === 403)
|
|
286
293
|
return { ok: false, said: `${service.label} doesn't let other apps sign in this way yet.${INSTEAD[service.id] === undefined ? "" : ` ${INSTEAD[service.id]}`}` };
|
|
287
|
-
const body = await registered.json();
|
|
288
|
-
|
|
294
|
+
const body = readRegistration(await registered.json());
|
|
295
|
+
// A `null` answer that says it registered is no answer, as it always read.
|
|
296
|
+
if (registered.ok && body === null)
|
|
297
|
+
return { ok: false, said: `${service.label} couldn't be reached.` };
|
|
298
|
+
if (!registered.ok || body?.client_id === undefined)
|
|
289
299
|
return { ok: false, said: `${service.label} didn't let Toolroll register (HTTP ${registered.status}).` };
|
|
290
300
|
clientId = body.client_id;
|
|
291
|
-
clientSecret =
|
|
301
|
+
clientSecret = body.client_secret ?? null;
|
|
292
302
|
}
|
|
293
303
|
catch {
|
|
294
304
|
return { ok: false, said: `${service.label} couldn't be reached.` };
|
|
@@ -324,9 +334,9 @@ export function connectedSpec(serviceId) {
|
|
|
324
334
|
secrets: [ACCESS, REFRESH, CLIENT, CLIENT_SECRET, TOKEN_URL, EXPIRES, RESOURCE].map(name => ({ name, optional: name !== ACCESS })), about: `${service.label}: ${service.about} Connected by signing in.` };
|
|
325
335
|
}
|
|
326
336
|
const tokensFrom = (body, now) => ({
|
|
327
|
-
access:
|
|
328
|
-
refresh:
|
|
329
|
-
expires:
|
|
337
|
+
access: body.access_token ?? null,
|
|
338
|
+
refresh: body.refresh_token ?? null,
|
|
339
|
+
expires: body.expires_in === undefined ? null : new Date(now + body.expires_in * 1000).toISOString(),
|
|
330
340
|
});
|
|
331
341
|
/**
|
|
332
342
|
* Finish connecting: trade the code for tokens, add the tool to the project
|
|
@@ -344,9 +354,13 @@ export async function finishConnect(store, visit, code, now, options = {}) {
|
|
|
344
354
|
try {
|
|
345
355
|
const answer = await fetcher(visit.token, { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" }, signal: AbortSignal.timeout(15_000),
|
|
346
356
|
body: new URLSearchParams({ grant_type: "authorization_code", code, redirect_uri: visit.redirect, client_id: visit.clientId, code_verifier: visit.verifier, resource: visit.resource, ...(visit.clientSecret === null ? {} : { client_secret: visit.clientSecret }) }) });
|
|
347
|
-
|
|
357
|
+
const read = readTokenResponse(await answer.json());
|
|
358
|
+
// An answer of `null` reads as none, like one that isn't JSON.
|
|
359
|
+
if (read === null)
|
|
360
|
+
return { ok: false, said: `${service.label} couldn't be reached to finish the sign-in.` };
|
|
361
|
+
body = read;
|
|
348
362
|
if (!answer.ok)
|
|
349
|
-
return { ok: false, said: `${service.label} didn't finish the sign-in (${
|
|
363
|
+
return { ok: false, said: `${service.label} didn't finish the sign-in (${body.error ?? `HTTP ${answer.status}`}).` };
|
|
350
364
|
}
|
|
351
365
|
catch {
|
|
352
366
|
return { ok: false, said: `${service.label} couldn't be reached to finish the sign-in.` };
|
|
@@ -360,7 +374,7 @@ export async function finishConnect(store, visit, code, now, options = {}) {
|
|
|
360
374
|
return { ok: false, said: added.message };
|
|
361
375
|
}
|
|
362
376
|
// The grant: what the answer says, or what was asked for when it says nothing; never one from an earlier sign-in.
|
|
363
|
-
const granted =
|
|
377
|
+
const granted = body.scopeSaid ? scopeList(body.scope) : scopeList(visit.scope ?? null);
|
|
364
378
|
setToolSecrets(visit.repo, spec.name, { [ACCESS]: tokens.access, [CLIENT]: visit.clientId, [TOKEN_URL]: visit.token, [RESOURCE]: visit.resource, [GRANTED]: granted?.join(" ") ?? "",
|
|
365
379
|
...(tokens.refresh === null ? {} : { [REFRESH]: tokens.refresh }), ...(tokens.expires === null ? {} : { [EXPIRES]: tokens.expires }), ...(visit.clientSecret === null ? {} : { [CLIENT_SECRET]: visit.clientSecret }) }, home);
|
|
366
380
|
// The success line names the project it went to: the page and chat may be looking at another.
|
|
@@ -391,7 +405,11 @@ export async function refreshConnections(store, repos, now, options = {}) {
|
|
|
391
405
|
try {
|
|
392
406
|
const answer = await fetcher(values[TOKEN_URL], { method: "POST", headers: { "content-type": "application/x-www-form-urlencoded", accept: "application/json" }, signal: AbortSignal.timeout(15_000),
|
|
393
407
|
body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: values[REFRESH], client_id: values[CLIENT], ...(values[CLIENT_SECRET] === undefined ? {} : { client_secret: values[CLIENT_SECRET] }), ...(values[RESOURCE] === undefined ? {} : { resource: values[RESOURCE] }) }) });
|
|
394
|
-
const body = await answer.json();
|
|
408
|
+
const body = readTokenResponse(await answer.json());
|
|
409
|
+
if (body === null) {
|
|
410
|
+
report.problems.push(`${tool.name}: couldn't reach it to renew its sign-in`);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
395
413
|
const tokens = tokensFrom(body, now.getTime());
|
|
396
414
|
if (!answer.ok || tokens.access === null) {
|
|
397
415
|
report.problems.push(`${tool.name}: its sign-in ran out; connect it again on the Tools page`);
|
|
@@ -399,7 +417,7 @@ export async function refreshConnections(store, repos, now, options = {}) {
|
|
|
399
417
|
}
|
|
400
418
|
// A refresh carries the same grant or a narrower one, and says so when it changes; said badly, it is unknown.
|
|
401
419
|
setToolSecrets(repo, tool.name, { [ACCESS]: tokens.access, ...(tokens.refresh === null ? {} : { [REFRESH]: tokens.refresh }), ...(tokens.expires === null ? {} : { [EXPIRES]: tokens.expires }),
|
|
402
|
-
...(
|
|
420
|
+
...(body.scopeSaid ? { [GRANTED]: scopeList(body.scope)?.join(" ") ?? "" } : {}) }, home);
|
|
403
421
|
renewTried.delete(key);
|
|
404
422
|
report.refreshed++;
|
|
405
423
|
}
|
package/dist/model-catalog.js
CHANGED
|
@@ -11,6 +11,7 @@ import { accessSync, constants, lstatSync, readFileSync, realpathSync } from "no
|
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { delimiter, join } from "node:path";
|
|
13
13
|
import { fetchOpenRouterModels } from "./openrouter-models.js";
|
|
14
|
+
import { codexModelSchema, codexModelsCacheSchema, npmLatestSchema } from "./contracts/model-catalog.js";
|
|
14
15
|
import { readCappedBody } from "./converse.js";
|
|
15
16
|
import { validModelId } from "./provider.js";
|
|
16
17
|
import { activeUpdateWork } from "./desktop-update-gate.js";
|
|
@@ -73,10 +74,11 @@ export function codexCatalog(home = homedir()) {
|
|
|
73
74
|
const path = join(home, ".codex", "models_cache.json");
|
|
74
75
|
if (lstatSync(path).size >= 2_000_000)
|
|
75
76
|
return [];
|
|
76
|
-
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
77
|
-
return (
|
|
78
|
-
|
|
79
|
-
|
|
77
|
+
const data = codexModelsCacheSchema.safeParse(JSON.parse(readFileSync(path, "utf8")));
|
|
78
|
+
return (data.success ? data.data.models : []).flatMap(entry => {
|
|
79
|
+
const one = codexModelSchema.safeParse(entry);
|
|
80
|
+
return one.success ? [one.data] : [];
|
|
81
|
+
}).slice(0, 30).map(one => ({ id: one.slug, name: typeof one.display_name === "string" ? one.display_name.slice(0, 80) : one.slug }));
|
|
80
82
|
}
|
|
81
83
|
catch {
|
|
82
84
|
return [];
|
|
@@ -255,8 +257,8 @@ async function latestVersion(pkg, fetcher) {
|
|
|
255
257
|
const body = await readCappedBody(response, 2_000_000);
|
|
256
258
|
if (body === null)
|
|
257
259
|
return null;
|
|
258
|
-
const
|
|
259
|
-
return
|
|
260
|
+
const latest = npmLatestSchema.safeParse(JSON.parse(new TextDecoder().decode(body)));
|
|
261
|
+
return latest.success ? latest.data.version : null;
|
|
260
262
|
}
|
|
261
263
|
catch {
|
|
262
264
|
return null;
|
package/dist/observations.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Runner } from "./builder.js";
|
|
2
2
|
import type { Store } from "./store.js";
|
|
3
|
+
import { type ObservationCase } from "./contracts/observation-request.js";
|
|
3
4
|
export declare const OBSERVATION_MAILBOX = "STANDING-ORDERS-OBSERVATIONS.json";
|
|
4
5
|
export declare const OBSERVATION_CAPTURE = "machine focused observations v1";
|
|
5
6
|
export declare const OBSERVATION_FILE = "REVIEW-OBSERVATIONS.json";
|
|
@@ -18,12 +19,7 @@ export type ObservationBrief = {
|
|
|
18
19
|
gateDigest: string;
|
|
19
20
|
};
|
|
20
21
|
export declare function focusedTestCommandSupported(command: string): boolean;
|
|
21
|
-
export type ObservationCase
|
|
22
|
-
criterion: string;
|
|
23
|
-
at: "base" | "head";
|
|
24
|
-
testPath: string;
|
|
25
|
-
testName: string;
|
|
26
|
-
};
|
|
22
|
+
export type { ObservationCase };
|
|
27
23
|
export declare function originalTaskBase(store: Store, taskId: string): string | null;
|
|
28
24
|
/** The existing sealed revision brief identifies this narrower kind of work.
|
|
29
25
|
* A title, agent-authored mailbox, or handoff can never elect gate reuse. */
|