@opentag/local-runtime 0.2.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/LICENSE +21 -0
- package/README.md +38 -0
- package/dist/chunk-3GVYPT5P.js +214 -0
- package/dist/chunk-3GVYPT5P.js.map +1 -0
- package/dist/chunk-CUP3SU3F.js +68 -0
- package/dist/chunk-CUP3SU3F.js.map +1 -0
- package/dist/chunk-EKMUVNR7.js +122 -0
- package/dist/chunk-EKMUVNR7.js.map +1 -0
- package/dist/chunk-UAMAECYE.js +255 -0
- package/dist/chunk-UAMAECYE.js.map +1 -0
- package/dist/chunk-UUQUUYQM.js +75 -0
- package/dist/chunk-UUQUUYQM.js.map +1 -0
- package/dist/chunk-ZXR3PYD7.js +168 -0
- package/dist/chunk-ZXR3PYD7.js.map +1 -0
- package/dist/config.d.ts +365 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +25 -0
- package/dist/config.js.map +1 -0
- package/dist/daemon.d.ts +42 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +14 -0
- package/dist/daemon.js.map +1 -0
- package/dist/dispatcher.d.ts +24 -0
- package/dist/dispatcher.d.ts.map +1 -0
- package/dist/dispatcher.js +9 -0
- package/dist/dispatcher.js.map +1 -0
- package/dist/doctor.d.ts +17 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +12 -0
- package/dist/doctor.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/pr.d.ts +19 -0
- package/dist/pr.d.ts.map +1 -0
- package/dist/pr.js +7 -0
- package/dist/pr.js.map +1 -0
- package/dist/runtime.d.ts +37 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +15 -0
- package/dist/runtime.js.map +1 -0
- package/package.json +80 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Amplift
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @opentag/local-runtime
|
|
2
|
+
|
|
3
|
+
Local OpenTag runtime helpers used by `@opentag/cli`.
|
|
4
|
+
|
|
5
|
+
Use this package when embedding the same local dispatcher, daemon, diagnostics, and GitHub PR helpers that the CLI uses.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @opentag/local-runtime
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Exports
|
|
14
|
+
|
|
15
|
+
- `startDispatcher`: starts the local dispatcher with callback sinks.
|
|
16
|
+
- `serveDaemon`: starts the local daemon loop.
|
|
17
|
+
- `createDaemonRuntimeInput`: derives daemon runtime input from config.
|
|
18
|
+
- `runDoctor`: checks dispatcher, bindings, checkouts, and executors.
|
|
19
|
+
- `parseDaemonConfig`: parses daemon config with compatibility aliases.
|
|
20
|
+
- `maybeCreatePullRequest`: creates GitHub pull requests from prepared run branches.
|
|
21
|
+
|
|
22
|
+
Subpath exports are also available:
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { startDispatcher } from "@opentag/local-runtime/dispatcher";
|
|
26
|
+
import { serveDaemon } from "@opentag/local-runtime/daemon";
|
|
27
|
+
import { runDoctor } from "@opentag/local-runtime/doctor";
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Node.js 20 or newer.
|
|
33
|
+
- A writable SQLite database path for the dispatcher.
|
|
34
|
+
- A local checkout for any Project Target you bind.
|
|
35
|
+
|
|
36
|
+
## Stability
|
|
37
|
+
|
|
38
|
+
This package is the local runtime boundary for the CLI. Keep app wrappers thin and put reusable local runtime behavior here.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import {
|
|
2
|
+
maybeCreatePullRequest
|
|
3
|
+
} from "./chunk-UUQUUYQM.js";
|
|
4
|
+
|
|
5
|
+
// src/daemon.ts
|
|
6
|
+
import { projectTargetRefFromEvent } from "@opentag/core";
|
|
7
|
+
import {
|
|
8
|
+
assessRunnerSecurity,
|
|
9
|
+
formatSecurityAssessment,
|
|
10
|
+
worktreePathForRun
|
|
11
|
+
} from "@opentag/runner";
|
|
12
|
+
function resolveRepositoryBinding(event, repositories) {
|
|
13
|
+
const projectTargetRef = projectTargetRefFromEvent(event);
|
|
14
|
+
if (!projectTargetRef) return null;
|
|
15
|
+
return repositories.find(
|
|
16
|
+
(candidate) => candidate.provider === projectTargetRef.provider && candidate.owner === projectTargetRef.owner && candidate.repo === projectTargetRef.repo
|
|
17
|
+
) ?? null;
|
|
18
|
+
}
|
|
19
|
+
function resolveWorkspacePath(event, repositories) {
|
|
20
|
+
return resolveRepositoryBinding(event, repositories)?.checkoutPath ?? null;
|
|
21
|
+
}
|
|
22
|
+
function errorMessage(error) {
|
|
23
|
+
return error instanceof Error ? error.message : String(error);
|
|
24
|
+
}
|
|
25
|
+
function failedRunResult(stage, error) {
|
|
26
|
+
return {
|
|
27
|
+
conclusion: "failure",
|
|
28
|
+
summary: `${stage} failed: ${errorMessage(error)}`
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function pullRequestPreparationFailureResult(result, error) {
|
|
32
|
+
return {
|
|
33
|
+
conclusion: "needs_human",
|
|
34
|
+
summary: `Executor completed, but OpenTag could not prepare the pull request action: ${errorMessage(error)}`,
|
|
35
|
+
...result.changedFiles ? { changedFiles: result.changedFiles } : {},
|
|
36
|
+
...result.artifacts ? { artifacts: result.artifacts } : {},
|
|
37
|
+
...result.verification ? { verification: result.verification } : {},
|
|
38
|
+
nextAction: "Fix branch push or pull request credentials, then retry the run before applying the PR action."
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async function runOneDaemonIteration(input) {
|
|
42
|
+
const claimed = await input.client.claim();
|
|
43
|
+
if (!claimed) return false;
|
|
44
|
+
const binding = resolveRepositoryBinding(claimed.event, input.repositories);
|
|
45
|
+
if (!binding) {
|
|
46
|
+
await input.client.complete(claimed.run.id, {
|
|
47
|
+
conclusion: "needs_human",
|
|
48
|
+
summary: "No local workspace mapping is configured for this run's repository."
|
|
49
|
+
});
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
const executorId = binding.defaultExecutor ?? claimed.event.target.executorHint ?? "echo";
|
|
53
|
+
const executor = input.executors[executorId];
|
|
54
|
+
if (!executor) {
|
|
55
|
+
await input.client.complete(claimed.run.id, {
|
|
56
|
+
conclusion: "needs_human",
|
|
57
|
+
summary: `No local executor is configured for '${executorId}'.`
|
|
58
|
+
});
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
const executionPath = executorId === "codex" ? worktreePathForRun({
|
|
62
|
+
workspacePath: binding.checkoutPath,
|
|
63
|
+
runId: claimed.run.id,
|
|
64
|
+
...binding.worktreeRoot ? { worktreeRoot: binding.worktreeRoot } : {}
|
|
65
|
+
}) : binding.checkoutPath;
|
|
66
|
+
const securityAssessment = assessRunnerSecurity({
|
|
67
|
+
executorId,
|
|
68
|
+
workspacePath: binding.checkoutPath,
|
|
69
|
+
executionPath,
|
|
70
|
+
command: claimed.event.command,
|
|
71
|
+
context: claimed.event.context,
|
|
72
|
+
permissions: claimed.event.permissions,
|
|
73
|
+
...input.security ? { policy: input.security } : {}
|
|
74
|
+
});
|
|
75
|
+
if (securityAssessment.findings.length > 0) {
|
|
76
|
+
await input.client.progress(claimed.run.id, {
|
|
77
|
+
type: securityAssessment.allowed ? "security.audit" : "security.blocked",
|
|
78
|
+
message: formatSecurityAssessment(securityAssessment),
|
|
79
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
if (!securityAssessment.allowed) {
|
|
83
|
+
await input.client.complete(claimed.run.id, {
|
|
84
|
+
conclusion: "needs_human",
|
|
85
|
+
summary: formatSecurityAssessment(securityAssessment),
|
|
86
|
+
nextAction: "Review the request and rerun with a narrower prompt or an explicit local policy override if appropriate."
|
|
87
|
+
});
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
let readiness;
|
|
91
|
+
try {
|
|
92
|
+
readiness = await executor.canRun({
|
|
93
|
+
runId: claimed.run.id,
|
|
94
|
+
workspacePath: binding.checkoutPath,
|
|
95
|
+
command: claimed.event.command,
|
|
96
|
+
context: claimed.event.context,
|
|
97
|
+
...claimed.run.contextPacket ? { contextPacket: claimed.run.contextPacket } : {},
|
|
98
|
+
permissions: claimed.event.permissions,
|
|
99
|
+
...binding.baseBranch ? { baseBranch: binding.baseBranch } : {},
|
|
100
|
+
...binding.worktreeRoot ? { worktreeRoot: binding.worktreeRoot } : {},
|
|
101
|
+
...binding.keepWorktree !== void 0 ? { keepWorktree: binding.keepWorktree } : {}
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
await input.client.complete(claimed.run.id, failedRunResult(`${executor.displayName} readiness check`, error));
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
if (!readiness.ready) {
|
|
108
|
+
await input.client.complete(claimed.run.id, {
|
|
109
|
+
conclusion: "needs_human",
|
|
110
|
+
summary: readiness.reason ?? `${executor.displayName} is not ready`
|
|
111
|
+
});
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
await input.client.markRunning(claimed.run.id, executor.id);
|
|
115
|
+
const heartbeatIntervalMs = input.heartbeatIntervalMs ?? 15e3;
|
|
116
|
+
let heartbeatHandle;
|
|
117
|
+
if (heartbeatIntervalMs > 0) {
|
|
118
|
+
heartbeatHandle = setInterval(() => {
|
|
119
|
+
void input.client.heartbeat(claimed.run.id).catch((error) => {
|
|
120
|
+
console.warn(`OpenTag heartbeat failed for ${claimed.run.id}:`, error);
|
|
121
|
+
});
|
|
122
|
+
}, heartbeatIntervalMs);
|
|
123
|
+
}
|
|
124
|
+
let executorResult;
|
|
125
|
+
try {
|
|
126
|
+
executorResult = await executor.run(
|
|
127
|
+
{
|
|
128
|
+
runId: claimed.run.id,
|
|
129
|
+
workspacePath: binding.checkoutPath,
|
|
130
|
+
command: claimed.event.command,
|
|
131
|
+
context: claimed.event.context,
|
|
132
|
+
...claimed.run.contextPacket ? { contextPacket: claimed.run.contextPacket } : {},
|
|
133
|
+
permissions: claimed.event.permissions,
|
|
134
|
+
...binding.baseBranch ? { baseBranch: binding.baseBranch } : {},
|
|
135
|
+
...binding.worktreeRoot ? { worktreeRoot: binding.worktreeRoot } : {},
|
|
136
|
+
...binding.keepWorktree !== void 0 ? { keepWorktree: binding.keepWorktree } : {}
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
emit: async (event) => {
|
|
140
|
+
console.log(`[${event.type}] ${event.message}`);
|
|
141
|
+
await input.client.progress(claimed.run.id, {
|
|
142
|
+
type: event.type,
|
|
143
|
+
message: event.message,
|
|
144
|
+
at: event.at
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
await input.client.complete(claimed.run.id, failedRunResult(executor.displayName, error));
|
|
151
|
+
return true;
|
|
152
|
+
} finally {
|
|
153
|
+
if (heartbeatHandle) clearInterval(heartbeatHandle);
|
|
154
|
+
}
|
|
155
|
+
let result;
|
|
156
|
+
try {
|
|
157
|
+
result = await maybeCreatePullRequest({
|
|
158
|
+
run: claimed.run,
|
|
159
|
+
event: claimed.event,
|
|
160
|
+
binding,
|
|
161
|
+
result: executorResult,
|
|
162
|
+
options: input.pullRequestOptions ?? {}
|
|
163
|
+
});
|
|
164
|
+
} catch (error) {
|
|
165
|
+
result = pullRequestPreparationFailureResult(executorResult, error);
|
|
166
|
+
}
|
|
167
|
+
await input.client.complete(claimed.run.id, result);
|
|
168
|
+
return true;
|
|
169
|
+
}
|
|
170
|
+
function sleep(ms, signal) {
|
|
171
|
+
if (signal?.aborted) return Promise.resolve();
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
const timeout = setTimeout(() => {
|
|
174
|
+
signal?.removeEventListener("abort", onAbort);
|
|
175
|
+
resolve();
|
|
176
|
+
}, ms);
|
|
177
|
+
function onAbort() {
|
|
178
|
+
clearTimeout(timeout);
|
|
179
|
+
resolve();
|
|
180
|
+
}
|
|
181
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async function serveDaemon(input) {
|
|
185
|
+
const pollIntervalMs = input.pollIntervalMs ?? 5e3;
|
|
186
|
+
while (!input.signal?.aborted) {
|
|
187
|
+
try {
|
|
188
|
+
const didWork = await runOneDaemonIteration({
|
|
189
|
+
runnerId: input.runnerId,
|
|
190
|
+
repositories: input.repositories,
|
|
191
|
+
executors: input.executors,
|
|
192
|
+
...input.security ? { security: input.security } : {},
|
|
193
|
+
...input.pullRequestOptions ? { pullRequestOptions: input.pullRequestOptions } : {},
|
|
194
|
+
...input.heartbeatIntervalMs !== void 0 ? { heartbeatIntervalMs: input.heartbeatIntervalMs } : {},
|
|
195
|
+
client: input.client
|
|
196
|
+
});
|
|
197
|
+
if (!didWork) {
|
|
198
|
+
await sleep(pollIntervalMs, input.signal);
|
|
199
|
+
}
|
|
200
|
+
} catch (error) {
|
|
201
|
+
if (input.signal?.aborted) break;
|
|
202
|
+
console.warn("OpenTag daemon iteration failed; retrying:", error);
|
|
203
|
+
await sleep(pollIntervalMs, input.signal);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export {
|
|
209
|
+
resolveRepositoryBinding,
|
|
210
|
+
resolveWorkspacePath,
|
|
211
|
+
runOneDaemonIteration,
|
|
212
|
+
serveDaemon
|
|
213
|
+
};
|
|
214
|
+
//# sourceMappingURL=chunk-3GVYPT5P.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/daemon.ts"],"sourcesContent":["import { projectTargetRefFromEvent, type OpenTagEvent, type OpenTagRun, type OpenTagRunResult } from \"@opentag/core\";\nimport {\n assessRunnerSecurity,\n formatSecurityAssessment,\n type ExecutorAdapter,\n type RunnerSecurityPolicy,\n worktreePathForRun\n} from \"@opentag/runner\";\nimport type { RepositoryBindingConfig } from \"./config.js\";\nimport { maybeCreatePullRequest, type PullRequestOptions } from \"./pr.js\";\n\nexport type ClaimedRun = {\n run: OpenTagRun;\n event: OpenTagEvent;\n};\n\nexport type DaemonClient = {\n claim(): Promise<ClaimedRun | null>;\n markRunning(runId: string, executor: string): Promise<void>;\n heartbeat(runId: string): Promise<void>;\n progress(runId: string, input: { type: string; message: string; at: string }): Promise<void>;\n complete(runId: string, result: OpenTagRunResult): Promise<void>;\n};\n\nexport function resolveRepositoryBinding(event: OpenTagEvent, repositories: RepositoryBindingConfig[]): RepositoryBindingConfig | null {\n const projectTargetRef = projectTargetRefFromEvent(event);\n if (!projectTargetRef) return null;\n\n return (\n repositories.find(\n (candidate) =>\n candidate.provider === projectTargetRef.provider &&\n candidate.owner === projectTargetRef.owner &&\n candidate.repo === projectTargetRef.repo\n ) ?? null\n );\n}\n\nexport function resolveWorkspacePath(event: OpenTagEvent, repositories: RepositoryBindingConfig[]): string | null {\n return resolveRepositoryBinding(event, repositories)?.checkoutPath ?? null;\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction failedRunResult(stage: string, error: unknown): OpenTagRunResult {\n return {\n conclusion: \"failure\",\n summary: `${stage} failed: ${errorMessage(error)}`\n };\n}\n\nfunction pullRequestPreparationFailureResult(result: OpenTagRunResult, error: unknown): OpenTagRunResult {\n return {\n conclusion: \"needs_human\",\n summary: `Executor completed, but OpenTag could not prepare the pull request action: ${errorMessage(error)}`,\n ...(result.changedFiles ? { changedFiles: result.changedFiles } : {}),\n ...(result.artifacts ? { artifacts: result.artifacts } : {}),\n ...(result.verification ? { verification: result.verification } : {}),\n nextAction: \"Fix branch push or pull request credentials, then retry the run before applying the PR action.\"\n };\n}\n\nexport async function runOneDaemonIteration(input: {\n runnerId: string;\n repositories: RepositoryBindingConfig[];\n executors: Record<string, ExecutorAdapter>;\n security?: RunnerSecurityPolicy;\n pullRequestOptions?: PullRequestOptions;\n heartbeatIntervalMs?: number;\n client: DaemonClient;\n}): Promise<boolean> {\n const claimed = await input.client.claim();\n if (!claimed) return false;\n\n const binding = resolveRepositoryBinding(claimed.event, input.repositories);\n if (!binding) {\n await input.client.complete(claimed.run.id, {\n conclusion: \"needs_human\",\n summary: \"No local workspace mapping is configured for this run's repository.\"\n });\n return true;\n }\n const executorId = binding.defaultExecutor ?? claimed.event.target.executorHint ?? \"echo\";\n const executor = input.executors[executorId];\n if (!executor) {\n await input.client.complete(claimed.run.id, {\n conclusion: \"needs_human\",\n summary: `No local executor is configured for '${executorId}'.`\n });\n return true;\n }\n\n const executionPath =\n executorId === \"codex\"\n ? worktreePathForRun({\n workspacePath: binding.checkoutPath,\n runId: claimed.run.id,\n ...(binding.worktreeRoot ? { worktreeRoot: binding.worktreeRoot } : {})\n })\n : binding.checkoutPath;\n\n const securityAssessment = assessRunnerSecurity({\n executorId,\n workspacePath: binding.checkoutPath,\n executionPath,\n command: claimed.event.command,\n context: claimed.event.context,\n permissions: claimed.event.permissions,\n ...(input.security ? { policy: input.security } : {})\n });\n if (securityAssessment.findings.length > 0) {\n await input.client.progress(claimed.run.id, {\n type: securityAssessment.allowed ? \"security.audit\" : \"security.blocked\",\n message: formatSecurityAssessment(securityAssessment),\n at: new Date().toISOString()\n });\n }\n if (!securityAssessment.allowed) {\n await input.client.complete(claimed.run.id, {\n conclusion: \"needs_human\",\n summary: formatSecurityAssessment(securityAssessment),\n nextAction: \"Review the request and rerun with a narrower prompt or an explicit local policy override if appropriate.\"\n });\n return true;\n }\n\n let readiness: Awaited<ReturnType<ExecutorAdapter[\"canRun\"]>>;\n try {\n readiness = await executor.canRun({\n runId: claimed.run.id,\n workspacePath: binding.checkoutPath,\n command: claimed.event.command,\n context: claimed.event.context,\n ...(claimed.run.contextPacket ? { contextPacket: claimed.run.contextPacket } : {}),\n permissions: claimed.event.permissions,\n ...(binding.baseBranch ? { baseBranch: binding.baseBranch } : {}),\n ...(binding.worktreeRoot ? { worktreeRoot: binding.worktreeRoot } : {}),\n ...(binding.keepWorktree !== undefined ? { keepWorktree: binding.keepWorktree } : {})\n });\n } catch (error) {\n await input.client.complete(claimed.run.id, failedRunResult(`${executor.displayName} readiness check`, error));\n return true;\n }\n\n if (!readiness.ready) {\n await input.client.complete(claimed.run.id, {\n conclusion: \"needs_human\",\n summary: readiness.reason ?? `${executor.displayName} is not ready`\n });\n return true;\n }\n\n await input.client.markRunning(claimed.run.id, executor.id);\n const heartbeatIntervalMs = input.heartbeatIntervalMs ?? 15_000;\n let heartbeatHandle: ReturnType<typeof setInterval> | undefined;\n if (heartbeatIntervalMs > 0) {\n heartbeatHandle = setInterval(() => {\n void input.client.heartbeat(claimed.run.id).catch((error: unknown) => {\n console.warn(`OpenTag heartbeat failed for ${claimed.run.id}:`, error);\n });\n }, heartbeatIntervalMs);\n }\n\n let executorResult: OpenTagRunResult;\n try {\n executorResult = await executor.run(\n {\n runId: claimed.run.id,\n workspacePath: binding.checkoutPath,\n command: claimed.event.command,\n context: claimed.event.context,\n ...(claimed.run.contextPacket ? { contextPacket: claimed.run.contextPacket } : {}),\n permissions: claimed.event.permissions,\n ...(binding.baseBranch ? { baseBranch: binding.baseBranch } : {}),\n ...(binding.worktreeRoot ? { worktreeRoot: binding.worktreeRoot } : {}),\n ...(binding.keepWorktree !== undefined ? { keepWorktree: binding.keepWorktree } : {})\n },\n {\n emit: async (event) => {\n console.log(`[${event.type}] ${event.message}`);\n await input.client.progress(claimed.run.id, {\n type: event.type,\n message: event.message,\n at: event.at\n });\n }\n }\n );\n } catch (error) {\n await input.client.complete(claimed.run.id, failedRunResult(executor.displayName, error));\n return true;\n } finally {\n if (heartbeatHandle) clearInterval(heartbeatHandle);\n }\n\n let result: OpenTagRunResult;\n try {\n result = await maybeCreatePullRequest({\n run: claimed.run,\n event: claimed.event,\n binding,\n result: executorResult,\n options: input.pullRequestOptions ?? {}\n });\n } catch (error) {\n result = pullRequestPreparationFailureResult(executorResult, error);\n }\n await input.client.complete(claimed.run.id, result);\n return true;\n}\n\nfunction sleep(ms: number, signal?: AbortSignal): Promise<void> {\n if (signal?.aborted) return Promise.resolve();\n\n return new Promise((resolve) => {\n const timeout = setTimeout(() => {\n signal?.removeEventListener(\"abort\", onAbort);\n resolve();\n }, ms);\n\n function onAbort() {\n clearTimeout(timeout);\n resolve();\n }\n\n signal?.addEventListener(\"abort\", onAbort, { once: true });\n });\n}\n\nexport async function serveDaemon(input: {\n runnerId: string;\n repositories: RepositoryBindingConfig[];\n executors: Record<string, ExecutorAdapter>;\n security?: RunnerSecurityPolicy;\n pullRequestOptions?: PullRequestOptions;\n heartbeatIntervalMs?: number;\n pollIntervalMs?: number;\n signal?: AbortSignal;\n client: DaemonClient;\n}): Promise<void> {\n const pollIntervalMs = input.pollIntervalMs ?? 5_000;\n while (!input.signal?.aborted) {\n try {\n const didWork = await runOneDaemonIteration({\n runnerId: input.runnerId,\n repositories: input.repositories,\n executors: input.executors,\n ...(input.security ? { security: input.security } : {}),\n ...(input.pullRequestOptions ? { pullRequestOptions: input.pullRequestOptions } : {}),\n ...(input.heartbeatIntervalMs !== undefined ? { heartbeatIntervalMs: input.heartbeatIntervalMs } : {}),\n client: input.client\n });\n if (!didWork) {\n await sleep(pollIntervalMs, input.signal);\n }\n } catch (error) {\n if (input.signal?.aborted) break;\n console.warn(\"OpenTag daemon iteration failed; retrying:\", error);\n await sleep(pollIntervalMs, input.signal);\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// src/runtime.ts
|
|
2
|
+
import { createDispatcherClient } from "@opentag/client";
|
|
3
|
+
import { createClaudeCodeExecutor, createCodexExecutor, createEchoExecutor } from "@opentag/runner";
|
|
4
|
+
function securityFromConfig(config) {
|
|
5
|
+
const security = config.security;
|
|
6
|
+
if (!security) return void 0;
|
|
7
|
+
const normalized = {};
|
|
8
|
+
if (security.mode !== void 0) normalized.mode = security.mode;
|
|
9
|
+
if (security.allowedWorkspaceRoot !== void 0) normalized.allowedWorkspaceRoot = security.allowedWorkspaceRoot;
|
|
10
|
+
if (security.allowUnsafePrompts !== void 0) normalized.allowUnsafePrompts = security.allowUnsafePrompts;
|
|
11
|
+
if (security.extraSafeEnv !== void 0) normalized.extraSafeEnv = security.extraSafeEnv;
|
|
12
|
+
return Object.keys(normalized).length > 0 ? normalized : void 0;
|
|
13
|
+
}
|
|
14
|
+
function executorsFromConfig(config) {
|
|
15
|
+
const security = securityFromConfig(config);
|
|
16
|
+
return {
|
|
17
|
+
echo: createEchoExecutor(),
|
|
18
|
+
codex: createCodexExecutor({
|
|
19
|
+
...security ? { security } : {}
|
|
20
|
+
}),
|
|
21
|
+
"claude-code": createClaudeCodeExecutor({
|
|
22
|
+
...config.claudeCode?.command ? { claudeCommand: config.claudeCode.command } : {},
|
|
23
|
+
...config.claudeCode?.model ? { model: config.claudeCode.model } : {},
|
|
24
|
+
...config.claudeCode?.permissionMode ? { permissionMode: config.claudeCode.permissionMode } : {},
|
|
25
|
+
...config.claudeCode?.dangerouslySkipPermissions !== void 0 ? { dangerouslySkipPermissions: config.claudeCode.dangerouslySkipPermissions } : {}
|
|
26
|
+
})
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
function createDaemonClient(config) {
|
|
30
|
+
return createDispatcherClient({
|
|
31
|
+
dispatcherUrl: config.dispatcherUrl,
|
|
32
|
+
runnerId: config.runnerId,
|
|
33
|
+
...config.pairingToken ? { pairingToken: config.pairingToken } : {}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function pullRequestOptionsFromConfig(config) {
|
|
37
|
+
if (!config.githubToken && config.preparePullRequestBranch === void 0 && config.allowAutoCreatePullRequest === void 0) {
|
|
38
|
+
return void 0;
|
|
39
|
+
}
|
|
40
|
+
return {
|
|
41
|
+
...config.githubToken ? { githubToken: config.githubToken } : {},
|
|
42
|
+
...config.preparePullRequestBranch !== void 0 ? { preparePullRequestBranch: config.preparePullRequestBranch } : {},
|
|
43
|
+
...config.allowAutoCreatePullRequest !== void 0 ? { allowAutoCreatePullRequest: config.allowAutoCreatePullRequest } : {}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function createDaemonRuntimeInput(config) {
|
|
47
|
+
const security = securityFromConfig(config);
|
|
48
|
+
const pullRequestOptions = pullRequestOptionsFromConfig(config);
|
|
49
|
+
return {
|
|
50
|
+
runnerId: config.runnerId,
|
|
51
|
+
repositories: config.repositories,
|
|
52
|
+
executors: executorsFromConfig(config),
|
|
53
|
+
...security ? { security } : {},
|
|
54
|
+
...pullRequestOptions ? { pullRequestOptions } : {},
|
|
55
|
+
...config.heartbeatIntervalMs ? { heartbeatIntervalMs: config.heartbeatIntervalMs } : {},
|
|
56
|
+
...config.pollIntervalMs ? { pollIntervalMs: config.pollIntervalMs } : {},
|
|
57
|
+
client: createDaemonClient(config)
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
securityFromConfig,
|
|
63
|
+
executorsFromConfig,
|
|
64
|
+
createDaemonClient,
|
|
65
|
+
pullRequestOptionsFromConfig,
|
|
66
|
+
createDaemonRuntimeInput
|
|
67
|
+
};
|
|
68
|
+
//# sourceMappingURL=chunk-CUP3SU3F.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/runtime.ts"],"sourcesContent":["import { createDispatcherClient } from \"@opentag/client\";\nimport { createClaudeCodeExecutor, createCodexExecutor, createEchoExecutor, type RunnerSecurityPolicy } from \"@opentag/runner\";\nimport type { OpenTagDaemonConfig } from \"./config.js\";\nimport type { DaemonClient } from \"./daemon.js\";\nimport type { PullRequestOptions } from \"./pr.js\";\n\nexport function securityFromConfig(config: OpenTagDaemonConfig): RunnerSecurityPolicy | undefined {\n const security = config.security;\n if (!security) return undefined;\n\n const normalized: RunnerSecurityPolicy = {};\n if (security.mode !== undefined) normalized.mode = security.mode;\n if (security.allowedWorkspaceRoot !== undefined) normalized.allowedWorkspaceRoot = security.allowedWorkspaceRoot;\n if (security.allowUnsafePrompts !== undefined) normalized.allowUnsafePrompts = security.allowUnsafePrompts;\n if (security.extraSafeEnv !== undefined) normalized.extraSafeEnv = security.extraSafeEnv;\n\n return Object.keys(normalized).length > 0 ? normalized : undefined;\n}\n\nexport function executorsFromConfig(config: OpenTagDaemonConfig) {\n const security = securityFromConfig(config);\n\n return {\n echo: createEchoExecutor(),\n codex: createCodexExecutor({\n ...(security ? { security } : {})\n }),\n \"claude-code\": createClaudeCodeExecutor({\n ...(config.claudeCode?.command ? { claudeCommand: config.claudeCode.command } : {}),\n ...(config.claudeCode?.model ? { model: config.claudeCode.model } : {}),\n ...(config.claudeCode?.permissionMode ? { permissionMode: config.claudeCode.permissionMode } : {}),\n ...(config.claudeCode?.dangerouslySkipPermissions !== undefined\n ? { dangerouslySkipPermissions: config.claudeCode.dangerouslySkipPermissions }\n : {})\n })\n };\n}\n\nexport function createDaemonClient(config: OpenTagDaemonConfig): DaemonClient {\n return createDispatcherClient({\n dispatcherUrl: config.dispatcherUrl,\n runnerId: config.runnerId,\n ...(config.pairingToken ? { pairingToken: config.pairingToken } : {})\n });\n}\n\nexport function pullRequestOptionsFromConfig(config: OpenTagDaemonConfig): PullRequestOptions | undefined {\n if (!config.githubToken && config.preparePullRequestBranch === undefined && config.allowAutoCreatePullRequest === undefined) {\n return undefined;\n }\n\n return {\n ...(config.githubToken ? { githubToken: config.githubToken } : {}),\n ...(config.preparePullRequestBranch !== undefined ? { preparePullRequestBranch: config.preparePullRequestBranch } : {}),\n ...(config.allowAutoCreatePullRequest !== undefined ? { allowAutoCreatePullRequest: config.allowAutoCreatePullRequest } : {})\n };\n}\n\nexport function createDaemonRuntimeInput(config: OpenTagDaemonConfig) {\n const security = securityFromConfig(config);\n const pullRequestOptions = pullRequestOptionsFromConfig(config);\n\n return {\n runnerId: config.runnerId,\n repositories: config.repositories,\n executors: executorsFromConfig(config),\n ...(security ? { security } : {}),\n ...(pullRequestOptions ? { pullRequestOptions } : {}),\n ...(config.heartbeatIntervalMs ? { heartbeatIntervalMs: config.heartbeatIntervalMs } : {}),\n ...(config.pollIntervalMs ? { pollIntervalMs: config.pollIntervalMs } : {}),\n client: createDaemonClient(config)\n };\n}\n"],"mappings":";AAAA,SAAS,8BAA8B;AACvC,SAAS,0BAA0B,qBAAqB,0BAAqD;AAKtG,SAAS,mBAAmB,QAA+D;AAChG,QAAM,WAAW,OAAO;AACxB,MAAI,CAAC,SAAU,QAAO;AAEtB,QAAM,aAAmC,CAAC;AAC1C,MAAI,SAAS,SAAS,OAAW,YAAW,OAAO,SAAS;AAC5D,MAAI,SAAS,yBAAyB,OAAW,YAAW,uBAAuB,SAAS;AAC5F,MAAI,SAAS,uBAAuB,OAAW,YAAW,qBAAqB,SAAS;AACxF,MAAI,SAAS,iBAAiB,OAAW,YAAW,eAAe,SAAS;AAE5E,SAAO,OAAO,KAAK,UAAU,EAAE,SAAS,IAAI,aAAa;AAC3D;AAEO,SAAS,oBAAoB,QAA6B;AAC/D,QAAM,WAAW,mBAAmB,MAAM;AAE1C,SAAO;AAAA,IACL,MAAM,mBAAmB;AAAA,IACzB,OAAO,oBAAoB;AAAA,MACzB,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,IACjC,CAAC;AAAA,IACD,eAAe,yBAAyB;AAAA,MACtC,GAAI,OAAO,YAAY,UAAU,EAAE,eAAe,OAAO,WAAW,QAAQ,IAAI,CAAC;AAAA,MACjF,GAAI,OAAO,YAAY,QAAQ,EAAE,OAAO,OAAO,WAAW,MAAM,IAAI,CAAC;AAAA,MACrE,GAAI,OAAO,YAAY,iBAAiB,EAAE,gBAAgB,OAAO,WAAW,eAAe,IAAI,CAAC;AAAA,MAChG,GAAI,OAAO,YAAY,+BAA+B,SAClD,EAAE,4BAA4B,OAAO,WAAW,2BAA2B,IAC3E,CAAC;AAAA,IACP,CAAC;AAAA,EACH;AACF;AAEO,SAAS,mBAAmB,QAA2C;AAC5E,SAAO,uBAAuB;AAAA,IAC5B,eAAe,OAAO;AAAA,IACtB,UAAU,OAAO;AAAA,IACjB,GAAI,OAAO,eAAe,EAAE,cAAc,OAAO,aAAa,IAAI,CAAC;AAAA,EACrE,CAAC;AACH;AAEO,SAAS,6BAA6B,QAA6D;AACxG,MAAI,CAAC,OAAO,eAAe,OAAO,6BAA6B,UAAa,OAAO,+BAA+B,QAAW;AAC3H,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,GAAI,OAAO,cAAc,EAAE,aAAa,OAAO,YAAY,IAAI,CAAC;AAAA,IAChE,GAAI,OAAO,6BAA6B,SAAY,EAAE,0BAA0B,OAAO,yBAAyB,IAAI,CAAC;AAAA,IACrH,GAAI,OAAO,+BAA+B,SAAY,EAAE,4BAA4B,OAAO,2BAA2B,IAAI,CAAC;AAAA,EAC7H;AACF;AAEO,SAAS,yBAAyB,QAA6B;AACpE,QAAM,WAAW,mBAAmB,MAAM;AAC1C,QAAM,qBAAqB,6BAA6B,MAAM;AAE9D,SAAO;AAAA,IACL,UAAU,OAAO;AAAA,IACjB,cAAc,OAAO;AAAA,IACrB,WAAW,oBAAoB,MAAM;AAAA,IACrC,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,IAC/B,GAAI,qBAAqB,EAAE,mBAAmB,IAAI,CAAC;AAAA,IACnD,GAAI,OAAO,sBAAsB,EAAE,qBAAqB,OAAO,oBAAoB,IAAI,CAAC;AAAA,IACxF,GAAI,OAAO,iBAAiB,EAAE,gBAAgB,OAAO,eAAe,IAAI,CAAC;AAAA,IACzE,QAAQ,mBAAmB,MAAM;AAAA,EACnC;AACF;","names":[]}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// src/dispatcher.ts
|
|
2
|
+
import { serve } from "@hono/node-server";
|
|
3
|
+
import {
|
|
4
|
+
createCompositeCallbackSink,
|
|
5
|
+
createDispatcherApp,
|
|
6
|
+
createGitHubCallbackSink,
|
|
7
|
+
createLarkCallbackSink,
|
|
8
|
+
createSlackCallbackSink,
|
|
9
|
+
createTelegramCallbackSink
|
|
10
|
+
} from "@opentag/dispatcher";
|
|
11
|
+
function parseAgentTokenMap(name, raw) {
|
|
12
|
+
if (!raw) return void 0;
|
|
13
|
+
try {
|
|
14
|
+
const parsed = JSON.parse(raw);
|
|
15
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
16
|
+
throw new Error("Value is not a JSON object");
|
|
17
|
+
}
|
|
18
|
+
const entries = Object.entries(parsed);
|
|
19
|
+
if (entries.length === 0) return void 0;
|
|
20
|
+
for (const [agentId, token] of entries) {
|
|
21
|
+
if (!agentId.trim()) {
|
|
22
|
+
throw new Error("Agent id must be a non-empty string");
|
|
23
|
+
}
|
|
24
|
+
if (typeof token !== "string" || !token.trim()) {
|
|
25
|
+
throw new Error(`Token for agent ${agentId} must be a non-empty string`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return Object.fromEntries(entries);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
throw new Error(`Failed to parse ${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function larkDomainFromEnv(value) {
|
|
34
|
+
if (value === void 0) return void 0;
|
|
35
|
+
if (value === "lark" || value === "feishu") return value;
|
|
36
|
+
throw new Error("LARK_DOMAIN must be either lark or feishu");
|
|
37
|
+
}
|
|
38
|
+
function dispatcherRuntimeInputFromEnv(env) {
|
|
39
|
+
const port = Number(env.PORT ?? "3030");
|
|
40
|
+
if (!Number.isInteger(port) || port <= 0) {
|
|
41
|
+
throw new Error(`PORT must be a positive integer, received ${env.PORT ?? "3030"}`);
|
|
42
|
+
}
|
|
43
|
+
const larkDomain = larkDomainFromEnv(env.LARK_DOMAIN);
|
|
44
|
+
if (Boolean(env.LARK_APP_ID) !== Boolean(env.LARK_APP_SECRET)) {
|
|
45
|
+
throw new Error("LARK_APP_ID and LARK_APP_SECRET must be configured together.");
|
|
46
|
+
}
|
|
47
|
+
const slackBotTokensByAgentId = parseAgentTokenMap("OPENTAG_SLACK_BOT_TOKENS_JSON", env.OPENTAG_SLACK_BOT_TOKENS_JSON);
|
|
48
|
+
const telegramBotTokensByAgentId = parseAgentTokenMap(
|
|
49
|
+
"OPENTAG_TELEGRAM_BOT_TOKENS_JSON",
|
|
50
|
+
env.OPENTAG_TELEGRAM_BOT_TOKENS_JSON
|
|
51
|
+
);
|
|
52
|
+
return {
|
|
53
|
+
port,
|
|
54
|
+
databasePath: env.OPENTAG_DATABASE_PATH ?? "opentag.db",
|
|
55
|
+
...env.OPENTAG_PAIRING_TOKEN ? { pairingToken: env.OPENTAG_PAIRING_TOKEN } : {},
|
|
56
|
+
...env.OPENTAG_GITHUB_TOKEN ? { githubToken: env.OPENTAG_GITHUB_TOKEN } : {},
|
|
57
|
+
...env.LARK_APP_ID && env.LARK_APP_SECRET ? {
|
|
58
|
+
lark: {
|
|
59
|
+
appId: env.LARK_APP_ID,
|
|
60
|
+
appSecret: env.LARK_APP_SECRET,
|
|
61
|
+
domain: larkDomain ?? "lark"
|
|
62
|
+
}
|
|
63
|
+
} : {},
|
|
64
|
+
...env.OPENTAG_SLACK_BOT_TOKEN ? { slackBotToken: env.OPENTAG_SLACK_BOT_TOKEN } : {},
|
|
65
|
+
...slackBotTokensByAgentId ? { slackBotTokensByAgentId } : {},
|
|
66
|
+
...env.OPENTAG_TELEGRAM_BOT_TOKEN ? { telegramBotToken: env.OPENTAG_TELEGRAM_BOT_TOKEN } : {},
|
|
67
|
+
...telegramBotTokensByAgentId ? { telegramBotTokensByAgentId } : {}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function startDispatcher(input) {
|
|
71
|
+
const server = serve({
|
|
72
|
+
fetch: createDispatcherApp({
|
|
73
|
+
databasePath: input.databasePath,
|
|
74
|
+
...input.pairingToken ? { pairingToken: input.pairingToken } : {},
|
|
75
|
+
...input.githubToken ? { githubApply: { token: input.githubToken } } : {},
|
|
76
|
+
callbackSink: createCompositeCallbackSink([
|
|
77
|
+
createGitHubCallbackSink({
|
|
78
|
+
...input.githubToken ? { token: input.githubToken } : {}
|
|
79
|
+
}),
|
|
80
|
+
createSlackCallbackSink({
|
|
81
|
+
...input.slackBotToken ? { botToken: input.slackBotToken } : {},
|
|
82
|
+
...input.slackBotTokensByAgentId ? { botTokensByAgentId: input.slackBotTokensByAgentId } : {}
|
|
83
|
+
}),
|
|
84
|
+
createLarkCallbackSink({
|
|
85
|
+
...input.lark ? {
|
|
86
|
+
appId: input.lark.appId,
|
|
87
|
+
appSecret: input.lark.appSecret,
|
|
88
|
+
domain: input.lark.domain
|
|
89
|
+
} : {}
|
|
90
|
+
}),
|
|
91
|
+
createTelegramCallbackSink({
|
|
92
|
+
...input.telegramBotToken ? { botToken: input.telegramBotToken } : {},
|
|
93
|
+
...input.telegramBotTokensByAgentId ? { botTokensByAgentId: input.telegramBotTokensByAgentId } : {}
|
|
94
|
+
})
|
|
95
|
+
])
|
|
96
|
+
}).fetch,
|
|
97
|
+
port: input.port
|
|
98
|
+
});
|
|
99
|
+
return {
|
|
100
|
+
url: `http://localhost:${input.port}`,
|
|
101
|
+
server,
|
|
102
|
+
close() {
|
|
103
|
+
return new Promise((resolve, reject) => {
|
|
104
|
+
server.closeIdleConnections?.();
|
|
105
|
+
server.close((error) => {
|
|
106
|
+
if (error) {
|
|
107
|
+
reject(error);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
resolve();
|
|
111
|
+
});
|
|
112
|
+
server.closeAllConnections?.();
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export {
|
|
119
|
+
dispatcherRuntimeInputFromEnv,
|
|
120
|
+
startDispatcher
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=chunk-EKMUVNR7.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/dispatcher.ts"],"sourcesContent":["import { serve } from \"@hono/node-server\";\nimport {\n createCompositeCallbackSink,\n createDispatcherApp,\n createGitHubCallbackSink,\n createLarkCallbackSink,\n createSlackCallbackSink,\n createTelegramCallbackSink\n} from \"@opentag/dispatcher\";\n\nexport type LocalDispatcherRuntimeInput = {\n port: number;\n databasePath: string;\n pairingToken?: string;\n githubToken?: string;\n lark?: {\n appId: string;\n appSecret: string;\n domain: \"lark\" | \"feishu\";\n };\n slackBotToken?: string;\n slackBotTokensByAgentId?: Record<string, string>;\n telegramBotToken?: string;\n telegramBotTokensByAgentId?: Record<string, string>;\n};\n\nexport type LocalDispatcherHandle = {\n url: string;\n server: ReturnType<typeof serve>;\n close(): Promise<void>;\n};\n\ntype ClosableServer = ReturnType<typeof serve> & {\n closeAllConnections?: () => void;\n closeIdleConnections?: () => void;\n};\n\nfunction parseAgentTokenMap(name: string, raw: string | undefined): Record<string, string> | undefined {\n if (!raw) return undefined;\n try {\n const parsed = JSON.parse(raw);\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(\"Value is not a JSON object\");\n }\n const entries = Object.entries(parsed);\n if (entries.length === 0) return undefined;\n for (const [agentId, token] of entries) {\n if (!agentId.trim()) {\n throw new Error(\"Agent id must be a non-empty string\");\n }\n if (typeof token !== \"string\" || !token.trim()) {\n throw new Error(`Token for agent ${agentId} must be a non-empty string`);\n }\n }\n return Object.fromEntries(entries) as Record<string, string>;\n } catch (error) {\n throw new Error(`Failed to parse ${name}: ${error instanceof Error ? error.message : String(error)}`);\n }\n}\n\nfunction larkDomainFromEnv(value: string | undefined): \"lark\" | \"feishu\" | undefined {\n if (value === undefined) return undefined;\n if (value === \"lark\" || value === \"feishu\") return value;\n throw new Error(\"LARK_DOMAIN must be either lark or feishu\");\n}\n\nexport function dispatcherRuntimeInputFromEnv(env: NodeJS.ProcessEnv): LocalDispatcherRuntimeInput {\n const port = Number(env.PORT ?? \"3030\");\n if (!Number.isInteger(port) || port <= 0) {\n throw new Error(`PORT must be a positive integer, received ${env.PORT ?? \"3030\"}`);\n }\n\n const larkDomain = larkDomainFromEnv(env.LARK_DOMAIN);\n if (Boolean(env.LARK_APP_ID) !== Boolean(env.LARK_APP_SECRET)) {\n throw new Error(\"LARK_APP_ID and LARK_APP_SECRET must be configured together.\");\n }\n const slackBotTokensByAgentId = parseAgentTokenMap(\"OPENTAG_SLACK_BOT_TOKENS_JSON\", env.OPENTAG_SLACK_BOT_TOKENS_JSON);\n const telegramBotTokensByAgentId = parseAgentTokenMap(\n \"OPENTAG_TELEGRAM_BOT_TOKENS_JSON\",\n env.OPENTAG_TELEGRAM_BOT_TOKENS_JSON\n );\n\n return {\n port,\n databasePath: env.OPENTAG_DATABASE_PATH ?? \"opentag.db\",\n ...(env.OPENTAG_PAIRING_TOKEN ? { pairingToken: env.OPENTAG_PAIRING_TOKEN } : {}),\n ...(env.OPENTAG_GITHUB_TOKEN ? { githubToken: env.OPENTAG_GITHUB_TOKEN } : {}),\n ...(env.LARK_APP_ID && env.LARK_APP_SECRET\n ? {\n lark: {\n appId: env.LARK_APP_ID,\n appSecret: env.LARK_APP_SECRET,\n domain: larkDomain ?? \"lark\"\n }\n }\n : {}),\n ...(env.OPENTAG_SLACK_BOT_TOKEN ? { slackBotToken: env.OPENTAG_SLACK_BOT_TOKEN } : {}),\n ...(slackBotTokensByAgentId ? { slackBotTokensByAgentId } : {}),\n ...(env.OPENTAG_TELEGRAM_BOT_TOKEN ? { telegramBotToken: env.OPENTAG_TELEGRAM_BOT_TOKEN } : {}),\n ...(telegramBotTokensByAgentId ? { telegramBotTokensByAgentId } : {})\n };\n}\n\nexport function startDispatcher(input: LocalDispatcherRuntimeInput): LocalDispatcherHandle {\n const server: ClosableServer = serve({\n fetch: createDispatcherApp({\n databasePath: input.databasePath,\n ...(input.pairingToken ? { pairingToken: input.pairingToken } : {}),\n ...(input.githubToken ? { githubApply: { token: input.githubToken } } : {}),\n callbackSink: createCompositeCallbackSink([\n createGitHubCallbackSink({\n ...(input.githubToken ? { token: input.githubToken } : {})\n }),\n createSlackCallbackSink({\n ...(input.slackBotToken ? { botToken: input.slackBotToken } : {}),\n ...(input.slackBotTokensByAgentId ? { botTokensByAgentId: input.slackBotTokensByAgentId } : {})\n }),\n createLarkCallbackSink({\n ...(input.lark\n ? {\n appId: input.lark.appId,\n appSecret: input.lark.appSecret,\n domain: input.lark.domain\n }\n : {})\n }),\n createTelegramCallbackSink({\n ...(input.telegramBotToken ? { botToken: input.telegramBotToken } : {}),\n ...(input.telegramBotTokensByAgentId ? { botTokensByAgentId: input.telegramBotTokensByAgentId } : {})\n })\n ])\n }).fetch,\n port: input.port\n });\n\n return {\n url: `http://localhost:${input.port}`,\n server,\n close() {\n return new Promise((resolve, reject) => {\n server.closeIdleConnections?.();\n server.close((error?: Error) => {\n if (error) {\n reject(error);\n return;\n }\n resolve();\n });\n server.closeAllConnections?.();\n });\n }\n };\n}\n"],"mappings":"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","names":[]}
|