@krapht/ork 0.3.0 → 1.0.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/README.md +16 -12
  2. package/dist/__build_info__-DdTVd2p5.mjs +615 -0
  3. package/dist/bin.mjs +1572 -909
  4. package/dist/clean-NnboOuz6.mjs +88 -0
  5. package/dist/completion-snnYGjgK.mjs +405 -0
  6. package/dist/{context-BFGC_C02.mjs → context-DO3sBy9v.mjs} +30 -191
  7. package/dist/decide-B81wCZQg.mjs +113 -0
  8. package/dist/{find-BcvhT6UN.mjs → find-BKW-uGey.mjs} +1 -1
  9. package/dist/{format-DqpObyMl.mjs → format-CmTlTOjT.mjs} +72 -198
  10. package/dist/help-groups-BwYoAWsh.mjs +354 -0
  11. package/dist/{in-a-run-BbLhZOFW.mjs → in-a-run-BPXX09Bs.mjs} +1 -1
  12. package/dist/{limits-BKYAmqCR.mjs → limits-JCve2QpI.mjs} +1 -1
  13. package/dist/list-C7UEgmtd.mjs +110 -0
  14. package/dist/{mcp-BWsdQyri.mjs → mcp-CKPyNaKD.mjs} +7 -7
  15. package/dist/{output-Q2EdeVUt.mjs → output-B1V6ZJSx.mjs} +130 -36
  16. package/dist/{pause-DKACDfV1.mjs → pause-B_SL_yDz.mjs} +4 -4
  17. package/dist/{relay-CbK-Wme9.mjs → relay-o-Luee1c.mjs} +2 -2
  18. package/dist/remove-DAZ7ZCzt.mjs +194 -0
  19. package/dist/{send-C6yr04Fe.mjs → send-3B64fBNB.mjs} +3 -3
  20. package/dist/{src-D5ruZFgD.mjs → src-BJTegEzY.mjs} +1212 -57
  21. package/dist/{src-DtN1k-B3.mjs → src-C38BPzit.mjs} +251 -18
  22. package/dist/{src-Cz1yOUTg.mjs → src-DHvWU4tX.mjs} +2742 -1659
  23. package/dist/{src-CZOVF2yU.mjs → src-Dyz_Gy1q.mjs} +35 -8
  24. package/dist/ui/THIRD_PARTY_LICENSES.txt +2 -2
  25. package/dist/ui/assets/app-C8nJ6NpT.js +67 -0
  26. package/dist/ui/assets/app-CH7YC3in.css +1 -0
  27. package/dist/ui/assets/{index-BiO54L90.css → index-BPIbqRRb.css} +1 -1
  28. package/dist/ui/assets/{index-BoG6O-Lp.js → index-D83F2OKZ.js} +2 -2
  29. package/dist/ui/index.html +2 -3
  30. package/dist/ui-CY4xxJUu.mjs +472 -0
  31. package/dist/{usage-BIjf9Xdt.mjs → usage-CJnytoe4.mjs} +6 -5
  32. package/dist/{watch-B8o4Szd_.mjs → watch-BJB82ZJ7.mjs} +7 -7
  33. package/dist/workflows-CiM7S3l0.mjs +88 -0
  34. package/dist/worktrees-86ySXxR6.mjs +212 -0
  35. package/package.json +6 -6
  36. package/dist/__build_info__-CXXMLvjt.mjs +0 -104
  37. package/dist/ui/assets/app-BlDdP396.js +0 -61
  38. package/dist/ui/assets/app-C722xNZ3.css +0 -1
  39. package/dist/ui-CZS1XKla.mjs +0 -160
package/README.md CHANGED
@@ -3,29 +3,33 @@
3
3
  The `ork` command: orchestrate Claude agents on your machine with hierarchical,
4
4
  serializable workflows.
5
5
 
6
- > **Pre-alpha.** The workflow format, `ork run` with delegation and worktrees,
7
- > the context compiler, agents, run logs, decisions from any terminal, messages
8
- > between nodes, chat, resume, claims and local feedback work today; a web UI
9
- > comes next. Expect changes between 0.x releases. See the
10
- > [project README](https://github.com/mimshins/ork#readme) and the
11
- > [guides](https://github.com/mimshins/ork/tree/main/docs#readme).
6
+ > **Pre-alpha.** Workflows of Claude agents and tools, run from the terminal or
7
+ > the browser (`ork ui`). Expect changes between 0.x releases. See the
8
+ > [project README](https://github.com/mimshins/ork#readme), the
9
+ > [guides](https://github.com/mimshins/ork/tree/main/docs#readme) and
10
+ > [what's new](https://github.com/mimshins/ork/blob/main/docs/whats-new.md).
11
+
12
+ Rather have your coding agent set it up? See
13
+ [With your agent](https://github.com/mimshins/ork/blob/main/docs/getting-started/with-your-agent.md).
12
14
 
13
15
  ```sh
14
16
  npm install --global @krapht/ork
15
17
 
16
18
  ork doctor # node, git, and your own claude / gh / aws logins
17
- ork init # scaffold .ork/ with a starter workflow
19
+ ork init # .ork/ with a starter workflow
18
20
  ork validate # check .ork/workflows/*.ork.yaml
19
21
  ork run hello --input task="add a README"
20
22
  # run the workflow; questions come to the terminal
21
- ork worktrees # the worktrees and branches runs left; `clean` tidies up
22
- ork run hello --node lead --input task="add a README" --in-place
23
- # run one Claude node on its own
23
+ ork ui # your runs in the browser
24
+ ork worktrees # the worktrees and branches runs left
25
+ ork worktrees clean # removes the merged ones; asks first
24
26
  ork upgrade # a newer version? upgrade the way you installed Ork
25
27
  ```
26
28
 
27
- `--driver sdk` (the default) needs `ANTHROPIC_API_KEY` or a cloud provider;
28
- `--driver cli` uses your own logged-in `claude`.
29
+ A workflow that names no Claude driver runs on the one that's ready: the CLI
30
+ driver uses your own logged-in `claude`; the SDK driver needs
31
+ `ANTHROPIC_API_KEY` or a cloud provider. `--driver cli` or `--driver sdk` picks
32
+ one.
29
33
 
30
34
  Ork never stores or asks for credentials: it uses the tools you already have,
31
35
  signed in with your own accounts. Once a day it asks the npm registry whether a
@@ -0,0 +1,615 @@
1
+ import { An as guardedBase, En as inside, Gt as publishPost, Mn as writePlainFile, On as LESSON_CAP_TOKENS, Rt as FEEDBACK_KINDS, Yt as targetOf, Zt as maskSecrets, bn as placeFiles, g as parseVersion, h as isNewer, jn as plainPathInside, kn as estimateTokens, m as compareVersions, qt as fingerprint, xn as placeFor, zt as FEEDBACK_SEVERITIES } from "./src-DHvWU4tX.mjs";
2
+ import path from "node:path";
3
+ import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
4
+ import { homedir } from "node:os";
5
+ import { spawn } from "node:child_process";
6
+ // ../core/src/feedback/actions.ts
7
+ const ALLOWED = { ok: true };
8
+ const no = (why, error) => ({
9
+ ok: false,
10
+ why,
11
+ error
12
+ });
13
+ /** A dismissed item allows nothing until it's restored. */
14
+ const dismissed = (short) => no("dismissed; restore it first", `${short} is dismissed; restore it first.`);
15
+ const already = (short, status) => no(`${status} already`, `${short} is ${status} already.`);
16
+ /**
17
+ * `fixRun` is a running `ork:feedback-apply` run for the item ({@link
18
+ * openFixRun}): a second fix isn't started beside it.
19
+ */
20
+ const feedbackActions = (item, fixRun) => {
21
+ const short = item.id.slice(0, 12);
22
+ const isOrk = item.target === "ork";
23
+ if (item.status === "dismissed") {
24
+ const check = dismissed(short);
25
+ return {
26
+ accept: check,
27
+ lesson: check,
28
+ apply: check,
29
+ publish: check
30
+ };
31
+ }
32
+ const settled = item.status === "resolved" || item.status === "published";
33
+ const accept = item.status === "accepted" || settled ? already(short, item.status) : ALLOWED;
34
+ let lesson = ALLOWED;
35
+ if (isOrk) lesson = no("about Ork itself", `${short} is about Ork itself, so it isn't a lesson for a workflow; share it with Ork's maintainers instead.`);
36
+ else if (item.workflows.length === 0) lesson = no("no workflow recorded, so no lessons file", `No workflow is recorded for ${short}, so it has no lessons file.`);
37
+ let apply = ALLOWED;
38
+ if (isOrk) apply = no("about Ork itself", `${short} is about Ork itself, so its fix isn't in this project; share it with Ork's maintainers instead.`);
39
+ else if (settled) apply = already(short, item.status);
40
+ else if (fixRun !== void 0) apply = no(`run ${fixRun} is proposing a fix`, `Run ${fixRun} is proposing a fix for ${short} already: answer its question in \`ork decide\` or the UI, or stop it first.`);
41
+ let publish = ALLOWED;
42
+ if (!isOrk) publish = no("only feedback about Ork is shared", `${short} is about this ${item.target}, not Ork itself: only feedback about Ork is shared, and the rest stays on your machine.`);
43
+ else if (item.status === "published") publish = already(short, item.status);
44
+ return {
45
+ accept,
46
+ lesson,
47
+ apply,
48
+ publish
49
+ };
50
+ };
51
+ /** The running `ork:feedback-apply` run proposing a fix for `item`, if any. */
52
+ const openFixRun = (store, item) => {
53
+ const short = item.id.slice(0, 12);
54
+ return store.runs().filter((run) => run.workflow === "ork-feedback-apply" && run.status === "running").find((run) => {
55
+ const [first] = store.events(run.id);
56
+ const event = first?.event;
57
+ return event?.type === "run.started" && event.inputs?.["item"] === short;
58
+ })?.id;
59
+ };
60
+ // ../core/src/feedback/lessons.ts
61
+ /**
62
+ * @file Accepting a feedback item as a lesson (specs/feedback.md, ADR-059): one
63
+ * line in the workflow's `.ork/lessons/<workflow>.md`, marked with the item
64
+ * it came from so it isn't added twice, and refused when it would take the
65
+ * file past the cap a session loads, or when the file, or a folder on the way
66
+ * to it, is a link a cloned repository could aim at the person's own files.
67
+ * Only a person's acceptance writes one: feedback is model output that
68
+ * repository content can steer.
69
+ */
70
+ /** A workflow's name, as the format allows it: never a path. */
71
+ const SLUG = /^[a-z0-9][a-z0-9._-]*$/u;
72
+ /** A workflow's lessons file in a project. */
73
+ const lessonsFileOf = (place, workflow) => path.join(placeFiles(place).lessons, `${workflow}.md`);
74
+ /** What marks a lesson with its item, in the file. */
75
+ const lessonMark = (item) => `<!-- feedback ${item.slice(0, 12)} -->`;
76
+ /** The lesson as one list entry, its later lines indented under the first. */
77
+ const entryOf = (text, item) => {
78
+ const [first = "", ...rest] = text.trim().split("\n").map((line) => line.trimEnd());
79
+ return [`- ${first} ${lessonMark(item)}`, ...rest.map((l) => ` ${l}`)].join("\n").concat("\n");
80
+ };
81
+ /** Adds a lesson to its workflow's file, or says why it can't. */
82
+ const addLesson = (ask) => {
83
+ if (ask.text.trim() === "") return {
84
+ ok: false,
85
+ error: "The lesson is empty."
86
+ };
87
+ if (!SLUG.test(ask.workflow) || !plainPathInside(ask.base, ask.file)) return {
88
+ ok: false,
89
+ error: `${ask.shownAs} is a link, or behind one, or outside the project: Ork writes lessons only to a plain file there, so it wrote nothing.`
90
+ };
91
+ const current = existsSync(ask.file) ? readFileSync(ask.file, "utf8") : `# Lessons for ${ask.workflow}\n\n`;
92
+ if (current.includes(lessonMark(ask.item))) return {
93
+ ok: false,
94
+ error: `${ask.item.slice(0, 12)} is already a lesson in ${ask.shownAs}.`
95
+ };
96
+ const next = `${current}${current === "" || current.endsWith("\n") ? "" : "\n"}${entryOf(ask.text, ask.item)}`;
97
+ const tokens = estimateTokens(next);
98
+ if (tokens > 1500) return {
99
+ ok: false,
100
+ error: `${ask.shownAs} would be about ${tokens.toLocaleString("en")} tokens, over the ${LESSON_CAP_TOKENS.toLocaleString("en")} a session loads. Edit it to make room, then accept again.`
101
+ };
102
+ const written = writePlainFile(ask.base, ask.file, next, ask.shownAs);
103
+ return written.ok ? {
104
+ ok: true,
105
+ tokens
106
+ } : written;
107
+ };
108
+ // ../core/src/feedback/accept.ts
109
+ /**
110
+ * @file Accepting a feedback item (`ork feedback accept`, the Feedback panel):
111
+ * marks an item accepted and, with `--lesson`, writes it as a lesson into the
112
+ * lessons file of the workflow it was seen in, in the project of its newest
113
+ * record there (ADR-059). An `ork` item is about Ork itself, so it's never a
114
+ * lesson: it can be published instead.
115
+ */
116
+ const listed = (names) => names.map((n) => `\`${n}\``).join(", ");
117
+ /** The workflow a lesson is for: the one named, or the only one it was seen in. */
118
+ const workflowFor = (item, named) => {
119
+ if (named !== void 0) return item.workflows.includes(named) ? {
120
+ ok: true,
121
+ workflow: named
122
+ } : {
123
+ ok: false,
124
+ error: `${item.id.slice(0, 12)} wasn't seen in \`${named}\`, only in ${listed(item.workflows)}.`
125
+ };
126
+ const [only, other] = item.workflows;
127
+ if (only === void 0) return {
128
+ ok: false,
129
+ error: `No workflow is recorded for ${item.id.slice(0, 12)}, so it has no lessons file.`
130
+ };
131
+ return other === void 0 ? {
132
+ ok: true,
133
+ workflow: only
134
+ } : {
135
+ ok: false,
136
+ error: `${item.id.slice(0, 12)} was seen in ${listed(item.workflows)}: name one with --workflow.`
137
+ };
138
+ };
139
+ const real = (dir) => {
140
+ try {
141
+ return realpathSync(dir);
142
+ } catch {
143
+ return path.resolve(dir);
144
+ }
145
+ };
146
+ /**
147
+ * The project of the item's newest record (in `workflow`, when given): the
148
+ * folder its run started in, when that's still the same project. A clone that
149
+ * was deleted is refused, never taken for a repository above it.
150
+ */
151
+ const placeOf = (store, item, workflow, registered) => {
152
+ const record = store.feedbackRecords(item.id).findLast((r) => workflow === void 0 || r.feedback.origin.workflow === workflow);
153
+ const [run] = record === void 0 ? [] : store.lookup(record.runId);
154
+ if (run === void 0) return {
155
+ ok: false,
156
+ error: `The runs ${item.id.slice(0, 12)} was seen in are gone, so its project isn't known.`
157
+ };
158
+ if (!existsSync(run.project)) return {
159
+ ok: false,
160
+ error: `Its project at ${run.project} is gone.`
161
+ };
162
+ const folder = path.dirname(run.file);
163
+ const place = placeFor(existsSync(folder) ? folder : run.project, registered);
164
+ return real(place.root) === real(run.project) ? {
165
+ ok: true,
166
+ place,
167
+ project: run.project
168
+ } : {
169
+ ok: false,
170
+ error: `Its project at ${run.project} isn't a project any more: Ork would take it for ${place.root}.`
171
+ };
172
+ };
173
+ /** Accepts `item`, as a lesson with `--lesson`. */
174
+ const acceptItem = (store, item, args, registered) => {
175
+ const name = `${item.id.slice(0, 12)}: ${item.title}`;
176
+ const actions = feedbackActions(item);
177
+ if (!args.lesson) {
178
+ if (!actions.accept.ok) return {
179
+ ok: false,
180
+ error: actions.accept.error
181
+ };
182
+ store.setFeedbackStatus(item.id, "accepted");
183
+ return {
184
+ ok: true,
185
+ lines: [`accepted ${name}`]
186
+ };
187
+ }
188
+ if (!actions.lesson.ok) return {
189
+ ok: false,
190
+ error: actions.lesson.error
191
+ };
192
+ const chosen = workflowFor(item, args.workflow);
193
+ if (!chosen.ok) return chosen;
194
+ const found = placeOf(store, item, chosen.workflow, registered);
195
+ if (!found.ok) return found;
196
+ const { place } = found;
197
+ const file = lessonsFileOf(place, chosen.workflow);
198
+ const shownAs = inside(place.root, file) ? path.relative(place.root, file) : file;
199
+ const added = addLesson({
200
+ file,
201
+ base: guardedBase(place),
202
+ shownAs,
203
+ workflow: chosen.workflow,
204
+ item: item.id,
205
+ text: args.text ?? item.title
206
+ });
207
+ if (!added.ok) return added;
208
+ if (actions.accept.ok) store.setFeedbackStatus(item.id, "accepted");
209
+ return {
210
+ ok: true,
211
+ lesson: {
212
+ file: shownAs,
213
+ root: place.root,
214
+ workflow: chosen.workflow,
215
+ tokens: added.tokens
216
+ },
217
+ lines: [`accepted ${name}`, `lesson added to ${shownAs} in ${place.root} (about ${added.tokens.toLocaleString("en")} of ${LESSON_CAP_TOKENS.toLocaleString("en")} tokens); every session of \`${chosen.workflow}\` reads it from now on`]
218
+ };
219
+ };
220
+ const lessonPlaceOf = (store, item, workflow, registered) => {
221
+ const found = placeOf(store, item, workflow, registered);
222
+ if (!found.ok) return {
223
+ ok: false,
224
+ workflow,
225
+ error: found.error
226
+ };
227
+ const { place } = found;
228
+ const file = lessonsFileOf(place, workflow);
229
+ const exists = existsSync(file);
230
+ return {
231
+ ok: true,
232
+ workflow,
233
+ file: inside(place.root, file) ? path.relative(place.root, file) : file,
234
+ root: place.root,
235
+ tokens: exists ? estimateTokens(readFileSync(file, "utf8")) : 0,
236
+ exists
237
+ };
238
+ };
239
+ // ../core/src/feedback/human.ts
240
+ const RATINGS = ["good", "bad"];
241
+ const MAX_TITLE = 120;
242
+ /** The run event a note becomes; its title is the note's first line. */
243
+ const humanNoteEvent = (note) => {
244
+ const text = maskSecrets(note.text.trim());
245
+ const [first = ""] = text.split("\n");
246
+ const title = first.slice(0, MAX_TITLE);
247
+ const kind = note.kind ?? "friction";
248
+ const target = targetOf({
249
+ kind,
250
+ title,
251
+ ...note.target === void 0 ? {} : { target: note.target }
252
+ });
253
+ const feedback = {
254
+ source: "human",
255
+ kind,
256
+ target,
257
+ severity: note.severity ?? (note.rating === "bad" ? "high" : "medium"),
258
+ title,
259
+ body: text,
260
+ evidence: note.rating === void 0 ? [] : [`rating: ${note.rating}`],
261
+ fingerprint: fingerprint({
262
+ target,
263
+ kind,
264
+ title
265
+ }),
266
+ origin: { workflow: note.workflow }
267
+ };
268
+ return {
269
+ type: "feedback.recorded",
270
+ ...note.node === void 0 ? {} : { node: note.node },
271
+ feedback
272
+ };
273
+ };
274
+ /** `value` when it's one of `allowed`, or why not. */
275
+ const checked = (name, value, allowed) => {
276
+ if (value === void 0) return { ok: true };
277
+ const found = allowed.find((a) => a === value);
278
+ return found === void 0 ? {
279
+ ok: false,
280
+ error: `${name} is one of ${allowed.join(", ")}`
281
+ } : {
282
+ ok: true,
283
+ value: found
284
+ };
285
+ };
286
+ /**
287
+ * Whether a note can be recorded, before anything else is done: its words,
288
+ * kind, severity and rating. `flag` names the options as the caller does (`--`
289
+ * for the CLI).
290
+ */
291
+ const checkNote = (ask, flag = "") => {
292
+ if (ask.text.trim() === "") return {
293
+ ok: false,
294
+ error: "The note is empty."
295
+ };
296
+ const kind = checked(`${flag}kind`, ask.kind, FEEDBACK_KINDS);
297
+ const severity = checked(`${flag}severity`, ask.severity, FEEDBACK_SEVERITIES);
298
+ const rating = checked(`${flag}rating`, ask.rating, RATINGS);
299
+ if (!kind.ok) return kind;
300
+ if (!severity.ok) return severity;
301
+ if (!rating.ok) return rating;
302
+ return {
303
+ ok: true,
304
+ ...kind.value === void 0 ? {} : { kind: kind.value },
305
+ ...severity.value === void 0 ? {} : { severity: severity.value },
306
+ ...rating.value === void 0 ? {} : { rating: rating.value }
307
+ };
308
+ };
309
+ /**
310
+ * Records a person's note on `run`, or one node that ran in it, as that run's
311
+ * feedback (a run that ended takes it too); returns the item it joins (the one
312
+ * it was merged into, when triage merged it), or why it can't. `ork feedback
313
+ * note`, `ork decide --note` and the UI write it.
314
+ */
315
+ const recordNote = (store, run, ask, flag = "") => {
316
+ const note = checkNote(ask, flag);
317
+ if (!note.ok) return note;
318
+ const ran = store.events(run.id).some(({ event }) => "id" in event && event.id === ask.node);
319
+ if (ask.node !== void 0 && !ran) return {
320
+ ok: false,
321
+ error: `No node ${ask.node} ran in run ${run.id}.`
322
+ };
323
+ const event = humanNoteEvent({
324
+ text: ask.text,
325
+ workflow: run.workflow,
326
+ ...ask.node === void 0 ? {} : { node: ask.node },
327
+ ...note.kind === void 0 ? {} : { kind: note.kind },
328
+ ...note.severity === void 0 ? {} : { severity: note.severity },
329
+ ...note.rating === void 0 ? {} : { rating: note.rating }
330
+ });
331
+ store.append(run.id, event);
332
+ const recorded = event.type === "feedback.recorded" ? event.feedback.fingerprint : "";
333
+ return {
334
+ ok: true,
335
+ item: store.findFeedback(recorded)[0]?.id ?? recorded
336
+ };
337
+ };
338
+ // ../core/src/feedback/preview.ts
339
+ /**
340
+ * @file The preview of an `ork` item as a post to Ork's Discussions (ADR-059),
341
+ * for `ork feedback publish` and the Feedback panel. The projects its records
342
+ * came from and every registered one are rewritten to `<project>`, and when
343
+ * names are hidden their folders' and the workflows' names go too. What the
344
+ * person confirms is what is sent.
345
+ */
346
+ /** The item as a post, or why it isn't shared. */
347
+ const previewPost = (store, item, ask) => {
348
+ const allowed = feedbackActions(item).publish;
349
+ if (!allowed.ok) return {
350
+ ok: false,
351
+ error: allowed.error
352
+ };
353
+ const records = store.feedbackRecords(item.id);
354
+ const runRoots = records.flatMap((r) => store.lookup(r.runId).find((run) => run.id === r.runId)?.project ?? []);
355
+ const roots = [.../* @__PURE__ */ new Set([...runRoots, ...ask.registered.map((p) => p.root)])];
356
+ const names = ask.hideNames ? [...item.workflows, ...roots.map((root) => path.basename(root))] : [];
357
+ return {
358
+ ok: true,
359
+ post: publishPost(item, records, {
360
+ roots,
361
+ home: ask.home,
362
+ names
363
+ })
364
+ };
365
+ };
366
+ // ../core/src/remote/session.ts
367
+ const chattable = (type) => type === "claude.leader" || type === "claude.worker";
368
+ /** Reads `node`'s session from a run's events, and every node's last output. */
369
+ const nodeSession = (events, node) => {
370
+ const session = {
371
+ known: false,
372
+ chattable: false,
373
+ running: false
374
+ };
375
+ const outputs = {};
376
+ for (const { event } of events) {
377
+ if (event.type === "run.started") {
378
+ const declared = event.nodes.find((n) => n.id === node);
379
+ if (declared !== void 0) {
380
+ session.known = true;
381
+ session.chattable = chattable(declared.type);
382
+ }
383
+ }
384
+ if (event.type === "node.finished") outputs[event.id] = event.output;
385
+ if (!("id" in event) || event.id !== node) continue;
386
+ if (event.type === "node.started") {
387
+ session.known = true;
388
+ session.chattable = chattable(event.nodeType);
389
+ session.running = true;
390
+ }
391
+ if (event.type === "node.finished") session.running = false;
392
+ if (event.type === "worktree" && event.action === "created") session.checkout = {
393
+ path: event.path,
394
+ ...event.branch === void 0 ? {} : { branch: event.branch },
395
+ own: true,
396
+ owner: node
397
+ };
398
+ if (event.type !== "node.event") continue;
399
+ const inner = event.event;
400
+ if (inner.fork === true) continue;
401
+ if (inner.type === "session.started") {
402
+ session.sessionId = inner.sessionId;
403
+ if (inner.cwd !== void 0) session.cwd = inner.cwd;
404
+ if (inner.driver !== void 0) session.driver = inner.driver;
405
+ }
406
+ if (inner.type === "session.finished" && inner.outcome.sessionId !== "") session.sessionId = inner.outcome.sessionId;
407
+ }
408
+ return {
409
+ session,
410
+ outputs
411
+ };
412
+ };
413
+ /** The dist-tags every install follows, toward and after 1.0.0. */
414
+ const CHANNEL_TAGS = [
415
+ "latest",
416
+ "alpha",
417
+ "beta",
418
+ "rc"
419
+ ];
420
+ /** The dist-tag of snapshots from `main`, followed only by a canary. */
421
+ const CANARY_TAG = "canary";
422
+ const updateCachePath = (home = homedir()) => path.join(home, ".ork", "update-check.json");
423
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
424
+ const text = (record, key) => {
425
+ const value = record[key];
426
+ return typeof value === "string" ? value : void 0;
427
+ };
428
+ const notifiedOf = (value) => {
429
+ if (!isRecord(value)) return void 0;
430
+ const version = text(value, "version");
431
+ const at = text(value, "at");
432
+ return version === void 0 || at === void 0 ? void 0 : {
433
+ version,
434
+ at
435
+ };
436
+ };
437
+ /** The cache, or `null` when there is none or it cannot be read. */
438
+ const readUpdateCache = (file) => {
439
+ let parsed;
440
+ try {
441
+ parsed = JSON.parse(readFileSync(file, "utf8"));
442
+ } catch {
443
+ return null;
444
+ }
445
+ if (!isRecord(parsed)) return null;
446
+ const checkedAt = text(parsed, "checkedAt");
447
+ if (checkedAt === void 0) return null;
448
+ const latest = text(parsed, "latest");
449
+ const notified = notifiedOf(parsed["notified"]);
450
+ return {
451
+ checkedAt,
452
+ ...latest === void 0 ? {} : { latest },
453
+ ...notified === void 0 ? {} : { notified }
454
+ };
455
+ };
456
+ /** Writes the cache whole (a temporary file, then a rename). */
457
+ const writeUpdateCache = (file, cache) => {
458
+ mkdirSync(path.dirname(file), { recursive: true });
459
+ const temporary = `${file}.${process.pid}.tmp`;
460
+ writeFileSync(temporary, `${JSON.stringify(cache, null, 2)}\n`);
461
+ renameSync(temporary, file);
462
+ };
463
+ const age = (iso, now) => {
464
+ const time = Date.parse(iso);
465
+ return Number.isNaN(time) ? Infinity : now.getTime() - time;
466
+ };
467
+ /** Whether it is time to ask the registry again. */
468
+ const checkDue = (cache, now) => cache === null || age(cache.checkedAt, now) >= 864e5;
469
+ /** Whether `version` is a canary snapshot: its pre-release starts `canary`. */
470
+ const isCanary = (version) => parseVersion(version)?.pre[0]?.startsWith(CANARY_TAG) === true;
471
+ /**
472
+ * The newest version, by semver precedence, among the dist-tags `current`
473
+ * follows: the channel tags, and `canary` too when `current` is a canary. A tag
474
+ * whose value doesn't parse is skipped; `null` when none does.
475
+ */
476
+ const newestTagged = (tags, current) => {
477
+ return (current !== void 0 && isCanary(current) ? [...CHANNEL_TAGS, "canary"] : CHANNEL_TAGS).flatMap((tag) => {
478
+ const value = tags[tag];
479
+ const version = typeof value === "string" ? parseVersion(value) : null;
480
+ return typeof value === "string" && version !== null ? [{
481
+ value,
482
+ version
483
+ }] : [];
484
+ }).reduce((best, candidate) => best === null || compareVersions(candidate.version, best.version) > 0 ? candidate : best, null)?.value ?? null;
485
+ };
486
+ /**
487
+ * The newest version on the registry that the running version follows ({@link
488
+ * newestTagged}), or `null` on any failure.
489
+ */
490
+ const fetchLatest = async (name, options = {}) => {
491
+ const registry = (options.registry ?? "https://registry.npmjs.org").replace(/\/+$/u, "");
492
+ const get = options.fetch ?? globalThis.fetch;
493
+ try {
494
+ const response = await get(`${registry}/-/package/${name}/dist-tags`, {
495
+ headers: { accept: "application/json" },
496
+ signal: AbortSignal.timeout(options.timeoutMs ?? 5e3)
497
+ });
498
+ if (!response.ok) return null;
499
+ const tags = await response.json();
500
+ return isRecord(tags) ? newestTagged(tags, options.current) : null;
501
+ } catch {
502
+ return null;
503
+ }
504
+ };
505
+ /**
506
+ * Asks the registry and records the answer. A failed check is recorded too, so
507
+ * an offline machine is not asked again until the interval passes; the last
508
+ * known version is kept. `options.current` is the running version.
509
+ */
510
+ const refreshUpdateCache = async (file, name, now, options = {}) => {
511
+ const before = readUpdateCache(file);
512
+ const latest = await fetchLatest(name, options) ?? before?.latest;
513
+ const cache = {
514
+ ...before,
515
+ checkedAt: now.toISOString(),
516
+ ...latest === void 0 ? {} : { latest }
517
+ };
518
+ writeUpdateCache(file, cache);
519
+ return cache;
520
+ };
521
+ /** The newer version the cache knows of, if any. */
522
+ const newerVersion = (cache, current) => cache?.latest !== void 0 && isNewer(cache.latest, current) ? cache.latest : null;
523
+ /**
524
+ * The version to tell the user about now: newer than theirs, and not already
525
+ * told today. `null` when there is nothing to say.
526
+ */
527
+ const pendingNotice = (cache, current, now) => {
528
+ const newer = newerVersion(cache, current);
529
+ if (newer === null) return null;
530
+ const told = cache?.notified;
531
+ return told?.version === newer && age(told.at, now) < 864e5 ? null : newer;
532
+ };
533
+ /**
534
+ * Runs the command; resolves its exit code. Without `onOutput` it writes on the
535
+ * terminal.
536
+ */
537
+ const runInstall = (argv, onOutput) => new Promise((resolve) => {
538
+ const [program = "", ...args] = argv;
539
+ const child = spawn(program, args, { stdio: onOutput === void 0 ? "inherit" : [
540
+ "ignore",
541
+ "pipe",
542
+ "pipe"
543
+ ] });
544
+ for (const stream of ["stdout", "stderr"]) {
545
+ child[stream]?.setEncoding("utf8");
546
+ child[stream]?.on("data", (text) => {
547
+ onOutput?.(stream, text);
548
+ });
549
+ }
550
+ child.on("error", (error) => {
551
+ const line = `Could not run ${program}: ${error.message}\n`;
552
+ if (onOutput === void 0) process.stderr.write(line);
553
+ else onOutput("stderr", line);
554
+ resolve(127);
555
+ });
556
+ child.on("close", (code) => {
557
+ resolve(code ?? 1);
558
+ });
559
+ });
560
+ // ../core/src/update/switches.ts
561
+ /**
562
+ * @file The environment's say over the update check (ADR-038): the variables
563
+ * that turn it off (`ORK_NO_UPDATE_CHECK`, the common `NO_UPDATE_NOTIFIER`,
564
+ * and `CI`), and the registry to ask instead of npm's. The CLI adds its own
565
+ * rules about where a notice may go; the server behind `ork ui` uses only
566
+ * these.
567
+ */
568
+ /** Turns the automatic check and its notice off. */
569
+ const NO_UPDATE_ENV = "ORK_NO_UPDATE_CHECK";
570
+ /** A registry to ask instead of npm's, e.g. a company mirror. */
571
+ const UPDATE_REGISTRY_ENV = "ORK_UPDATE_REGISTRY";
572
+ /** The variables that turn the check off, in the order they are looked at. */
573
+ const UPDATE_SWITCHES = [
574
+ NO_UPDATE_ENV,
575
+ "NO_UPDATE_NOTIFIER",
576
+ "CI"
577
+ ];
578
+ /** Set to anything but empty, `0` or `false`. */
579
+ const isSwitchedOn = (value) => value !== void 0 && value !== "" && value !== "0" && value.toLowerCase() !== "false";
580
+ /** The variable that turns the check off in `env`, or `null` when none does. */
581
+ const updateSwitchOff = (env) => UPDATE_SWITCHES.find((name) => isSwitchedOn(env[name])) ?? null;
582
+ /** The registry `ORK_UPDATE_REGISTRY` names, if it names one. */
583
+ const registryOf = (env) => {
584
+ const registry = env[UPDATE_REGISTRY_ENV];
585
+ return registry === void 0 || registry === "" ? void 0 : registry;
586
+ };
587
+ /** The host of a registry that isn't an http(s) URL, past any `user:pass@`. */
588
+ const hostOf = (registry) => registry.replace(/^[a-z][\w+.-]*:\/\//iu, "").replace(/^[^/?#]*@/u, "").split(/[/?#]/u)[0] ?? "";
589
+ /**
590
+ * The registry as it may be shown (on a page, in a message): an http(s) URL
591
+ * without its user name and password, which a mirror's URL may carry; anything
592
+ * else only as its host.
593
+ */
594
+ const registryShown = (registry) => {
595
+ try {
596
+ const url = new URL(registry);
597
+ if (url.protocol === "http:" || url.protocol === "https:") {
598
+ if (url.username === "" && url.password === "") return registry;
599
+ url.username = "";
600
+ url.password = "";
601
+ return url.href;
602
+ }
603
+ } catch {}
604
+ return hostOf(registry);
605
+ };
606
+ // src/__build_info__.ts
607
+ /**
608
+ * @file The published package's name and version, bound to
609
+ * `packages/cli/package.json`. Do not edit by hand: `pnpm changesets:version`
610
+ * and `pnpm build` rewrite it (`tools/release/build-info.ts`), and a test
611
+ * fails when it is stale.
612
+ */
613
+ const ORK_NAME = "@krapht/ork";
614
+ const ORK_VERSION = "1.0.0-alpha.0";
615
+ export { feedbackActions as C, placeOf as S, previewPost as _, registryShown as a, acceptItem as b, checkDue as c, pendingNotice as d, readUpdateCache as f, nodeSession as g, writeUpdateCache as h, registryOf as i, fetchLatest as l, updateCachePath as m, ORK_VERSION as n, updateSwitchOff as o, refreshUpdateCache as p, isSwitchedOn as r, runInstall as s, ORK_NAME as t, newerVersion as u, checkNote as v, openFixRun as w, lessonPlaceOf as x, recordNote as y };