@hellcoder/companion 0.96.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/bin/cli.ts +168 -0
- package/bin/ctl.ts +528 -0
- package/bin/generate-token.ts +28 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/assets/AgentsPage-DCFhrJ28.js +13 -0
- package/dist/assets/CronManager-EGwLJONv.js +1 -0
- package/dist/assets/IntegrationsPage-CTMRnbQS.js +1 -0
- package/dist/assets/LinearOAuthSettingsPage-CgQFMIgr.js +1 -0
- package/dist/assets/LinearSettingsPage-C9nok1qi.js +1 -0
- package/dist/assets/Playground-BV3k0RbV.js +109 -0
- package/dist/assets/PromptsPage-CFojqNKP.js +4 -0
- package/dist/assets/RunsPage-DUJ1QUSa.js +1 -0
- package/dist/assets/SandboxManager-CrVQ-VU_.js +8 -0
- package/dist/assets/SettingsPage-D1fPCL19.js +1 -0
- package/dist/assets/TailscalePage-D06cyvyC.js +1 -0
- package/dist/assets/index-BhUa1e6X.css +1 -0
- package/dist/assets/index-DkqeP-R9.js +134 -0
- package/dist/assets/sw-register-BibwRdvC.js +1 -0
- package/dist/assets/workbox-window.prod.es5-BIl4cyR9.js +2 -0
- package/dist/favicon.svg +8 -0
- package/dist/fonts/MesloLGSNerdFontMono-Bold.woff2 +0 -0
- package/dist/fonts/MesloLGSNerdFontMono-Regular.woff2 +0 -0
- package/dist/icon-192.png +0 -0
- package/dist/icon-512.png +0 -0
- package/dist/index.html +20 -0
- package/dist/logo-codex.svg +14 -0
- package/dist/logo-docker.svg +4 -0
- package/dist/logo.svg +14 -0
- package/dist/manifest.json +24 -0
- package/dist/sw.js +2 -0
- package/package.json +104 -0
- package/server/agent-cron-migrator.test.ts +610 -0
- package/server/agent-cron-migrator.ts +85 -0
- package/server/agent-executor.test.ts +1108 -0
- package/server/agent-executor.ts +346 -0
- package/server/agent-store.test.ts +588 -0
- package/server/agent-store.ts +185 -0
- package/server/agent-types.ts +138 -0
- package/server/ai-validation-settings.test.ts +128 -0
- package/server/ai-validation-settings.ts +35 -0
- package/server/ai-validator.test.ts +387 -0
- package/server/ai-validator.ts +271 -0
- package/server/auth-manager.test.ts +83 -0
- package/server/auth-manager.ts +150 -0
- package/server/auto-namer.test.ts +252 -0
- package/server/auto-namer.ts +78 -0
- package/server/backend-adapter.test.ts +38 -0
- package/server/backend-adapter.ts +54 -0
- package/server/cache-headers.test.ts +98 -0
- package/server/cache-headers.ts +61 -0
- package/server/claude-adapter.test.ts +1363 -0
- package/server/claude-adapter.ts +889 -0
- package/server/claude-container-auth.test.ts +44 -0
- package/server/claude-container-auth.ts +30 -0
- package/server/claude-protocol-contract.test.ts +71 -0
- package/server/claude-protocol-drift.test.ts +78 -0
- package/server/claude-session-discovery.test.ts +132 -0
- package/server/claude-session-discovery.ts +157 -0
- package/server/claude-session-history.test.ts +158 -0
- package/server/claude-session-history.ts +410 -0
- package/server/cli-launcher.test.ts +1343 -0
- package/server/cli-launcher.ts +1298 -0
- package/server/cli.test.ts +16 -0
- package/server/codex-adapter.test.ts +5545 -0
- package/server/codex-adapter.ts +3062 -0
- package/server/codex-container-auth.test.ts +50 -0
- package/server/codex-container-auth.ts +24 -0
- package/server/codex-home.test.ts +61 -0
- package/server/codex-home.ts +26 -0
- package/server/codex-protocol-contract.test.ts +96 -0
- package/server/codex-protocol-drift.test.ts +123 -0
- package/server/codex-ws-proxy.cjs +226 -0
- package/server/commands-discovery.test.ts +179 -0
- package/server/commands-discovery.ts +81 -0
- package/server/constants.ts +7 -0
- package/server/container-manager.test.ts +1211 -0
- package/server/container-manager.ts +1053 -0
- package/server/cron-scheduler.test.ts +957 -0
- package/server/cron-scheduler.ts +243 -0
- package/server/cron-store.test.ts +422 -0
- package/server/cron-store.ts +148 -0
- package/server/cron-types.ts +63 -0
- package/server/env-manager.test.ts +268 -0
- package/server/env-manager.ts +161 -0
- package/server/event-bus-types.ts +64 -0
- package/server/event-bus.test.ts +244 -0
- package/server/event-bus.ts +124 -0
- package/server/execution-store.test.ts +307 -0
- package/server/execution-store.ts +170 -0
- package/server/fs-utils.ts +15 -0
- package/server/git-utils.test.ts +938 -0
- package/server/git-utils.ts +421 -0
- package/server/github-pr.test.ts +498 -0
- package/server/github-pr.ts +379 -0
- package/server/image-pull-manager.test.ts +303 -0
- package/server/image-pull-manager.ts +279 -0
- package/server/index.ts +396 -0
- package/server/linear-agent-bridge.test.ts +1157 -0
- package/server/linear-agent-bridge.ts +629 -0
- package/server/linear-agent.test.ts +473 -0
- package/server/linear-agent.ts +479 -0
- package/server/linear-cache.test.ts +136 -0
- package/server/linear-cache.ts +113 -0
- package/server/linear-connections.test.ts +350 -0
- package/server/linear-connections.ts +231 -0
- package/server/linear-credential-migration.test.ts +337 -0
- package/server/linear-credential-migration.ts +63 -0
- package/server/linear-oauth-connections-migration.test.ts +268 -0
- package/server/linear-oauth-connections.test.ts +365 -0
- package/server/linear-oauth-connections.ts +294 -0
- package/server/linear-project-manager.test.ts +162 -0
- package/server/linear-project-manager.ts +111 -0
- package/server/linear-prompt-builder.test.ts +74 -0
- package/server/linear-prompt-builder.ts +61 -0
- package/server/linear-staging.test.ts +276 -0
- package/server/linear-staging.ts +142 -0
- package/server/logger.test.ts +393 -0
- package/server/logger.ts +259 -0
- package/server/metrics-collector.test.ts +413 -0
- package/server/metrics-collector.ts +350 -0
- package/server/metrics-types.ts +108 -0
- package/server/middleware/managed-auth.test.ts +264 -0
- package/server/middleware/managed-auth.ts +195 -0
- package/server/novnc-proxy.test.ts +333 -0
- package/server/novnc-proxy.ts +99 -0
- package/server/path-resolver.test.ts +552 -0
- package/server/path-resolver.ts +186 -0
- package/server/paths.test.ts +31 -0
- package/server/paths.ts +11 -0
- package/server/pr-poller.test.ts +191 -0
- package/server/pr-poller.ts +162 -0
- package/server/prompt-manager.test.ts +211 -0
- package/server/prompt-manager.ts +211 -0
- package/server/protocol/claude-upstream/README.md +19 -0
- package/server/protocol/claude-upstream/sdk.d.ts.txt +1943 -0
- package/server/protocol/codex-upstream/ClientNotification.ts.txt +5 -0
- package/server/protocol/codex-upstream/ClientRequest.ts.txt +60 -0
- package/server/protocol/codex-upstream/README.md +18 -0
- package/server/protocol/codex-upstream/ServerNotification.ts.txt +41 -0
- package/server/protocol/codex-upstream/ServerRequest.ts.txt +16 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallParams.ts.txt +6 -0
- package/server/protocol/codex-upstream/v2/DynamicToolCallResponse.ts.txt +6 -0
- package/server/protocol-monitor.ts +50 -0
- package/server/recorder.test.ts +454 -0
- package/server/recorder.ts +374 -0
- package/server/recording-hub/compat-validator.test.ts +150 -0
- package/server/recording-hub/compat-validator.ts +284 -0
- package/server/recording-hub/diagnostics.test.ts +140 -0
- package/server/recording-hub/diagnostics.ts +299 -0
- package/server/recording-hub/hub-config.test.ts +44 -0
- package/server/recording-hub/hub-config.ts +19 -0
- package/server/recording-hub/hub-routes.test.ts +417 -0
- package/server/recording-hub/hub-routes.ts +236 -0
- package/server/recording-hub/hub-store.test.ts +262 -0
- package/server/recording-hub/hub-store.ts +265 -0
- package/server/recording-hub/replay-adapter.test.ts +294 -0
- package/server/recording-hub/replay-adapter.ts +207 -0
- package/server/relay-client.test.ts +337 -0
- package/server/relay-client.ts +320 -0
- package/server/replay.test.ts +200 -0
- package/server/replay.ts +78 -0
- package/server/routes/agent-routes.test.ts +1400 -0
- package/server/routes/agent-routes.ts +409 -0
- package/server/routes/cron-routes.test.ts +881 -0
- package/server/routes/cron-routes.ts +103 -0
- package/server/routes/env-routes.test.ts +383 -0
- package/server/routes/env-routes.ts +95 -0
- package/server/routes/fs-routes.test.ts +1198 -0
- package/server/routes/fs-routes.ts +605 -0
- package/server/routes/git-routes.test.ts +813 -0
- package/server/routes/git-routes.ts +97 -0
- package/server/routes/linear-agent-routes.test.ts +721 -0
- package/server/routes/linear-agent-routes.ts +304 -0
- package/server/routes/linear-connection-routes.test.ts +927 -0
- package/server/routes/linear-connection-routes.ts +244 -0
- package/server/routes/linear-oauth-connection-routes.test.ts +406 -0
- package/server/routes/linear-oauth-connection-routes.ts +129 -0
- package/server/routes/linear-routes.test.ts +1510 -0
- package/server/routes/linear-routes.ts +953 -0
- package/server/routes/metrics-routes.test.ts +103 -0
- package/server/routes/metrics-routes.ts +13 -0
- package/server/routes/prompt-routes.ts +67 -0
- package/server/routes/sandbox-routes.test.ts +513 -0
- package/server/routes/sandbox-routes.ts +127 -0
- package/server/routes/settings-routes.ts +270 -0
- package/server/routes/skills-routes.test.ts +690 -0
- package/server/routes/skills-routes.ts +100 -0
- package/server/routes/system-routes.test.ts +637 -0
- package/server/routes/system-routes.ts +228 -0
- package/server/routes/tailscale-routes.test.ts +176 -0
- package/server/routes/tailscale-routes.ts +22 -0
- package/server/routes.test.ts +4655 -0
- package/server/routes.ts +1277 -0
- package/server/sandbox-manager.test.ts +378 -0
- package/server/sandbox-manager.ts +168 -0
- package/server/service.test.ts +1419 -0
- package/server/service.ts +718 -0
- package/server/session-creation-service.test.ts +661 -0
- package/server/session-creation-service.ts +473 -0
- package/server/session-git-info.ts +104 -0
- package/server/session-linear-issues.test.ts +118 -0
- package/server/session-linear-issues.ts +88 -0
- package/server/session-names.test.ts +94 -0
- package/server/session-names.ts +67 -0
- package/server/session-orchestrator.test.ts +1784 -0
- package/server/session-orchestrator.ts +973 -0
- package/server/session-state-machine.test.ts +606 -0
- package/server/session-state-machine.ts +207 -0
- package/server/session-store.test.ts +290 -0
- package/server/session-store.ts +146 -0
- package/server/session-types.ts +509 -0
- package/server/settings-manager.test.ts +275 -0
- package/server/settings-manager.ts +173 -0
- package/server/tailscale-manager.test.ts +553 -0
- package/server/tailscale-manager.ts +451 -0
- package/server/terminal-manager.ts +240 -0
- package/server/update-checker.test.ts +306 -0
- package/server/update-checker.ts +197 -0
- package/server/usage-limits.test.ts +536 -0
- package/server/usage-limits.ts +225 -0
- package/server/worktree-tracker.test.ts +243 -0
- package/server/worktree-tracker.ts +84 -0
- package/server/ws-auth.test.ts +59 -0
- package/server/ws-auth.ts +41 -0
- package/server/ws-bridge-browser-ingest.test.ts +272 -0
- package/server/ws-bridge-browser-ingest.ts +72 -0
- package/server/ws-bridge-browser.ts +112 -0
- package/server/ws-bridge-cli-ingest.test.ts +302 -0
- package/server/ws-bridge-cli-ingest.ts +81 -0
- package/server/ws-bridge-codex.test.ts +1837 -0
- package/server/ws-bridge-codex.ts +266 -0
- package/server/ws-bridge-controls.test.ts +124 -0
- package/server/ws-bridge-controls.ts +20 -0
- package/server/ws-bridge-persist.test.ts +296 -0
- package/server/ws-bridge-persist.ts +66 -0
- package/server/ws-bridge-publish.test.ts +234 -0
- package/server/ws-bridge-publish.ts +79 -0
- package/server/ws-bridge-replay.test.ts +44 -0
- package/server/ws-bridge-replay.ts +61 -0
- package/server/ws-bridge-types.ts +106 -0
- package/server/ws-bridge.test.ts +4777 -0
- package/server/ws-bridge.ts +1279 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import type { Hono } from "hono";
|
|
3
|
+
import * as agentStore from "../agent-store.js";
|
|
4
|
+
import type { AgentExecutor } from "../agent-executor.js";
|
|
5
|
+
import type { AgentConfig, AgentConfigCreateInput, AgentConfigExport } from "../agent-types.js";
|
|
6
|
+
import { getSettings, updateSettings } from "../settings-manager.js";
|
|
7
|
+
import * as staging from "../linear-staging.js";
|
|
8
|
+
import { getOAuthConnection, createOAuthConnection } from "../linear-oauth-connections.js";
|
|
9
|
+
|
|
10
|
+
/** Fields the user can set when creating/updating an agent */
|
|
11
|
+
const EDITABLE_FIELDS = [
|
|
12
|
+
"name", "description", "icon", "version",
|
|
13
|
+
"backendType", "model", "permissionMode", "cwd",
|
|
14
|
+
"envSlug", "env", "allowedTools", "codexInternetAccess",
|
|
15
|
+
"prompt", "mcpServers", "skills",
|
|
16
|
+
"container", "branch", "createBranch", "useWorktree",
|
|
17
|
+
"triggers", "enabled",
|
|
18
|
+
] as const;
|
|
19
|
+
|
|
20
|
+
function pickEditable(body: Record<string, unknown>): Partial<AgentConfig> {
|
|
21
|
+
const result: Record<string, unknown> = {};
|
|
22
|
+
for (const key of EDITABLE_FIELDS) {
|
|
23
|
+
if (key in body) result[key] = body[key];
|
|
24
|
+
}
|
|
25
|
+
return result as Partial<AgentConfig>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function buildCreateInput(
|
|
29
|
+
body: Record<string, unknown>,
|
|
30
|
+
overrides?: Partial<Pick<AgentConfigCreateInput, "enabled" | "version">>,
|
|
31
|
+
): AgentConfigCreateInput {
|
|
32
|
+
return {
|
|
33
|
+
version: overrides?.version ?? 1,
|
|
34
|
+
name: (body.name as string | undefined) || "",
|
|
35
|
+
description: (body.description as string | undefined) || "",
|
|
36
|
+
icon: body.icon as string | undefined,
|
|
37
|
+
backendType: (body.backendType as AgentConfig["backendType"] | undefined) || "claude",
|
|
38
|
+
model: (body.model as string | undefined) || "",
|
|
39
|
+
permissionMode: (body.permissionMode as string | undefined) || "bypassPermissions",
|
|
40
|
+
cwd: (body.cwd as string | undefined) || "",
|
|
41
|
+
envSlug: body.envSlug as string | undefined,
|
|
42
|
+
env: body.env as Record<string, string> | undefined,
|
|
43
|
+
allowedTools: body.allowedTools as string[] | undefined,
|
|
44
|
+
codexInternetAccess: body.codexInternetAccess as boolean | undefined,
|
|
45
|
+
prompt: (body.prompt as string | undefined) || "",
|
|
46
|
+
mcpServers: body.mcpServers as AgentConfig["mcpServers"] | undefined,
|
|
47
|
+
skills: body.skills as string[] | undefined,
|
|
48
|
+
container: body.container as AgentConfig["container"] | undefined,
|
|
49
|
+
branch: body.branch as string | undefined,
|
|
50
|
+
createBranch: body.createBranch as boolean | undefined,
|
|
51
|
+
useWorktree: body.useWorktree as boolean | undefined,
|
|
52
|
+
triggers: body.triggers as AgentConfig["triggers"] | undefined,
|
|
53
|
+
enabled: overrides?.enabled ?? ((body.enabled as boolean | undefined) ?? true),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Strip sensitive Linear OAuth credentials before sending to the browser */
|
|
58
|
+
function sanitizeAgent(agent: AgentConfig & { nextRunAt?: number | null }): Record<string, unknown> {
|
|
59
|
+
if (!agent.triggers?.linear) return agent as unknown as Record<string, unknown>;
|
|
60
|
+
const { oauthClientSecret, webhookSecret, accessToken, refreshToken, ...safeLinear } = agent.triggers.linear;
|
|
61
|
+
|
|
62
|
+
// Resolve connection info for display and flag derivation
|
|
63
|
+
const conn = safeLinear.oauthConnectionId
|
|
64
|
+
? getOAuthConnection(safeLinear.oauthConnectionId)
|
|
65
|
+
: null;
|
|
66
|
+
const oauthConnectionName = conn?.name;
|
|
67
|
+
const oauthConnectionStatus = conn?.status;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
...agent,
|
|
71
|
+
triggers: {
|
|
72
|
+
...agent.triggers,
|
|
73
|
+
linear: {
|
|
74
|
+
...safeLinear,
|
|
75
|
+
hasAccessToken: !!(accessToken || oauthConnectionStatus === "connected"),
|
|
76
|
+
hasClientSecret: !!(oauthClientSecret || conn?.oauthClientSecret),
|
|
77
|
+
hasWebhookSecret: !!(webhookSecret || conn?.webhookSecret),
|
|
78
|
+
oauthConnectionName,
|
|
79
|
+
oauthConnectionStatus,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
} as unknown as Record<string, unknown>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Strip internal tracking fields to produce a portable export */
|
|
86
|
+
function toExport(agent: AgentConfig): AgentConfigExport {
|
|
87
|
+
const {
|
|
88
|
+
id: _id,
|
|
89
|
+
createdAt: _ca,
|
|
90
|
+
updatedAt: _ua,
|
|
91
|
+
totalRuns: _tr,
|
|
92
|
+
consecutiveFailures: _cf,
|
|
93
|
+
lastRunAt: _lr,
|
|
94
|
+
lastSessionId: _ls,
|
|
95
|
+
enabled: _en,
|
|
96
|
+
...exportable
|
|
97
|
+
} = agent;
|
|
98
|
+
// Strip Linear OAuth credentials from export (keep oauthConnectionId for reference)
|
|
99
|
+
if (exportable.triggers?.linear) {
|
|
100
|
+
const { oauthClientId, oauthClientSecret, webhookSecret, accessToken, refreshToken, ...safeLinear } = exportable.triggers.linear;
|
|
101
|
+
exportable.triggers = { ...exportable.triggers, linear: safeLinear };
|
|
102
|
+
}
|
|
103
|
+
return exportable;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function registerAgentRoutes(
|
|
107
|
+
api: Hono,
|
|
108
|
+
agentExecutor?: AgentExecutor,
|
|
109
|
+
): void {
|
|
110
|
+
// ── CRUD ────────────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
api.get("/agents", (c) => {
|
|
113
|
+
const agents = agentStore.listAgents();
|
|
114
|
+
const enriched = agents.map((a) => sanitizeAgent({
|
|
115
|
+
...a,
|
|
116
|
+
nextRunAt: agentExecutor?.getNextRunTime(a.id)?.getTime() ?? null,
|
|
117
|
+
}));
|
|
118
|
+
return c.json(enriched);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
api.get("/agents/:id", (c) => {
|
|
122
|
+
const agent = agentStore.getAgent(c.req.param("id"));
|
|
123
|
+
if (!agent) return c.json({ error: "Agent not found" }, 404);
|
|
124
|
+
return c.json(sanitizeAgent({
|
|
125
|
+
...agent,
|
|
126
|
+
nextRunAt: agentExecutor?.getNextRunTime(agent.id)?.getTime() ?? null,
|
|
127
|
+
}));
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
api.post("/agents", async (c) => {
|
|
131
|
+
const body = await c.req.json().catch(() => ({}));
|
|
132
|
+
try {
|
|
133
|
+
const agent = agentStore.createAgent(buildCreateInput(body));
|
|
134
|
+
|
|
135
|
+
// If this is a Linear agent, resolve credentials:
|
|
136
|
+
// New model: oauthConnectionId already set in triggers.linear
|
|
137
|
+
// Legacy model: resolve from staging/clone/global
|
|
138
|
+
if (agent.triggers?.linear?.enabled) {
|
|
139
|
+
// New model: oauthConnectionId passed directly — nothing more to do
|
|
140
|
+
if (agent.triggers.linear.oauthConnectionId) {
|
|
141
|
+
// Already stored via triggers, just proceed
|
|
142
|
+
} else if (!agent.triggers.linear.oauthClientId) {
|
|
143
|
+
// Legacy model: resolve credentials from staging/clone/global
|
|
144
|
+
let linearCreds: {
|
|
145
|
+
oauthClientId: string;
|
|
146
|
+
oauthClientSecret: string;
|
|
147
|
+
webhookSecret: string;
|
|
148
|
+
accessToken: string;
|
|
149
|
+
refreshToken: string;
|
|
150
|
+
} | null = null;
|
|
151
|
+
|
|
152
|
+
// Priority 1: staging slot → create OAuth connection from it
|
|
153
|
+
if (body.stagingId) {
|
|
154
|
+
const slot = staging.consumeSlot(body.stagingId);
|
|
155
|
+
if (slot?.clientId) {
|
|
156
|
+
// Create a new OAuth connection from the staging slot
|
|
157
|
+
const conn = createOAuthConnection({
|
|
158
|
+
name: `${agent.name} OAuth App`,
|
|
159
|
+
oauthClientId: slot.clientId,
|
|
160
|
+
oauthClientSecret: slot.clientSecret,
|
|
161
|
+
webhookSecret: slot.webhookSecret,
|
|
162
|
+
accessToken: slot.accessToken,
|
|
163
|
+
refreshToken: slot.refreshToken,
|
|
164
|
+
});
|
|
165
|
+
const updated = agentStore.updateAgent(agent.id, {
|
|
166
|
+
triggers: {
|
|
167
|
+
...agent.triggers,
|
|
168
|
+
linear: {
|
|
169
|
+
...agent.triggers.linear,
|
|
170
|
+
oauthConnectionId: conn.id,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
if (updated) {
|
|
175
|
+
if (updated.enabled && updated.triggers?.schedule?.enabled) {
|
|
176
|
+
agentExecutor?.scheduleAgent(updated);
|
|
177
|
+
}
|
|
178
|
+
return c.json(sanitizeAgent({ ...updated, nextRunAt: null }), 201);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Priority 2: clone from existing agent
|
|
184
|
+
if (!linearCreds && body.cloneFromAgentId) {
|
|
185
|
+
const source = agentStore.getAgent(body.cloneFromAgentId);
|
|
186
|
+
// Prefer cloning the oauthConnectionId reference
|
|
187
|
+
if (source?.triggers?.linear?.oauthConnectionId) {
|
|
188
|
+
const updated = agentStore.updateAgent(agent.id, {
|
|
189
|
+
triggers: {
|
|
190
|
+
...agent.triggers,
|
|
191
|
+
linear: {
|
|
192
|
+
...agent.triggers.linear,
|
|
193
|
+
oauthConnectionId: source.triggers.linear.oauthConnectionId,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
});
|
|
197
|
+
if (updated) {
|
|
198
|
+
if (updated.enabled && updated.triggers?.schedule?.enabled) {
|
|
199
|
+
agentExecutor?.scheduleAgent(updated);
|
|
200
|
+
}
|
|
201
|
+
return c.json(sanitizeAgent({ ...updated, nextRunAt: null }), 201);
|
|
202
|
+
}
|
|
203
|
+
} else if (source?.triggers?.linear?.oauthClientId) {
|
|
204
|
+
linearCreds = {
|
|
205
|
+
oauthClientId: source.triggers.linear.oauthClientId,
|
|
206
|
+
oauthClientSecret: source.triggers.linear.oauthClientSecret || "",
|
|
207
|
+
webhookSecret: source.triggers.linear.webhookSecret || "",
|
|
208
|
+
accessToken: source.triggers.linear.accessToken || "",
|
|
209
|
+
refreshToken: source.triggers.linear.refreshToken || "",
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Priority 3: global staging (backward compat)
|
|
215
|
+
if (!linearCreds) {
|
|
216
|
+
const settings = getSettings();
|
|
217
|
+
if (settings.linearOAuthClientId) {
|
|
218
|
+
linearCreds = {
|
|
219
|
+
oauthClientId: settings.linearOAuthClientId,
|
|
220
|
+
oauthClientSecret: settings.linearOAuthClientSecret,
|
|
221
|
+
webhookSecret: settings.linearOAuthWebhookSecret,
|
|
222
|
+
accessToken: settings.linearOAuthAccessToken,
|
|
223
|
+
refreshToken: settings.linearOAuthRefreshToken,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (linearCreds) {
|
|
229
|
+
const updated = agentStore.updateAgent(agent.id, {
|
|
230
|
+
triggers: {
|
|
231
|
+
...agent.triggers,
|
|
232
|
+
linear: {
|
|
233
|
+
...agent.triggers.linear,
|
|
234
|
+
...linearCreds,
|
|
235
|
+
},
|
|
236
|
+
},
|
|
237
|
+
});
|
|
238
|
+
if (updated) {
|
|
239
|
+
// Clear global staging if we used it (no stagingId and no clone source)
|
|
240
|
+
if (!body.stagingId && !body.cloneFromAgentId) {
|
|
241
|
+
updateSettings({
|
|
242
|
+
linearOAuthClientId: "",
|
|
243
|
+
linearOAuthClientSecret: "",
|
|
244
|
+
linearOAuthWebhookSecret: "",
|
|
245
|
+
linearOAuthAccessToken: "",
|
|
246
|
+
linearOAuthRefreshToken: "",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
if (updated.enabled && updated.triggers?.schedule?.enabled) {
|
|
250
|
+
agentExecutor?.scheduleAgent(updated);
|
|
251
|
+
}
|
|
252
|
+
return c.json(sanitizeAgent({ ...updated, nextRunAt: null }), 201);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (agent.enabled && agent.triggers?.schedule?.enabled) {
|
|
259
|
+
agentExecutor?.scheduleAgent(agent);
|
|
260
|
+
}
|
|
261
|
+
return c.json(sanitizeAgent({ ...agent, nextRunAt: null }), 201);
|
|
262
|
+
} catch (e: unknown) {
|
|
263
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
api.put("/agents/:id", async (c) => {
|
|
268
|
+
const id = c.req.param("id");
|
|
269
|
+
const body = await c.req.json().catch(() => ({}));
|
|
270
|
+
try {
|
|
271
|
+
const allowed = pickEditable(body);
|
|
272
|
+
const agent = agentStore.updateAgent(id, allowed);
|
|
273
|
+
if (!agent) return c.json({ error: "Agent not found" }, 404);
|
|
274
|
+
// Stop old timer (id may differ after a rename)
|
|
275
|
+
if (agent.id !== id) {
|
|
276
|
+
agentExecutor?.stopAgent(id);
|
|
277
|
+
}
|
|
278
|
+
// Reschedule if enabled
|
|
279
|
+
if (agent.enabled && agent.triggers?.schedule?.enabled) {
|
|
280
|
+
agentExecutor?.scheduleAgent(agent);
|
|
281
|
+
} else {
|
|
282
|
+
agentExecutor?.stopAgent(agent.id);
|
|
283
|
+
}
|
|
284
|
+
return c.json(sanitizeAgent({ ...agent, nextRunAt: agentExecutor?.getNextRunTime(agent.id)?.getTime() ?? null }));
|
|
285
|
+
} catch (e: unknown) {
|
|
286
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
api.delete("/agents/:id", (c) => {
|
|
291
|
+
const id = c.req.param("id");
|
|
292
|
+
agentExecutor?.stopAgent(id);
|
|
293
|
+
const deleted = agentStore.deleteAgent(id);
|
|
294
|
+
if (!deleted) return c.json({ error: "Agent not found" }, 404);
|
|
295
|
+
return c.json({ ok: true });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
// ── Toggle ──────────────────────────────────────────────────────────────
|
|
299
|
+
|
|
300
|
+
api.post("/agents/:id/toggle", (c) => {
|
|
301
|
+
const id = c.req.param("id");
|
|
302
|
+
const agent = agentStore.getAgent(id);
|
|
303
|
+
if (!agent) return c.json({ error: "Agent not found" }, 404);
|
|
304
|
+
const updated = agentStore.updateAgent(id, { enabled: !agent.enabled });
|
|
305
|
+
if (updated?.enabled && updated.triggers?.schedule?.enabled) {
|
|
306
|
+
agentExecutor?.scheduleAgent(updated);
|
|
307
|
+
} else if (updated) {
|
|
308
|
+
agentExecutor?.stopAgent(updated.id);
|
|
309
|
+
}
|
|
310
|
+
return c.json(updated ? sanitizeAgent({ ...updated, nextRunAt: agentExecutor?.getNextRunTime(updated.id)?.getTime() ?? null }) : updated);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
// ── Run (manual trigger) ───────────────────────────────────────────────
|
|
314
|
+
|
|
315
|
+
api.post("/agents/:id/run", async (c) => {
|
|
316
|
+
const id = c.req.param("id");
|
|
317
|
+
const agent = agentStore.getAgent(id);
|
|
318
|
+
if (!agent) return c.json({ error: "Agent not found" }, 404);
|
|
319
|
+
const body = await c.req.json().catch(() => ({}));
|
|
320
|
+
const input = typeof body.input === "string" ? body.input : undefined;
|
|
321
|
+
agentExecutor?.executeAgentManually(id, input);
|
|
322
|
+
return c.json({ ok: true, message: "Agent triggered" });
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// ── Executions ─────────────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
api.get("/agents/:id/executions", (c) => {
|
|
328
|
+
const id = c.req.param("id");
|
|
329
|
+
return c.json(agentExecutor?.getExecutions(id) ?? []);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
/** List executions across all agents with filtering and pagination (for Runs view). */
|
|
333
|
+
api.get("/executions", (c) => {
|
|
334
|
+
const agentId = c.req.query("agentId");
|
|
335
|
+
const triggerType = c.req.query("triggerType");
|
|
336
|
+
const rawStatus = c.req.query("status");
|
|
337
|
+
const status = (rawStatus === "running" || rawStatus === "success" || rawStatus === "error")
|
|
338
|
+
? rawStatus : undefined;
|
|
339
|
+
const limit = Math.min(Math.max(Number(c.req.query("limit")) || 50, 1), 500);
|
|
340
|
+
const offset = Math.max(Number(c.req.query("offset")) || 0, 0);
|
|
341
|
+
return c.json(agentExecutor?.listAllExecutions({ agentId, triggerType, status, limit, offset }) ?? { executions: [], total: 0 });
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
// ── Import / Export ────────────────────────────────────────────────────
|
|
345
|
+
|
|
346
|
+
api.post("/agents/import", async (c) => {
|
|
347
|
+
const body = await c.req.json().catch(() => ({}));
|
|
348
|
+
try {
|
|
349
|
+
// Accept an exported agent JSON and create a new agent from it
|
|
350
|
+
const agent = agentStore.createAgent(buildCreateInput(body, {
|
|
351
|
+
version: (body.version as AgentConfigCreateInput["version"] | undefined) || 1,
|
|
352
|
+
enabled: false, // Imported agents start disabled for safety
|
|
353
|
+
}));
|
|
354
|
+
return c.json(sanitizeAgent({ ...agent, nextRunAt: null }), 201);
|
|
355
|
+
} catch (e: unknown) {
|
|
356
|
+
return c.json({ error: e instanceof Error ? e.message : String(e) }, 400);
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
api.get("/agents/:id/export", (c) => {
|
|
361
|
+
const agent = agentStore.getAgent(c.req.param("id"));
|
|
362
|
+
if (!agent) return c.json({ error: "Agent not found" }, 404);
|
|
363
|
+
return c.json(toExport(agent));
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
// ── Webhook Secret ─────────────────────────────────────────────────────
|
|
367
|
+
|
|
368
|
+
api.post("/agents/:id/regenerate-secret", (c) => {
|
|
369
|
+
const id = c.req.param("id");
|
|
370
|
+
const agent = agentStore.regenerateWebhookSecret(id);
|
|
371
|
+
if (!agent) return c.json({ error: "Agent not found" }, 404);
|
|
372
|
+
return c.json(sanitizeAgent({ ...agent, nextRunAt: agentExecutor?.getNextRunTime(agent.id)?.getTime() ?? null }));
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// ── Webhook Trigger ────────────────────────────────────────────────────
|
|
376
|
+
|
|
377
|
+
api.post("/agents/:id/webhook/:secret", async (c) => {
|
|
378
|
+
const id = c.req.param("id");
|
|
379
|
+
const secret = c.req.param("secret");
|
|
380
|
+
|
|
381
|
+
const agent = agentStore.getAgent(id);
|
|
382
|
+
if (!agent) return c.json({ error: "Agent not found" }, 404);
|
|
383
|
+
|
|
384
|
+
// Validate webhook is enabled and secret matches
|
|
385
|
+
if (!agent.triggers?.webhook?.enabled) {
|
|
386
|
+
return c.json({ error: "Webhook not enabled for this agent" }, 403);
|
|
387
|
+
}
|
|
388
|
+
// Use constant-time comparison to prevent timing attacks
|
|
389
|
+
const expected = Buffer.from(agent.triggers.webhook.secret);
|
|
390
|
+
const received = Buffer.from(secret);
|
|
391
|
+
if (expected.length !== received.length || !crypto.timingSafeEqual(expected, received)) {
|
|
392
|
+
return c.json({ error: "Invalid webhook secret" }, 401);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Extract input from body — accept JSON { input: "..." } or plain text
|
|
396
|
+
let input: string | undefined;
|
|
397
|
+
const contentType = c.req.header("content-type") || "";
|
|
398
|
+
if (contentType.includes("application/json")) {
|
|
399
|
+
const body = await c.req.json().catch(() => ({}));
|
|
400
|
+
input = typeof body.input === "string" ? body.input : undefined;
|
|
401
|
+
} else {
|
|
402
|
+
const text = await c.req.text().catch(() => "");
|
|
403
|
+
if (text.trim()) input = text.trim();
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
agentExecutor?.executeAgentManually(id, input);
|
|
407
|
+
return c.json({ ok: true, message: "Agent triggered via webhook" });
|
|
408
|
+
});
|
|
409
|
+
}
|