@krapht/ork 0.1.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -5
- package/THIRD_PARTY_NOTICES.md +7 -0
- package/dist/__build_info__-CXXMLvjt.mjs +104 -0
- package/dist/bin.mjs +1579 -3343
- package/dist/context-BFGC_C02.mjs +435 -0
- package/dist/find-BcvhT6UN.mjs +47 -0
- package/dist/format-DqpObyMl.mjs +553 -0
- package/dist/in-a-run-BbLhZOFW.mjs +22 -0
- package/dist/limits-BKYAmqCR.mjs +44 -0
- package/dist/mcp-BWsdQyri.mjs +91 -0
- package/dist/output-Q2EdeVUt.mjs +856 -0
- package/dist/pause-DKACDfV1.mjs +65 -0
- package/dist/relay-CbK-Wme9.mjs +122 -0
- package/dist/report-DK4LGqwF.mjs +129 -0
- package/dist/send-C6yr04Fe.mjs +163 -0
- package/dist/{src-CDkj_-3F.mjs → src-CZOVF2yU.mjs} +496 -193
- package/dist/src-Cz1yOUTg.mjs +11841 -0
- package/dist/src-D5ruZFgD.mjs +1973 -0
- package/dist/src-DtN1k-B3.mjs +1899 -0
- package/dist/ui/THIRD_PARTY_LICENSES.txt +631 -0
- package/dist/ui/assets/app-BlDdP396.js +61 -0
- package/dist/ui/assets/app-C722xNZ3.css +1 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-C51Di1Mf.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-400-normal-CPJFfJgk.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-D2cXvDHF.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-500-normal-DfuvdVgn.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-CkxHOdCb.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-400-normal-XB2BPmDK.woff2 +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-BY1KTvO5.woff +0 -0
- package/dist/ui/assets/geist-mono-cyrillic-ext-500-normal-C7aYOB1D.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-B2yvC1Cq.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-400-normal-DTRLJnHl.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-D-GG86Jb.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-500-normal-YINYabwD.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-CfzLURNc.woff +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-400-normal-DI-rJ0UV.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-Buglb9-a.woff2 +0 -0
- package/dist/ui/assets/geist-mono-latin-ext-500-normal-CrWEgvU_.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-BS9BGVxz.woff2 +0 -0
- package/dist/ui/assets/geist-mono-symbols2-400-normal-DjbiTEEA.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-CmnCPGa0.woff +0 -0
- package/dist/ui/assets/geist-mono-symbols2-500-normal-Q_ksOahU.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CKEaXIlx.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-400-normal-CZEPLOgu.woff +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Bs29RrUZ.woff2 +0 -0
- package/dist/ui/assets/geist-mono-vietnamese-500-normal-Dx9-epDo.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-BOaIZNA2.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-400-normal-gapTbOY8.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-CN2lyvyL.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-500-normal-uokXdC-Q.woff2 +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-CA1yjETN.woff +0 -0
- package/dist/ui/assets/geist-sans-latin-600-normal-DFOURf8L.woff2 +0 -0
- package/dist/ui/assets/index-BiO54L90.css +1 -0
- package/dist/ui/assets/index-BoG6O-Lp.js +10 -0
- package/dist/ui/assets/theme-3xH5jxKs.js +1 -0
- package/dist/ui/favicon.ico +0 -0
- package/dist/ui/favicon.svg +4 -0
- package/dist/ui/index.html +30 -0
- package/dist/ui/ork-wordmark-small-on-light.svg +4 -0
- package/dist/ui/ork-wordmark-small.svg +4 -0
- package/dist/ui-CZS1XKla.mjs +160 -0
- package/dist/usage-BIjf9Xdt.mjs +418 -0
- package/dist/watch-B8o4Szd_.mjs +394 -0
- package/package.json +4 -2
- package/dist/src-CqNlvqV_.mjs +0 -5357
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { B as requestContinue, V as requestPause } from "./src-Cz1yOUTg.mjs";
|
|
2
|
+
import { t as findRun } from "./find-BcvhT6UN.mjs";
|
|
3
|
+
import { a as write, c as openStore, i as style, o as writeErr } from "./output-Q2EdeVUt.mjs";
|
|
4
|
+
import { r as continueRefusal } from "./in-a-run-BbLhZOFW.mjs";
|
|
5
|
+
import { defineCommand } from "citty";
|
|
6
|
+
// src/commands/pause.ts
|
|
7
|
+
/**
|
|
8
|
+
* @file `ork pause` and `ork continue`: pause a live run, or one of its nodes,
|
|
9
|
+
* from any terminal, and let it go on (ADR-041). Nothing new starts while a
|
|
10
|
+
* run is paused, and a running session waits before its next tool call;
|
|
11
|
+
* nothing it is doing is cut short. Both write to the store, then wake the
|
|
12
|
+
* run on its socket (specs/messaging.md).
|
|
13
|
+
*/
|
|
14
|
+
const args = {
|
|
15
|
+
run: {
|
|
16
|
+
type: "positional",
|
|
17
|
+
description: "A run id or a prefix (default: this project's newest)",
|
|
18
|
+
required: false
|
|
19
|
+
},
|
|
20
|
+
node: {
|
|
21
|
+
type: "string",
|
|
22
|
+
description: "Only this node, not the whole run"
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
/** Finds the run, asks it, and says what happens next. */
|
|
26
|
+
const ask = async (runArg, request, done) => {
|
|
27
|
+
const store = openStore();
|
|
28
|
+
try {
|
|
29
|
+
const found = findRun(store, runArg, process.cwd());
|
|
30
|
+
const asked = found.ok ? await request(store, found.run) : found;
|
|
31
|
+
if (!asked.ok) {
|
|
32
|
+
writeErr(asked.error);
|
|
33
|
+
process.exitCode = 1;
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (found.ok) write(`${style("green", "✓")} ${done(found.run)}`);
|
|
37
|
+
} finally {
|
|
38
|
+
store.close();
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const pause = defineCommand({
|
|
42
|
+
meta: {
|
|
43
|
+
name: "pause",
|
|
44
|
+
description: "Pause a live run, or one node, before its next tool call"
|
|
45
|
+
},
|
|
46
|
+
args,
|
|
47
|
+
run: ({ args: given }) => ask(given.run, (store, run) => requestPause(store, run, void 0, given.node), (run) => given.node === void 0 ? `Run ${run.id} pauses: nothing new starts, and running sessions wait before their next tool call. Go on with: ork continue ${run.id}` : `${given.node} in run ${run.id} pauses before its next tool call. Go on with: ork continue ${run.id} --node ${given.node}`)
|
|
48
|
+
});
|
|
49
|
+
const continueRun = defineCommand({
|
|
50
|
+
meta: {
|
|
51
|
+
name: "continue",
|
|
52
|
+
description: "Let a paused run, or one paused node, go on"
|
|
53
|
+
},
|
|
54
|
+
args,
|
|
55
|
+
run: async ({ args: given }) => {
|
|
56
|
+
const refused = continueRefusal();
|
|
57
|
+
if (refused !== null) {
|
|
58
|
+
writeErr(refused);
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
await ask(given.run, (store, run) => requestContinue(store, run, void 0, given.node), (run) => given.node === void 0 ? `Run ${run.id} goes on.` : `${given.node} in run ${run.id} goes on.`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
export { continueRun, pause };
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { F as linkNodes, M as addressOf, R as unlinkNodes } from "./src-Cz1yOUTg.mjs";
|
|
2
|
+
import { a as write, c as openStore, i as style, o as writeErr } from "./output-Q2EdeVUt.mjs";
|
|
3
|
+
import { defineCommand } from "citty";
|
|
4
|
+
// src/commands/relay.ts
|
|
5
|
+
/**
|
|
6
|
+
* @file `ork relay`: introduce two running nodes of different runs so they can
|
|
7
|
+
* message each other, end that, and list what is linked (RFC-003,
|
|
8
|
+
* specs/messaging.md). Each side's session is told at its next step, through
|
|
9
|
+
* the store and its run's socket. Not `ork link`, which points tools' folders
|
|
10
|
+
* at `.ork/`. A node's address always has a slash, so `end` and `list` cannot
|
|
11
|
+
* be taken for one.
|
|
12
|
+
*/
|
|
13
|
+
const relayAction = (first, second, third) => {
|
|
14
|
+
if (first === "list") return second === void 0 ? { kind: "list" } : {
|
|
15
|
+
kind: "list",
|
|
16
|
+
run: second
|
|
17
|
+
};
|
|
18
|
+
if (first === "end") return second === void 0 || third === void 0 ? { kind: "usage" } : {
|
|
19
|
+
kind: "end",
|
|
20
|
+
a: second,
|
|
21
|
+
b: third
|
|
22
|
+
};
|
|
23
|
+
return first === void 0 || second === void 0 ? { kind: "usage" } : {
|
|
24
|
+
kind: "link",
|
|
25
|
+
a: first,
|
|
26
|
+
b: second
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
const USAGE = `Name two nodes of different runs as <run>/<node>:
|
|
30
|
+
ork relay <a> <b> let them message each other
|
|
31
|
+
ork relay end <a> <b> end that
|
|
32
|
+
ork relay list [run] list what is linked`;
|
|
33
|
+
/** The link's two ends, its count, and whether it is over. */
|
|
34
|
+
const formatLink = (link) => `${addressOf(link.a)} <-> ${addressOf(link.b)} ${String(link.messages)}/${String(link.maxMessages)} messages${link.endedAt === void 0 ? "" : style("dim", " (ended)")}`;
|
|
35
|
+
const refuse = (error) => {
|
|
36
|
+
writeErr(error);
|
|
37
|
+
process.exitCode = 1;
|
|
38
|
+
};
|
|
39
|
+
/** A whole number above zero, `undefined` when not given, `null` when wrong. */
|
|
40
|
+
const maxMessagesOf = (given) => {
|
|
41
|
+
if (given === void 0) return void 0;
|
|
42
|
+
const limit = Number(given);
|
|
43
|
+
return Number.isInteger(limit) && limit > 0 ? limit : null;
|
|
44
|
+
};
|
|
45
|
+
const relay = defineCommand({
|
|
46
|
+
meta: {
|
|
47
|
+
name: "relay",
|
|
48
|
+
description: "Let two running nodes of different runs message each other; `end` and `list` too"
|
|
49
|
+
},
|
|
50
|
+
args: {
|
|
51
|
+
first: {
|
|
52
|
+
type: "positional",
|
|
53
|
+
description: "A node as <run>/<node>, or `end`, or `list`",
|
|
54
|
+
required: false
|
|
55
|
+
},
|
|
56
|
+
second: {
|
|
57
|
+
type: "positional",
|
|
58
|
+
description: "The other node",
|
|
59
|
+
required: false
|
|
60
|
+
},
|
|
61
|
+
third: {
|
|
62
|
+
type: "positional",
|
|
63
|
+
description: "With `end`: the other node",
|
|
64
|
+
required: false
|
|
65
|
+
},
|
|
66
|
+
"max-messages": {
|
|
67
|
+
type: "string",
|
|
68
|
+
description: `Ask you whether to go on after this many messages (default ${String(100)})`
|
|
69
|
+
},
|
|
70
|
+
all: {
|
|
71
|
+
type: "boolean",
|
|
72
|
+
description: "With `list`: include links that have ended",
|
|
73
|
+
default: false
|
|
74
|
+
},
|
|
75
|
+
json: {
|
|
76
|
+
type: "boolean",
|
|
77
|
+
description: "With `list`: print JSON",
|
|
78
|
+
default: false
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
run: async ({ args }) => {
|
|
82
|
+
const action = relayAction(args.first, args.second, args.third);
|
|
83
|
+
if (action.kind === "usage") {
|
|
84
|
+
refuse(USAGE);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const store = openStore();
|
|
88
|
+
try {
|
|
89
|
+
if (action.kind === "list") {
|
|
90
|
+
const found = store.links({
|
|
91
|
+
...action.run === void 0 ? {} : { run: action.run },
|
|
92
|
+
live: !args.all
|
|
93
|
+
});
|
|
94
|
+
if (args.json) write(JSON.stringify(found));
|
|
95
|
+
else if (found.length === 0) write("No links.");
|
|
96
|
+
else for (const one of found) write(formatLink(one));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
if (action.kind === "end") {
|
|
100
|
+
const ended = await unlinkNodes(store, action.a, action.b);
|
|
101
|
+
if (ended.ok) write(`${style("green", "✓")} Ended the link. Both are told.`);
|
|
102
|
+
else refuse(ended.error);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const maxMessages = maxMessagesOf(args["max-messages"]);
|
|
106
|
+
if (maxMessages === null) {
|
|
107
|
+
refuse("--max-messages takes a whole number above zero.");
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const linked = await linkNodes(store, action.a, action.b, { maxMessages });
|
|
111
|
+
if (!linked.ok) {
|
|
112
|
+
refuse(linked.error);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const [a, b] = [addressOf(linked.link.a), addressOf(linked.link.b)];
|
|
116
|
+
write(linked.already ? `${a} and ${b} are already linked.` : `${style("green", "✓")} Linked ${a} and ${b}. Both are told at their next step.`);
|
|
117
|
+
} finally {
|
|
118
|
+
store.close();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
export { relay };
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
// ../core/src/usage/report.ts
|
|
2
|
+
const USAGE_DIMENSIONS = [
|
|
3
|
+
"day",
|
|
4
|
+
"week",
|
|
5
|
+
"month",
|
|
6
|
+
"workflow",
|
|
7
|
+
"node",
|
|
8
|
+
"model",
|
|
9
|
+
"driver",
|
|
10
|
+
"project"
|
|
11
|
+
];
|
|
12
|
+
const pad = (value) => String(value).padStart(2, "0");
|
|
13
|
+
/** `2026-11-03`, in the machine's own time zone. */
|
|
14
|
+
const dayOf = (date) => `${String(date.getFullYear())}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
|
|
15
|
+
/** The Monday of a time's week. */
|
|
16
|
+
const weekOf = (date) => {
|
|
17
|
+
const monday = new Date(date);
|
|
18
|
+
monday.setDate(date.getDate() - (date.getDay() + 6) % 7);
|
|
19
|
+
return dayOf(monday);
|
|
20
|
+
};
|
|
21
|
+
const KEYS = {
|
|
22
|
+
day: (row) => dayOf(new Date(row.at)),
|
|
23
|
+
week: (row) => weekOf(new Date(row.at)),
|
|
24
|
+
month: (row) => dayOf(new Date(row.at)).slice(0, 7),
|
|
25
|
+
workflow: (row) => row.workflow,
|
|
26
|
+
node: (row) => `${row.workflow}/${row.template ?? row.node}`,
|
|
27
|
+
model: (row) => row.model ?? "unknown",
|
|
28
|
+
driver: (row) => row.driver ?? "unknown",
|
|
29
|
+
project: (row) => row.project
|
|
30
|
+
};
|
|
31
|
+
const TIMES = /* @__PURE__ */ new Set([
|
|
32
|
+
"day",
|
|
33
|
+
"week",
|
|
34
|
+
"month"
|
|
35
|
+
]);
|
|
36
|
+
const totalsOf = (rows) => {
|
|
37
|
+
const totals = {
|
|
38
|
+
costUsd: 0,
|
|
39
|
+
inputTokens: 0,
|
|
40
|
+
outputTokens: 0,
|
|
41
|
+
cacheReadTokens: 0,
|
|
42
|
+
cacheWriteTokens: 0,
|
|
43
|
+
turns: 0,
|
|
44
|
+
durationMs: 0,
|
|
45
|
+
sessions: rows.length,
|
|
46
|
+
runs: new Set(rows.map((row) => row.runId)).size,
|
|
47
|
+
failedUsd: 0,
|
|
48
|
+
reworkUsd: 0,
|
|
49
|
+
chatUsd: 0,
|
|
50
|
+
cacheHitRate: 0
|
|
51
|
+
};
|
|
52
|
+
for (const row of rows) {
|
|
53
|
+
totals.costUsd += row.costUsd;
|
|
54
|
+
totals.inputTokens += row.inputTokens;
|
|
55
|
+
totals.outputTokens += row.outputTokens;
|
|
56
|
+
totals.cacheReadTokens += row.cacheReadTokens;
|
|
57
|
+
totals.cacheWriteTokens += row.cacheWriteTokens;
|
|
58
|
+
totals.turns += row.turns;
|
|
59
|
+
totals.durationMs += row.durationMs;
|
|
60
|
+
if (row.failed) totals.failedUsd += row.costUsd;
|
|
61
|
+
if (row.kind === "chat") totals.chatUsd += row.costUsd;
|
|
62
|
+
else if (row.round > 1) totals.reworkUsd += row.costUsd;
|
|
63
|
+
}
|
|
64
|
+
const read = totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens;
|
|
65
|
+
totals.cacheHitRate = read === 0 ? 0 : totals.cacheReadTokens / read;
|
|
66
|
+
return totals;
|
|
67
|
+
};
|
|
68
|
+
const groupBy = (rows, dimension) => {
|
|
69
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
70
|
+
for (const row of rows) {
|
|
71
|
+
const key = KEYS[dimension](row);
|
|
72
|
+
const bucket = buckets.get(key);
|
|
73
|
+
if (bucket === void 0) buckets.set(key, [row]);
|
|
74
|
+
else bucket.push(row);
|
|
75
|
+
}
|
|
76
|
+
const groups = [...buckets].map(([key, bucket]) => Object.assign(totalsOf(bucket), { key }));
|
|
77
|
+
return TIMES.has(dimension) ? groups.toSorted((a, b) => a.key.localeCompare(b.key)) : groups.toSorted((a, b) => b.costUsd - a.costUsd);
|
|
78
|
+
};
|
|
79
|
+
/** Below this, a percentage says nothing worth acting on. */
|
|
80
|
+
const MIN_SPEND_USD = .5;
|
|
81
|
+
const REWORK_SHARE = .2;
|
|
82
|
+
const FAILED_SHARE = .1;
|
|
83
|
+
const CONCENTRATED_SHARE = .5;
|
|
84
|
+
const LOW_CACHE_RATE = .5;
|
|
85
|
+
const MIN_CACHE_TOKENS = 1e5;
|
|
86
|
+
const percent = (share) => `${String(Math.round(share * 100))}%`;
|
|
87
|
+
const hintsOf = (totals, nodes) => {
|
|
88
|
+
const spend = totals.costUsd;
|
|
89
|
+
if (spend < MIN_SPEND_USD) return [];
|
|
90
|
+
const hints = [];
|
|
91
|
+
if (totals.reworkUsd / spend >= REWORK_SHARE) hints.push({
|
|
92
|
+
code: "rework",
|
|
93
|
+
message: `${percent(totals.reworkUsd / spend)} of the spend went to loop rounds after the first. Sharper briefs or an earlier check can stop work coming back.`
|
|
94
|
+
});
|
|
95
|
+
if (totals.failedUsd / spend >= FAILED_SHARE) hints.push({
|
|
96
|
+
code: "failed",
|
|
97
|
+
message: `${percent(totals.failedUsd / spend)} of the spend went to sessions that failed. A cheap check before them, or a lower budget per session, can stop them sooner.`
|
|
98
|
+
});
|
|
99
|
+
const top = nodes[0];
|
|
100
|
+
if (top !== void 0 && nodes.length > 1 && top.costUsd / spend >= CONCENTRATED_SHARE) hints.push({
|
|
101
|
+
code: "concentrated",
|
|
102
|
+
message: `${top.key} took ${percent(top.costUsd / spend)} of the spend. It is the first place to try a smaller model, a lower effort or a tighter brief.`
|
|
103
|
+
});
|
|
104
|
+
if (totals.inputTokens + totals.cacheReadTokens + totals.cacheWriteTokens >= MIN_CACHE_TOKENS && totals.cacheHitRate < LOW_CACHE_RATE) hints.push({
|
|
105
|
+
code: "cache",
|
|
106
|
+
message: `Only ${percent(totals.cacheHitRate)} of input tokens came from the prompt cache. Long runs of fresh sessions cost more than resumed ones.`
|
|
107
|
+
});
|
|
108
|
+
return hints;
|
|
109
|
+
};
|
|
110
|
+
/** Everything a report shows, from the ledger's rows. */
|
|
111
|
+
const usageReport = (rows) => {
|
|
112
|
+
const totals = totalsOf(rows);
|
|
113
|
+
const groups = {
|
|
114
|
+
day: groupBy(rows, "day"),
|
|
115
|
+
week: groupBy(rows, "week"),
|
|
116
|
+
month: groupBy(rows, "month"),
|
|
117
|
+
workflow: groupBy(rows, "workflow"),
|
|
118
|
+
node: groupBy(rows, "node"),
|
|
119
|
+
model: groupBy(rows, "model"),
|
|
120
|
+
driver: groupBy(rows, "driver"),
|
|
121
|
+
project: groupBy(rows, "project")
|
|
122
|
+
};
|
|
123
|
+
return {
|
|
124
|
+
totals,
|
|
125
|
+
groups,
|
|
126
|
+
hints: hintsOf(totals, groups.node)
|
|
127
|
+
};
|
|
128
|
+
};
|
|
129
|
+
export { usageReport as n, USAGE_DIMENSIONS as t };
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { Ft as socketPath, It as wakeRun, gt as scriptContactsOf, rt as liveStatus } from "./src-Cz1yOUTg.mjs";
|
|
2
|
+
import { c as openStore, o as writeErr } from "./output-Q2EdeVUt.mjs";
|
|
3
|
+
import { u as inheritedScrubber } from "./src-DtN1k-B3.mjs";
|
|
4
|
+
import { defineCommand } from "citty";
|
|
5
|
+
// ../core/src/remote/send.ts
|
|
6
|
+
/**
|
|
7
|
+
* @file `ork send` (RFC-005): a program in a tool node writes to a session on
|
|
8
|
+
* its run's bus. The check is on the sender's side, as a refusal must be an
|
|
9
|
+
* exit code a script can act on; then a row in the store and a wake, as for a
|
|
10
|
+
* message between linked runs. `ORK_RUN` and `ORK_NODE` say who it is, and
|
|
11
|
+
* they are attribution, not proof: the check stops mistakes, not a program
|
|
12
|
+
* running as the same user.
|
|
13
|
+
*/
|
|
14
|
+
/** A first value, as for links; real use will show whether it is the right one. */
|
|
15
|
+
const DEFAULT_SCRIPT_MESSAGES = 100;
|
|
16
|
+
/**
|
|
17
|
+
* Why this sender may not write to this node; `null` when it may. The body is
|
|
18
|
+
* not needed, so a program is told before it pipes a whole message in.
|
|
19
|
+
*/
|
|
20
|
+
const scriptRefusal = (store, send) => {
|
|
21
|
+
const { run: runId, node, to } = send;
|
|
22
|
+
if (runId === void 0 || node === void 0) return "`ork send` runs inside a tool node of a run: `ORK_RUN` and `ORK_NODE` are not set.";
|
|
23
|
+
if (runId.startsWith("node-")) return `Run ${runId} runs one node, which has no message bus; run the whole workflow.`;
|
|
24
|
+
const run = store.lookup(runId).find((found) => found.id === runId);
|
|
25
|
+
if (run === void 0) return `There is no run ${runId}.`;
|
|
26
|
+
const status = liveStatus(run);
|
|
27
|
+
if (status !== "running") return `Run ${runId} is not running (${status}).`;
|
|
28
|
+
const started = store.events(runId).map((stored) => stored.event).findLast((event) => event.type === "run.started");
|
|
29
|
+
if (started === void 0) return `Run ${runId} has not started yet.`;
|
|
30
|
+
const own = started.nodes.find((found) => found.id === node);
|
|
31
|
+
if (own === void 0 || !own.type.startsWith("tool.")) return `${node} is not a tool node of run ${runId}.`;
|
|
32
|
+
const contacts = scriptContactsOf(node, started.nodes, started.channels ?? []);
|
|
33
|
+
if (contacts.includes(to)) return null;
|
|
34
|
+
return contacts.length === 0 ? `\`${node}\` has no one to write to: give it an owner, or a channel under \`messages:\`.` : `\`${to}\` is not a contact of \`${node}\`, which can write to \`${contacts.join("`, `")}\` (its owner and its channels).`;
|
|
35
|
+
};
|
|
36
|
+
/** Reading all of it would cost as much as sending it. */
|
|
37
|
+
const TOO_LONG = "The message is over 64 KiB. Write it to a file and send the file's path.";
|
|
38
|
+
/** Checks the message, writes it for the run, and wakes it. */
|
|
39
|
+
const sendFromScript = async (store, send, home) => {
|
|
40
|
+
const refused = scriptRefusal(store, send);
|
|
41
|
+
if (refused !== null) return {
|
|
42
|
+
ok: false,
|
|
43
|
+
error: refused
|
|
44
|
+
};
|
|
45
|
+
if (Buffer.byteLength(send.body) > 65536) return {
|
|
46
|
+
ok: false,
|
|
47
|
+
error: TOO_LONG
|
|
48
|
+
};
|
|
49
|
+
const run = String(send.run);
|
|
50
|
+
const node = String(send.node);
|
|
51
|
+
if (!store.scriptCommand(run, {
|
|
52
|
+
kind: "script",
|
|
53
|
+
node: send.to,
|
|
54
|
+
from: node,
|
|
55
|
+
body: send.body
|
|
56
|
+
}, DEFAULT_SCRIPT_MESSAGES)) return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: `\`${node}\` has sent ${String(DEFAULT_SCRIPT_MESSAGES)} messages in this run, which is the most it may.`
|
|
59
|
+
};
|
|
60
|
+
await wakeRun(socketPath(run, home));
|
|
61
|
+
return { ok: true };
|
|
62
|
+
};
|
|
63
|
+
// src/commands/send.ts
|
|
64
|
+
/**
|
|
65
|
+
* @file `ork send` (RFC-005): a program in a tool node writes to a session on
|
|
66
|
+
* its run's bus. It prints nothing when it works, as what a program prints is
|
|
67
|
+
* its node's output, and says why on stderr, with exit 1, when it is refused:
|
|
68
|
+
* before reading a message it could not send, and for anything wrong with the
|
|
69
|
+
* command itself, which would otherwise go to stdout as usage.
|
|
70
|
+
*/
|
|
71
|
+
const refuse = (message) => {
|
|
72
|
+
writeErr(message);
|
|
73
|
+
process.exitCode = 1;
|
|
74
|
+
};
|
|
75
|
+
/** Stdin, up to the limit: past it, the rest is not worth holding. */
|
|
76
|
+
const readBounded = async () => {
|
|
77
|
+
const chunks = [];
|
|
78
|
+
let size = 0;
|
|
79
|
+
for await (const chunk of process.stdin) {
|
|
80
|
+
const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
81
|
+
size += part.length;
|
|
82
|
+
if (size > 65536) return null;
|
|
83
|
+
chunks.push(part);
|
|
84
|
+
}
|
|
85
|
+
return Buffer.concat(chunks);
|
|
86
|
+
};
|
|
87
|
+
/** The message, or why it can't go. */
|
|
88
|
+
const bodyOf = async (given) => {
|
|
89
|
+
if (given !== void 0) return { body: given };
|
|
90
|
+
const read = await readBounded();
|
|
91
|
+
if (read === null) return { refused: TOO_LONG };
|
|
92
|
+
try {
|
|
93
|
+
return { body: new TextDecoder("utf-8", { fatal: true }).decode(read) };
|
|
94
|
+
} catch {
|
|
95
|
+
return { refused: "The message is not UTF-8 text." };
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const deliver = async (store, to, given) => {
|
|
99
|
+
const sender = {
|
|
100
|
+
run: process.env["ORK_RUN"],
|
|
101
|
+
node: process.env["ORK_NODE"],
|
|
102
|
+
to
|
|
103
|
+
};
|
|
104
|
+
const refused = scriptRefusal(store, sender);
|
|
105
|
+
if (refused !== null) {
|
|
106
|
+
refuse(refused);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
const message = await bodyOf(given);
|
|
110
|
+
if ("refused" in message) {
|
|
111
|
+
refuse(message.refused);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (message.body.replaceAll(/[\p{Cc}\s]/gu, "") === "") {
|
|
115
|
+
refuse("The message is empty.");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
const sent = await sendFromScript(store, {
|
|
119
|
+
...sender,
|
|
120
|
+
body: inheritedScrubber(process.env)(message.body)
|
|
121
|
+
});
|
|
122
|
+
if (!sent.ok) refuse(sent.error);
|
|
123
|
+
};
|
|
124
|
+
const send = defineCommand({
|
|
125
|
+
meta: {
|
|
126
|
+
name: "send",
|
|
127
|
+
description: "From a tool node's program: write to a session in its run"
|
|
128
|
+
},
|
|
129
|
+
args: {
|
|
130
|
+
to: {
|
|
131
|
+
type: "positional",
|
|
132
|
+
description: "The node to write to: the owner, or a channel",
|
|
133
|
+
required: false
|
|
134
|
+
},
|
|
135
|
+
message: {
|
|
136
|
+
type: "string",
|
|
137
|
+
alias: "m",
|
|
138
|
+
description: "The message"
|
|
139
|
+
},
|
|
140
|
+
stdin: {
|
|
141
|
+
type: "boolean",
|
|
142
|
+
description: "Read the message from stdin",
|
|
143
|
+
default: false
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
run: async ({ args }) => {
|
|
147
|
+
if (args.to === void 0 || args._.length !== 1) {
|
|
148
|
+
refuse("Give one node to write to: ork send <to> -m <body>.");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (args.message === void 0 === !args.stdin) {
|
|
152
|
+
refuse("Give the message with -m or --stdin, one of them.");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
const store = openStore();
|
|
156
|
+
try {
|
|
157
|
+
await deliver(store, args.to, args.message);
|
|
158
|
+
} finally {
|
|
159
|
+
store.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
export { send };
|