@krapht/ork 1.0.0-alpha.2 → 1.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -7
- package/dist/__build_info__-0wqQ-rtf.mjs +180 -0
- package/dist/apply-Mb0fNpCX.mjs +17 -0
- package/dist/argument-error-CihH46Le.mjs +27 -0
- package/dist/attach-C0-YyNoY.mjs +92 -0
- package/dist/attach-CAKNqnUK.mjs +1259 -0
- package/dist/bin.mjs +1885 -1609
- package/dist/brief-C2hew8cS.mjs +116 -0
- package/dist/{decide-CGyx9rv7.mjs → builtin-files-YYAnbZGD.mjs} +3 -51
- package/dist/chips-CP_pgKlj.mjs +48 -0
- package/dist/command-tree-CEkzWNy6.mjs +63 -0
- package/dist/{completion-CSU5vkEt.mjs → completion-K4rAVsrf.mjs} +200 -30
- package/dist/context-B-fjPJky.mjs +106 -0
- package/dist/context-DMK0U1w0.mjs +160 -0
- package/dist/detached-q0AYG0Bs.mjs +39 -0
- package/dist/extra-arguments-BxQMXARO.mjs +13 -0
- package/dist/{find-D6Gj1sxg.mjs → find-Ct5CNl1J.mjs} +2 -2
- package/dist/flags-C90N4J1i.mjs +145 -0
- package/dist/follow-ups-D3HPyNYM.mjs +568 -0
- package/dist/format-CiPhQqbX.mjs +260 -0
- package/dist/{help-groups-BwYoAWsh.mjs → help-groups-BQvcKQ9H.mjs} +49 -78
- package/dist/in-a-run-D_wg4iJi.mjs +27 -0
- package/dist/install-Hji6LIei.mjs +259 -0
- package/dist/{limits-B1-DYc3d.mjs → limits-BD6RZRCS.mjs} +2 -2
- package/dist/{mcp-DE_EmyVm.mjs → mcp-CpbDmBLG.mjs} +6 -6
- package/dist/once-J8Qn5fTG.mjs +112 -0
- package/dist/orphan-guard-BFKg8VSd.mjs +58 -0
- package/dist/output-DXZbnFt_.mjs +458 -0
- package/dist/outputs-text-CNTJ7Qq_.mjs +19 -0
- package/dist/{pause-CL6N_Xrj.mjs → pause-Cl4ZYA35.mjs} +7 -4
- package/dist/print-DzugGZQB.mjs +19 -0
- package/dist/project-records-CtkmlsHN.mjs +233 -0
- package/dist/refusals-DQQX3AIb.mjs +116 -0
- package/dist/{relay-0SD0erwf.mjs → relay-CiP77vga.mjs} +2 -2
- package/dist/run-CtuNY3mE.mjs +234 -0
- package/dist/send-DRYGks8K.mjs +104 -0
- package/dist/short-flags-DkJLWVqh.mjs +1 -0
- package/dist/short-flags-Qf21as0E.mjs +83 -0
- package/dist/src-BMFQKHz4.mjs +3122 -0
- package/dist/{src-BbyEoMn4.mjs → src-BrFfDKux.mjs} +800 -235
- package/dist/{src-Dujmvv4s.mjs → src-BuF9Lbow.mjs} +12580 -5505
- package/dist/{src-DOcK8Jfp.mjs → src-k0A_QpST.mjs} +1024 -590
- package/dist/store-rTgRu8xy.mjs +25 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +612 -0
- package/dist/ui/assets/app-BYxpKJRh.css +1 -0
- package/dist/ui/assets/app-SEFEJLkF.js +112 -0
- package/dist/ui/assets/{index-9dFha9Qn.js → index-Cb6Voq_W.js} +3 -3
- package/dist/ui/assets/theme-CdXvqTQ6.js +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/{ui-CTmO2Xzu.mjs → ui-C2f37TSb.mjs} +57 -25
- package/dist/usage-BuvGcQSk.mjs +256 -0
- package/dist/validate-ObP5lt29.mjs +1 -0
- package/dist/validate-_HjACtk9.mjs +632 -0
- package/dist/verdict-B592j1W3.mjs +84 -0
- package/dist/{watch-CtbgDAcz.mjs → watch-DV3ixCYj.mjs} +45 -17
- package/dist/{workflows-D1_WVukk.mjs → workflows-QLjBhGNS.mjs} +30 -5
- package/dist/worktrees-D3ubG_Qw.mjs +302 -0
- package/dist/yolo-BSH1E_kE.mjs +33 -0
- package/package.json +10 -8
- package/dist/__build_info__-fy6L1Mrd.mjs +0 -615
- package/dist/clean-DaHjs__2.mjs +0 -88
- package/dist/context-DT8MfEIR.mjs +0 -274
- package/dist/format-Dnfv8zMT.mjs +0 -427
- package/dist/in-a-run-pLAZeNys.mjs +0 -22
- package/dist/list-BrH8KRSJ.mjs +0 -110
- package/dist/output-BLQWD6MJ.mjs +0 -950
- package/dist/remove-BeGdqroy.mjs +0 -194
- package/dist/report-DK4LGqwF.mjs +0 -129
- package/dist/send-C8tunWhv.mjs +0 -163
- package/dist/src-CCUKRX51.mjs +0 -1204
- package/dist/ui/assets/app-BNT0SuIz.css +0 -1
- package/dist/ui/assets/app-T7wVAhgx.js +0 -67
- package/dist/ui/assets/theme-3xH5jxKs.js +0 -1
- package/dist/usage-DYUmP0i4.mjs +0 -419
- package/dist/worktrees-DdMYoNOC.mjs +0 -212
|
@@ -1,152 +1,87 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { n as
|
|
5
|
-
import {
|
|
6
|
-
import { n as
|
|
7
|
-
import {
|
|
8
|
-
import { a as
|
|
1
|
+
import { $n as findFollowUp, $t as planClean, An as requestContinue, C as detectInstaller, Et as walkSummary, Gn as nestItems, Gt as feedbackListTable, Hn as processAlive, Hr as loadAgents, I as activeTree, In as pausedOf, Jt as usageListTable, Kr as placeFiles, Kt as followUpListTable, L as isNodeEvent, Ln as runStatus, Mn as requestStop, Nn as tellLiveRun, O as triggersReport, On as linkNodes, Pn as liveStatus, Qn as dueCause, Qr as inside, Qt as keyOf, R as openStore, S as runningScript, Si as NodeSchema, Tn as DEFAULT_FOLLOW_UPS_RULE, Tr as readCondition, Tt as walkChecks, Ut as LIST_TABLES, Vn as yoloOf, Wt as decisionListTable, Xt as applyClean, Yn as loadProject, Yr as runDirOf, Yt as worktreeListTable, Zn as changeFollowUp, Zr as workflowDirOf, Zt as cleanStep, _ as usageReport, _i as EdgeInputSchema, _n as postFollowUpMessage, _r as sendPost, _t as limitsReport, a as planRemove, ai as skillsIn, b as releaseNotes, bi as expandEdge, br as feedbackActions, ci as newWorkflowText, cr as previewPost, d as mergeStateOf, dt as resumable, ei as LESSON_CAP_TOKENS, en as stepActs, er as followUpActions, f as planLine, gr as runGh, hr as planPost, i as lostWork, jn as requestPause, kn as unlinkNodes, li as readDocument, m as stateParts, mi as parseWorkflow, mr as DISCUSSIONS, mt as followEvents, o as projectHead, oi as WorkflowEditSchema, or as END_KINDS, ot as answerDecision, p as removeRefusal, pi as renderJsonSchema, pt as detachRun, qr as placeFor, qt as runListTable, r as applyRemove, rr as endOf, s as readWorktrees, si as applyEdits, st as pendingDecisions, tn as stepPrunes, u as labelOf, un as nodeSays, ur as recordNote, ut as detachResume, v as capsHere, vn as checkFollowUpsDue, wn as liveSessionOf, wt as checkWorkflowText, x as runningRunIds, xn as startFollowUpChecks, xr as openFixRun, y as planUpgrade, yt as explainWorkflowText, z as storePath } from "./src-BuF9Lbow.mjs";
|
|
2
|
+
import { a as placeOf$1, i as lessonPlaceOf, n as nodeSession, r as acceptItem, t as runInstall } from "./install-Hji6LIei.mjs";
|
|
3
|
+
import { t as applyFilters } from "./apply-Mb0fNpCX.mjs";
|
|
4
|
+
import { i as parseLimit, n as unknownProjectRefusal, r as parseFilters, t as limitRefusal } from "./refusals-DQQX3AIb.mjs";
|
|
5
|
+
import { t as builtinProblem } from "./builtin-files-YYAnbZGD.mjs";
|
|
6
|
+
import { n as newRunId, t as findRun } from "./find-Ct5CNl1J.mjs";
|
|
7
|
+
import { t as outputsText } from "./outputs-text-CNTJ7Qq_.mjs";
|
|
8
|
+
import { a as registryShown, c as fetchLatest, d as readUpdateCache, f as refreshUpdateCache, i as registryOf, l as newerVersion, m as writeUpdateCache, o as updateSwitchOff, p as updateCachePath, s as checkDue } from "./__build_info__-0wqQ-rtf.mjs";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { z } from "zod";
|
|
11
11
|
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
13
13
|
import { homedir } from "node:os";
|
|
14
14
|
import { createServer } from "node:http";
|
|
15
|
-
// ../
|
|
16
|
-
|
|
15
|
+
// ../server/src/contract-lists.ts
|
|
16
|
+
/** A filter's value: any text; the handler checks it with core. */
|
|
17
|
+
const text = () => z.string().min(1).optional();
|
|
17
18
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
19
|
+
* A `since` or `until`: `all`, an age (`24h`), a day, or an ISO time. Read with
|
|
20
|
+
* core by the handler, so a bad one is refused in the CLI's words.
|
|
20
21
|
*/
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
const oneLine = (text) => {
|
|
31
|
-
const line = text.replaceAll(/\s+/gu, " ").trim();
|
|
32
|
-
return line.length > LONGEST_LINE ? `${line.slice(0, 119)}…` : line;
|
|
33
|
-
};
|
|
34
|
-
const toolLine = ({ name, input }) => {
|
|
35
|
-
const target = TARGETS.map((field) => input[field]).find((value) => typeof value === "string" && value.trim() !== "");
|
|
36
|
-
if (typeof target !== "string") return name;
|
|
37
|
-
return oneLine(target.startsWith(`${name} `) ? target : `${name} ${target}`);
|
|
38
|
-
};
|
|
39
|
-
/** What a finished node said, or why it failed. */
|
|
40
|
-
const finishedNow = (ok, error, summary) => {
|
|
41
|
-
if (!ok) return error === void 0 ? void 0 : oneLine(error);
|
|
42
|
-
return typeof summary === "string" && summary.trim() !== "" ? oneLine(summary) : void 0;
|
|
43
|
-
};
|
|
44
|
-
/** Each node's activity, by id; a node that never started has none. */
|
|
45
|
-
const runActivity = (stored) => {
|
|
46
|
-
const nodes = /* @__PURE__ */ new Map();
|
|
47
|
-
const of = (id) => {
|
|
48
|
-
const found = nodes.get(id);
|
|
49
|
-
if (found !== void 0) return found;
|
|
50
|
-
const fresh = {};
|
|
51
|
-
nodes.set(id, fresh);
|
|
52
|
-
return fresh;
|
|
53
|
-
};
|
|
54
|
-
for (const { at, event } of stored) if (event.type === "node.started") {
|
|
55
|
-
const node = of(event.id);
|
|
56
|
-
node.startedAt ??= at;
|
|
57
|
-
delete node.finishedAt;
|
|
58
|
-
delete node.now;
|
|
59
|
-
} else if (event.type === "node.event") {
|
|
60
|
-
if (event.event.type === "tool.called") of(event.id).now = toolLine(event.event);
|
|
61
|
-
} else if (event.type === "decision.opened") of(event.node).now = oneLine(`Asked: ${event.question}`);
|
|
62
|
-
else if (event.type === "decision.answered") of(event.node).now = oneLine(`Answered: ${event.answer}`);
|
|
63
|
-
else if (event.type === "decision.withdrawn") delete of(event.node).now;
|
|
64
|
-
else if (event.type === "node.finished") {
|
|
65
|
-
const node = of(event.id);
|
|
66
|
-
node.finishedAt = at;
|
|
67
|
-
const now = finishedNow(event.ok, event.error, event.output["summary"]);
|
|
68
|
-
if (now === void 0) delete node.now;
|
|
69
|
-
else node.now = now;
|
|
70
|
-
}
|
|
71
|
-
return nodes;
|
|
72
|
-
};
|
|
73
|
-
/** The ownership tree, with what each node is doing and when it ran. */
|
|
74
|
-
const activeTree = (stored) => {
|
|
75
|
-
const activity = runActivity(stored);
|
|
76
|
-
const dress = (node) => {
|
|
77
|
-
Object.assign(node, activity.get(node.id));
|
|
78
|
-
for (const child of node.children) dress(child);
|
|
79
|
-
};
|
|
80
|
-
const roots = runTree(stored.map(({ event }) => event));
|
|
81
|
-
for (const root of roots) dress(root);
|
|
82
|
-
return roots;
|
|
83
|
-
};
|
|
84
|
-
// ../server/src/contract-builder.ts
|
|
85
|
-
/** A new workflow's name, as its file and its `name:` take it. */
|
|
86
|
-
const NewWorkflowSchema = z.strictObject({ name: z.string().min(1) });
|
|
87
|
-
/** The server checks each edit; this file imports only types of the schema. */
|
|
88
|
-
const WorkflowEditsSchema = z.strictObject({ edits: z.array(z.custom()).min(1) });
|
|
89
|
-
/** Earlier text, and the file it was based on, to ask before replacing it. */
|
|
90
|
-
const DraftTextSchema = z.strictObject({
|
|
91
|
-
text: z.string(),
|
|
92
|
-
baseHash: z.string().nullable()
|
|
93
|
-
});
|
|
94
|
-
const SaveWorkflowSchema = z.strictObject({
|
|
95
|
-
/** Write over a file that changed since this draft began. */
|
|
96
|
-
force: z.boolean().optional() });
|
|
97
|
-
const BUILDER_API = {
|
|
98
|
-
builderSchema: {
|
|
99
|
-
method: "GET",
|
|
100
|
-
path: "/api/workflow-schema"
|
|
101
|
-
},
|
|
102
|
-
builderCatalog: {
|
|
103
|
-
method: "GET",
|
|
104
|
-
path: "/api/projects/:id/catalog"
|
|
105
|
-
},
|
|
106
|
-
builderDrafts: {
|
|
107
|
-
method: "GET",
|
|
108
|
-
path: "/api/projects/:id/builder"
|
|
109
|
-
},
|
|
110
|
-
builderView: {
|
|
111
|
-
method: "GET",
|
|
112
|
-
path: "/api/projects/:id/builder/:name"
|
|
113
|
-
},
|
|
114
|
-
builderCreate: {
|
|
115
|
-
method: "POST",
|
|
116
|
-
path: "/api/projects/:id/builder",
|
|
117
|
-
body: NewWorkflowSchema
|
|
22
|
+
const time = text;
|
|
23
|
+
/** Each list's filter fields, by flag name, in its core table's order. */
|
|
24
|
+
const LIST_SHAPES = {
|
|
25
|
+
runs: {
|
|
26
|
+
status: text(),
|
|
27
|
+
workflow: text(),
|
|
28
|
+
project: text(),
|
|
29
|
+
since: time(),
|
|
30
|
+
until: time()
|
|
118
31
|
},
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
32
|
+
feedback: {
|
|
33
|
+
status: text(),
|
|
34
|
+
kind: text(),
|
|
35
|
+
severity: text(),
|
|
36
|
+
workflow: text(),
|
|
37
|
+
run: text()
|
|
123
38
|
},
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
39
|
+
"follow-ups": {
|
|
40
|
+
status: text(),
|
|
41
|
+
for: text(),
|
|
42
|
+
kind: text(),
|
|
43
|
+
due: text(),
|
|
44
|
+
project: text()
|
|
128
45
|
},
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
46
|
+
decisions: {
|
|
47
|
+
workflow: text(),
|
|
48
|
+
run: text(),
|
|
49
|
+
project: text()
|
|
132
50
|
},
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
body: SaveWorkflowSchema
|
|
137
|
-
}
|
|
138
|
-
};
|
|
139
|
-
// ../server/src/contract-context.ts
|
|
140
|
-
const CONTEXT_API = {
|
|
141
|
-
runContext: {
|
|
142
|
-
method: "GET",
|
|
143
|
-
path: "/api/runs/:id/nodes/:node/context"
|
|
51
|
+
worktrees: {
|
|
52
|
+
run: text(),
|
|
53
|
+
project: text()
|
|
144
54
|
},
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
55
|
+
usage: {
|
|
56
|
+
workflow: text(),
|
|
57
|
+
project: text(),
|
|
58
|
+
since: time(),
|
|
59
|
+
until: time()
|
|
148
60
|
}
|
|
149
61
|
};
|
|
62
|
+
/** A list's filter fields as query params: its core table's, each optional. */
|
|
63
|
+
const listShape = (list) => LIST_SHAPES[list];
|
|
64
|
+
Object.keys(LIST_SHAPES.runs), Object.keys(LIST_SHAPES.feedback), Object.keys(LIST_SHAPES["follow-ups"]), Object.keys(LIST_SHAPES.decisions), Object.keys(LIST_SHAPES.worktrees), Object.keys(LIST_SHAPES.usage);
|
|
65
|
+
const Count$1 = z.coerce.number().int().positive();
|
|
66
|
+
/** `GET /api/runs`: `runListTable`'s fields, and the newest `limit`. */
|
|
67
|
+
const RunsQuerySchema = z.strictObject({
|
|
68
|
+
limit: Count$1.optional(),
|
|
69
|
+
...LIST_SHAPES.runs
|
|
70
|
+
});
|
|
71
|
+
/**
|
|
72
|
+
* `GET /api/usage`: `usageListTable`'s fields; `since` and `until` also take
|
|
73
|
+
* ISO times (`since` included, `until` not), as before.
|
|
74
|
+
*/
|
|
75
|
+
const UsageQuerySchema = z.strictObject(LIST_SHAPES.usage);
|
|
76
|
+
/**
|
|
77
|
+
* `GET /api/decisions`: `decisionListTable`'s fields; `workflow` is ANDed with
|
|
78
|
+
* `run` or `project`.
|
|
79
|
+
*/
|
|
80
|
+
const DecisionsQuerySchema = z.strictObject(LIST_SHAPES.decisions);
|
|
81
|
+
const LISTS_API = { listFields: {
|
|
82
|
+
method: "GET",
|
|
83
|
+
path: "/api/list-fields"
|
|
84
|
+
} };
|
|
150
85
|
// ../server/src/contract-feedback.ts
|
|
151
86
|
const NoteSchema = z.strictObject({
|
|
152
87
|
/** Its first line is the title. */
|
|
@@ -167,7 +102,13 @@ const AcceptSchema = z.strictObject({
|
|
|
167
102
|
/** Which workflow's lessons, when it was seen in more than one. */
|
|
168
103
|
workflow: z.string().min(1).optional()
|
|
169
104
|
});
|
|
170
|
-
const
|
|
105
|
+
const DriverIdSchema = z.enum({
|
|
106
|
+
"claude-code": "claude-code",
|
|
107
|
+
"open-code": "open-code",
|
|
108
|
+
"command-code": "command-code"
|
|
109
|
+
});
|
|
110
|
+
DriverIdSchema.options;
|
|
111
|
+
const ApplySchema = z.strictObject({ driver: DriverIdSchema.optional() });
|
|
171
112
|
const PublishPreviewSchema = z.strictObject({
|
|
172
113
|
/** Also hide the repositories' and workflows' names. */
|
|
173
114
|
hideNames: z.boolean() });
|
|
@@ -176,6 +117,11 @@ const PublishSchema = z.strictObject({
|
|
|
176
117
|
/** The preview's digest: what the person saw, so nothing else is sent. */
|
|
177
118
|
digest: z.string().min(1)
|
|
178
119
|
});
|
|
120
|
+
/**
|
|
121
|
+
* The filters are core's `feedbackListTable` fields; with none, every item
|
|
122
|
+
* comes, dismissed too (the panel hides them itself).
|
|
123
|
+
*/
|
|
124
|
+
const FeedbackItemsQuerySchema = z.strictObject(listShape("feedback"));
|
|
179
125
|
const FEEDBACK_API = {
|
|
180
126
|
note: {
|
|
181
127
|
method: "POST",
|
|
@@ -184,7 +130,8 @@ const FEEDBACK_API = {
|
|
|
184
130
|
},
|
|
185
131
|
feedbackItems: {
|
|
186
132
|
method: "GET",
|
|
187
|
-
path: "/api/feedback"
|
|
133
|
+
path: "/api/feedback",
|
|
134
|
+
query: FeedbackItemsQuerySchema
|
|
188
135
|
},
|
|
189
136
|
feedbackItem: {
|
|
190
137
|
method: "GET",
|
|
@@ -220,6 +167,125 @@ const FEEDBACK_API = {
|
|
|
220
167
|
path: "/api/feedback/:id/split"
|
|
221
168
|
}
|
|
222
169
|
};
|
|
170
|
+
// ../server/src/contract-follow-ups.ts
|
|
171
|
+
/**
|
|
172
|
+
* The filters are core's `followUpListTable` fields. `status` is `open` (due
|
|
173
|
+
* and waiting, the default), `all` (adds done and dropped), or the table's.
|
|
174
|
+
*/
|
|
175
|
+
const FollowUpsQuerySchema = z.strictObject(listShape("follow-ups"));
|
|
176
|
+
const FollowUpChangeSchema = z.strictObject({ action: z.enum([
|
|
177
|
+
"done",
|
|
178
|
+
"drop",
|
|
179
|
+
"reopen",
|
|
180
|
+
"due"
|
|
181
|
+
]) });
|
|
182
|
+
/**
|
|
183
|
+
* Posts a drafted GitHub comment through the person's own `gh`. `confirm` says
|
|
184
|
+
* yes, and `digest` is the detail's `draftDigest`, which stands for the
|
|
185
|
+
* recipient and text they saw: the route refuses when the draft is no longer
|
|
186
|
+
* that, so nothing else is sent.
|
|
187
|
+
*/
|
|
188
|
+
const FollowUpPostSchema = z.strictObject({
|
|
189
|
+
confirm: z.literal(true),
|
|
190
|
+
digest: z.string().min(1)
|
|
191
|
+
});
|
|
192
|
+
const FOLLOW_UPS_API = {
|
|
193
|
+
followUps: {
|
|
194
|
+
method: "GET",
|
|
195
|
+
path: "/api/projects/:id/follow-ups",
|
|
196
|
+
query: FollowUpsQuerySchema
|
|
197
|
+
},
|
|
198
|
+
followUp: {
|
|
199
|
+
method: "GET",
|
|
200
|
+
path: "/api/projects/:id/follow-ups/:followUp"
|
|
201
|
+
},
|
|
202
|
+
followUpChange: {
|
|
203
|
+
method: "POST",
|
|
204
|
+
path: "/api/projects/:id/follow-ups/:followUp",
|
|
205
|
+
body: FollowUpChangeSchema
|
|
206
|
+
},
|
|
207
|
+
followUpPost: {
|
|
208
|
+
method: "POST",
|
|
209
|
+
path: "/api/projects/:id/follow-ups/:followUp/post",
|
|
210
|
+
body: FollowUpPostSchema
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
// ../server/src/contract-after.ts
|
|
214
|
+
/**
|
|
215
|
+
* @file What the API offers after runs: the feedback loop's routes
|
|
216
|
+
* (contract-feedback.ts) and the follow-ups' (contract-follow-ups.ts), joined
|
|
217
|
+
* so the contract reads one part for both.
|
|
218
|
+
*/
|
|
219
|
+
const AFTER_API = {
|
|
220
|
+
...FEEDBACK_API,
|
|
221
|
+
...FOLLOW_UPS_API
|
|
222
|
+
};
|
|
223
|
+
// ../server/src/contract-builder.ts
|
|
224
|
+
/** A new workflow's name, as its file and its `name:` take it. */
|
|
225
|
+
const NewWorkflowSchema = z.strictObject({ name: z.string().min(1) });
|
|
226
|
+
/** The server checks each edit; this file imports only types of the schema. */
|
|
227
|
+
const WorkflowEditsSchema = z.strictObject({ edits: z.array(z.custom()).min(1) });
|
|
228
|
+
/** Earlier text, and the file it was based on, to ask before replacing it. */
|
|
229
|
+
const DraftTextSchema = z.strictObject({
|
|
230
|
+
text: z.string(),
|
|
231
|
+
baseHash: z.string().nullable()
|
|
232
|
+
});
|
|
233
|
+
const SaveWorkflowSchema = z.strictObject({
|
|
234
|
+
/** Write over a file that changed since this draft began. */
|
|
235
|
+
force: z.boolean().optional() });
|
|
236
|
+
const BUILDER_API = {
|
|
237
|
+
builderSchema: {
|
|
238
|
+
method: "GET",
|
|
239
|
+
path: "/api/workflow-schema"
|
|
240
|
+
},
|
|
241
|
+
builderCatalog: {
|
|
242
|
+
method: "GET",
|
|
243
|
+
path: "/api/projects/:id/catalog"
|
|
244
|
+
},
|
|
245
|
+
builderDrafts: {
|
|
246
|
+
method: "GET",
|
|
247
|
+
path: "/api/projects/:id/builder"
|
|
248
|
+
},
|
|
249
|
+
builderView: {
|
|
250
|
+
method: "GET",
|
|
251
|
+
path: "/api/projects/:id/builder/:name"
|
|
252
|
+
},
|
|
253
|
+
builderCreate: {
|
|
254
|
+
method: "POST",
|
|
255
|
+
path: "/api/projects/:id/builder",
|
|
256
|
+
body: NewWorkflowSchema
|
|
257
|
+
},
|
|
258
|
+
builderEdit: {
|
|
259
|
+
method: "POST",
|
|
260
|
+
path: "/api/projects/:id/builder/:name/edits",
|
|
261
|
+
body: WorkflowEditsSchema
|
|
262
|
+
},
|
|
263
|
+
builderRestore: {
|
|
264
|
+
method: "POST",
|
|
265
|
+
path: "/api/projects/:id/builder/:name/text",
|
|
266
|
+
body: DraftTextSchema
|
|
267
|
+
},
|
|
268
|
+
builderDiscard: {
|
|
269
|
+
method: "POST",
|
|
270
|
+
path: "/api/projects/:id/builder/:name/discard"
|
|
271
|
+
},
|
|
272
|
+
builderSave: {
|
|
273
|
+
method: "POST",
|
|
274
|
+
path: "/api/projects/:id/builder/:name/save",
|
|
275
|
+
body: SaveWorkflowSchema
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
// ../server/src/contract-context.ts
|
|
279
|
+
const CONTEXT_API = {
|
|
280
|
+
runContext: {
|
|
281
|
+
method: "GET",
|
|
282
|
+
path: "/api/runs/:id/nodes/:node/context"
|
|
283
|
+
},
|
|
284
|
+
builderContext: {
|
|
285
|
+
method: "GET",
|
|
286
|
+
path: "/api/projects/:id/builder/:name/nodes/:node/context"
|
|
287
|
+
}
|
|
288
|
+
};
|
|
223
289
|
// ../server/src/contract-links.ts
|
|
224
290
|
/**
|
|
225
291
|
* @file The routes that link two nodes of live runs (RFC-003): list, link and
|
|
@@ -275,9 +341,11 @@ planId: z.string().min(1) })
|
|
|
275
341
|
};
|
|
276
342
|
// ../server/src/contract-worktrees.ts
|
|
277
343
|
const Root = z.string().min(1);
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
project
|
|
344
|
+
/**
|
|
345
|
+
* The filters are core's `worktreeListTable` fields (`project`: a root; every
|
|
346
|
+
* project without one).
|
|
347
|
+
*/
|
|
348
|
+
const WorktreesQuerySchema = z.strictObject(listShape("worktrees"));
|
|
281
349
|
const RemovePlanSchema = z.strictObject({
|
|
282
350
|
project: Root,
|
|
283
351
|
/** The item's branch, or a detached worktree's path. */
|
|
@@ -327,18 +395,6 @@ const WORKTREES_API = {
|
|
|
327
395
|
// ../server/src/contract.ts
|
|
328
396
|
const Count = z.coerce.number().int().positive();
|
|
329
397
|
const Seq = z.coerce.number().int().nonnegative();
|
|
330
|
-
const Driver = z.enum(["sdk", "cli"]);
|
|
331
|
-
const RunsQuerySchema = z.strictObject({
|
|
332
|
-
project: z.string().min(1).optional(),
|
|
333
|
-
limit: Count.optional()
|
|
334
|
-
});
|
|
335
|
-
const UsageQuerySchema = z.strictObject({
|
|
336
|
-
project: z.string().min(1).optional(),
|
|
337
|
-
workflow: z.string().min(1).optional(),
|
|
338
|
-
/** ISO times: `since` included, `until` not. */
|
|
339
|
-
since: z.iso.datetime().optional(),
|
|
340
|
-
until: z.iso.datetime().optional()
|
|
341
|
-
});
|
|
342
398
|
const EventsQuerySchema = z.strictObject({
|
|
343
399
|
after: Seq.optional(),
|
|
344
400
|
node: z.string().min(1).optional(),
|
|
@@ -347,10 +403,6 @@ const EventsQuerySchema = z.strictObject({
|
|
|
347
403
|
/** Only the newest this many, still oldest first. */
|
|
348
404
|
last: Count.optional()
|
|
349
405
|
});
|
|
350
|
-
const DecisionsQuerySchema = z.strictObject({
|
|
351
|
-
run: z.string().min(1).optional(),
|
|
352
|
-
project: z.string().min(1).optional()
|
|
353
|
-
});
|
|
354
406
|
/** An answer: the option, and what the person said with it, if anything. */
|
|
355
407
|
const AnswerSchema = z.strictObject({
|
|
356
408
|
choose: z.string().min(1),
|
|
@@ -358,7 +410,7 @@ const AnswerSchema = z.strictObject({
|
|
|
358
410
|
});
|
|
359
411
|
const RunFlags = {
|
|
360
412
|
inputs: z.record(z.string(), z.string()).optional(),
|
|
361
|
-
driver:
|
|
413
|
+
driver: DriverIdSchema.optional(),
|
|
362
414
|
inPlace: z.boolean().optional(),
|
|
363
415
|
allowDirty: z.boolean().optional()
|
|
364
416
|
};
|
|
@@ -384,10 +436,16 @@ const API = {
|
|
|
384
436
|
method: "GET",
|
|
385
437
|
path: "/api/health"
|
|
386
438
|
},
|
|
439
|
+
/** Bearer token only (RFC-019): mints a single-use code for `/?code=`. */
|
|
440
|
+
signIn: {
|
|
441
|
+
method: "POST",
|
|
442
|
+
path: "/api/sign-in"
|
|
443
|
+
},
|
|
387
444
|
...BUILDER_API,
|
|
388
445
|
...CONTEXT_API,
|
|
389
|
-
...
|
|
446
|
+
...AFTER_API,
|
|
390
447
|
...WORKTREES_API,
|
|
448
|
+
...LISTS_API,
|
|
391
449
|
...UPDATE_API,
|
|
392
450
|
projects: {
|
|
393
451
|
method: "GET",
|
|
@@ -483,11 +541,70 @@ const STREAMS = {
|
|
|
483
541
|
run: "/api/runs/:id/stream",
|
|
484
542
|
/**
|
|
485
543
|
* `runs`, `decisions`, `version`, each when it changes; `?project=` keeps
|
|
486
|
-
* one
|
|
544
|
+
* one, `?workflow=` narrows `decisions` to that workflow's. `upgrade`: the
|
|
545
|
+
* upgrade's output, then `end` (contract-update.ts).
|
|
487
546
|
*/
|
|
488
547
|
overview: "/api/stream",
|
|
489
548
|
upgrade: "/api/upgrade/stream"
|
|
490
549
|
};
|
|
550
|
+
/** A route's path with its `:params` filled in, each encoded. */
|
|
551
|
+
const fillPath = (path, params) => path.replaceAll(/:(\w+)/gu, (_, name) => encodeURIComponent(params[name] ?? ""));
|
|
552
|
+
// ../server/src/client.ts
|
|
553
|
+
/**
|
|
554
|
+
* @file The typed client of the API (ADR-039), built from the contract: a call
|
|
555
|
+
* names a route and passes its params, query and body, and gets the route's
|
|
556
|
+
* response type back, or the server's error. The UI uses it in the browser
|
|
557
|
+
* (the session cookie goes with each request on its own); scripts pass the
|
|
558
|
+
* token. It uses only `fetch`, so it runs in a browser and in Node.
|
|
559
|
+
*/
|
|
560
|
+
/** An object's string and number fields, as text. */
|
|
561
|
+
const entries = (value) => typeof value === "object" && value !== null ? Object.entries(value).flatMap(([key, field]) => typeof field === "string" || typeof field === "number" ? [[key, String(field)]] : []) : [];
|
|
562
|
+
const queryString = (query) => {
|
|
563
|
+
const search = new URLSearchParams();
|
|
564
|
+
for (const [key, value] of entries(query)) search.set(key, value);
|
|
565
|
+
const text = search.toString();
|
|
566
|
+
return text === "" ? "" : `?${text}`;
|
|
567
|
+
};
|
|
568
|
+
const errorOf = (body, status) => typeof body === "object" && body !== null && "error" in body && typeof body.error === "string" ? body.error : `The server answered ${String(status)}.`;
|
|
569
|
+
const createClient = (options = {}) => {
|
|
570
|
+
const send = (url, init) => (options.fetch ?? globalThis.fetch)(url, init);
|
|
571
|
+
return { call: async (name, ...[input]) => {
|
|
572
|
+
const route = API[name];
|
|
573
|
+
const parts = input ?? {};
|
|
574
|
+
const params = Object.fromEntries(entries(parts.params));
|
|
575
|
+
const url = `${options.base ?? ""}${fillPath(route.path, params)}${queryString(parts.query)}`;
|
|
576
|
+
const headers = { accept: "application/json" };
|
|
577
|
+
if (options.token !== void 0) headers["authorization"] = `Bearer ${options.token}`;
|
|
578
|
+
const init = {
|
|
579
|
+
method: route.method,
|
|
580
|
+
headers
|
|
581
|
+
};
|
|
582
|
+
if (route.method === "POST") {
|
|
583
|
+
headers["content-type"] = "application/json";
|
|
584
|
+
init.body = JSON.stringify(parts.body ?? {});
|
|
585
|
+
}
|
|
586
|
+
let response;
|
|
587
|
+
try {
|
|
588
|
+
response = await send(url, init);
|
|
589
|
+
} catch (error) {
|
|
590
|
+
return {
|
|
591
|
+
ok: false,
|
|
592
|
+
status: 0,
|
|
593
|
+
error: `No answer: ${error instanceof Error ? error.message : String(error)}`
|
|
594
|
+
};
|
|
595
|
+
}
|
|
596
|
+
const body = await response.json().catch(() => null);
|
|
597
|
+
return response.ok ? {
|
|
598
|
+
ok: true,
|
|
599
|
+
status: response.status,
|
|
600
|
+
data: body
|
|
601
|
+
} : {
|
|
602
|
+
ok: false,
|
|
603
|
+
status: response.status,
|
|
604
|
+
error: errorOf(body, response.status)
|
|
605
|
+
};
|
|
606
|
+
} };
|
|
607
|
+
};
|
|
491
608
|
// ../server/src/graph.ts
|
|
492
609
|
/**
|
|
493
610
|
* @file A workflow or a run as the graph the UI draws: its nodes, its edges
|
|
@@ -509,7 +626,8 @@ const workflowGraph = (workflow) => ({
|
|
|
509
626
|
maxRounds
|
|
510
627
|
})) : [];
|
|
511
628
|
}),
|
|
512
|
-
channels: (workflow.messages ?? []).map(({ between: [a, b] }) => [a, b])
|
|
629
|
+
channels: (workflow.messages ?? []).map(({ between: [a, b] }) => [a, b]),
|
|
630
|
+
...workflow.limits?.budgetTokens === void 0 ? {} : { budgetTokens: workflow.limits.budgetTokens }
|
|
513
631
|
});
|
|
514
632
|
/** The graph of a workflow file, or `null` if it is gone or does not parse. */
|
|
515
633
|
const readWorkflowGraph = (file) => {
|
|
@@ -536,18 +654,22 @@ const runGraph = (events, file) => {
|
|
|
536
654
|
})),
|
|
537
655
|
edges: edges ?? onFile?.edges ?? [],
|
|
538
656
|
channels: channels ?? onFile?.channels ?? [],
|
|
539
|
-
...started?.type === "run.started" && started.budgetUsd !== void 0 ? { budgetUsd: started.budgetUsd } : {}
|
|
657
|
+
...started?.type === "run.started" && started.budgetUsd !== void 0 ? { budgetUsd: started.budgetUsd } : {},
|
|
658
|
+
...onFile?.budgetTokens === void 0 ? {} : { budgetTokens: onFile.budgetTokens }
|
|
540
659
|
};
|
|
541
660
|
};
|
|
542
661
|
// ../server/src/guards.ts
|
|
543
662
|
/**
|
|
544
|
-
* @file What the server lets in (ADR-039,
|
|
663
|
+
* @file What the server lets in (ADR-039, RFC-019,
|
|
545
664
|
* specs/server/streams-contract-and-security.md#security): only its own
|
|
546
|
-
* loopback `Host` (against DNS rebinding); only with
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
*
|
|
550
|
-
*
|
|
665
|
+
* loopback `Host` (against DNS rebinding); only with the token in an
|
|
666
|
+
* `Authorization: Bearer` header (scripts, `ork mcp`) or the session secret
|
|
667
|
+
* in the cookie (the browser); and, for a write sent with the cookie, only
|
|
668
|
+
* from its own `Origin` (against CSRF). A single-use code in `?code=` is
|
|
669
|
+
* swapped once for the cookie, with a redirect that takes it out of the
|
|
670
|
+
* address bar; the token itself never reaches a browser. It reads the
|
|
671
|
+
* request's parts and decides; its only state is the code store it spends
|
|
672
|
+
* from.
|
|
551
673
|
*/
|
|
552
674
|
const WRITES = /* @__PURE__ */ new Set([
|
|
553
675
|
"POST",
|
|
@@ -555,11 +677,13 @@ const WRITES = /* @__PURE__ */ new Set([
|
|
|
555
677
|
"PATCH",
|
|
556
678
|
"DELETE"
|
|
557
679
|
]);
|
|
680
|
+
/** What a used, expired or wrong code all get, so none tells them apart. */
|
|
681
|
+
const BAD_CODE = "That sign-in link is not valid any more; open it again with `ork ui`.";
|
|
558
682
|
/** A new token: 32 random bytes. */
|
|
559
683
|
const newToken = () => randomBytes(32).toString("base64url");
|
|
560
684
|
/** The session cookie's name; one per port, as cookies ignore ports. */
|
|
561
685
|
const cookieName = (port) => `ork_ui_${String(port)}`;
|
|
562
|
-
/** Whether `given` is the token, compared in constant time. */
|
|
686
|
+
/** Whether `given` is the token (or session secret), compared in constant time. */
|
|
563
687
|
const sameToken = (given, token) => {
|
|
564
688
|
const a = Buffer.from(given);
|
|
565
689
|
const b = Buffer.from(token);
|
|
@@ -575,22 +699,22 @@ const cookieValue = (request, name) => (header(request, "cookie") ?? "").split("
|
|
|
575
699
|
const bearer = (request) => {
|
|
576
700
|
return /^Bearer\s+(\S+)$/iu.exec(header(request, "authorization") ?? "")?.[1];
|
|
577
701
|
};
|
|
578
|
-
/** The session cookie that holds
|
|
579
|
-
const sessionCookie = (port,
|
|
702
|
+
/** The session cookie that holds the session secret. */
|
|
703
|
+
const sessionCookie = (port, session) => `${cookieName(port)}=${session}; HttpOnly; SameSite=Strict; Path=/`;
|
|
580
704
|
const exchange = (request, context) => {
|
|
581
|
-
const url = new URL(request.url, "http://127.0.0.1");
|
|
582
|
-
const given = url.searchParams.get("
|
|
705
|
+
const url = new URL(request.url.replace(/^[/\\]+/u, "/"), "http://127.0.0.1");
|
|
706
|
+
const given = url.searchParams.get("code");
|
|
583
707
|
if (given === null || request.method !== "GET") return null;
|
|
584
|
-
if (!
|
|
708
|
+
if (!context.codes.redeem(given)) return {
|
|
585
709
|
kind: "refuse",
|
|
586
710
|
status: 401,
|
|
587
|
-
error:
|
|
711
|
+
error: BAD_CODE
|
|
588
712
|
};
|
|
589
|
-
url.searchParams.delete("
|
|
713
|
+
url.searchParams.delete("code");
|
|
590
714
|
return {
|
|
591
715
|
kind: "exchange",
|
|
592
|
-
cookie: sessionCookie(context.port, context.
|
|
593
|
-
location: `${url.pathname}${url.search}`
|
|
716
|
+
cookie: sessionCookie(context.port, context.session),
|
|
717
|
+
location: `${url.pathname.replace(/^\/+/u, "/")}${url.search}`
|
|
594
718
|
};
|
|
595
719
|
};
|
|
596
720
|
/** Decides whether a request may go on. */
|
|
@@ -605,15 +729,20 @@ const guard = (request, context) => {
|
|
|
605
729
|
if (exchanged !== null) return exchanged;
|
|
606
730
|
const fromBearer = bearer(request);
|
|
607
731
|
const fromCookie = cookieValue(request, cookieName(context.port));
|
|
732
|
+
const via = fromBearer === void 0 ? "cookie" : "bearer";
|
|
608
733
|
const given = fromBearer ?? fromCookie;
|
|
609
|
-
|
|
734
|
+
const secret = via === "bearer" ? context.token : context.session;
|
|
735
|
+
if (given === void 0 || !sameToken(given, secret)) return {
|
|
610
736
|
kind: "refuse",
|
|
611
737
|
status: 401,
|
|
612
738
|
error: "Not signed in; open the UI with `ork ui`."
|
|
613
739
|
};
|
|
614
|
-
if (!WRITES.has(request.method)) return {
|
|
740
|
+
if (!WRITES.has(request.method)) return {
|
|
741
|
+
kind: "pass",
|
|
742
|
+
via
|
|
743
|
+
};
|
|
615
744
|
const origin = header(request, "origin");
|
|
616
|
-
if ((
|
|
745
|
+
if ((via === "cookie" || origin !== void 0) && (origin === void 0 || !ownOrigins(context.port).includes(origin))) return {
|
|
617
746
|
kind: "refuse",
|
|
618
747
|
status: 403,
|
|
619
748
|
error: `Origin ${origin ?? "(none)"} may not write here.`
|
|
@@ -624,7 +753,10 @@ const guard = (request, context) => {
|
|
|
624
753
|
status: 415,
|
|
625
754
|
error: "Send JSON (Content-Type: application/json)."
|
|
626
755
|
};
|
|
627
|
-
return {
|
|
756
|
+
return {
|
|
757
|
+
kind: "pass",
|
|
758
|
+
via
|
|
759
|
+
};
|
|
628
760
|
};
|
|
629
761
|
/** How often an open event stream gets a keep-alive comment. */
|
|
630
762
|
const KEEP_ALIVE_MS = 15e3;
|
|
@@ -821,6 +953,29 @@ const route = async (handlers, req, res, url) => {
|
|
|
821
953
|
else sendJson(res, reply.status, reply.body);
|
|
822
954
|
return true;
|
|
823
955
|
};
|
|
956
|
+
// ../server/src/list-fields-routes.ts
|
|
957
|
+
/**
|
|
958
|
+
* @file `GET /api/list-fields`: the filter fields of each list, from core's
|
|
959
|
+
* tables (RFC-017), as text without their functions, so the UI builds its
|
|
960
|
+
* filter bar without importing core.
|
|
961
|
+
*/
|
|
962
|
+
const infoOf = (field) => ({
|
|
963
|
+
name: field.name,
|
|
964
|
+
description: field.description,
|
|
965
|
+
type: field.type,
|
|
966
|
+
...field.values === void 0 ? {} : { values: [...field.values] },
|
|
967
|
+
...field.all === true ? { all: true } : {}
|
|
968
|
+
});
|
|
969
|
+
/** The answer, built once: the tables do not change while the server runs. */
|
|
970
|
+
const listFields = () => ({
|
|
971
|
+
runs: LIST_TABLES.ps.fields.map(infoOf),
|
|
972
|
+
feedback: LIST_TABLES["feedback list"].fields.map(infoOf),
|
|
973
|
+
"follow-ups": LIST_TABLES["follow-ups list"].fields.map(infoOf),
|
|
974
|
+
decisions: LIST_TABLES["decide --list"].fields.map(infoOf),
|
|
975
|
+
worktrees: LIST_TABLES["worktrees list"].fields.map(infoOf),
|
|
976
|
+
usage: LIST_TABLES.usage.fields.map(infoOf)
|
|
977
|
+
});
|
|
978
|
+
const listFieldsRoutes = () => ({ listFields: () => ok(listFields()) });
|
|
824
979
|
// ../server/src/context.ts
|
|
825
980
|
/**
|
|
826
981
|
* @file What the server's handlers share: the store it reads and writes, where
|
|
@@ -841,11 +996,21 @@ const runOf = (context, id) => {
|
|
|
841
996
|
const projectOf = (context, key) => context.store.projects().find((p) => p.id === key || p.root === key);
|
|
842
997
|
/** A run as the API shows it: with its live status and what is paused. */
|
|
843
998
|
const viewOf$2 = (context, run) => {
|
|
844
|
-
const events = run.status === "running" ? context.store.
|
|
999
|
+
const events = run.status === "running" ? context.store.controlEvents(run.id).map((stored) => stored.event) : [];
|
|
845
1000
|
return {
|
|
846
1001
|
...run,
|
|
847
1002
|
live: runStatus(run, events),
|
|
848
|
-
paused: pausedOf(events)
|
|
1003
|
+
paused: pausedOf(events),
|
|
1004
|
+
...yoloOf(events) ? { yolo: true } : {}
|
|
1005
|
+
};
|
|
1006
|
+
};
|
|
1007
|
+
/** The run's view plus its last finish's declared outputs, when it has any. */
|
|
1008
|
+
const viewWithOutputs = (context, run) => {
|
|
1009
|
+
const finished = context.store.controlEvents(run.id).map((stored) => stored.event).findLast((event) => event.type === "run.finished");
|
|
1010
|
+
const outputs = outputsText(finished?.type === "run.finished" ? finished.result.outputs : void 0);
|
|
1011
|
+
return {
|
|
1012
|
+
...viewOf$2(context, run),
|
|
1013
|
+
...outputs.length === 0 ? {} : { outputs }
|
|
849
1014
|
};
|
|
850
1015
|
};
|
|
851
1016
|
// ../server/src/mcp-answers.ts
|
|
@@ -1108,17 +1273,14 @@ const checkTool = (context) => tool("check", "Checks a workflow as `ork validate
|
|
|
1108
1273
|
return answer({
|
|
1109
1274
|
errors: count("error"),
|
|
1110
1275
|
warnings: count("warning"),
|
|
1111
|
-
diagnostics: diagnostics.map(({ severity, code, message, line, column }) =>
|
|
1276
|
+
diagnostics: diagnostics.map(({ severity, code, message, file, line, column }) => Object.assign({
|
|
1112
1277
|
severity,
|
|
1113
1278
|
code,
|
|
1114
1279
|
message
|
|
1115
|
-
} : {
|
|
1116
|
-
severity,
|
|
1117
|
-
code,
|
|
1118
|
-
message,
|
|
1280
|
+
}, file === void 0 ? {} : { file }, line === void 0 ? {} : {
|
|
1119
1281
|
line,
|
|
1120
1282
|
column
|
|
1121
|
-
}),
|
|
1283
|
+
})),
|
|
1122
1284
|
walk: walk === void 0 ? null : walkSummary(walk),
|
|
1123
1285
|
...args.paths === true && walk !== void 0 ? { endings: walk.endings.slice(0, MOST_ENDINGS) } : {}
|
|
1124
1286
|
});
|
|
@@ -1512,6 +1674,136 @@ const builderRoutes = (context) => {
|
|
|
1512
1674
|
builderSave: ({ params, body }) => inProject(params.id, (project) => saveDraft(context, project, params.name, body.force === true))
|
|
1513
1675
|
};
|
|
1514
1676
|
};
|
|
1677
|
+
// ../server/src/end-view.ts
|
|
1678
|
+
/**
|
|
1679
|
+
* @file Resolves how nodes ended for the API: each `node.finished` event (read
|
|
1680
|
+
* through core's `endOf`, so a run stored before ends existed gets a kind
|
|
1681
|
+
* too) and each tree node get an `EndView` (contract-ends.ts).
|
|
1682
|
+
*/
|
|
1683
|
+
/** Core's hints send the reader to Details; with no detail there is none. */
|
|
1684
|
+
const HINT_TO_DETAILS = /\bIts (?:last )?(?:output|detail) is under Details(?:; |\.\s*)/u;
|
|
1685
|
+
const hintFor = (hint, hasDetail) => {
|
|
1686
|
+
if (hasDetail) return hint;
|
|
1687
|
+
const rest = hint.replace(HINT_TO_DETAILS, "");
|
|
1688
|
+
return rest === "" ? rest : rest[0]?.toUpperCase() + rest.slice(1);
|
|
1689
|
+
};
|
|
1690
|
+
const endViewOf = (end) => {
|
|
1691
|
+
const info = END_KINDS[end.kind];
|
|
1692
|
+
return {
|
|
1693
|
+
kind: end.kind,
|
|
1694
|
+
message: end.message,
|
|
1695
|
+
...end.detail === void 0 ? {} : { detail: end.detail },
|
|
1696
|
+
label: info.label,
|
|
1697
|
+
actor: info.actor,
|
|
1698
|
+
transient: info.transient,
|
|
1699
|
+
hint: hintFor(info.hint, end.detail !== void 0),
|
|
1700
|
+
docsAnchor: info.docsAnchor
|
|
1701
|
+
};
|
|
1702
|
+
};
|
|
1703
|
+
/** The event as served: a `node.finished` gets its `end`, old ones too. */
|
|
1704
|
+
const servedEvent = (stored) => {
|
|
1705
|
+
const { event } = stored;
|
|
1706
|
+
return event.type === "node.finished" ? {
|
|
1707
|
+
...stored,
|
|
1708
|
+
event: {
|
|
1709
|
+
...event,
|
|
1710
|
+
end: endViewOf(endOf(event))
|
|
1711
|
+
}
|
|
1712
|
+
} : {
|
|
1713
|
+
...stored,
|
|
1714
|
+
event
|
|
1715
|
+
};
|
|
1716
|
+
};
|
|
1717
|
+
/**
|
|
1718
|
+
* The tree with each node's latest end, from the events it was folded from. A
|
|
1719
|
+
* node that started again has none until it finishes; a failed node's `now` is
|
|
1720
|
+
* the end's message, not the raw error.
|
|
1721
|
+
*/
|
|
1722
|
+
const treeWithEnds = (roots, stored) => {
|
|
1723
|
+
const ends = /* @__PURE__ */ new Map();
|
|
1724
|
+
for (const { event } of stored) if (event.type === "node.started") ends.delete(event.id);
|
|
1725
|
+
else if (event.type === "node.finished") ends.set(event.id, endViewOf(endOf(event)));
|
|
1726
|
+
const dress = (node) => {
|
|
1727
|
+
const end = ends.get(node.id);
|
|
1728
|
+
const children = node.children.map(dress);
|
|
1729
|
+
if (end === void 0) return {
|
|
1730
|
+
...node,
|
|
1731
|
+
children
|
|
1732
|
+
};
|
|
1733
|
+
const failed = node.state === "failed" && end.actor !== null;
|
|
1734
|
+
return {
|
|
1735
|
+
...node,
|
|
1736
|
+
...failed ? { now: end.message } : {},
|
|
1737
|
+
end,
|
|
1738
|
+
children
|
|
1739
|
+
};
|
|
1740
|
+
};
|
|
1741
|
+
return roots.map((node) => dress(node));
|
|
1742
|
+
};
|
|
1743
|
+
// ../server/src/list-query.ts
|
|
1744
|
+
/**
|
|
1745
|
+
* @file Reads a list route's filter fields the way the CLI reads its flags
|
|
1746
|
+
* (RFC-017, ADR-088): each value checked by core's `parseFilters` against the
|
|
1747
|
+
* list's table, a refused one answered as 400 in the CLI's words. A full ISO
|
|
1748
|
+
* time in `since` or `until` is taken as given (the form `usage` had before
|
|
1749
|
+
* the tables), the rest are an age, a day or `all`.
|
|
1750
|
+
*/
|
|
1751
|
+
/** A `2026-11-01T10:00:00Z`-style time, as an ISO time; else `undefined`. */
|
|
1752
|
+
const isoTime = (value) => {
|
|
1753
|
+
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}T/u.test(value)) return;
|
|
1754
|
+
const time = Date.parse(value);
|
|
1755
|
+
return Number.isNaN(time) ? void 0 : new Date(time).toISOString();
|
|
1756
|
+
};
|
|
1757
|
+
/**
|
|
1758
|
+
* The filter `query` asks for, checked against `table`; a value the table
|
|
1759
|
+
* refuses is a 400.
|
|
1760
|
+
*/
|
|
1761
|
+
const filterOf = (table, query) => {
|
|
1762
|
+
const times = {};
|
|
1763
|
+
const rest = { ...query };
|
|
1764
|
+
for (const name of ["since", "until"]) {
|
|
1765
|
+
const iso = isoTime(query[name]);
|
|
1766
|
+
if (iso === void 0) continue;
|
|
1767
|
+
times[name] = iso;
|
|
1768
|
+
delete rest[name];
|
|
1769
|
+
}
|
|
1770
|
+
const parsed = parseFilters(table, rest, Date.now());
|
|
1771
|
+
return parsed.ok ? {
|
|
1772
|
+
ok: true,
|
|
1773
|
+
filter: {
|
|
1774
|
+
...parsed.filter,
|
|
1775
|
+
...times
|
|
1776
|
+
}
|
|
1777
|
+
} : {
|
|
1778
|
+
ok: false,
|
|
1779
|
+
refusal: fail(400, parsed.error)
|
|
1780
|
+
};
|
|
1781
|
+
};
|
|
1782
|
+
/**
|
|
1783
|
+
* The root a filter's `project` names (a registered project's id or root, else
|
|
1784
|
+
* the value as a root); `undefined` without one or for `all`.
|
|
1785
|
+
*/
|
|
1786
|
+
const projectRootOf = (context, filter) => {
|
|
1787
|
+
const key = filter["project"];
|
|
1788
|
+
return key === void 0 || key === "all" ? void 0 : projectOf(context, key)?.root ?? key;
|
|
1789
|
+
};
|
|
1790
|
+
/** `now` is injectable so a test can move the clock. */
|
|
1791
|
+
const createSignInCodes = (now = Date.now) => {
|
|
1792
|
+
const expiries = /* @__PURE__ */ new Map();
|
|
1793
|
+
return {
|
|
1794
|
+
mint: () => {
|
|
1795
|
+
for (const [code, expiry] of expiries) if (expiry <= now()) expiries.delete(code);
|
|
1796
|
+
const code = randomBytes(32).toString("base64url");
|
|
1797
|
+
expiries.set(code, now() + 6e4);
|
|
1798
|
+
return code;
|
|
1799
|
+
},
|
|
1800
|
+
redeem: (code) => {
|
|
1801
|
+
const expiry = expiries.get(code);
|
|
1802
|
+
expiries.delete(code);
|
|
1803
|
+
return expiry !== void 0 && expiry > now();
|
|
1804
|
+
}
|
|
1805
|
+
};
|
|
1806
|
+
};
|
|
1515
1807
|
// ../server/src/workflows.ts
|
|
1516
1808
|
/**
|
|
1517
1809
|
* @file A project's workflows, for the UI's Run workflow dialog: each
|
|
@@ -1570,47 +1862,71 @@ const projectWorkflows = (project) => {
|
|
|
1570
1862
|
* decisions, and usage. Each reads the store as the matching CLI command does
|
|
1571
1863
|
* (`ork projects`, `ps`, `tree`, `logs`, `decide --list`, `usage`).
|
|
1572
1864
|
*/
|
|
1573
|
-
/** A project filter's root: a registered project's, or the value as a root. */
|
|
1574
1865
|
/** A registered project's place; caps are only known for one. */
|
|
1575
1866
|
const placeOf = (context, key) => {
|
|
1576
1867
|
const known = key === void 0 ? void 0 : projectOf(context, key);
|
|
1577
1868
|
return known === void 0 ? void 0 : placeFor(known.root, context.store.projects());
|
|
1578
1869
|
};
|
|
1579
|
-
|
|
1870
|
+
/** `ork ps`'s table, matching `--status` on the live status the API shows. */
|
|
1871
|
+
const liveRunTable = {
|
|
1872
|
+
...runListTable,
|
|
1873
|
+
fields: runListTable.fields.map((field) => field.name === "status" ? Object.assign({}, field, { get: (run) => run.live }) : field)
|
|
1874
|
+
};
|
|
1580
1875
|
const readRoutes = (context) => ({
|
|
1876
|
+
...listFieldsRoutes(),
|
|
1581
1877
|
health: () => ok({
|
|
1582
1878
|
version: context.version,
|
|
1583
1879
|
pid: context.pid,
|
|
1584
1880
|
startedAt: context.startedAt
|
|
1585
1881
|
}),
|
|
1882
|
+
signIn: () => ok({
|
|
1883
|
+
code: context.codes.mint(),
|
|
1884
|
+
expiresIn: 60
|
|
1885
|
+
}),
|
|
1586
1886
|
projects: () => ok(context.store.projects()),
|
|
1587
1887
|
triggers: () => ok(triggersReport(context.store, context.store.projects(), context.home)),
|
|
1588
1888
|
links: ({ query }) => ok(context.store.links(query.run === void 0 ? {} : { run: query.run })),
|
|
1589
|
-
usage: ({ query }) =>
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1889
|
+
usage: ({ query }) => {
|
|
1890
|
+
const read = filterOf(usageListTable, query);
|
|
1891
|
+
if (!read.ok) return read.refusal;
|
|
1892
|
+
const { filter } = read;
|
|
1893
|
+
const { workflow, since, until } = filter;
|
|
1894
|
+
const root = projectRootOf(context, filter);
|
|
1895
|
+
return ok({
|
|
1896
|
+
...usageReport(context.store.usage({
|
|
1897
|
+
...root === void 0 ? {} : { project: root },
|
|
1898
|
+
...workflow === void 0 ? {} : { workflow },
|
|
1899
|
+
...since === void 0 || since === "all" ? {} : { since },
|
|
1900
|
+
...until === void 0 ? {} : { until }
|
|
1901
|
+
})),
|
|
1902
|
+
limits: limitsReport(context.store, capsHere(context.home, placeOf(context, root)), /* @__PURE__ */ new Date())
|
|
1903
|
+
});
|
|
1904
|
+
},
|
|
1598
1905
|
workflows: ({ params }) => {
|
|
1599
1906
|
const project = projectOf(context, params.id);
|
|
1600
1907
|
return project === void 0 ? fail(404, `No registered project \`${params.id}\`.`) : ok(projectWorkflows(project));
|
|
1601
1908
|
},
|
|
1602
|
-
runs: ({ query }) =>
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1909
|
+
runs: ({ query }) => {
|
|
1910
|
+
const read = filterOf(liveRunTable, query);
|
|
1911
|
+
if (!read.ok) return read.refusal;
|
|
1912
|
+
const root = projectRootOf(context, read.filter);
|
|
1913
|
+
const limit = query.limit ?? 20;
|
|
1914
|
+
const narrowed = Object.keys(read.filter).some((name) => name !== "project");
|
|
1915
|
+
const views = context.store.runs({
|
|
1916
|
+
...root === void 0 ? {} : { project: root },
|
|
1917
|
+
...narrowed ? {} : { limit }
|
|
1918
|
+
}).map((run) => viewOf$2(context, run));
|
|
1919
|
+
return ok(applyFilters(liveRunTable, views, read.filter).slice(0, limit));
|
|
1920
|
+
},
|
|
1606
1921
|
run: ({ params }) => {
|
|
1607
1922
|
const found = runOf(context, params.id);
|
|
1608
|
-
return found.ok ? ok(
|
|
1923
|
+
return found.ok ? ok(viewWithOutputs(context, found.run)) : found.refusal;
|
|
1609
1924
|
},
|
|
1610
1925
|
tree: ({ params }) => {
|
|
1611
1926
|
const found = runOf(context, params.id);
|
|
1612
1927
|
if (!found.ok) return found.refusal;
|
|
1613
|
-
|
|
1928
|
+
const stored = context.store.events(found.run.id);
|
|
1929
|
+
return ok(treeWithEnds(nestItems(activeTree(stored)), stored));
|
|
1614
1930
|
},
|
|
1615
1931
|
graph: ({ params }) => {
|
|
1616
1932
|
const found = runOf(context, params.id);
|
|
@@ -1620,15 +1936,28 @@ const readRoutes = (context) => ({
|
|
|
1620
1936
|
events: ({ params, query }) => {
|
|
1621
1937
|
const found = runOf(context, params.id);
|
|
1622
1938
|
if (!found.ok) return found.refusal;
|
|
1623
|
-
const events = context.store.events(found.run.id, query.after ?? 0).filter((s) => query.node === void 0 ||
|
|
1939
|
+
const events = context.store.events(found.run.id, query.after ?? 0).filter((s) => query.node === void 0 || isNodeEvent(s.event, query.node));
|
|
1624
1940
|
const newest = query.last === void 0 ? events : events.slice(-query.last);
|
|
1625
|
-
return ok(query.limit === void 0 ? newest : newest.slice(0, query.limit));
|
|
1941
|
+
return ok((query.limit === void 0 ? newest : newest.slice(0, query.limit)).map((stored) => servedEvent(stored)));
|
|
1626
1942
|
},
|
|
1627
1943
|
decisions: ({ query }) => {
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1944
|
+
const read = filterOf(decisionListTable, query);
|
|
1945
|
+
if (!read.ok) return read.refusal;
|
|
1946
|
+
const { run, workflow: named } = read.filter;
|
|
1947
|
+
const workflow = named === void 0 ? {} : { workflow: named };
|
|
1948
|
+
if (run === void 0) {
|
|
1949
|
+
const root = projectRootOf(context, read.filter);
|
|
1950
|
+
return ok(pendingDecisions(context.store, {
|
|
1951
|
+
...workflow,
|
|
1952
|
+
...root === void 0 ? {} : { project: root }
|
|
1953
|
+
}));
|
|
1954
|
+
}
|
|
1955
|
+
const found = runOf(context, run);
|
|
1956
|
+
if (!found.ok) return fail(400, `--run finds no run: ${found.refusal.error}`);
|
|
1957
|
+
return ok(pendingDecisions(context.store, {
|
|
1958
|
+
...workflow,
|
|
1959
|
+
runId: found.run.id
|
|
1960
|
+
}));
|
|
1632
1961
|
},
|
|
1633
1962
|
decision: ({ params }) => {
|
|
1634
1963
|
const found = context.store.decision(params.id);
|
|
@@ -1654,6 +1983,8 @@ const RECENT_EVENTS = 30;
|
|
|
1654
1983
|
const ANSWERED_BY_A_PERSON = "Only a person answers these: in `ork ui`, or with `ork decide` in their own terminal. Tell them what waits, and where.";
|
|
1655
1984
|
const project = z.string().min(1).describe("A registered project's id or root");
|
|
1656
1985
|
const run = z.string().min(1).describe("A run's id, or a unique start of it");
|
|
1986
|
+
/** An error answer, in the CLI's words, when `project` names none registered. */
|
|
1987
|
+
const unknownProject = (context, key) => key === void 0 || key === "all" || projectOf(context, key) !== void 0 ? void 0 : refused$1(unknownProjectRefusal(key, context.store.projects().map((p) => p.id)));
|
|
1657
1988
|
const mcpTools = (context) => {
|
|
1658
1989
|
const read = readRoutes(context);
|
|
1659
1990
|
const { builderCatalog } = builderRoutes(context);
|
|
@@ -1668,13 +1999,22 @@ const mcpTools = (context) => {
|
|
|
1668
1999
|
...NO_QUERY,
|
|
1669
2000
|
params: { id: args.project }
|
|
1670
2001
|
}))),
|
|
1671
|
-
tool("runs", "Runs, newest first, with their live status and cost. 10 unless a limit is given.
|
|
1672
|
-
...
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
2002
|
+
tool("runs", "Runs, newest first, with their live status and cost. 10 unless a limit is given. Narrow them with `status` (running, paused, crashed, succeeded, failed, … or all), `workflow`, `project` (or all), `since` (24h, 30d, 2026-11-01, all) and `until` (a day).", {
|
|
2003
|
+
...RunsQuerySchema.shape,
|
|
2004
|
+
limit: z.union([z.number(), z.string()]).optional()
|
|
2005
|
+
}, ({ limit, ...args }) => {
|
|
2006
|
+
const bad = unknownProject(context, args.project);
|
|
2007
|
+
if (bad !== void 0) return bad;
|
|
2008
|
+
const count = limit === void 0 ? 10 : parseLimit(String(limit));
|
|
2009
|
+
if (count === null) return refused$1(limitRefusal(String(limit)));
|
|
2010
|
+
return fromReply(read.runs({
|
|
2011
|
+
...NO_QUERY,
|
|
2012
|
+
query: {
|
|
2013
|
+
...args,
|
|
2014
|
+
limit: count
|
|
2015
|
+
}
|
|
2016
|
+
}));
|
|
2017
|
+
}),
|
|
1678
2018
|
tool("run", "One run: its status, what it waits on, its cost, and each node's state.", { run }, async (args) => {
|
|
1679
2019
|
const view = await read.run({
|
|
1680
2020
|
...NO_QUERY,
|
|
@@ -1704,11 +2044,11 @@ const mcpTools = (context) => {
|
|
|
1704
2044
|
}
|
|
1705
2045
|
}));
|
|
1706
2046
|
}),
|
|
1707
|
-
tool("usage", "What runs spent, by workflow and model, and how close the limits are. `since` and `until
|
|
2047
|
+
tool("usage", "What runs spent, by workflow and model, and how close the limits are. Narrow it with `workflow`, `project` (or all), `since` and `until`: an age (24h, 30d), a day (2026-11-01) or an ISO time; `since` also takes all.", UsageQuerySchema.shape, (args) => unknownProject(context, args.project) ?? fromReply(read.usage({
|
|
1708
2048
|
...NO_QUERY,
|
|
1709
2049
|
query: args
|
|
1710
2050
|
}))),
|
|
1711
|
-
tool("decisions", "The decisions open in live runs, waiting on a person. This server can't answer them.", DecisionsQuerySchema.shape, (args) => fromReply(read.decisions({
|
|
2051
|
+
tool("decisions", "The decisions open in live runs, waiting on a person. This server can't answer them. Narrow them with `workflow`, `run` (an id or a start of one) and `project` (or all).", DecisionsQuerySchema.shape, (args) => unknownProject(context, args.project) ?? fromReply(read.decisions({
|
|
1712
2052
|
...NO_QUERY,
|
|
1713
2053
|
query: args
|
|
1714
2054
|
}), (open) => ({
|
|
@@ -1820,7 +2160,8 @@ const openMcp = (options) => {
|
|
|
1820
2160
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1821
2161
|
...options.home === void 0 ? {} : { home: options.home },
|
|
1822
2162
|
...options.script === void 0 ? {} : { script: options.script },
|
|
1823
|
-
updates: createUpdates({ version: options.version })
|
|
2163
|
+
updates: createUpdates({ version: options.version }),
|
|
2164
|
+
codes: createSignInCodes()
|
|
1824
2165
|
};
|
|
1825
2166
|
return {
|
|
1826
2167
|
tools: [...mcpTools(context), ...actingTools(context, { start: options.start })],
|
|
@@ -1838,6 +2179,17 @@ const openMcp = (options) => {
|
|
|
1838
2179
|
}
|
|
1839
2180
|
};
|
|
1840
2181
|
};
|
|
2182
|
+
// ../server/src/access.ts
|
|
2183
|
+
/**
|
|
2184
|
+
* @file The secrets a server starts with (RFC-019): the API token (bearer), the
|
|
2185
|
+
* session secret (the browser's cookie) and the sign-in code store. Each is
|
|
2186
|
+
* the caller's or a new one.
|
|
2187
|
+
*/
|
|
2188
|
+
const createAccess = (given) => ({
|
|
2189
|
+
token: given.token ?? newToken(),
|
|
2190
|
+
session: given.session ?? newToken(),
|
|
2191
|
+
codes: given.codes ?? createSignInCodes()
|
|
2192
|
+
});
|
|
1841
2193
|
// ../server/src/assets.ts
|
|
1842
2194
|
/**
|
|
1843
2195
|
* @file The built UI's files (`packages/ui/dist`, shipped as the CLI's
|
|
@@ -2010,7 +2362,7 @@ const applyRoute = async (context, item, driver) => {
|
|
|
2010
2362
|
* workflows would go and how big that file is, the project a fix would be
|
|
2011
2363
|
* proposed in, and the run proposing one now, if any.
|
|
2012
2364
|
*/
|
|
2013
|
-
const detailOf = (store, item) => {
|
|
2365
|
+
const detailOf$1 = (store, item) => {
|
|
2014
2366
|
const registered = store.projects();
|
|
2015
2367
|
const fixRun = openFixRun(store, item);
|
|
2016
2368
|
const actions = feedbackActions(item, fixRun);
|
|
@@ -2106,14 +2458,18 @@ const feedbackRoutes = (context) => {
|
|
|
2106
2458
|
note: ({ params, body }) => {
|
|
2107
2459
|
const found = runOf(context, params.id);
|
|
2108
2460
|
if (!found.ok) return found.refusal;
|
|
2109
|
-
const
|
|
2461
|
+
const scrub = context.noteScrub?.(found.run, store.projects());
|
|
2462
|
+
const noted = recordNote(store, found.run, body, "", scrub);
|
|
2110
2463
|
return noted.ok ? ok({ item: noted.item }) : fail(400, noted.error);
|
|
2111
2464
|
},
|
|
2112
|
-
feedbackItems: () =>
|
|
2465
|
+
feedbackItems: ({ query }) => {
|
|
2466
|
+
const read = filterOf(feedbackListTable, query);
|
|
2467
|
+
return read.ok ? ok(applyFilters(feedbackListTable, store.feedbackItems({ all: true }), read.filter)) : read.refusal;
|
|
2468
|
+
},
|
|
2113
2469
|
feedbackItem: ({ params }) => {
|
|
2114
2470
|
const found = itemOf(store, params.id);
|
|
2115
2471
|
if (!found.ok) return found.refusal;
|
|
2116
|
-
return ok(detailOf(store, found.item));
|
|
2472
|
+
return ok(detailOf$1(store, found.item));
|
|
2117
2473
|
},
|
|
2118
2474
|
feedbackStatus: ({ params, body }) => {
|
|
2119
2475
|
const found = itemOf(store, params.id);
|
|
@@ -2162,6 +2518,174 @@ const feedbackRoutes = (context) => {
|
|
|
2162
2518
|
}
|
|
2163
2519
|
};
|
|
2164
2520
|
};
|
|
2521
|
+
// ../server/src/follow-up-routes.ts
|
|
2522
|
+
/**
|
|
2523
|
+
* @file The follow-ups' routes (specs/follow-ups.md), through the same core
|
|
2524
|
+
* functions as `ork follow-ups`: a project's list (after the due check, which
|
|
2525
|
+
* reads pull requests through the person's own `gh` and only reports a `gh`
|
|
2526
|
+
* that fails), one follow-up with what to copy and what can be done to it,
|
|
2527
|
+
* done, drop, reopen and due, and posting a drafted GitHub comment only with
|
|
2528
|
+
* the person's confirmation of the draft they saw (its digest). Ork never
|
|
2529
|
+
* runs a task's commands.
|
|
2530
|
+
*/
|
|
2531
|
+
const OPEN = ["due", "waiting"];
|
|
2532
|
+
/** Stands for what a person saw of a draft: its recipient and text. */
|
|
2533
|
+
const digestOf$1 = (f) => f.draft === void 0 ? void 0 : createHash("sha256").update(JSON.stringify([f.draft.to, f.draft.text])).digest("hex");
|
|
2534
|
+
/** The place a project's rule keeps follow-ups in. */
|
|
2535
|
+
const ruleOf = (project) => (loadProject(project.root, placeFiles(project).rules).project?.followUps ?? DEFAULT_FOLLOW_UPS_RULE).store;
|
|
2536
|
+
/** Why a follow-up isn't where the project's rule says, when it isn't. */
|
|
2537
|
+
const fallbackOf = (placed, rule) => {
|
|
2538
|
+
if (placed === rule) return void 0;
|
|
2539
|
+
return rule === "github" ? "GitHub couldn't be used when it was added (gh not signed in, or the issue failed), so Ork's store keeps it." : `It couldn't be written to ${rule} when it was added, so Ork's store keeps it.`;
|
|
2540
|
+
};
|
|
2541
|
+
const deliveryOf = (store, f) => {
|
|
2542
|
+
if (!f.for.startsWith("project:")) return void 0;
|
|
2543
|
+
const name = f.for.slice(8).trim();
|
|
2544
|
+
const target = store.projects().find((p) => p.id === name);
|
|
2545
|
+
if (target === void 0) return {
|
|
2546
|
+
project: name,
|
|
2547
|
+
registered: false
|
|
2548
|
+
};
|
|
2549
|
+
const live = liveSessionOf(store, target.root);
|
|
2550
|
+
return {
|
|
2551
|
+
project: name,
|
|
2552
|
+
registered: true,
|
|
2553
|
+
root: target.root,
|
|
2554
|
+
keptIn: ruleOf(target),
|
|
2555
|
+
...live === void 0 ? {} : { live }
|
|
2556
|
+
};
|
|
2557
|
+
};
|
|
2558
|
+
/** `f`, what can be done to it, where it is kept, and what to copy from it. */
|
|
2559
|
+
const detailOf = (context, project, f) => {
|
|
2560
|
+
const rule = ruleOf(project);
|
|
2561
|
+
const fallback = fallbackOf(f.place, rule);
|
|
2562
|
+
const cause = dueCause(f);
|
|
2563
|
+
const delivery = deliveryOf(context.store, f);
|
|
2564
|
+
const digest = digestOf$1(f);
|
|
2565
|
+
return {
|
|
2566
|
+
followUp: f,
|
|
2567
|
+
actions: followUpActions(f),
|
|
2568
|
+
...cause === void 0 ? {} : { cause },
|
|
2569
|
+
keptIn: {
|
|
2570
|
+
place: f.place,
|
|
2571
|
+
rule,
|
|
2572
|
+
...f.url === void 0 ? {} : { url: f.url },
|
|
2573
|
+
...fallback === void 0 ? {} : { fallback }
|
|
2574
|
+
},
|
|
2575
|
+
...delivery === void 0 ? {} : { delivery },
|
|
2576
|
+
...digest === void 0 ? {} : { draftDigest: digest },
|
|
2577
|
+
copy: {
|
|
2578
|
+
text: f.draft?.text ?? "",
|
|
2579
|
+
commands: (f.steps ?? []).flatMap((step) => step.command === void 0 ? [] : [step.command])
|
|
2580
|
+
}
|
|
2581
|
+
};
|
|
2582
|
+
};
|
|
2583
|
+
const toItem = (f) => ({
|
|
2584
|
+
id: f.id,
|
|
2585
|
+
project: f.project,
|
|
2586
|
+
title: f.title,
|
|
2587
|
+
for: f.for,
|
|
2588
|
+
kind: f.kind,
|
|
2589
|
+
...f.note === void 0 ? {} : { note: f.note }
|
|
2590
|
+
});
|
|
2591
|
+
/** The due follow-ups of `roots`, most recently changed first. */
|
|
2592
|
+
const dueOf = (store, roots) => {
|
|
2593
|
+
const due = roots.flatMap((root) => store.followUps(root)).filter((f) => f.status === "due").toSorted((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
2594
|
+
return {
|
|
2595
|
+
count: due.length,
|
|
2596
|
+
items: due.slice(0, 20).map((f) => toItem(f))
|
|
2597
|
+
};
|
|
2598
|
+
};
|
|
2599
|
+
const found = (context, params) => {
|
|
2600
|
+
const project = projectOf(context, params.id);
|
|
2601
|
+
if (project === void 0) return {
|
|
2602
|
+
ok: false,
|
|
2603
|
+
refusal: fail(404, `No registered project \`${params.id}\`.`)
|
|
2604
|
+
};
|
|
2605
|
+
if (params.followUp === void 0) return {
|
|
2606
|
+
ok: true,
|
|
2607
|
+
project
|
|
2608
|
+
};
|
|
2609
|
+
const one = findFollowUp(context.store, project.root, params.followUp);
|
|
2610
|
+
if (!one.ok) {
|
|
2611
|
+
const status = one.error.startsWith("No follow-up") ? 404 : 400;
|
|
2612
|
+
return {
|
|
2613
|
+
ok: false,
|
|
2614
|
+
refusal: fail(status, one.error)
|
|
2615
|
+
};
|
|
2616
|
+
}
|
|
2617
|
+
return {
|
|
2618
|
+
ok: true,
|
|
2619
|
+
project,
|
|
2620
|
+
followUp: one.followUp
|
|
2621
|
+
};
|
|
2622
|
+
};
|
|
2623
|
+
const followUpRoutes = (context) => {
|
|
2624
|
+
const { store } = context;
|
|
2625
|
+
const gh = context.gh ?? runGh;
|
|
2626
|
+
return {
|
|
2627
|
+
followUps: async ({ params, query }) => {
|
|
2628
|
+
const one = found(context, params);
|
|
2629
|
+
if (!one.ok) return one.refusal;
|
|
2630
|
+
const { root } = one.project;
|
|
2631
|
+
const errors = await checkFollowUpsDue({
|
|
2632
|
+
rows: store,
|
|
2633
|
+
project: root,
|
|
2634
|
+
gh
|
|
2635
|
+
}).then((checked) => checked.errors).catch((error) => [error instanceof Error ? error.message : String(error)]);
|
|
2636
|
+
const all = store.followUps(root);
|
|
2637
|
+
const open = (query.status ?? "open") === "open";
|
|
2638
|
+
const read = filterOf(followUpListTable, {
|
|
2639
|
+
...query,
|
|
2640
|
+
...open ? { status: "all" } : {}
|
|
2641
|
+
});
|
|
2642
|
+
if (!read.ok) return read.refusal;
|
|
2643
|
+
const other = projectRootOf(context, read.filter);
|
|
2644
|
+
const matched = other === void 0 || other === root ? applyFilters(followUpListTable, all, read.filter) : [];
|
|
2645
|
+
const shown = open ? matched.filter((f) => OPEN.some((s) => s === f.status)) : matched;
|
|
2646
|
+
return ok({
|
|
2647
|
+
project: root,
|
|
2648
|
+
followUps: shown,
|
|
2649
|
+
due: all.filter((f) => f.status === "due").length,
|
|
2650
|
+
checker: {
|
|
2651
|
+
available: errors.length === 0,
|
|
2652
|
+
errors
|
|
2653
|
+
}
|
|
2654
|
+
});
|
|
2655
|
+
},
|
|
2656
|
+
followUp: ({ params }) => {
|
|
2657
|
+
const one = found(context, params);
|
|
2658
|
+
if (!one.ok) return one.refusal;
|
|
2659
|
+
return one.followUp === void 0 ? fail(404, "No such follow-up.") : ok(detailOf(context, one.project, one.followUp));
|
|
2660
|
+
},
|
|
2661
|
+
followUpChange: ({ params, body }) => {
|
|
2662
|
+
const one = found(context, params);
|
|
2663
|
+
if (!one.ok) return one.refusal;
|
|
2664
|
+
if (one.followUp === void 0) return fail(404, "No such follow-up.");
|
|
2665
|
+
const changed = changeFollowUp(store, one.followUp, body.action);
|
|
2666
|
+
return changed.ok ? ok(detailOf(context, one.project, changed.followUp)) : fail(409, changed.error);
|
|
2667
|
+
},
|
|
2668
|
+
followUpPost: async ({ params, body }) => {
|
|
2669
|
+
const one = found(context, params);
|
|
2670
|
+
if (!one.ok) return one.refusal;
|
|
2671
|
+
const { followUp } = one;
|
|
2672
|
+
if (followUp === void 0) return fail(404, "No such follow-up.");
|
|
2673
|
+
const check = followUpActions(followUp).post;
|
|
2674
|
+
if (!check.ok) return fail(409, check.error);
|
|
2675
|
+
if (digestOf$1(followUp) !== body.digest) return fail(409, "The draft is no longer the one you confirmed; open the follow-up again.");
|
|
2676
|
+
const posted = await postFollowUpMessage(followUp, {
|
|
2677
|
+
rows: store,
|
|
2678
|
+
gh,
|
|
2679
|
+
confirmed: body.confirm,
|
|
2680
|
+
home: context.home ?? homedir()
|
|
2681
|
+
});
|
|
2682
|
+
return posted.ok ? ok({
|
|
2683
|
+
...posted.url === void 0 ? {} : { url: posted.url },
|
|
2684
|
+
followUp: detailOf(context, one.project, posted.followUp)
|
|
2685
|
+
}) : fail(409, posted.error);
|
|
2686
|
+
}
|
|
2687
|
+
};
|
|
2688
|
+
};
|
|
2165
2689
|
const ANSI = /\u001B\[[0-?]*[ -/]*[@-~]|\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)|\u001B[@-Z\\-_]/gu;
|
|
2166
2690
|
/** A line as a terminal would end up showing it. */
|
|
2167
2691
|
const plainLine = (raw) => (raw.replaceAll(ANSI, "").split("\r").findLast((part) => part !== "") ?? "").replaceAll(/\s+$/gu, "");
|
|
@@ -2668,9 +3192,15 @@ const worktreesRoutes = (context) => {
|
|
|
2668
3192
|
};
|
|
2669
3193
|
return {
|
|
2670
3194
|
worktrees: async ({ query }) => {
|
|
2671
|
-
const
|
|
3195
|
+
const filtered = filterOf(worktreeListTable, query);
|
|
3196
|
+
if (!filtered.ok) return filtered.refusal;
|
|
3197
|
+
const all = await readAll(projectRootOf(context, filtered.filter));
|
|
2672
3198
|
if (!all.ok) return all.refusal;
|
|
2673
|
-
return ok({ projects: all.reads.map((read) =>
|
|
3199
|
+
return ok({ projects: all.reads.map((read) => {
|
|
3200
|
+
const project = listed(context, read);
|
|
3201
|
+
project.items = applyFilters(worktreeListTable, project.items, filtered.filter);
|
|
3202
|
+
return project;
|
|
3203
|
+
}) });
|
|
2674
3204
|
},
|
|
2675
3205
|
worktreeRemovePlan: async ({ body }) => {
|
|
2676
3206
|
const planned = await removePlanOf(context, body);
|
|
@@ -2716,6 +3246,7 @@ const apiRoutes = (context, port) => ({
|
|
|
2716
3246
|
...builderRoutes(context),
|
|
2717
3247
|
...contextRoutes(context),
|
|
2718
3248
|
...feedbackRoutes(context),
|
|
3249
|
+
...followUpRoutes(context),
|
|
2719
3250
|
...worktreesRoutes(context),
|
|
2720
3251
|
...updateRoutes(context, port)
|
|
2721
3252
|
});
|
|
@@ -2856,7 +3387,7 @@ const runStream = async (context, req, res, url, id, shutdown) => {
|
|
|
2856
3387
|
}
|
|
2857
3388
|
const stream = openEventStream(res);
|
|
2858
3389
|
const end = await followEvents(context.store, found.run.id, (stored) => {
|
|
2859
|
-
stream.send("run-event", stored, stored.seq);
|
|
3390
|
+
stream.send("run-event", servedEvent(stored), stored.seq);
|
|
2860
3391
|
}, {
|
|
2861
3392
|
follow: true,
|
|
2862
3393
|
after,
|
|
@@ -2869,11 +3400,16 @@ const runStream = async (context, req, res, url, id, shutdown) => {
|
|
|
2869
3400
|
const overviewStream = (context, res, url, shutdown) => {
|
|
2870
3401
|
const key = url.searchParams.get("project");
|
|
2871
3402
|
const root = key === null ? void 0 : projectOf(context, key)?.root ?? key;
|
|
3403
|
+
const workflow = url.searchParams.get("workflow");
|
|
2872
3404
|
const stream = openEventStream(res);
|
|
2873
3405
|
const last = {
|
|
2874
3406
|
runs: "",
|
|
2875
3407
|
decisions: "",
|
|
2876
|
-
version: ""
|
|
3408
|
+
version: "",
|
|
3409
|
+
followUps: JSON.stringify({
|
|
3410
|
+
count: 0,
|
|
3411
|
+
items: []
|
|
3412
|
+
})
|
|
2877
3413
|
};
|
|
2878
3414
|
const tick = () => {
|
|
2879
3415
|
const filter = root === void 0 ? {} : { project: root };
|
|
@@ -2881,11 +3417,15 @@ const overviewStream = (context, res, url, shutdown) => {
|
|
|
2881
3417
|
...filter,
|
|
2882
3418
|
limit: OVERVIEW_RUNS
|
|
2883
3419
|
}).map((run) => viewOf$2(context, run));
|
|
2884
|
-
const decisions = pendingDecisions(context.store,
|
|
3420
|
+
const decisions = pendingDecisions(context.store, {
|
|
3421
|
+
...filter,
|
|
3422
|
+
...workflow === null ? {} : { workflow }
|
|
3423
|
+
});
|
|
2885
3424
|
for (const [name, value] of [
|
|
2886
3425
|
["runs", runs],
|
|
2887
3426
|
["decisions", decisions],
|
|
2888
|
-
["version", context.updates.info()]
|
|
3427
|
+
["version", context.updates.info()],
|
|
3428
|
+
["followUps", dueOf(context.store, root === void 0 ? context.store.projects().map((p) => p.root) : [root])]
|
|
2889
3429
|
]) {
|
|
2890
3430
|
const text = JSON.stringify(value);
|
|
2891
3431
|
if (text === last[name]) continue;
|
|
@@ -2959,7 +3499,9 @@ const createDispatch = (context, access) => {
|
|
|
2959
3499
|
headers: req.headers
|
|
2960
3500
|
}, {
|
|
2961
3501
|
port: access.port(),
|
|
2962
|
-
token: access.token
|
|
3502
|
+
token: access.token,
|
|
3503
|
+
session: access.session,
|
|
3504
|
+
codes: context.codes
|
|
2963
3505
|
});
|
|
2964
3506
|
if (verdict.kind === "refuse") {
|
|
2965
3507
|
if (api) sendError(res, verdict.status, verdict.error);
|
|
@@ -2974,6 +3516,10 @@ const createDispatch = (context, access) => {
|
|
|
2974
3516
|
}).end();
|
|
2975
3517
|
return;
|
|
2976
3518
|
}
|
|
3519
|
+
if (url.pathname === API.signIn.path && verdict.via !== "bearer") {
|
|
3520
|
+
sendError(res, 403, "Sign-in codes are minted with the API token only.");
|
|
3521
|
+
return;
|
|
3522
|
+
}
|
|
2977
3523
|
const streamed = RUN_STREAM.pattern.exec(url.pathname);
|
|
2978
3524
|
if (method === "GET" && streamed !== null) {
|
|
2979
3525
|
const id = decodeURIComponent(streamed[1] ?? "");
|
|
@@ -3043,6 +3589,8 @@ const runningServer = (file) => {
|
|
|
3043
3589
|
*/
|
|
3044
3590
|
/** The port `ork ui` serves on unless told otherwise. */
|
|
3045
3591
|
const DEFAULT_PORT = 4417;
|
|
3592
|
+
/** The overview stream sends the due follow-ups itself; nothing more here. */
|
|
3593
|
+
const noticed = () => {};
|
|
3046
3594
|
const listen = (server, port) => new Promise((resolve, reject) => {
|
|
3047
3595
|
server.once("error", (error) => {
|
|
3048
3596
|
reject(error.code === "EADDRINUSE" ? /* @__PURE__ */ new Error(`Port ${String(port)} is in use; start Ork's UI on another with \`--port <n>\`.`) : error);
|
|
@@ -3060,7 +3608,13 @@ const startServer = async (options) => {
|
|
|
3060
3608
|
...options.home === void 0 ? {} : { home: options.home },
|
|
3061
3609
|
...options.update === void 0 ? {} : { options: options.update }
|
|
3062
3610
|
});
|
|
3611
|
+
const { token, session, codes } = createAccess({
|
|
3612
|
+
...options.token === void 0 ? {} : { token: options.token },
|
|
3613
|
+
...options.sessionSecret === void 0 ? {} : { session: options.sessionSecret },
|
|
3614
|
+
...options.codes === void 0 ? {} : { codes: options.codes }
|
|
3615
|
+
});
|
|
3063
3616
|
const context = {
|
|
3617
|
+
codes,
|
|
3064
3618
|
store,
|
|
3065
3619
|
version: options.version,
|
|
3066
3620
|
pid: process.pid,
|
|
@@ -3070,9 +3624,9 @@ const startServer = async (options) => {
|
|
|
3070
3624
|
...uiDir === void 0 ? {} : { uiDir },
|
|
3071
3625
|
...options.gh === void 0 ? {} : { gh: options.gh },
|
|
3072
3626
|
updates,
|
|
3073
|
-
...options.restart === void 0 ? {} : { restart: options.restart }
|
|
3627
|
+
...options.restart === void 0 ? {} : { restart: options.restart },
|
|
3628
|
+
...options.noteScrub === void 0 ? {} : { noteScrub: options.noteScrub }
|
|
3074
3629
|
};
|
|
3075
|
-
const token = options.token ?? newToken();
|
|
3076
3630
|
const shutdown = new AbortController();
|
|
3077
3631
|
const log = options.log ?? ((line) => {
|
|
3078
3632
|
process.stderr.write(`${line}\n`);
|
|
@@ -3081,6 +3635,7 @@ const startServer = async (options) => {
|
|
|
3081
3635
|
const handle = createDispatch(context, {
|
|
3082
3636
|
port: () => port,
|
|
3083
3637
|
token,
|
|
3638
|
+
session,
|
|
3084
3639
|
shutdown: shutdown.signal
|
|
3085
3640
|
});
|
|
3086
3641
|
const server = createServer((req, res) => {
|
|
@@ -3104,6 +3659,15 @@ const startServer = async (options) => {
|
|
|
3104
3659
|
token,
|
|
3105
3660
|
startedAt
|
|
3106
3661
|
});
|
|
3662
|
+
const stopChecks = startFollowUpChecks({
|
|
3663
|
+
rows: store,
|
|
3664
|
+
gh: options.gh ?? runGh,
|
|
3665
|
+
projects: () => store.projects().map((project) => project.root),
|
|
3666
|
+
onDue: noticed,
|
|
3667
|
+
onError: (error) => {
|
|
3668
|
+
log(`ork ui: follow-ups: ${error}`);
|
|
3669
|
+
}
|
|
3670
|
+
});
|
|
3107
3671
|
let closed;
|
|
3108
3672
|
return {
|
|
3109
3673
|
port,
|
|
@@ -3116,6 +3680,7 @@ const startServer = async (options) => {
|
|
|
3116
3680
|
close: () => {
|
|
3117
3681
|
closed ??= new Promise((resolve) => {
|
|
3118
3682
|
shutdown.abort();
|
|
3683
|
+
stopChecks();
|
|
3119
3684
|
updates.close();
|
|
3120
3685
|
server.close(() => {
|
|
3121
3686
|
store.close();
|
|
@@ -3128,4 +3693,4 @@ const startServer = async (options) => {
|
|
|
3128
3693
|
}
|
|
3129
3694
|
};
|
|
3130
3695
|
};
|
|
3131
|
-
export { openMcp as a, refused$1 as c, uiFilePath as i, newToken as l, startServer as n, tool as o, runningServer as r, answer as s, DEFAULT_PORT as t };
|
|
3696
|
+
export { openMcp as a, refused$1 as c, uiFilePath as i, newToken as l, startServer as n, tool as o, runningServer as r, answer as s, DEFAULT_PORT as t, createClient as u };
|