loopable-cli 0.1.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/.output/nitro.json +17 -0
- package/.output/public/apple-touch-icon.png +0 -0
- package/.output/public/assets/_connectorId-BWjYn5tm.js +1 -0
- package/.output/public/assets/_loopId-Ds4RkyEc.js +1 -0
- package/.output/public/assets/_taskId-BOzICXDf.js +1 -0
- package/.output/public/assets/alert-Bn9-Zm0l.js +1 -0
- package/.output/public/assets/arrow-left-BF9juV9T.js +1 -0
- package/.output/public/assets/arrow-right-CzqvnOY1.js +1 -0
- package/.output/public/assets/button-HCY6DCY-.js +1 -0
- package/.output/public/assets/connection-status-CYZB8kLy.js +1 -0
- package/.output/public/assets/connector-icon-BZ_MUJV_.js +1 -0
- package/.output/public/assets/connectors-BvzttfSu.js +1 -0
- package/.output/public/assets/connectors-DdJNwRN_.js +1 -0
- package/.output/public/assets/external-link-D46X-bXa.js +1 -0
- package/.output/public/assets/gaps-D-I6xiDc.js +1 -0
- package/.output/public/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/.output/public/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/.output/public/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/.output/public/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/.output/public/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/.output/public/assets/inbox-BUUu9xxE.js +1 -0
- package/.output/public/assets/index-ztzex0FN.js +10 -0
- package/.output/public/assets/loader-CcBl2u8m.js +1 -0
- package/.output/public/assets/loop-form-dB3CLcZb.js +1 -0
- package/.output/public/assets/loops-DRunQrxL.js +1 -0
- package/.output/public/assets/loops-oSdqMd29.js +1 -0
- package/.output/public/assets/manifests-DNMqf7Vi.js +7 -0
- package/.output/public/assets/new-uyFxVjuB.js +1 -0
- package/.output/public/assets/preload-helper-ORbuV86A.js +3 -0
- package/.output/public/assets/runners-DSU3r5Ww.js +1 -0
- package/.output/public/assets/setting-fields-ixkpdBb-.js +3 -0
- package/.output/public/assets/styles-DZL5V1o7.css +2 -0
- package/.output/public/assets/switch-d5ummmHY.js +1 -0
- package/.output/public/assets/task-list-BpOStt9v.js +1 -0
- package/.output/public/assets/tasks-B8MsKgic.js +1 -0
- package/.output/public/assets/trash-2-D6u6NpzO.js +1 -0
- package/.output/public/favicon.svg +9 -0
- package/.output/server/_chunks/ssr-renderer.mjs +26 -0
- package/.output/server/_connectorId-8nb5yOzG.mjs +421 -0
- package/.output/server/_libs/@base-ui/react+[...].mjs +12841 -0
- package/.output/server/_libs/@tanstack/react-router+[...].mjs +14349 -0
- package/.output/server/_libs/class-variance-authority+clsx.mjs +69 -0
- package/.output/server/_libs/dijkstrajs.mjs +118 -0
- package/.output/server/_libs/drizzle-orm.mjs +4349 -0
- package/.output/server/_libs/h3+rou3+srvx.mjs +1361 -0
- package/.output/server/_libs/lucide-react.mjs +911 -0
- package/.output/server/_libs/next-themes.mjs +53 -0
- package/.output/server/_libs/pngjs.mjs +1680 -0
- package/.output/server/_libs/qrcode.mjs +2583 -0
- package/.output/server/_libs/sonner.mjs +955 -0
- package/.output/server/_libs/tailwind-merge.mjs +3380 -0
- package/.output/server/_libs/tanstack__history.mjs +341 -0
- package/.output/server/_libs/ufo.mjs +64 -0
- package/.output/server/_loopId-BsUL1SeL.mjs +272 -0
- package/.output/server/_runtime.mjs +35 -0
- package/.output/server/_ssr/access-zIl15o8y.mjs +42 -0
- package/.output/server/_ssr/agents-Btql102J.mjs +657 -0
- package/.output/server/_ssr/agents-DAOZPgO2.mjs +76 -0
- package/.output/server/_ssr/alert-NEiOCTOl.mjs +37 -0
- package/.output/server/_ssr/callback-CUINvl7M.mjs +62 -0
- package/.output/server/_ssr/client-CKkOfh-s.mjs +335 -0
- package/.output/server/_ssr/connection-status-Bi5peUAk.mjs +31 -0
- package/.output/server/_ssr/connections-qnw2GVlP.mjs +2542 -0
- package/.output/server/_ssr/connector-icon-DZXQLLFo.mjs +37 -0
- package/.output/server/_ssr/connectors-BHNwuIMj.mjs +106 -0
- package/.output/server/_ssr/connectors-C28g0i4c.mjs +83 -0
- package/.output/server/_ssr/createServerRpc-DPDrnFdH.mjs +12 -0
- package/.output/server/_ssr/empty-plugin-adapters-BJ4m9L8g.mjs +5 -0
- package/.output/server/_ssr/gaps-RAFSXLZf.mjs +53 -0
- package/.output/server/_ssr/inbox-k5zHergU.mjs +35 -0
- package/.output/server/_ssr/loop-form-DlObSewZ.mjs +412 -0
- package/.output/server/_ssr/loops-APFcP0y6.mjs +311 -0
- package/.output/server/_ssr/loops-xt2Bn7_q.mjs +222 -0
- package/.output/server/_ssr/new-DfKyd2Wa.mjs +169 -0
- package/.output/server/_ssr/router-3WmpE5b_.mjs +1553 -0
- package/.output/server/_ssr/runners-By-mwc3F.mjs +32 -0
- package/.output/server/_ssr/runners-DI_NxpHC.mjs +116 -0
- package/.output/server/_ssr/runners-DtXs4Q8a.mjs +106 -0
- package/.output/server/_ssr/secrets-Hqti-niS.mjs +199 -0
- package/.output/server/_ssr/setting-fields-8vKFj4df.mjs +92 -0
- package/.output/server/_ssr/ssr.mjs +6605 -0
- package/.output/server/_ssr/start-Bdc2GJUI.mjs +51 -0
- package/.output/server/_ssr/switch-ReNglgH6.mjs +18 -0
- package/.output/server/_ssr/task-list-CUITIqkb.mjs +186 -0
- package/.output/server/_ssr/tasks-SAjNovX6.mjs +110 -0
- package/.output/server/_ssr/tasks-vcoodp_D.mjs +533 -0
- package/.output/server/_tanstack-start-manifest_v-C0cP5F0M.mjs +125 -0
- package/.output/server/_taskId-CXS3lod0.mjs +249 -0
- package/.output/server/index.mjs +541 -0
- package/README.md +81 -0
- package/dist/cli.mjs +165 -0
- package/dist/dispatcher.mjs +9871 -0
- package/dist/runner.mjs +657 -0
- package/drizzle/0000_cold_demogoblin.sql +29 -0
- package/drizzle/0001_fancy_blonde_phantom.sql +13 -0
- package/drizzle/0002_spooky_smiling_tiger.sql +16 -0
- package/drizzle/0003_small_golden_guardian.sql +25 -0
- package/drizzle/0004_wonderful_thunderbolts.sql +39 -0
- package/drizzle/0005_named_workflows.sql +7 -0
- package/drizzle/0006_signals.sql +18 -0
- package/drizzle/0007_review_comments.sql +1 -0
- package/drizzle/0008_held_signals.sql +1 -0
- package/drizzle/0009_work_item_ref.sql +58 -0
- package/drizzle/0010_signal_payload.sql +2 -0
- package/drizzle/0011_rule_poll_cursor.sql +1 -0
- package/drizzle/0012_loops.sql +63 -0
- package/drizzle/0013_loop_owns_action_and_prompt.sql +154 -0
- package/drizzle/0014_loop_poll_every.sql +4 -0
- package/drizzle/0015_machines.sql +10 -0
- package/drizzle/0016_task_machine.sql +1 -0
- package/drizzle/0017_loop_machine.sql +1 -0
- package/drizzle/0018_runners.sql +1 -0
- package/drizzle/0019_task_runner.sql +1 -0
- package/drizzle/0020_loop_runner.sql +1 -0
- package/drizzle/0021_drop_runner_kind.sql +1 -0
- package/drizzle/0022_drop_loop_runner.sql +1 -0
- package/drizzle/0023_shared_host.sql +6 -0
- package/drizzle/meta/0000_snapshot.json +206 -0
- package/drizzle/meta/0001_snapshot.json +284 -0
- package/drizzle/meta/0002_snapshot.json +390 -0
- package/drizzle/meta/0003_snapshot.json +566 -0
- package/drizzle/meta/0004_snapshot.json +633 -0
- package/drizzle/meta/0005_snapshot.json +626 -0
- package/drizzle/meta/0006_snapshot.json +752 -0
- package/drizzle/meta/0007_snapshot.json +759 -0
- package/drizzle/meta/0008_snapshot.json +766 -0
- package/drizzle/meta/0009_snapshot.json +752 -0
- package/drizzle/meta/0010_snapshot.json +766 -0
- package/drizzle/meta/0011_snapshot.json +773 -0
- package/drizzle/meta/0012_snapshot.json +773 -0
- package/drizzle/meta/0013_snapshot.json +817 -0
- package/drizzle/meta/_journal.json +174 -0
- package/package.json +88 -0
|
@@ -0,0 +1,533 @@
|
|
|
1
|
+
import { f as eq, u as desc } from "../_libs/drizzle-orm.mjs";
|
|
2
|
+
import { c as loops, d as runDir, g as tasks, s as db } from "./client-CKkOfh-s.mjs";
|
|
3
|
+
import { a as connectorManifest, c as credentialForConnector, i as connectorAction, o as connectorRuntime, s as connectorWorkflow } from "./connections-qnw2GVlP.mjs";
|
|
4
|
+
import { o as settingsFor } from "./agents-Btql102J.mjs";
|
|
5
|
+
import "./runners-DtXs4Q8a.mjs";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { isAbsolute, join } from "node:path";
|
|
9
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/tasks-vcoodp_D.js
|
|
10
|
+
/** The agent says this when the honest answer is silence. */
|
|
11
|
+
var AGENT_NOTHING = "NOTHING_TO_DO";
|
|
12
|
+
function isAgentNothing(text) {
|
|
13
|
+
const compact = (value) => value.replace(/[`*_.\s]/g, "").toUpperCase();
|
|
14
|
+
return compact(text) === compact(AGENT_NOTHING);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Folder jobs name a path that only exists on Loopable's host.
|
|
18
|
+
* A runner on another hostname cannot see that folder.
|
|
19
|
+
*/
|
|
20
|
+
function jobRequiresHost(job) {
|
|
21
|
+
return Boolean(job.cwd);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Appended to the workflow's prompt, because the shape of the answer is the
|
|
25
|
+
* machinery's business and the workflow should only have to describe the job.
|
|
26
|
+
*/
|
|
27
|
+
var REVIEW_FORMAT = [
|
|
28
|
+
`Reply with one JSON object and nothing else, in this shape:`,
|
|
29
|
+
``,
|
|
30
|
+
`{`,
|
|
31
|
+
` "summary": "a few sentences on the change as a whole",`,
|
|
32
|
+
` "findings": [`,
|
|
33
|
+
` { "path": "src/thing.ts", "line": 42, "body": "what is wrong, and what to do" },`,
|
|
34
|
+
` { "path": "src/thing.ts", "startLine": 60, "line": 64, "body": "for a point about several lines" }`,
|
|
35
|
+
` ]`,
|
|
36
|
+
`}`,
|
|
37
|
+
``,
|
|
38
|
+
`- "path" is the path of the file in the repository.`,
|
|
39
|
+
`- "line" is the line number in the new version of the file. Only comment on`,
|
|
40
|
+
` lines that this pull request changed; a comment on a line that is not in`,
|
|
41
|
+
` the diff will be moved into the summary instead of attached.`,
|
|
42
|
+
`- One finding per point, placed where a reader has to look rather than where`,
|
|
43
|
+
` you happened to notice it.`,
|
|
44
|
+
`- "body" is markdown with no heading.`,
|
|
45
|
+
`- "findings" may be empty when the summary says everything worth saying.`
|
|
46
|
+
].join("\n");
|
|
47
|
+
/**
|
|
48
|
+
* Everything in the reply that might be the object, best first.
|
|
49
|
+
*
|
|
50
|
+
* More than one is offered on purpose. A streamed answer can arrive twice,
|
|
51
|
+
* once in pieces and once whole, and a reply can carry an example alongside
|
|
52
|
+
* the real thing, so the first block that looks like JSON is not reliably the
|
|
53
|
+
* one that is. Each is tried in turn and the first that parses wins.
|
|
54
|
+
*/
|
|
55
|
+
function jsonCandidates(text) {
|
|
56
|
+
const fences = (pattern) => [...text.matchAll(pattern)].map((match) => (match[1] ?? "").trim()).filter(Boolean);
|
|
57
|
+
const labelled = fences(/```json\s*\n([\s\S]*?)```/gi);
|
|
58
|
+
const rest = fences(/```\s*\n([\s\S]*?)```/g);
|
|
59
|
+
const start = text.indexOf("{");
|
|
60
|
+
const end = text.lastIndexOf("}");
|
|
61
|
+
if (start !== -1 && end > start) rest.push(text.slice(start, end + 1));
|
|
62
|
+
return {
|
|
63
|
+
labelled,
|
|
64
|
+
rest
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function answerFrom(json) {
|
|
68
|
+
let parsed;
|
|
69
|
+
try {
|
|
70
|
+
parsed = JSON.parse(json);
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
if (typeof parsed !== "object" || parsed === null) return null;
|
|
75
|
+
const value = parsed;
|
|
76
|
+
const summary = typeof value.summary === "string" ? value.summary.trim() : "";
|
|
77
|
+
const findings = Array.isArray(value.findings) ? value.findings.flatMap((entry) => {
|
|
78
|
+
const finding = findingFrom(entry);
|
|
79
|
+
return finding ? [finding] : [];
|
|
80
|
+
}) : [];
|
|
81
|
+
return summary || findings.length > 0 ? {
|
|
82
|
+
summary,
|
|
83
|
+
findings
|
|
84
|
+
} : null;
|
|
85
|
+
}
|
|
86
|
+
function findingFrom(value) {
|
|
87
|
+
if (typeof value !== "object" || value === null) return null;
|
|
88
|
+
const entry = value;
|
|
89
|
+
const path = typeof entry.path === "string" ? entry.path.trim() : "";
|
|
90
|
+
const line = typeof entry.line === "number" ? Math.trunc(entry.line) : NaN;
|
|
91
|
+
const body = typeof entry.body === "string" ? entry.body.trim() : "";
|
|
92
|
+
if (!path || !body || !Number.isFinite(line) || line < 1) return null;
|
|
93
|
+
const start = typeof entry.startLine === "number" ? Math.trunc(entry.startLine) : void 0;
|
|
94
|
+
return {
|
|
95
|
+
path,
|
|
96
|
+
line,
|
|
97
|
+
...start !== void 0 && start >= 1 && start < line ? { startLine: start } : {},
|
|
98
|
+
body
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Lenient about the wrapper and strict about the contents.
|
|
103
|
+
*
|
|
104
|
+
* An agent that answered in prose is taken at its word and gets a review with
|
|
105
|
+
* no anchors, which is what Loopable did before it could anchor anything. An
|
|
106
|
+
* agent that plainly meant to answer in JSON, by fencing it as such, and
|
|
107
|
+
* produced nothing readable is an error: the alternative is posting the
|
|
108
|
+
* wreckage to somebody's pull request.
|
|
109
|
+
*/
|
|
110
|
+
function parseReview(text) {
|
|
111
|
+
const { labelled, rest } = jsonCandidates(text);
|
|
112
|
+
for (const candidate of [...labelled, ...rest]) {
|
|
113
|
+
const answer = answerFrom(candidate);
|
|
114
|
+
if (answer) return answer;
|
|
115
|
+
}
|
|
116
|
+
if (labelled.length > 0) throw new Error("The agent was asked for JSON and produced something that could not be read.");
|
|
117
|
+
const summary = text.trim();
|
|
118
|
+
if (!summary) throw new Error("The agent's answer was empty.");
|
|
119
|
+
return {
|
|
120
|
+
summary,
|
|
121
|
+
findings: []
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Splits findings into the ones that can be attached to a line and the ones
|
|
126
|
+
* that cannot. With nothing to check against, everything is treated as
|
|
127
|
+
* unanchorable: guessing would risk the whole review being refused.
|
|
128
|
+
*/
|
|
129
|
+
function anchorFindings(findings, commentable) {
|
|
130
|
+
const comments = [];
|
|
131
|
+
const loose = [];
|
|
132
|
+
for (const finding of findings) {
|
|
133
|
+
const lines = commentable?.[finding.path];
|
|
134
|
+
const allowed = lines ? new Set(lines) : null;
|
|
135
|
+
(allowed !== null && allowed.has(finding.line) && (finding.startLine === void 0 || allowed.has(finding.startLine)) ? comments : loose).push(finding);
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
comments,
|
|
139
|
+
loose
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** The review body: the overview, then anything that could not be anchored. */
|
|
143
|
+
function reviewBody(summary, loose) {
|
|
144
|
+
const parts = summary ? [summary] : [];
|
|
145
|
+
for (const finding of loose) {
|
|
146
|
+
const at = finding.startLine ? `${finding.startLine}-${finding.line}` : String(finding.line);
|
|
147
|
+
parts.push(`**${finding.path}:${at}**\n\n${finding.body}`);
|
|
148
|
+
}
|
|
149
|
+
return parts.join("\n\n");
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* The branch name a code-writing task uses, so a retry pushes the same branch
|
|
153
|
+
* the first attempt made rather than leaving a trail of near-identical ones.
|
|
154
|
+
*/
|
|
155
|
+
/** Reads as a branch name and cannot collide with a person's own. */
|
|
156
|
+
function slug(text) {
|
|
157
|
+
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "work";
|
|
158
|
+
}
|
|
159
|
+
function branchFor(input) {
|
|
160
|
+
return `loopable/${slug(input.ref)}-${input.taskId.slice(0, 8)}`;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* The agent says this when the honest answer is "nothing". Without it a loop
|
|
164
|
+
* that runs by itself would post filler, which is worse than silence.
|
|
165
|
+
*/
|
|
166
|
+
var NOTHING = AGENT_NOTHING;
|
|
167
|
+
/**
|
|
168
|
+
* The answer is the checkout, not the reply, so the reply is asked for as the
|
|
169
|
+
* account somebody reads before deciding whether to look at the diff.
|
|
170
|
+
*/
|
|
171
|
+
var CODE_FORMAT = [
|
|
172
|
+
"Then reply with the description of a pull request: a first line under about",
|
|
173
|
+
"seventy characters saying what it does, a blank line, and a short account of",
|
|
174
|
+
"what you changed and anything you were unsure about. No preamble."
|
|
175
|
+
].join("\n");
|
|
176
|
+
function toView(row) {
|
|
177
|
+
return {
|
|
178
|
+
id: row.id,
|
|
179
|
+
loopId: row.loopId,
|
|
180
|
+
loopName: row.loopName ?? "Deleted loop",
|
|
181
|
+
connectorId: row.connectorId,
|
|
182
|
+
state: row.state,
|
|
183
|
+
sourceUrl: row.sourceUrl,
|
|
184
|
+
sourceKind: row.sourceKind,
|
|
185
|
+
sourceRef: row.sourceRef,
|
|
186
|
+
sourceTitle: row.sourceTitle,
|
|
187
|
+
dryRun: row.dryRun,
|
|
188
|
+
agentId: row.agentId,
|
|
189
|
+
runnerId: row.runnerId,
|
|
190
|
+
agentCommand: row.agentCommand,
|
|
191
|
+
output: row.output,
|
|
192
|
+
comments: row.comments ?? [],
|
|
193
|
+
actionConnectorId: row.actionConnectorId,
|
|
194
|
+
actionId: row.actionId,
|
|
195
|
+
resultUrl: row.resultUrl,
|
|
196
|
+
error: row.error,
|
|
197
|
+
attempts: row.attempts,
|
|
198
|
+
cancelRequested: row.cancelRequested,
|
|
199
|
+
durationMs: row.durationMs,
|
|
200
|
+
createdAt: row.createdAt.toISOString(),
|
|
201
|
+
startedAt: row.startedAt?.toISOString() ?? null,
|
|
202
|
+
updatedAt: row.updatedAt.toISOString()
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
function select() {
|
|
206
|
+
return db().select({
|
|
207
|
+
id: tasks.id,
|
|
208
|
+
loopId: tasks.loopId,
|
|
209
|
+
connectorId: tasks.connectorId,
|
|
210
|
+
state: tasks.state,
|
|
211
|
+
sourceUrl: tasks.sourceUrl,
|
|
212
|
+
sourceKind: tasks.sourceKind,
|
|
213
|
+
sourceRef: tasks.sourceRef,
|
|
214
|
+
sourceTitle: tasks.sourceTitle,
|
|
215
|
+
dryRun: tasks.dryRun,
|
|
216
|
+
agentId: tasks.agentId,
|
|
217
|
+
runnerId: tasks.runnerId,
|
|
218
|
+
agentCommand: tasks.agentCommand,
|
|
219
|
+
output: tasks.output,
|
|
220
|
+
comments: tasks.comments,
|
|
221
|
+
actionConnectorId: tasks.actionConnectorId,
|
|
222
|
+
actionId: tasks.actionId,
|
|
223
|
+
actionTarget: tasks.actionTarget,
|
|
224
|
+
resultUrl: tasks.resultUrl,
|
|
225
|
+
error: tasks.error,
|
|
226
|
+
logPath: tasks.logPath,
|
|
227
|
+
logText: tasks.logText,
|
|
228
|
+
attempts: tasks.attempts,
|
|
229
|
+
runAfter: tasks.runAfter,
|
|
230
|
+
leaseUntil: tasks.leaseUntil,
|
|
231
|
+
cancelRequested: tasks.cancelRequested,
|
|
232
|
+
dedupeKey: tasks.dedupeKey,
|
|
233
|
+
durationMs: tasks.durationMs,
|
|
234
|
+
createdAt: tasks.createdAt,
|
|
235
|
+
startedAt: tasks.startedAt,
|
|
236
|
+
updatedAt: tasks.updatedAt,
|
|
237
|
+
loopName: loops.name
|
|
238
|
+
}).from(tasks).leftJoin(loops, eq(tasks.loopId, loops.id));
|
|
239
|
+
}
|
|
240
|
+
async function listTasks(options = {}) {
|
|
241
|
+
const query = select().orderBy(desc(tasks.createdAt)).limit(options.limit ?? 100);
|
|
242
|
+
return (options.loopId ? await query.where(eq(tasks.loopId, options.loopId)).all() : await query.all()).map(toView);
|
|
243
|
+
}
|
|
244
|
+
async function getTask(id) {
|
|
245
|
+
const row = await select().where(eq(tasks.id, id)).get();
|
|
246
|
+
return row ? toView(row) : null;
|
|
247
|
+
}
|
|
248
|
+
async function taskRow(id) {
|
|
249
|
+
return await db().select().from(tasks).where(eq(tasks.id, id)).get();
|
|
250
|
+
}
|
|
251
|
+
async function updateTask(id, values) {
|
|
252
|
+
await db().update(tasks).set({
|
|
253
|
+
...values,
|
|
254
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
255
|
+
}).where(eq(tasks.id, id)).run();
|
|
256
|
+
}
|
|
257
|
+
/** The one place a task is created, whether a person or a poll asked for it. */
|
|
258
|
+
async function queue(input) {
|
|
259
|
+
const id = randomUUID();
|
|
260
|
+
await db().insert(tasks).values({
|
|
261
|
+
id,
|
|
262
|
+
loopId: input.loop.id,
|
|
263
|
+
connectorId: input.loop.connectorId,
|
|
264
|
+
state: "queued",
|
|
265
|
+
sourceUrl: input.url.trim(),
|
|
266
|
+
sourceKind: input.item.kind,
|
|
267
|
+
sourceRef: input.item.ref,
|
|
268
|
+
sourcePayload: input.payload,
|
|
269
|
+
sourceTitle: input.title,
|
|
270
|
+
dryRun: input.dryRun,
|
|
271
|
+
actionConnectorId: input.loop.actionConnectorId,
|
|
272
|
+
actionId: input.loop.actionId,
|
|
273
|
+
actionTarget: input.loop.actionTarget,
|
|
274
|
+
dedupeKey: input.dedupeKey
|
|
275
|
+
}).run();
|
|
276
|
+
return await getTask(id);
|
|
277
|
+
}
|
|
278
|
+
/** Everything a loop needs before it can produce a task, or a reason it cannot. */
|
|
279
|
+
async function runnable(loopId) {
|
|
280
|
+
const loop = await db().select().from(loops).where(eq(loops.id, loopId)).get();
|
|
281
|
+
if (!loop) throw new Error("Loop not found");
|
|
282
|
+
const manifest = connectorManifest(loop.connectorId);
|
|
283
|
+
if (!manifest) throw new Error(`Unknown connector: ${loop.connectorId}`);
|
|
284
|
+
if (!connectorWorkflow(loop.connectorId, loop.workflowId)) throw new Error(`${manifest.name} no longer offers the workflow this loop was built on.`);
|
|
285
|
+
if (!connectorRuntime(loop.connectorId).resolveWorkItem) throw new Error(`${manifest.name} cannot run tasks yet.`);
|
|
286
|
+
const writer = connectorManifest(loop.actionConnectorId);
|
|
287
|
+
if (!writer) throw new Error(`Unknown connector: ${loop.actionConnectorId}`);
|
|
288
|
+
if (!connectorRuntime(loop.actionConnectorId).applyAction) throw new Error(`${writer.name} cannot write anything yet.`);
|
|
289
|
+
return loop;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Queues a run and returns at once. Identifying the link needs no network, so
|
|
293
|
+
* a typo still fails while the person is looking at the box, but everything
|
|
294
|
+
* that can be slow or can fail belongs to the dispatcher.
|
|
295
|
+
*/
|
|
296
|
+
async function enqueueTask(input) {
|
|
297
|
+
const loop = await runnable(input.loopId);
|
|
298
|
+
const manifest = connectorManifest(loop.connectorId);
|
|
299
|
+
const runtime = connectorRuntime(loop.connectorId);
|
|
300
|
+
if (manifest.byHand.kind === "now" && runtime.itemForNow) return await queue({
|
|
301
|
+
loop,
|
|
302
|
+
item: runtime.itemForNow(),
|
|
303
|
+
url: "",
|
|
304
|
+
dryRun: input.dryRun
|
|
305
|
+
});
|
|
306
|
+
if (!runtime.identifyLink) throw new Error(`${manifest.name} cannot read links.`);
|
|
307
|
+
const item = runtime.identifyLink(input.url);
|
|
308
|
+
if (!item) throw new Error(`That does not look like a ${manifest.name} link Loopable can work on.`);
|
|
309
|
+
return await queue({
|
|
310
|
+
loop,
|
|
311
|
+
item,
|
|
312
|
+
url: input.url,
|
|
313
|
+
dryRun: input.dryRun
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Queues a run for something a poll noticed. The title is already known, so
|
|
318
|
+
* the inbox can say what the task is about before the dispatcher has fetched
|
|
319
|
+
* anything.
|
|
320
|
+
*/
|
|
321
|
+
async function enqueueSignal(input) {
|
|
322
|
+
const loop = await runnable(input.loop.id);
|
|
323
|
+
return await queue({
|
|
324
|
+
loop,
|
|
325
|
+
item: input.signal,
|
|
326
|
+
payload: input.signal.payload,
|
|
327
|
+
url: input.signal.url,
|
|
328
|
+
title: input.signal.title,
|
|
329
|
+
dryRun: false,
|
|
330
|
+
dedupeKey: `${loop.id}:${input.signal.key}`
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
/** The tail is what matters while a run is going; the whole thing rarely is. */
|
|
334
|
+
var LOG_TAIL_BYTES = 64e3;
|
|
335
|
+
async function readTaskLog(id) {
|
|
336
|
+
const task = await taskRow(id);
|
|
337
|
+
if (!task) return null;
|
|
338
|
+
let text = task.logText;
|
|
339
|
+
if (!text && task.logPath && existsSync(task.logPath)) text = readFileSync(task.logPath, "utf8");
|
|
340
|
+
if (!text) return null;
|
|
341
|
+
if (text.length <= LOG_TAIL_BYTES) return text;
|
|
342
|
+
return `[earlier output not shown]\n\n${text.slice(-64e3)}`;
|
|
343
|
+
}
|
|
344
|
+
async function requestCancel(id) {
|
|
345
|
+
if (!await taskRow(id)) return null;
|
|
346
|
+
await updateTask(id, { cancelRequested: true });
|
|
347
|
+
return await getTask(id);
|
|
348
|
+
}
|
|
349
|
+
var TaskCancelled = class extends Error {
|
|
350
|
+
constructor() {
|
|
351
|
+
super("Stopped before it finished.");
|
|
352
|
+
this.name = "Cancelled";
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
/**
|
|
356
|
+
* The directory a loop says its agent should work in. Checked here rather than
|
|
357
|
+
* left to the agent, which would otherwise run somewhere unexpected and answer
|
|
358
|
+
* confidently about the wrong code.
|
|
359
|
+
*/
|
|
360
|
+
function folderFor(loop) {
|
|
361
|
+
const folder = typeof loop.settings.folder === "string" ? loop.settings.folder.trim() : "";
|
|
362
|
+
if (!folder) throw new Error("This loop has no folder set. Set one and try again.");
|
|
363
|
+
if (!isAbsolute(folder)) throw new Error(`The folder must be an absolute path: ${folder}`);
|
|
364
|
+
if (!existsSync(folder)) throw new Error(`There is no folder at ${folder}.`);
|
|
365
|
+
if (!statSync(folder).isDirectory()) throw new Error(`${folder} is not a folder.`);
|
|
366
|
+
return folder;
|
|
367
|
+
}
|
|
368
|
+
var KIND_NOUN = {
|
|
369
|
+
pull_request: "pull request",
|
|
370
|
+
issue: "issue",
|
|
371
|
+
message: "the message",
|
|
372
|
+
email: "the email",
|
|
373
|
+
occurrence: "the run due at"
|
|
374
|
+
};
|
|
375
|
+
function checkoutInstructions(item, branch, writesCode) {
|
|
376
|
+
const checkout = item.checkout;
|
|
377
|
+
if (!checkout) return [];
|
|
378
|
+
const at = checkout.sha ? `${checkout.ref} (${checkout.sha})` : checkout.ref;
|
|
379
|
+
if (writesCode && branch) return [
|
|
380
|
+
`Clone ${checkout.url} and check out ${at}.`,
|
|
381
|
+
`Commit your change and push it to ${branch}. Do not open a pull request; Loopable will.`,
|
|
382
|
+
``
|
|
383
|
+
];
|
|
384
|
+
return [`Clone ${checkout.url} and check out ${at}. Work in that checkout. Do not commit or push.`, ``];
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* What the agent is actually sent.
|
|
388
|
+
*
|
|
389
|
+
* The loop supplies the job and, if it has anything to add, its guidance after
|
|
390
|
+
* it. What shape the answer must take is not either of theirs: it is what the
|
|
391
|
+
* code downstream is able to read, so it is appended here and neither of them
|
|
392
|
+
* has to know about it.
|
|
393
|
+
*/
|
|
394
|
+
function promptFor(input) {
|
|
395
|
+
const shape = input.workflow.answer === "review" ? [REVIEW_FORMAT] : input.workflow.answer === "code" ? [CODE_FORMAT] : [`Reply with only the text to post. No preamble, no explanation of what you are about to do.`];
|
|
396
|
+
return [
|
|
397
|
+
`You are working on ${KIND_NOUN[input.item.kind]} ${input.item.ref}.`,
|
|
398
|
+
...input.files.length > 0 ? [`Read ${input.files.join(" and ")} first.`] : [],
|
|
399
|
+
``,
|
|
400
|
+
...checkoutInstructions(input.item, input.branch, input.workflow.answer === "code"),
|
|
401
|
+
`Your task:`,
|
|
402
|
+
input.ask,
|
|
403
|
+
...input.guidance ? [
|
|
404
|
+
``,
|
|
405
|
+
`From the person who set this up:`,
|
|
406
|
+
input.guidance
|
|
407
|
+
] : [],
|
|
408
|
+
``,
|
|
409
|
+
...shape,
|
|
410
|
+
``,
|
|
411
|
+
`If there is genuinely nothing worth posting, reply with exactly ${NOTHING}.`
|
|
412
|
+
].join("\n");
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Turns what the agent said into what will be posted. A review is parsed and
|
|
416
|
+
* its findings checked against the diff; anything that cannot be attached to a
|
|
417
|
+
* line goes into the body with its location written out, because losing the
|
|
418
|
+
* point is worse than losing its position.
|
|
419
|
+
*/
|
|
420
|
+
function answerFor(workflow, action, item, text) {
|
|
421
|
+
if (workflow.answer !== "review") return {
|
|
422
|
+
output: text,
|
|
423
|
+
comments: []
|
|
424
|
+
};
|
|
425
|
+
const { summary, findings } = parseReview(text);
|
|
426
|
+
const { comments, loose } = action.accepts.includes("review") ? anchorFindings(findings, item.commentable) : {
|
|
427
|
+
comments: [],
|
|
428
|
+
loose: findings
|
|
429
|
+
};
|
|
430
|
+
return {
|
|
431
|
+
output: reviewBody(summary, loose) || "Comments are on the lines they are about.",
|
|
432
|
+
comments
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
async function loadWork(id, signal) {
|
|
436
|
+
const task = await taskRow(id);
|
|
437
|
+
if (!task) throw new Error(`Task not found: ${id}`);
|
|
438
|
+
if (signal?.aborted || task.cancelRequested) throw new TaskCancelled();
|
|
439
|
+
const loop = await db().select().from(loops).where(eq(loops.id, task.loopId)).get();
|
|
440
|
+
if (!loop) throw new Error("The loop behind this task has been deleted.");
|
|
441
|
+
const workflow = connectorWorkflow(loop.connectorId, loop.workflowId);
|
|
442
|
+
if (!workflow) throw new Error(`${loop.connectorId} no longer offers ${loop.workflowId}.`);
|
|
443
|
+
const runtime = connectorRuntime(task.connectorId);
|
|
444
|
+
if (!runtime.resolveWorkItem) throw new Error(`${task.connectorId} cannot run tasks.`);
|
|
445
|
+
const writer = connectorRuntime(task.actionConnectorId);
|
|
446
|
+
if (!writer.applyAction) throw new Error(`${task.actionConnectorId} cannot write anything.`);
|
|
447
|
+
const action = connectorAction(task.actionConnectorId, task.actionId);
|
|
448
|
+
if (!action) throw new Error(`${task.actionConnectorId} no longer offers ${task.actionId}.`);
|
|
449
|
+
const { credential } = await credentialForConnector(task.connectorId);
|
|
450
|
+
return {
|
|
451
|
+
task,
|
|
452
|
+
loop,
|
|
453
|
+
workflow,
|
|
454
|
+
writer,
|
|
455
|
+
action,
|
|
456
|
+
credential,
|
|
457
|
+
item: await runtime.resolveWorkItem({
|
|
458
|
+
url: task.sourceUrl,
|
|
459
|
+
payload: task.sourcePayload ?? null,
|
|
460
|
+
credential
|
|
461
|
+
})
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
async function jobForTask(id) {
|
|
465
|
+
const { task, loop, workflow, item } = await loadWork(id);
|
|
466
|
+
if (!task.agentId) throw new Error("This task has no agent yet.");
|
|
467
|
+
const workspace = runDir(id);
|
|
468
|
+
const cwd = workflow.runsIn === "folder" ? folderFor(loop) : void 0;
|
|
469
|
+
const branch = workflow.answer === "code" ? branchFor({
|
|
470
|
+
ref: item.ref,
|
|
471
|
+
taskId: id
|
|
472
|
+
}) : void 0;
|
|
473
|
+
const settings = { ...await settingsFor(task.agentId) };
|
|
474
|
+
if (item.checkout && settings.permissionMode === "read_only") settings.permissionMode = "workspace_write";
|
|
475
|
+
return {
|
|
476
|
+
taskId: id,
|
|
477
|
+
agentId: task.agentId,
|
|
478
|
+
prompt: promptFor({
|
|
479
|
+
workflow,
|
|
480
|
+
ask: loop.prompt,
|
|
481
|
+
guidance: loop.guidance,
|
|
482
|
+
item,
|
|
483
|
+
files: item.context.map((file) => cwd ? join(workspace, file.name) : file.name),
|
|
484
|
+
branch
|
|
485
|
+
}),
|
|
486
|
+
settings,
|
|
487
|
+
files: item.context.map((file) => ({
|
|
488
|
+
name: file.name,
|
|
489
|
+
body: file.body
|
|
490
|
+
})),
|
|
491
|
+
...cwd ? { cwd } : {}
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
async function absorbAgentOutput(id, raw) {
|
|
495
|
+
const { task, workflow, action, item } = await loadWork(id);
|
|
496
|
+
const workspace = runDir(id);
|
|
497
|
+
const said = raw.trim();
|
|
498
|
+
writeFileSync(join(workspace, "reply.txt"), said);
|
|
499
|
+
if (!said) throw new Error("The agent produced nothing.");
|
|
500
|
+
if (isAgentNothing(said)) {
|
|
501
|
+
await finish(id, task, {
|
|
502
|
+
state: "skipped",
|
|
503
|
+
output: said
|
|
504
|
+
});
|
|
505
|
+
return await getTask(id);
|
|
506
|
+
}
|
|
507
|
+
const parsed = answerFor(workflow, action, item, said);
|
|
508
|
+
if (!parsed.output && parsed.comments.length === 0) throw new Error("The agent produced nothing worth posting.");
|
|
509
|
+
await updateTask(id, {
|
|
510
|
+
output: parsed.output,
|
|
511
|
+
comments: parsed.comments
|
|
512
|
+
});
|
|
513
|
+
if (task.dryRun) {
|
|
514
|
+
await finish(id, task, { state: "prepared" });
|
|
515
|
+
return await getTask(id);
|
|
516
|
+
}
|
|
517
|
+
await updateTask(id, {
|
|
518
|
+
state: "applying",
|
|
519
|
+
leaseUntil: null
|
|
520
|
+
});
|
|
521
|
+
return await getTask(id);
|
|
522
|
+
}
|
|
523
|
+
async function finish(id, task, values) {
|
|
524
|
+
const started = task.startedAt?.getTime() ?? task.createdAt.getTime();
|
|
525
|
+
await updateTask(id, {
|
|
526
|
+
error: null,
|
|
527
|
+
...values,
|
|
528
|
+
durationMs: Date.now() - started,
|
|
529
|
+
leaseUntil: null
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
//#endregion
|
|
533
|
+
export { jobForTask as a, readTaskLog as c, updateTask as d, getTask as i, requestCancel as l, enqueueSignal as n, jobRequiresHost as o, enqueueTask as r, listTasks as s, absorbAgentOutput as t, taskRow as u };
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
//#region node_modules/.nitro/vite/services/ssr/assets/_tanstack-start-manifest_v-C0cP5F0M.js
|
|
2
|
+
var tsrStartManifest = () => ({ routes: {
|
|
3
|
+
__root__: {
|
|
4
|
+
filePath: "/Users/bytedance/codex-projects/loopable/src/routes/__root.tsx",
|
|
5
|
+
children: [
|
|
6
|
+
"/",
|
|
7
|
+
"/connectors/$connectorId",
|
|
8
|
+
"/inbox/$taskId",
|
|
9
|
+
"/loops/$loopId",
|
|
10
|
+
"/loops/new",
|
|
11
|
+
"/agents/",
|
|
12
|
+
"/connectors/",
|
|
13
|
+
"/inbox/",
|
|
14
|
+
"/loops/",
|
|
15
|
+
"/machines/",
|
|
16
|
+
"/runners/",
|
|
17
|
+
"/api/runners/claim",
|
|
18
|
+
"/api/runners/heartbeat",
|
|
19
|
+
"/api/runners/hello",
|
|
20
|
+
"/api/connectors/$connectorId/authorize",
|
|
21
|
+
"/api/connectors/$connectorId/callback",
|
|
22
|
+
"/api/runners/jobs/$taskId/complete",
|
|
23
|
+
"/api/runners/jobs/$taskId/log"
|
|
24
|
+
],
|
|
25
|
+
preloads: [
|
|
26
|
+
"/assets/index-ztzex0FN.js",
|
|
27
|
+
"/assets/preload-helper-ORbuV86A.js",
|
|
28
|
+
"/assets/button-HCY6DCY-.js",
|
|
29
|
+
"/assets/tasks-B8MsKgic.js",
|
|
30
|
+
"/assets/manifests-DNMqf7Vi.js",
|
|
31
|
+
"/assets/connectors-BvzttfSu.js",
|
|
32
|
+
"/assets/loops-oSdqMd29.js"
|
|
33
|
+
],
|
|
34
|
+
scripts: [{ attrs: {
|
|
35
|
+
type: "module",
|
|
36
|
+
async: !0,
|
|
37
|
+
src: "/assets/index-ztzex0FN.js"
|
|
38
|
+
} }]
|
|
39
|
+
},
|
|
40
|
+
"/connectors/$connectorId": {
|
|
41
|
+
filePath: "/Users/bytedance/codex-projects/loopable/src/routes/connectors/$connectorId.tsx",
|
|
42
|
+
children: void 0,
|
|
43
|
+
preloads: [
|
|
44
|
+
"/assets/_connectorId-BWjYn5tm.js",
|
|
45
|
+
"/assets/arrow-left-BF9juV9T.js",
|
|
46
|
+
"/assets/connector-icon-BZ_MUJV_.js",
|
|
47
|
+
"/assets/external-link-D46X-bXa.js",
|
|
48
|
+
"/assets/loader-CcBl2u8m.js",
|
|
49
|
+
"/assets/trash-2-D6u6NpzO.js",
|
|
50
|
+
"/assets/setting-fields-ixkpdBb-.js",
|
|
51
|
+
"/assets/connection-status-CYZB8kLy.js",
|
|
52
|
+
"/assets/alert-Bn9-Zm0l.js"
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
"/inbox/$taskId": {
|
|
56
|
+
filePath: "/Users/bytedance/codex-projects/loopable/src/routes/inbox/$taskId.tsx",
|
|
57
|
+
children: void 0,
|
|
58
|
+
preloads: [
|
|
59
|
+
"/assets/_taskId-BOzICXDf.js",
|
|
60
|
+
"/assets/arrow-left-BF9juV9T.js",
|
|
61
|
+
"/assets/task-list-BpOStt9v.js",
|
|
62
|
+
"/assets/external-link-D46X-bXa.js",
|
|
63
|
+
"/assets/loader-CcBl2u8m.js",
|
|
64
|
+
"/assets/alert-Bn9-Zm0l.js"
|
|
65
|
+
]
|
|
66
|
+
},
|
|
67
|
+
"/loops/$loopId": {
|
|
68
|
+
filePath: "/Users/bytedance/codex-projects/loopable/src/routes/loops/$loopId.tsx",
|
|
69
|
+
children: void 0,
|
|
70
|
+
preloads: [
|
|
71
|
+
"/assets/_loopId-Ds4RkyEc.js",
|
|
72
|
+
"/assets/arrow-left-BF9juV9T.js",
|
|
73
|
+
"/assets/loop-form-dB3CLcZb.js",
|
|
74
|
+
"/assets/task-list-BpOStt9v.js",
|
|
75
|
+
"/assets/switch-d5ummmHY.js",
|
|
76
|
+
"/assets/alert-Bn9-Zm0l.js",
|
|
77
|
+
"/assets/gaps-D-I6xiDc.js"
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
"/loops/new": {
|
|
81
|
+
filePath: "/Users/bytedance/codex-projects/loopable/src/routes/loops/new.tsx",
|
|
82
|
+
children: void 0,
|
|
83
|
+
preloads: [
|
|
84
|
+
"/assets/new-uyFxVjuB.js",
|
|
85
|
+
"/assets/arrow-left-BF9juV9T.js",
|
|
86
|
+
"/assets/arrow-right-CzqvnOY1.js",
|
|
87
|
+
"/assets/loop-form-dB3CLcZb.js",
|
|
88
|
+
"/assets/connector-icon-BZ_MUJV_.js"
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
"/connectors/": {
|
|
92
|
+
filePath: "/Users/bytedance/codex-projects/loopable/src/routes/connectors/index.tsx",
|
|
93
|
+
children: void 0,
|
|
94
|
+
preloads: [
|
|
95
|
+
"/assets/connectors-DdJNwRN_.js",
|
|
96
|
+
"/assets/arrow-right-CzqvnOY1.js",
|
|
97
|
+
"/assets/connector-icon-BZ_MUJV_.js",
|
|
98
|
+
"/assets/connection-status-CYZB8kLy.js"
|
|
99
|
+
]
|
|
100
|
+
},
|
|
101
|
+
"/inbox/": {
|
|
102
|
+
filePath: "/Users/bytedance/codex-projects/loopable/src/routes/inbox/index.tsx",
|
|
103
|
+
children: void 0,
|
|
104
|
+
preloads: ["/assets/inbox-BUUu9xxE.js", "/assets/task-list-BpOStt9v.js"]
|
|
105
|
+
},
|
|
106
|
+
"/loops/": {
|
|
107
|
+
filePath: "/Users/bytedance/codex-projects/loopable/src/routes/loops/index.tsx",
|
|
108
|
+
children: void 0,
|
|
109
|
+
preloads: [
|
|
110
|
+
"/assets/loops-DRunQrxL.js",
|
|
111
|
+
"/assets/connector-icon-BZ_MUJV_.js",
|
|
112
|
+
"/assets/trash-2-D6u6NpzO.js",
|
|
113
|
+
"/assets/switch-d5ummmHY.js",
|
|
114
|
+
"/assets/alert-Bn9-Zm0l.js",
|
|
115
|
+
"/assets/gaps-D-I6xiDc.js"
|
|
116
|
+
]
|
|
117
|
+
},
|
|
118
|
+
"/runners/": {
|
|
119
|
+
filePath: "/Users/bytedance/codex-projects/loopable/src/routes/runners/index.tsx",
|
|
120
|
+
children: void 0,
|
|
121
|
+
preloads: ["/assets/runners-DSU3r5Ww.js", "/assets/alert-Bn9-Zm0l.js"]
|
|
122
|
+
}
|
|
123
|
+
} });
|
|
124
|
+
//#endregion
|
|
125
|
+
export { tsrStartManifest };
|