@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,553 @@
|
|
|
1
|
+
import { C as gitOk, S as git, rt as liveStatus, w as lines, x as removeWorktree } from "./src-Cz1yOUTg.mjs";
|
|
2
|
+
import { i as style, n as cost } from "./output-Q2EdeVUt.mjs";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { existsSync } from "node:fs";
|
|
5
|
+
// ../core/src/worktree/list.ts
|
|
6
|
+
/**
|
|
7
|
+
* @file What `ork worktrees` shows and cleans: every worktree and branch Ork's
|
|
8
|
+
* runs recorded creating, whatever the project's git rules named them, and
|
|
9
|
+
* any `ork/*` branch besides, with their state read from git itself.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* The worktrees the project's runs recorded creating, oldest run first; one
|
|
13
|
+
* entry per branch or folder, from the latest run that created it.
|
|
14
|
+
*/
|
|
15
|
+
const createdWorktrees = (store, project) => {
|
|
16
|
+
const byName = /* @__PURE__ */ new Map();
|
|
17
|
+
for (const run of store.runs({ project }).toReversed()) for (const { event } of store.events(run.id)) {
|
|
18
|
+
if (event.type !== "worktree" || event.action !== "created") continue;
|
|
19
|
+
byName.set(event.branch ?? event.path, {
|
|
20
|
+
runId: run.id,
|
|
21
|
+
nodeId: event.id,
|
|
22
|
+
path: event.path,
|
|
23
|
+
...event.branch === void 0 ? {} : { branch: event.branch }
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return [...byName.values()];
|
|
27
|
+
};
|
|
28
|
+
const parsePorcelain = (text) => text.split("\n\n").map((block) => lines(block)).map((fields) => {
|
|
29
|
+
const value = (key) => fields.find((f) => f.startsWith(`${key} `))?.slice(key.length + 1);
|
|
30
|
+
return {
|
|
31
|
+
path: value("worktree") ?? "",
|
|
32
|
+
branch: (value("branch") ?? "").replace(/^refs\/heads\//u, ""),
|
|
33
|
+
prunable: fields.some((f) => f.startsWith("prunable"))
|
|
34
|
+
};
|
|
35
|
+
}).filter((entry) => entry.path !== "");
|
|
36
|
+
/** The `ork/<run>/<node>` branches of runs that recorded nothing. */
|
|
37
|
+
const prefixed = (branch) => {
|
|
38
|
+
const [, runId = "", ...rest] = branch.split("/");
|
|
39
|
+
return {
|
|
40
|
+
runId,
|
|
41
|
+
nodeId: rest.join("/"),
|
|
42
|
+
path: "",
|
|
43
|
+
branch
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/** How many commits a detached worktree holds that no branch, tag or remote has. */
|
|
47
|
+
const onNoBranch = async (entry) => entry === void 0 ? "0" : (await git(entry.path, [
|
|
48
|
+
"rev-list",
|
|
49
|
+
"--count",
|
|
50
|
+
"HEAD",
|
|
51
|
+
"--not",
|
|
52
|
+
"--branches",
|
|
53
|
+
"--tags",
|
|
54
|
+
"--remotes"
|
|
55
|
+
])).stdout;
|
|
56
|
+
/** One created worktree or branch, with its state read from git. */
|
|
57
|
+
const stateOf = async (root, entries, item) => {
|
|
58
|
+
const { branch } = item;
|
|
59
|
+
const entry = branch === void 0 ? entries.find((e) => e.path === item.path) : entries.find((e) => e.branch === branch);
|
|
60
|
+
const missing = entry !== void 0 && (entry.prunable || !existsSync(entry.path));
|
|
61
|
+
const status = entry === void 0 || missing ? "" : (await git(entry.path, ["status", "--porcelain"])).stdout;
|
|
62
|
+
const checkedOut = branch === void 0 && entry !== void 0 && entry.branch !== "" ? entry.branch : void 0;
|
|
63
|
+
const on = branch ?? checkedOut;
|
|
64
|
+
const counted = on === void 0 ? await onNoBranch(entry === void 0 || missing ? void 0 : entry) : (await git(root, [
|
|
65
|
+
"rev-list",
|
|
66
|
+
"--count",
|
|
67
|
+
`HEAD..${on}`
|
|
68
|
+
])).stdout;
|
|
69
|
+
const ahead = Number(counted);
|
|
70
|
+
return {
|
|
71
|
+
...branch === void 0 ? {} : { branch },
|
|
72
|
+
...checkedOut === void 0 ? {} : { checkedOut },
|
|
73
|
+
runId: item.runId,
|
|
74
|
+
nodeId: item.nodeId,
|
|
75
|
+
path: entry?.path ?? null,
|
|
76
|
+
missing,
|
|
77
|
+
dirty: lines(status).length > 0,
|
|
78
|
+
ahead: Number.isNaN(ahead) ? 0 : ahead,
|
|
79
|
+
merged: on !== void 0 && ahead === 0,
|
|
80
|
+
inMainCheckout: entry?.path === root
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Every worktree and branch Ork's runs created in the repository at `root`,
|
|
85
|
+
* with its state: those the runs recorded (`created`, whatever the project's
|
|
86
|
+
* git rules named them), and any `ork/` branch besides. A recorded one that git
|
|
87
|
+
* no longer has is left out.
|
|
88
|
+
*/
|
|
89
|
+
const listOrkWorktrees = async (root, created = []) => {
|
|
90
|
+
const entries = parsePorcelain(await gitOk(root, [
|
|
91
|
+
"worktree",
|
|
92
|
+
"list",
|
|
93
|
+
"--porcelain"
|
|
94
|
+
]));
|
|
95
|
+
const branches = lines(await gitOk(root, [
|
|
96
|
+
"for-each-ref",
|
|
97
|
+
"--format=%(refname:short)",
|
|
98
|
+
"refs/heads/"
|
|
99
|
+
]));
|
|
100
|
+
const recordedBranches = new Set(created.flatMap((c) => c.branch ?? []));
|
|
101
|
+
const items = [...created.filter((c) => c.branch === void 0 ? entries.some((e) => e.path === c.path) : branches.includes(c.branch)), ...branches.filter((b) => b.startsWith("ork/") && !recordedBranches.has(b)).map((b) => prefixed(b))];
|
|
102
|
+
return Promise.all(items.map((item) => stateOf(root, entries, item)));
|
|
103
|
+
};
|
|
104
|
+
const cleanStep = (item, options) => {
|
|
105
|
+
if (item.inMainCheckout) return {
|
|
106
|
+
item,
|
|
107
|
+
removeWorktree: false,
|
|
108
|
+
deleteBranch: false,
|
|
109
|
+
skip: "checked out in the main checkout"
|
|
110
|
+
};
|
|
111
|
+
if (item.dirty && !options.force) return {
|
|
112
|
+
item,
|
|
113
|
+
removeWorktree: false,
|
|
114
|
+
deleteBranch: false,
|
|
115
|
+
skip: "uncommitted changes"
|
|
116
|
+
};
|
|
117
|
+
if (item.branch === void 0 && item.checkedOut === void 0 && item.ahead > 0 && !options.force) return {
|
|
118
|
+
item,
|
|
119
|
+
removeWorktree: false,
|
|
120
|
+
deleteBranch: false,
|
|
121
|
+
skip: "commits on no branch"
|
|
122
|
+
};
|
|
123
|
+
const removable = item.path !== null && !item.missing;
|
|
124
|
+
if (options.branches && item.branch !== void 0 && !item.merged && !options.force) return {
|
|
125
|
+
item,
|
|
126
|
+
removeWorktree: removable,
|
|
127
|
+
deleteBranch: false,
|
|
128
|
+
skip: "branch not merged"
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
item,
|
|
132
|
+
removeWorktree: removable,
|
|
133
|
+
deleteBranch: options.branches && item.branch !== void 0
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* What `ork worktrees clean` would do. A worktree with uncommitted changes, an
|
|
138
|
+
* unmerged branch, or a detached worktree holding commits no branch has, is
|
|
139
|
+
* kept unless `force`.
|
|
140
|
+
*/
|
|
141
|
+
const planClean = (items, filter, options) => items.filter((item) => filter.runId === void 0 || item.runId === filter.runId).filter((item) => !filter.merged || item.merged).map((item) => cleanStep(item, options));
|
|
142
|
+
/** Carries out a clean plan, then prunes stale worktree entries. */
|
|
143
|
+
const applyClean = async (root, steps, force) => {
|
|
144
|
+
const done = [];
|
|
145
|
+
await gitOk(root, ["worktree", "prune"]);
|
|
146
|
+
for (const step of steps) {
|
|
147
|
+
const { item } = step;
|
|
148
|
+
if (step.removeWorktree && item.path !== null) {
|
|
149
|
+
await removeWorktree(root, item.path, force);
|
|
150
|
+
done.push(`removed ${item.path}`);
|
|
151
|
+
}
|
|
152
|
+
if (step.deleteBranch && item.branch !== void 0) {
|
|
153
|
+
await gitOk(root, [
|
|
154
|
+
"branch",
|
|
155
|
+
force ? "-D" : "-d",
|
|
156
|
+
item.branch
|
|
157
|
+
]);
|
|
158
|
+
done.push(`deleted ${item.branch}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
await gitOk(root, ["worktree", "prune"]);
|
|
162
|
+
return done;
|
|
163
|
+
};
|
|
164
|
+
// src/human.ts
|
|
165
|
+
/**
|
|
166
|
+
* @file The terminal `HumanChannel`: answers `ask_user` questions, gates and
|
|
167
|
+
* permission prompts on the terminal, one at a time, and denies or defers
|
|
168
|
+
* when no one is there. In a run it is the decision queue's local answerer,
|
|
169
|
+
* racing `ork decide`.
|
|
170
|
+
*/
|
|
171
|
+
const PREVIEW = 160;
|
|
172
|
+
const NO_ONE_TO_PERMIT = "No one is at the terminal to approve this tool, so it is not allowed in this run.";
|
|
173
|
+
const NO_ONE_TO_ASK = "no one (nobody is at the terminal; use your best judgment and say so in your report)";
|
|
174
|
+
/** One line that shows what a tool is about to do. */
|
|
175
|
+
const previewInput = (toolName, input) => {
|
|
176
|
+
const key = [
|
|
177
|
+
"command",
|
|
178
|
+
"file_path",
|
|
179
|
+
"path",
|
|
180
|
+
"url",
|
|
181
|
+
"pattern"
|
|
182
|
+
].find((k) => typeof input[k] === "string");
|
|
183
|
+
const line = (key === void 0 ? JSON.stringify(input) : `${key}: ${String(input[key])}`).replaceAll(/\s+/gu, " ").trim();
|
|
184
|
+
return line.length > PREVIEW ? `${toolName} ${line.slice(0, 159)}…` : `${toolName} ${line}`;
|
|
185
|
+
};
|
|
186
|
+
/** The option an answer names: its number, or its id. */
|
|
187
|
+
const pickOption = (answer, options, recommended) => {
|
|
188
|
+
const value = answer.trim();
|
|
189
|
+
if (value === "") return recommended;
|
|
190
|
+
const index = Number(value);
|
|
191
|
+
if (Number.isInteger(index) && index >= 1 && index <= options.length) return options[index - 1]?.id;
|
|
192
|
+
return options.find((o) => o.id === value)?.id;
|
|
193
|
+
};
|
|
194
|
+
const optionLine = (option, index, recommended) => {
|
|
195
|
+
const label = option.label === void 0 ? "" : ` ${option.label}`;
|
|
196
|
+
const description = option.description === void 0 ? "" : style("dim", ` - ${option.description}`);
|
|
197
|
+
const mark = option.id === recommended ? style("green", " (recommended)") : "";
|
|
198
|
+
return ` ${String(index + 1)}) ${style("bold", option.id)}${label}${description}${mark}`;
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Answers permission prompts and `ask_user` questions on the terminal, one at a
|
|
202
|
+
* time. Without a terminal it denies tools and leaves questions to the node. A
|
|
203
|
+
* withdrawn prompt (answered elsewhere) rejects, which frees the turn.
|
|
204
|
+
*/
|
|
205
|
+
const createTerminalHuman = (io) => {
|
|
206
|
+
let rl;
|
|
207
|
+
let turn = Promise.resolve();
|
|
208
|
+
const say = (text) => {
|
|
209
|
+
io.output.write(`${text}\n`);
|
|
210
|
+
};
|
|
211
|
+
const prompt = (text, signal) => {
|
|
212
|
+
rl ??= createInterface({
|
|
213
|
+
input: io.input,
|
|
214
|
+
output: io.output
|
|
215
|
+
});
|
|
216
|
+
return signal === void 0 ? rl.question(text) : rl.question(text, { signal });
|
|
217
|
+
};
|
|
218
|
+
const choose = async (hint, options, recommended, signal) => {
|
|
219
|
+
const picked = pickOption(await prompt(` Choose${hint}: `, signal), options, recommended);
|
|
220
|
+
if (picked !== void 0) return picked;
|
|
221
|
+
say(style("dim", " Type an option's number or id."));
|
|
222
|
+
return choose(hint, options, recommended, signal);
|
|
223
|
+
};
|
|
224
|
+
const oneAtATime = (work) => {
|
|
225
|
+
const result = turn.then(work);
|
|
226
|
+
turn = result.catch(() => null);
|
|
227
|
+
return result;
|
|
228
|
+
};
|
|
229
|
+
return {
|
|
230
|
+
permit: ({ nodeId, toolName, input, signal }) => oneAtATime(async () => {
|
|
231
|
+
if (signal?.aborted === true) throw new Error("the prompt was withdrawn");
|
|
232
|
+
say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} wants to run ${previewInput(toolName, input)}`);
|
|
233
|
+
if (!io.interactive) {
|
|
234
|
+
say(style("dim", ` denied: ${NO_ONE_TO_PERMIT}`));
|
|
235
|
+
return {
|
|
236
|
+
allow: false,
|
|
237
|
+
message: NO_ONE_TO_PERMIT
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
const answer = (await prompt(" Allow? [y/N] ", signal)).trim().toLowerCase();
|
|
241
|
+
return answer === "y" || answer === "yes" ? { allow: true } : {
|
|
242
|
+
allow: false,
|
|
243
|
+
message: "The user did not allow this tool."
|
|
244
|
+
};
|
|
245
|
+
}),
|
|
246
|
+
ask: ({ nodeId, question, options, recommended, context, signal }) => oneAtATime(() => {
|
|
247
|
+
if (signal?.aborted === true) return Promise.reject(/* @__PURE__ */ new Error("the question was withdrawn"));
|
|
248
|
+
say(`${style(["yellow", "bold"], "?")} ${style("bold", nodeId)} asks: ${question}`);
|
|
249
|
+
if (context !== void 0) say(style("dim", ` ${context}`));
|
|
250
|
+
for (const [index, option] of options.entries()) say(optionLine(option, index, recommended));
|
|
251
|
+
if (!io.interactive) {
|
|
252
|
+
const answer = recommended ?? NO_ONE_TO_ASK;
|
|
253
|
+
say(style("dim", ` no terminal; answered ${answer}`));
|
|
254
|
+
return Promise.resolve(answer);
|
|
255
|
+
}
|
|
256
|
+
const fallback = options.findIndex((o) => o.id === recommended);
|
|
257
|
+
const hint = fallback === -1 ? "" : ` [${String(fallback + 1)}]`;
|
|
258
|
+
return choose(hint, options, recommended, signal);
|
|
259
|
+
}),
|
|
260
|
+
close: () => {
|
|
261
|
+
rl?.close();
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
};
|
|
265
|
+
// src/run/format-bus.ts
|
|
266
|
+
/** The first line of a message body, cut to fit one terminal line. */
|
|
267
|
+
const firstLine = (body) => {
|
|
268
|
+
const [line = ""] = body.trim().split("\n");
|
|
269
|
+
return line.length > 100 ? `${line.slice(0, 99)}…` : line;
|
|
270
|
+
};
|
|
271
|
+
/** Messages, owner questions and claims, without the node prefix. */
|
|
272
|
+
const formatBusEvent = (event) => {
|
|
273
|
+
if (event.type === "claim.taken") return [style("dim", `⚑ claimed ${event.resource}`)];
|
|
274
|
+
if (event.type === "claim.refused") {
|
|
275
|
+
const held = event.held === event.resource ? "" : ` (it holds ${event.held})`;
|
|
276
|
+
return [style("yellow", `⚑ ${event.resource} is ${event.holder}'s${held}`)];
|
|
277
|
+
}
|
|
278
|
+
if (event.type === "claim.released") return [style("dim", `⚑ released ${event.resource}`)];
|
|
279
|
+
if (event.type === "message.sent") {
|
|
280
|
+
const how = event.via === void 0 ? "" : ` (${event.via})`;
|
|
281
|
+
return [style("dim", `✉ to ${event.to}${how}: ${firstLine(event.body)}`)];
|
|
282
|
+
}
|
|
283
|
+
if (event.type === "message.received") {
|
|
284
|
+
const how = event.via === void 0 ? "" : ` (${event.via})`;
|
|
285
|
+
return [style("dim", `✉ from ${event.from}${how}: ${firstLine(event.body)}`)];
|
|
286
|
+
}
|
|
287
|
+
if (event.type === "call.started") return [style("dim", `↳ started ${event.workflow} as run ${event.run}`)];
|
|
288
|
+
if (event.type === "link.notice") return [style("dim", `↔ ${event.text}`)];
|
|
289
|
+
if (event.type === "message.read") return [];
|
|
290
|
+
if (event.type === "owner.asked") return [`${style(["yellow", "bold"], "?")} asked as the owner of ${event.about}: ${event.text} ${style("dim", `(${event.question})`)}`];
|
|
291
|
+
if (event.type === "owner.answered") return [style("dim", `→ ${event.answer} (owner)`)];
|
|
292
|
+
return [style("dim", "→ its session ended first; asking you")];
|
|
293
|
+
};
|
|
294
|
+
// src/run/format.ts
|
|
295
|
+
/** Who answered a decision, as the log says it. */
|
|
296
|
+
const ANSWERED_BY = {
|
|
297
|
+
terminal: "terminal",
|
|
298
|
+
decide: "ork decide",
|
|
299
|
+
ui: "ork ui"
|
|
300
|
+
};
|
|
301
|
+
const STATUS_MARK = {
|
|
302
|
+
pass: style("green", "✓"),
|
|
303
|
+
done: style("green", "✓"),
|
|
304
|
+
changes_requested: style("yellow", "!"),
|
|
305
|
+
fail: style("red", "✗")
|
|
306
|
+
};
|
|
307
|
+
const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
|
|
308
|
+
const indent = (text) => text.trimEnd().split("\n").map((line) => ` ${line}`);
|
|
309
|
+
/** The usage line: cost, tokens, turns and time. */
|
|
310
|
+
const formatUsage = ({ usage }) => style("dim", [
|
|
311
|
+
cost(usage.costUsd),
|
|
312
|
+
`${String(usage.inputTokens)} in / ${String(usage.outputTokens)} out (${String(usage.cacheReadTokens)} cached)`,
|
|
313
|
+
`${String(usage.turns)} turns`,
|
|
314
|
+
seconds(usage.durationMs)
|
|
315
|
+
].join(" · "));
|
|
316
|
+
/** How a finished session reads on the terminal. */
|
|
317
|
+
const formatOutcome = (nodeId, outcome) => {
|
|
318
|
+
const { report } = outcome;
|
|
319
|
+
if (report === null) return [
|
|
320
|
+
`${style("red", "✗")} ${style("bold", nodeId)} ended without a report (${outcome.subtype})`,
|
|
321
|
+
...outcome.errors.map((e) => ` ${style("red", e)}`),
|
|
322
|
+
` ${formatUsage(outcome)}`
|
|
323
|
+
];
|
|
324
|
+
const lines = [`${STATUS_MARK[report.status] ?? "-"} ${style("bold", nodeId)} ${report.status}: ${report.summary}`];
|
|
325
|
+
if (report.branch !== void 0) lines.push(` branch: ${report.branch}`);
|
|
326
|
+
for (const [name, file] of Object.entries(report.artifacts)) lines.push(` ${name}: ${file}`);
|
|
327
|
+
for (const [name, value] of Object.entries(report.fields)) lines.push(` ${name}: ${JSON.stringify(value)}`);
|
|
328
|
+
lines.push(` ${formatUsage(outcome)}`);
|
|
329
|
+
return lines;
|
|
330
|
+
};
|
|
331
|
+
/** The transcript lines for one event; tool names come from `tools`. */
|
|
332
|
+
const formatEvent = (nodeId, event, tools) => {
|
|
333
|
+
if (event.type === "session.started") return [style("dim", `▸ ${nodeId} (${event.model}, session ${event.sessionId})`)];
|
|
334
|
+
if (event.type === "assistant.text") return indent(event.text);
|
|
335
|
+
if (event.type === "tool.called") {
|
|
336
|
+
tools.set(event.id, event.name);
|
|
337
|
+
return [style("dim", ` · ${previewInput(event.name, event.input)}`)];
|
|
338
|
+
}
|
|
339
|
+
if (event.type === "tool.result") return event.isError ? [style("red", ` ✗ ${tools.get(event.id) ?? "tool"} failed`)] : [];
|
|
340
|
+
if (event.type === "plan.limit") return event.status === "allowed" ? [] : [style("yellow", ` ! plan: ${event.window.replaceAll("_", " ")} limit ${event.status === "rejected" ? "reached" : "is close"}`)];
|
|
341
|
+
return formatOutcome(nodeId, event.outcome);
|
|
342
|
+
};
|
|
343
|
+
/** A one-line verdict for a finished node, by what its type outputs. */
|
|
344
|
+
const nodeVerdict = (output) => {
|
|
345
|
+
const { status, summary, exitCode, choice, answeredBy, count } = output;
|
|
346
|
+
if (typeof status === "string") return typeof summary === "string" && summary !== "" ? `${status}: ${summary}` : status;
|
|
347
|
+
if (typeof exitCode === "number") return `exit ${String(exitCode)}`;
|
|
348
|
+
if (typeof choice === "string") return `${choice} (${String(answeredBy)})`;
|
|
349
|
+
if (typeof count === "number") return `${String(count)} ran`;
|
|
350
|
+
return "done";
|
|
351
|
+
};
|
|
352
|
+
/** Transcript lines for one run event, prefixed with the node they are from. */
|
|
353
|
+
const formatRunEvent = (event, width, tools) => {
|
|
354
|
+
const tag = (id) => style("cyan", `${id.padEnd(width)} │`);
|
|
355
|
+
if (event.type === "run.started") return [style("dim", `▸ run ${event.runId} of ${event.workflow} (starts at ${event.entries.join(", ")})`), ...event.settings?.trigger === void 0 ? [] : [style("dim", ` started by trigger ${event.settings.trigger}; untrusted inputs: ${(event.settings.untrusted ?? []).join(", ") || "none"}`)]];
|
|
356
|
+
if (event.type === "node.started") {
|
|
357
|
+
const round = event.round > 1 ? ` (round ${String(event.round)})` : "";
|
|
358
|
+
const owner = event.owner === void 0 ? "" : `, for ${event.owner}`;
|
|
359
|
+
return [`${tag(event.id)} ${style("dim", `${event.nodeType}${round}${owner}`)}`];
|
|
360
|
+
}
|
|
361
|
+
if (event.type === "node.event") {
|
|
362
|
+
const inner = event.event;
|
|
363
|
+
if (inner.type === "session.finished") return [];
|
|
364
|
+
if (inner.type === "session.started") return [`${tag(event.id)} ${style("dim", `${inner.model}, session ${inner.sessionId}`)}`];
|
|
365
|
+
return formatEvent(event.id, event.event, tools).map((line) => `${tag(event.id)} ${line.replace(/^ {2}/u, "")}`);
|
|
366
|
+
}
|
|
367
|
+
if (event.type === "node.finished") {
|
|
368
|
+
const mark = event.ok ? style("green", "✓") : style("red", "✗");
|
|
369
|
+
const spent = event.usage === void 0 ? "" : style("dim", ` · ${cost(event.usage.costUsd)}`);
|
|
370
|
+
return [`${tag(event.id)} ${mark} ${nodeVerdict(event.output)}${spent}`];
|
|
371
|
+
}
|
|
372
|
+
if (event.type === "node.skipped") return [`${tag(event.id)} ${style("dim", "- skipped")}`];
|
|
373
|
+
if (event.type === "loop.exhausted") return [`${style(["yellow", "bold"], "!")} the loop ${event.from} -> ${event.to} used all ${String(event.maxRounds)} rounds`];
|
|
374
|
+
if (event.type === "watch.started") return [`${tag(event.id)} ${style("dim", `watching until ${event.until}, every ${event.every}`)}`];
|
|
375
|
+
if (event.type === "watch.changed") return [`${tag(event.id)} ${style("dim", `look ${String(event.looks)}:`)} ${event.said}`];
|
|
376
|
+
if (event.type === "watch.ended") return [`${tag(event.id)} ${style("dim", `${event.outcome} after ${String(event.looks)} looks`)}`];
|
|
377
|
+
if (event.type === "edge.taken") return [];
|
|
378
|
+
if (event.type === "worktree") {
|
|
379
|
+
const detail = event.detail === void 0 ? "" : ` (${event.detail})`;
|
|
380
|
+
const line = `${event.action} ${event.branch ?? event.path}${detail}`;
|
|
381
|
+
return [`${tag(event.id)} ${event.action === "conflict" ? style("yellow", `! ${line}`) : style("dim", `⎇ ${line}`)}`];
|
|
382
|
+
}
|
|
383
|
+
if (event.type === "decision.opened") {
|
|
384
|
+
const what = event.kind === "permission" ? "asks to" : "asks:";
|
|
385
|
+
const question = event.kind === "permission" ? event.question.replace(/^Allow /u, "run ").replace(/\?$/u, "") : event.question;
|
|
386
|
+
return [`${tag(event.node)} ${style(["yellow", "bold"], "?")} ${what} ${question} ${style("dim", `(decision ${event.decision}; ork decide)`)}`];
|
|
387
|
+
}
|
|
388
|
+
if (event.type === "decision.answered") {
|
|
389
|
+
const by = ANSWERED_BY[event.by];
|
|
390
|
+
return [`${tag(event.node)} ${style("dim", `→ ${event.answer} (${by})`)}`];
|
|
391
|
+
}
|
|
392
|
+
if (event.type === "decision.withdrawn") return [`${tag(event.node)} ${style("dim", "→ question withdrawn")}`];
|
|
393
|
+
if (event.type === "run.command") {
|
|
394
|
+
const node = event.node === void 0 ? "" : ` --node ${event.node}`;
|
|
395
|
+
return [style(["yellow", "bold"], `! ork ${event.command}${node} received`)];
|
|
396
|
+
}
|
|
397
|
+
if (event.type === "node.held") {
|
|
398
|
+
const at = event.at === "tool" ? "its tool call waits for ork continue" : "it starts after ork continue";
|
|
399
|
+
return [`${tag(event.id)} ${style("yellow", `‖ paused: ${at}`)}`];
|
|
400
|
+
}
|
|
401
|
+
if (event.type === "node.released") return [`${tag(event.id)} ${style("dim", "▸ continues")}`];
|
|
402
|
+
if (event.type === "run.finished") return formatRunResult(event.result);
|
|
403
|
+
if (event.type === "run.resumed") {
|
|
404
|
+
const [one] = event.kept;
|
|
405
|
+
const kept = one === void 0 ? "every node runs again" : event.kept.length === 1 ? `${one} keeps its result` : `${event.kept.join(", ")} keep their results`;
|
|
406
|
+
return ["", style("dim", `▸ resumed: ${kept}`)];
|
|
407
|
+
}
|
|
408
|
+
if (event.type === "feedback.recorded") {
|
|
409
|
+
const { target, kind, title } = event.feedback;
|
|
410
|
+
const line = style("dim", `✎ feedback (${target}, ${kind}): ${title}`);
|
|
411
|
+
return [event.node === void 0 ? line : `${tag(event.node)} ${line}`];
|
|
412
|
+
}
|
|
413
|
+
if (event.type === "user.message") {
|
|
414
|
+
const note = event.delivered === "queued" ? " (for its next session)" : "";
|
|
415
|
+
return [`${tag(event.node)} ${style(["magenta", "bold"], "you:")} ${firstLine(event.body)}${style("dim", note)}`];
|
|
416
|
+
}
|
|
417
|
+
return formatBusEvent(event).map((line) => `${tag(event.node)} ${line}`);
|
|
418
|
+
};
|
|
419
|
+
const RUN_MARK = {
|
|
420
|
+
succeeded: style("green", "✓"),
|
|
421
|
+
failed: style("red", "✗"),
|
|
422
|
+
budget_exhausted: style("yellow", "!"),
|
|
423
|
+
quota_exhausted: style("yellow", "!"),
|
|
424
|
+
timed_out: style("yellow", "!"),
|
|
425
|
+
interrupted: style("yellow", "!")
|
|
426
|
+
};
|
|
427
|
+
/** The run summary: status, cost, what ran, branches to look at, and why. */
|
|
428
|
+
const formatRunResult = (result) => {
|
|
429
|
+
const ran = result.nodes.filter((n) => n.runs > 0).length;
|
|
430
|
+
const skipped = result.nodes.length - ran;
|
|
431
|
+
const lines = ["", `${RUN_MARK[result.status]} run ${result.runId} ${result.status.replace("_", " ")} ${style("dim", `· ${cost(result.costUsd)} · ${String(ran)} ran, ${String(skipped)} skipped`)}`];
|
|
432
|
+
for (const kept of result.worktrees) lines.push(` ${kept.id}: ${kept.branch ?? "detached"} ${style("dim", `at ${kept.path} (${kept.reason})`)}`);
|
|
433
|
+
for (const reason of result.reasons) lines.push(` ${style("red", reason)}`);
|
|
434
|
+
return lines;
|
|
435
|
+
};
|
|
436
|
+
// src/runs/format.ts
|
|
437
|
+
/**
|
|
438
|
+
* @file How stored runs read on the terminal: the `ork ps` table and the `ork
|
|
439
|
+
* tree` ownership tree with each node's state, tokens and cost, and what is
|
|
440
|
+
* paused (ADR-041).
|
|
441
|
+
*/
|
|
442
|
+
const STATUS_STYLE = {
|
|
443
|
+
running: "cyan",
|
|
444
|
+
paused: "yellow",
|
|
445
|
+
succeeded: "green",
|
|
446
|
+
failed: "red",
|
|
447
|
+
crashed: "red",
|
|
448
|
+
budget_exhausted: "yellow",
|
|
449
|
+
quota_exhausted: "yellow",
|
|
450
|
+
timed_out: "yellow",
|
|
451
|
+
interrupted: "yellow"
|
|
452
|
+
};
|
|
453
|
+
/** `just now`, `5m ago`, `3h ago`, `2d ago`, then the date. */
|
|
454
|
+
const ago = (iso, now = Date.now()) => {
|
|
455
|
+
const minutes = Math.floor((now - Date.parse(iso)) / 6e4);
|
|
456
|
+
if (minutes < 1) return "just now";
|
|
457
|
+
if (minutes < 60) return `${String(minutes)}m ago`;
|
|
458
|
+
if (minutes < 1440) return `${String(Math.floor(minutes / 60))}h ago`;
|
|
459
|
+
if (minutes < 10080) return `${String(Math.floor(minutes / 1440))}d ago`;
|
|
460
|
+
return iso.slice(0, 10);
|
|
461
|
+
};
|
|
462
|
+
/** `42s`, `3m12s`, `1h05m`. */
|
|
463
|
+
const duration = (ms) => {
|
|
464
|
+
const s = Math.max(0, Math.round(ms / 1e3));
|
|
465
|
+
if (s < 60) return `${String(s)}s`;
|
|
466
|
+
if (s < 3600) return `${String(Math.floor(s / 60))}m${String(s % 60).padStart(2, "0")}s`;
|
|
467
|
+
return `${String(Math.floor(s / 3600))}h${String(Math.floor(s % 3600 / 60)).padStart(2, "0")}m`;
|
|
468
|
+
};
|
|
469
|
+
/** The `ork ps` table; `withProject` adds each run's project. */
|
|
470
|
+
const formatRuns = (runs, withProject, now = Date.now(), statusOf = liveStatus) => {
|
|
471
|
+
if (runs.length === 0) return ["No runs."];
|
|
472
|
+
const rows = runs.map((run) => {
|
|
473
|
+
const status = statusOf(run);
|
|
474
|
+
const end = run.finishedAt === void 0 ? now : Date.parse(run.finishedAt);
|
|
475
|
+
return {
|
|
476
|
+
id: run.id,
|
|
477
|
+
workflow: run.workflow,
|
|
478
|
+
status,
|
|
479
|
+
when: `${ago(run.startedAt, now)} · ${duration(end - Date.parse(run.startedAt))}`,
|
|
480
|
+
cost: cost(run.costUsd),
|
|
481
|
+
project: run.project
|
|
482
|
+
};
|
|
483
|
+
});
|
|
484
|
+
const width = (pick, title) => Math.max(title.length, ...rows.map((r) => pick(r).length));
|
|
485
|
+
const w = {
|
|
486
|
+
id: width((r) => r.id, "RUN"),
|
|
487
|
+
workflow: width((r) => r.workflow, "WORKFLOW"),
|
|
488
|
+
status: width((r) => r.status, "STATUS"),
|
|
489
|
+
when: width((r) => r.when, "STARTED"),
|
|
490
|
+
cost: width((r) => r.cost, "COST")
|
|
491
|
+
};
|
|
492
|
+
const header = [
|
|
493
|
+
"RUN".padEnd(w.id),
|
|
494
|
+
"WORKFLOW".padEnd(w.workflow),
|
|
495
|
+
"STATUS".padEnd(w.status),
|
|
496
|
+
"STARTED".padEnd(w.when),
|
|
497
|
+
"COST".padStart(w.cost),
|
|
498
|
+
...withProject ? ["PROJECT"] : []
|
|
499
|
+
].join(" ");
|
|
500
|
+
return [style("dim", header), ...rows.map((r) => [
|
|
501
|
+
style("bold", r.id.padEnd(w.id)),
|
|
502
|
+
r.workflow.padEnd(w.workflow),
|
|
503
|
+
style(STATUS_STYLE[r.status] ?? "reset", r.status.padEnd(w.status)),
|
|
504
|
+
style("dim", r.when.padEnd(w.when)),
|
|
505
|
+
r.cost.padStart(w.cost),
|
|
506
|
+
...withProject ? [style("dim", r.project)] : []
|
|
507
|
+
].join(" "))];
|
|
508
|
+
};
|
|
509
|
+
const STATE_MARK = {
|
|
510
|
+
waiting: style("dim", "·"),
|
|
511
|
+
running: style("cyan", "●"),
|
|
512
|
+
ok: style("green", "✓"),
|
|
513
|
+
failed: style("red", "✗"),
|
|
514
|
+
skipped: style("dim", "-")
|
|
515
|
+
};
|
|
516
|
+
/** `950`, `12.3k`, `1.2M`. */
|
|
517
|
+
const count = (n) => {
|
|
518
|
+
if (n < 1e3) return String(n);
|
|
519
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
|
|
520
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
521
|
+
};
|
|
522
|
+
const stateText = (node) => {
|
|
523
|
+
if (node.state === "running") return node.runs > 1 ? `running (round ${String(node.runs)})` : "running";
|
|
524
|
+
if (node.state === "ok" || node.state === "failed") {
|
|
525
|
+
const verdict = nodeVerdict(node.output ?? {});
|
|
526
|
+
const line = verdict.length > 60 ? `${verdict.slice(0, 59)}…` : verdict;
|
|
527
|
+
return node.runs > 1 ? `${line} (${String(node.runs)} runs)` : line;
|
|
528
|
+
}
|
|
529
|
+
return node.state;
|
|
530
|
+
};
|
|
531
|
+
/** The ownership tree, one line per node, children under their owner. */
|
|
532
|
+
const formatTree = (roots) => {
|
|
533
|
+
const lines = [];
|
|
534
|
+
const walk = (node, lead, branch) => {
|
|
535
|
+
lines.push({
|
|
536
|
+
label: `${lead}${branch}${node.id}`,
|
|
537
|
+
node
|
|
538
|
+
});
|
|
539
|
+
const next = lead + (branch === "├─ " ? "│ " : branch === "" ? "" : " ");
|
|
540
|
+
node.children.forEach((child, index) => {
|
|
541
|
+
walk(child, next, index === node.children.length - 1 ? "└─ " : "├─ ");
|
|
542
|
+
});
|
|
543
|
+
};
|
|
544
|
+
for (const root of roots) walk(root, "", "");
|
|
545
|
+
const label = Math.max(...lines.map((l) => l.label.length));
|
|
546
|
+
const type = Math.max(...lines.map((l) => l.node.type.length));
|
|
547
|
+
return lines.map(({ label: text, node }) => {
|
|
548
|
+
const usage = node.inputTokens + node.outputTokens === 0 ? "" : style("dim", ` ${count(node.inputTokens)} in / ${count(node.outputTokens)} out · ${cost(node.costUsd)}`);
|
|
549
|
+
const state = node.held === true ? `${style("yellow", "‖")} paused` : `${STATE_MARK[node.state]} ${stateText(node)}`;
|
|
550
|
+
return `${text.padEnd(label)} ${style("dim", node.type.padEnd(type))} ${state}${usage}`;
|
|
551
|
+
});
|
|
552
|
+
};
|
|
553
|
+
export { formatRunEvent as a, createdWorktrees as c, formatEvent as i, listOrkWorktrees as l, formatRuns as n, createTerminalHuman as o, formatTree as r, applyClean as s, ago as t, planClean as u };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { p as runMarkerIn } from "./src-DtN1k-B3.mjs";
|
|
2
|
+
// src/commands/in-a-run.ts
|
|
3
|
+
/**
|
|
4
|
+
* @file The commands that act as the user refuse a program or an agent in a run
|
|
5
|
+
* (ADR-047). The refusal names the variable it saw, for someone at their own
|
|
6
|
+
* terminal who inherited it. It stops accidents; it is not a boundary.
|
|
7
|
+
*/
|
|
8
|
+
const refusal = (why, env) => {
|
|
9
|
+
const marker = runMarkerIn(env);
|
|
10
|
+
return marker === null ? null : `${why} ${marker} is set here. If this is your own terminal, it inherited that from a program in a run (a tmux server started by one, say); open a shell without it.`;
|
|
11
|
+
};
|
|
12
|
+
/** Why `ork watch --accept` refuses here, or `null` when it may run. */
|
|
13
|
+
const acceptRefusal = (env = process.env) => refusal("`ork watch --accept` approves code to run with nobody there, so a program or an agent in a run cannot use it.", env);
|
|
14
|
+
/** Why `ork chat` refuses here, or `null` when it may run. */
|
|
15
|
+
const chatRefusal = (env = process.env) => refusal("`ork chat` speaks as you, so a program or an agent in a run cannot use it. A program writes to a session with `ork send`.", env);
|
|
16
|
+
/** Why `ork decide` will not answer here, or `null` when it may. */
|
|
17
|
+
const decideRefusal = (env = process.env) => refusal("`ork decide` answers as you, so a program or an agent in a run cannot answer with it; `ork decide --list` still shows what is open.", env);
|
|
18
|
+
/** Why `ork continue` refuses here, or `null` when it may run. */
|
|
19
|
+
const continueRefusal = (env = process.env) => refusal("`ork continue` undoes a pause you set, so a program or an agent in a run cannot use it.", env);
|
|
20
|
+
/** Whether `ork ui` may print its sign-in link, which acts as the user. */
|
|
21
|
+
const mayShowSignIn = (env = process.env) => runMarkerIn(env) === null;
|
|
22
|
+
export { mayShowSignIn as a, decideRefusal as i, chatRefusal as n, continueRefusal as r, acceptRefusal as t };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { a as limitsNow, i as NOT_STARTED, o as limitsReport, r as capsHere } from "./src-Cz1yOUTg.mjs";
|
|
2
|
+
// src/run/limits.ts
|
|
3
|
+
/**
|
|
4
|
+
* @file The limits across runs that apply here (RFC-007): the user's settings
|
|
5
|
+
* and the project's rules, read fresh for each command, and what a run does
|
|
6
|
+
* when one is spent.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* What a run started here is held to. The files are read each time, so a cap
|
|
10
|
+
* changed during a run of hours reaches it; one that breaks meanwhile keeps the
|
|
11
|
+
* last it could read.
|
|
12
|
+
*/
|
|
13
|
+
const runLimits = (store, place, home) => {
|
|
14
|
+
const first = capsHere(home, place);
|
|
15
|
+
let { caps } = first;
|
|
16
|
+
return {
|
|
17
|
+
problems: first.problems,
|
|
18
|
+
now: () => {
|
|
19
|
+
const read = capsHere(home, place);
|
|
20
|
+
if (read.problems.length === 0) ({caps} = read);
|
|
21
|
+
return limitsNow(store, caps, /* @__PURE__ */ new Date());
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Whether what is left of a workflow can spend: a tool-only one, or a resume
|
|
27
|
+
* with only tools to go, is not held back by a quota.
|
|
28
|
+
*/
|
|
29
|
+
const spendsAnything = (workflow, history) => Object.entries(workflow.nodes).some(([id, node]) => (node.type === "claude.leader" || node.type === "claude.worker" || node.type === "workflow.call") && history?.get(id)?.at(-1)?.ok !== true);
|
|
30
|
+
/** Why a run won't start: a settings file it can't read, or a spent limit. */
|
|
31
|
+
const limitsRefusal = (limits, workflow, history) => {
|
|
32
|
+
if (!spendsAnything(workflow, history)) return [];
|
|
33
|
+
if (limits.problems.length > 0) return limits.problems;
|
|
34
|
+
const { blocked } = limits.now();
|
|
35
|
+
return blocked === void 0 ? [] : [`${NOT_STARTED}${blocked}.`];
|
|
36
|
+
};
|
|
37
|
+
/** What `ork usage` shows. */
|
|
38
|
+
const limitsHere = (store, place, home) => limitsReport(store, capsHere(home, place), /* @__PURE__ */ new Date());
|
|
39
|
+
/** A line for `ork chat`, which counts and isn't stopped; `null` with no limit. */
|
|
40
|
+
const chatNotice = (store, place, home) => {
|
|
41
|
+
const { blocked } = runLimits(store, place, home).now();
|
|
42
|
+
return blocked === void 0 ? null : `Note: ${blocked}. This chat counts towards it.`;
|
|
43
|
+
};
|
|
44
|
+
export { runLimits as i, limitsHere as n, limitsRefusal as r, chatNotice as t };
|