@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,185 @@
|
|
|
1
|
+
import {
|
|
2
|
+
mkdirSync,
|
|
3
|
+
readdirSync,
|
|
4
|
+
readFileSync,
|
|
5
|
+
writeFileSync,
|
|
6
|
+
unlinkSync,
|
|
7
|
+
existsSync,
|
|
8
|
+
} from "node:fs";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { COMPANION_HOME } from "./paths.js";
|
|
11
|
+
import { randomBytes } from "node:crypto";
|
|
12
|
+
import type { AgentConfig, AgentConfigCreateInput } from "./agent-types.js";
|
|
13
|
+
|
|
14
|
+
// ─── Paths ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
const AGENTS_DIR = join(COMPANION_HOME, "agents");
|
|
17
|
+
|
|
18
|
+
function ensureDir(): void {
|
|
19
|
+
mkdirSync(AGENTS_DIR, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function filePath(id: string): string {
|
|
23
|
+
return join(AGENTS_DIR, `${id}.json`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
function slugify(name: string): string {
|
|
29
|
+
return name
|
|
30
|
+
.toLowerCase()
|
|
31
|
+
.replace(/\s+/g, "-")
|
|
32
|
+
.replace(/[^a-z0-9-]/g, "")
|
|
33
|
+
.replace(/-+/g, "-")
|
|
34
|
+
.replace(/^-|-$/g, "");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function generateWebhookSecret(): string {
|
|
38
|
+
return randomBytes(24).toString("hex");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Strip the legacy `triggers.chat` block from agents loaded from disk.
|
|
43
|
+
* The Chat SDK was removed but agents saved with the old schema may still
|
|
44
|
+
* have chat platform credentials on disk. Stripping on load prevents
|
|
45
|
+
* leaking those secrets via the API.
|
|
46
|
+
*/
|
|
47
|
+
function stripLegacyChatTrigger(agent: AgentConfig): AgentConfig {
|
|
48
|
+
if (!agent.triggers || !("chat" in agent.triggers)) return agent;
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
50
|
+
const { chat: _chat, ...rest } = agent.triggers as Record<string, unknown>;
|
|
51
|
+
return { ...agent, triggers: rest as AgentConfig["triggers"] };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ─── CRUD ───────────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
export function listAgents(): AgentConfig[] {
|
|
57
|
+
ensureDir();
|
|
58
|
+
try {
|
|
59
|
+
const files = readdirSync(AGENTS_DIR).filter((f) => f.endsWith(".json"));
|
|
60
|
+
const agents: AgentConfig[] = [];
|
|
61
|
+
for (const file of files) {
|
|
62
|
+
try {
|
|
63
|
+
const raw = readFileSync(join(AGENTS_DIR, file), "utf-8");
|
|
64
|
+
agents.push(stripLegacyChatTrigger(JSON.parse(raw)));
|
|
65
|
+
} catch {
|
|
66
|
+
// Skip corrupt files
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
agents.sort((a, b) => a.name.localeCompare(b.name));
|
|
70
|
+
return agents;
|
|
71
|
+
} catch {
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getAgent(id: string): AgentConfig | null {
|
|
77
|
+
ensureDir();
|
|
78
|
+
try {
|
|
79
|
+
const raw = readFileSync(filePath(id), "utf-8");
|
|
80
|
+
return stripLegacyChatTrigger(JSON.parse(raw) as AgentConfig);
|
|
81
|
+
} catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createAgent(data: AgentConfigCreateInput): AgentConfig {
|
|
87
|
+
if (!data.name || !data.name.trim()) throw new Error("Agent name is required");
|
|
88
|
+
if (!data.prompt || !data.prompt.trim()) throw new Error("Agent prompt is required");
|
|
89
|
+
|
|
90
|
+
const id = slugify(data.name.trim());
|
|
91
|
+
if (!id) throw new Error("Agent name must contain alphanumeric characters");
|
|
92
|
+
|
|
93
|
+
ensureDir();
|
|
94
|
+
if (existsSync(filePath(id))) {
|
|
95
|
+
throw new Error(`An agent with a similar name already exists ("${id}")`);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Auto-generate webhook secret if webhook trigger is enabled but has no secret
|
|
99
|
+
const triggers = data.triggers ? { ...data.triggers } : undefined;
|
|
100
|
+
if (triggers?.webhook && !triggers.webhook.secret) {
|
|
101
|
+
triggers.webhook = { ...triggers.webhook, secret: generateWebhookSecret() };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const now = Date.now();
|
|
105
|
+
const agent: AgentConfig = {
|
|
106
|
+
...data,
|
|
107
|
+
triggers,
|
|
108
|
+
id,
|
|
109
|
+
name: data.name.trim(),
|
|
110
|
+
prompt: data.prompt.trim(),
|
|
111
|
+
description: data.description?.trim() || "",
|
|
112
|
+
cwd: data.cwd?.trim() || "",
|
|
113
|
+
createdAt: now,
|
|
114
|
+
updatedAt: now,
|
|
115
|
+
totalRuns: 0,
|
|
116
|
+
consecutiveFailures: 0,
|
|
117
|
+
};
|
|
118
|
+
writeFileSync(filePath(id), JSON.stringify(agent, null, 2), "utf-8");
|
|
119
|
+
return agent;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function updateAgent(
|
|
123
|
+
id: string,
|
|
124
|
+
updates: Partial<AgentConfig>,
|
|
125
|
+
): AgentConfig | null {
|
|
126
|
+
ensureDir();
|
|
127
|
+
const existing = getAgent(id);
|
|
128
|
+
if (!existing) return null;
|
|
129
|
+
|
|
130
|
+
const newName = updates.name?.trim() || existing.name;
|
|
131
|
+
const newId = slugify(newName);
|
|
132
|
+
if (!newId) throw new Error("Agent name must contain alphanumeric characters");
|
|
133
|
+
|
|
134
|
+
// If name changed, check for slug collision with a different agent
|
|
135
|
+
if (newId !== id && existsSync(filePath(newId))) {
|
|
136
|
+
throw new Error(`An agent with a similar name already exists ("${newId}")`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const agent: AgentConfig = {
|
|
140
|
+
...existing,
|
|
141
|
+
...updates,
|
|
142
|
+
id: newId,
|
|
143
|
+
name: newName,
|
|
144
|
+
updatedAt: Date.now(),
|
|
145
|
+
// Preserve immutable fields
|
|
146
|
+
createdAt: existing.createdAt,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// If id changed, delete old file
|
|
150
|
+
if (newId !== id) {
|
|
151
|
+
try {
|
|
152
|
+
unlinkSync(filePath(id));
|
|
153
|
+
} catch {
|
|
154
|
+
/* ok */
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
writeFileSync(filePath(newId), JSON.stringify(agent, null, 2), "utf-8");
|
|
159
|
+
return agent;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function deleteAgent(id: string): boolean {
|
|
163
|
+
ensureDir();
|
|
164
|
+
if (!existsSync(filePath(id))) return false;
|
|
165
|
+
try {
|
|
166
|
+
unlinkSync(filePath(id));
|
|
167
|
+
return true;
|
|
168
|
+
} catch {
|
|
169
|
+
return false;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Generate a new webhook secret for an agent */
|
|
174
|
+
export function regenerateWebhookSecret(id: string): AgentConfig | null {
|
|
175
|
+
const agent = getAgent(id);
|
|
176
|
+
if (!agent) return null;
|
|
177
|
+
|
|
178
|
+
const triggers = agent.triggers || {};
|
|
179
|
+
triggers.webhook = {
|
|
180
|
+
enabled: triggers.webhook?.enabled ?? false,
|
|
181
|
+
secret: generateWebhookSecret(),
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
return updateAgent(id, { triggers });
|
|
185
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// ─── Agent Types ─────────────────────────────────────────────────────────────
|
|
2
|
+
|
|
3
|
+
export interface McpServerConfigAgent {
|
|
4
|
+
type: "stdio" | "sse" | "http";
|
|
5
|
+
command?: string;
|
|
6
|
+
args?: string[];
|
|
7
|
+
url?: string;
|
|
8
|
+
env?: Record<string, string>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface AgentConfig {
|
|
12
|
+
/** Unique slug-based ID (derived from name) */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Schema version for forward compat */
|
|
15
|
+
version: 1;
|
|
16
|
+
/** Human-readable name */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Short description of what this agent does */
|
|
19
|
+
description: string;
|
|
20
|
+
/** Emoji or icon identifier */
|
|
21
|
+
icon?: string;
|
|
22
|
+
|
|
23
|
+
// ── Session Config ──
|
|
24
|
+
/** "claude" or "codex" */
|
|
25
|
+
backendType: "claude" | "codex";
|
|
26
|
+
/** Model to use (e.g. "claude-sonnet-4-6") */
|
|
27
|
+
model: string;
|
|
28
|
+
/** Permission mode — "bypassPermissions" for Claude auto mode */
|
|
29
|
+
permissionMode: string;
|
|
30
|
+
/** Working directory path, or "temp" for an auto-created temp dir */
|
|
31
|
+
cwd: string;
|
|
32
|
+
/** Optional environment slug (references ~/.companion/envs/) */
|
|
33
|
+
envSlug?: string;
|
|
34
|
+
/** Extra environment variables */
|
|
35
|
+
env?: Record<string, string>;
|
|
36
|
+
/** Tool allowlist (empty = all tools) */
|
|
37
|
+
allowedTools?: string[];
|
|
38
|
+
/** Codex-specific: internet access */
|
|
39
|
+
codexInternetAccess?: boolean;
|
|
40
|
+
|
|
41
|
+
// ── Prompt ──
|
|
42
|
+
/** Prompt template. Use {{input}} as placeholder for trigger-provided input */
|
|
43
|
+
prompt: string;
|
|
44
|
+
|
|
45
|
+
// ── MCP Servers ──
|
|
46
|
+
/** MCP server configs to set on the session after CLI connects */
|
|
47
|
+
mcpServers?: Record<string, McpServerConfigAgent>;
|
|
48
|
+
|
|
49
|
+
// ── Skills ──
|
|
50
|
+
/** Skill slugs to attach (from ~/.claude/skills/) */
|
|
51
|
+
skills?: string[];
|
|
52
|
+
|
|
53
|
+
// ── Docker ──
|
|
54
|
+
/** Optional Docker container configuration */
|
|
55
|
+
container?: {
|
|
56
|
+
image?: string;
|
|
57
|
+
ports?: number[];
|
|
58
|
+
volumes?: string[];
|
|
59
|
+
initScript?: string;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// ── Git ──
|
|
63
|
+
branch?: string;
|
|
64
|
+
createBranch?: boolean;
|
|
65
|
+
useWorktree?: boolean;
|
|
66
|
+
|
|
67
|
+
// ── Triggers ──
|
|
68
|
+
triggers?: {
|
|
69
|
+
/** Webhook trigger config */
|
|
70
|
+
webhook?: {
|
|
71
|
+
enabled: boolean;
|
|
72
|
+
/** Auto-generated secret token for URL auth */
|
|
73
|
+
secret: string;
|
|
74
|
+
};
|
|
75
|
+
/** Cron/schedule trigger config */
|
|
76
|
+
schedule?: {
|
|
77
|
+
enabled: boolean;
|
|
78
|
+
/** Cron expression or ISO datetime */
|
|
79
|
+
expression: string;
|
|
80
|
+
/** true = recurring cron, false = one-shot */
|
|
81
|
+
recurring: boolean;
|
|
82
|
+
};
|
|
83
|
+
/** Linear Agent Interaction SDK trigger (per-agent OAuth app) */
|
|
84
|
+
linear?: {
|
|
85
|
+
enabled: boolean;
|
|
86
|
+
/** Reference to a LinearOAuthConnection by ID (new model) */
|
|
87
|
+
oauthConnectionId?: string;
|
|
88
|
+
/** @deprecated OAuth app client ID from Linear — use oauthConnectionId instead */
|
|
89
|
+
oauthClientId?: string;
|
|
90
|
+
/** @deprecated OAuth app client secret — use oauthConnectionId instead */
|
|
91
|
+
oauthClientSecret?: string;
|
|
92
|
+
/** @deprecated Webhook signing secret — use oauthConnectionId instead */
|
|
93
|
+
webhookSecret?: string;
|
|
94
|
+
/** @deprecated OAuth access token — use oauthConnectionId instead */
|
|
95
|
+
accessToken?: string;
|
|
96
|
+
/** @deprecated OAuth refresh token — use oauthConnectionId instead */
|
|
97
|
+
refreshToken?: string;
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// ── Tracking ──
|
|
102
|
+
enabled: boolean;
|
|
103
|
+
createdAt: number;
|
|
104
|
+
updatedAt: number;
|
|
105
|
+
lastRunAt?: number;
|
|
106
|
+
lastSessionId?: string;
|
|
107
|
+
totalRuns: number;
|
|
108
|
+
consecutiveFailures: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Input for creating an agent (without auto-generated fields) */
|
|
112
|
+
export type AgentConfigCreateInput = Omit<
|
|
113
|
+
AgentConfig,
|
|
114
|
+
"id" | "createdAt" | "updatedAt" | "totalRuns" | "consecutiveFailures" | "lastRunAt" | "lastSessionId"
|
|
115
|
+
>;
|
|
116
|
+
|
|
117
|
+
/** The portable/shareable JSON format (no internal tracking fields) */
|
|
118
|
+
export type AgentConfigExport = Omit<
|
|
119
|
+
AgentConfig,
|
|
120
|
+
"id" | "createdAt" | "updatedAt" | "totalRuns" | "consecutiveFailures" | "lastRunAt" | "lastSessionId" | "enabled"
|
|
121
|
+
>;
|
|
122
|
+
|
|
123
|
+
export interface AgentExecution {
|
|
124
|
+
/** The session ID created for this execution */
|
|
125
|
+
sessionId: string;
|
|
126
|
+
/** The agent ID that triggered this */
|
|
127
|
+
agentId: string;
|
|
128
|
+
/** Trigger type that initiated this execution */
|
|
129
|
+
triggerType: "manual" | "webhook" | "schedule" | "linear";
|
|
130
|
+
/** When the execution started */
|
|
131
|
+
startedAt: number;
|
|
132
|
+
/** When the execution completed */
|
|
133
|
+
completedAt?: number;
|
|
134
|
+
/** Whether the execution succeeded */
|
|
135
|
+
success?: boolean;
|
|
136
|
+
/** Error message if it failed */
|
|
137
|
+
error?: string;
|
|
138
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
5
|
+
import {
|
|
6
|
+
_resetForTest,
|
|
7
|
+
updateSettings,
|
|
8
|
+
} from "./settings-manager.js";
|
|
9
|
+
import { getEffectiveAiValidation } from "./ai-validation-settings.js";
|
|
10
|
+
import type { SessionState } from "./session-types.js";
|
|
11
|
+
|
|
12
|
+
let tempDir: string;
|
|
13
|
+
let settingsPath: string;
|
|
14
|
+
|
|
15
|
+
/** Minimal SessionState with only the fields needed for testing */
|
|
16
|
+
function makeSessionState(overrides: Partial<SessionState> = {}): SessionState {
|
|
17
|
+
return {
|
|
18
|
+
session_id: "test-session",
|
|
19
|
+
model: "",
|
|
20
|
+
cwd: "",
|
|
21
|
+
tools: [],
|
|
22
|
+
permissionMode: "default",
|
|
23
|
+
claude_code_version: "",
|
|
24
|
+
mcp_servers: [],
|
|
25
|
+
agents: [],
|
|
26
|
+
slash_commands: [],
|
|
27
|
+
skills: [],
|
|
28
|
+
total_cost_usd: 0,
|
|
29
|
+
num_turns: 0,
|
|
30
|
+
context_used_percent: 0,
|
|
31
|
+
is_compacting: false,
|
|
32
|
+
git_branch: "",
|
|
33
|
+
is_worktree: false,
|
|
34
|
+
is_containerized: false,
|
|
35
|
+
repo_root: "",
|
|
36
|
+
git_ahead: 0,
|
|
37
|
+
git_behind: 0,
|
|
38
|
+
total_lines_added: 0,
|
|
39
|
+
total_lines_removed: 0,
|
|
40
|
+
...overrides,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
beforeEach(() => {
|
|
45
|
+
tempDir = mkdtempSync(join(tmpdir(), "ai-validation-settings-test-"));
|
|
46
|
+
settingsPath = join(tempDir, "settings.json");
|
|
47
|
+
_resetForTest(settingsPath);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
52
|
+
_resetForTest();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe("getEffectiveAiValidation", () => {
|
|
56
|
+
it("returns global defaults when session fields are undefined", () => {
|
|
57
|
+
// Global defaults: enabled=false, autoApprove=true, autoDeny=false
|
|
58
|
+
const session = makeSessionState();
|
|
59
|
+
const result = getEffectiveAiValidation(session);
|
|
60
|
+
expect(result.enabled).toBe(false);
|
|
61
|
+
expect(result.autoApprove).toBe(true);
|
|
62
|
+
expect(result.autoDeny).toBe(false);
|
|
63
|
+
expect(result.anthropicApiKey).toBe("");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns global defaults when session fields are null (explicit inherit)", () => {
|
|
67
|
+
const session = makeSessionState({
|
|
68
|
+
aiValidationEnabled: null,
|
|
69
|
+
aiValidationAutoApprove: null,
|
|
70
|
+
aiValidationAutoDeny: null,
|
|
71
|
+
});
|
|
72
|
+
const result = getEffectiveAiValidation(session);
|
|
73
|
+
expect(result.enabled).toBe(false);
|
|
74
|
+
expect(result.autoApprove).toBe(true);
|
|
75
|
+
expect(result.autoDeny).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("session override wins over global when session fields are set", () => {
|
|
79
|
+
// Set global to enabled
|
|
80
|
+
updateSettings({ aiValidationEnabled: true, aiValidationAutoApprove: true, aiValidationAutoDeny: true });
|
|
81
|
+
|
|
82
|
+
// Session overrides all three to different values
|
|
83
|
+
const session = makeSessionState({
|
|
84
|
+
aiValidationEnabled: false,
|
|
85
|
+
aiValidationAutoApprove: false,
|
|
86
|
+
aiValidationAutoDeny: false,
|
|
87
|
+
});
|
|
88
|
+
const result = getEffectiveAiValidation(session);
|
|
89
|
+
expect(result.enabled).toBe(false);
|
|
90
|
+
expect(result.autoApprove).toBe(false);
|
|
91
|
+
expect(result.autoDeny).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("session enabled=true overrides global enabled=false", () => {
|
|
95
|
+
// Global: disabled
|
|
96
|
+
const session = makeSessionState({ aiValidationEnabled: true });
|
|
97
|
+
const result = getEffectiveAiValidation(session);
|
|
98
|
+
expect(result.enabled).toBe(true);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("mixed: session sets enabled, inherits autoApprove/autoDeny from global", () => {
|
|
102
|
+
updateSettings({ aiValidationAutoApprove: false, aiValidationAutoDeny: false });
|
|
103
|
+
|
|
104
|
+
const session = makeSessionState({
|
|
105
|
+
aiValidationEnabled: true,
|
|
106
|
+
aiValidationAutoApprove: null, // inherit global (false)
|
|
107
|
+
aiValidationAutoDeny: undefined, // inherit global (false)
|
|
108
|
+
});
|
|
109
|
+
const result = getEffectiveAiValidation(session);
|
|
110
|
+
expect(result.enabled).toBe(true);
|
|
111
|
+
expect(result.autoApprove).toBe(false);
|
|
112
|
+
expect(result.autoDeny).toBe(false);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("anthropicApiKey always comes from global settings", () => {
|
|
116
|
+
updateSettings({ anthropicApiKey: "sk-test-key-123" });
|
|
117
|
+
|
|
118
|
+
const session = makeSessionState({ aiValidationEnabled: true });
|
|
119
|
+
const result = getEffectiveAiValidation(session);
|
|
120
|
+
expect(result.anthropicApiKey).toBe("sk-test-key-123");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("returns empty API key when global has none, regardless of session settings", () => {
|
|
124
|
+
const session = makeSessionState({ aiValidationEnabled: true });
|
|
125
|
+
const result = getEffectiveAiValidation(session);
|
|
126
|
+
expect(result.anthropicApiKey).toBe("");
|
|
127
|
+
});
|
|
128
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { getSettings } from "./settings-manager.js";
|
|
2
|
+
import type { SessionState } from "./session-types.js";
|
|
3
|
+
|
|
4
|
+
export interface EffectiveAiValidationSettings {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
autoApprove: boolean;
|
|
7
|
+
autoDeny: boolean;
|
|
8
|
+
anthropicApiKey: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Resolve effective AI validation settings for a session.
|
|
13
|
+
* Session-level overrides take priority; falls back to global settings.
|
|
14
|
+
* The anthropicApiKey is always from global settings.
|
|
15
|
+
*/
|
|
16
|
+
export function getEffectiveAiValidation(
|
|
17
|
+
sessionState: SessionState,
|
|
18
|
+
): EffectiveAiValidationSettings {
|
|
19
|
+
const global = getSettings();
|
|
20
|
+
return {
|
|
21
|
+
enabled:
|
|
22
|
+
sessionState.aiValidationEnabled != null
|
|
23
|
+
? sessionState.aiValidationEnabled
|
|
24
|
+
: global.aiValidationEnabled,
|
|
25
|
+
autoApprove:
|
|
26
|
+
sessionState.aiValidationAutoApprove != null
|
|
27
|
+
? sessionState.aiValidationAutoApprove
|
|
28
|
+
: global.aiValidationAutoApprove,
|
|
29
|
+
autoDeny:
|
|
30
|
+
sessionState.aiValidationAutoDeny != null
|
|
31
|
+
? sessionState.aiValidationAutoDeny
|
|
32
|
+
: global.aiValidationAutoDeny,
|
|
33
|
+
anthropicApiKey: global.anthropicApiKey,
|
|
34
|
+
};
|
|
35
|
+
}
|