codeden 0.6.2 → 0.6.3
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/dist/cli.js +1 -1
- package/dist/drizzle/20261112140714_turn_fork_point/migration.sql +1 -0
- package/dist/drizzle/20261112140714_turn_fork_point/snapshot.json +3524 -0
- package/dist/{main-hdQfX9V6.js → main-Bx58CdV4.js} +1375 -559
- package/dist/main-Bx58CdV4.js.map +1 -0
- package/dist/{main-DZBmzxSH.js → main-CMt4z1Z9.js} +2 -2
- package/dist/{main-DZBmzxSH.js.map → main-CMt4z1Z9.js.map} +1 -1
- package/dist/{mediaType-nJs-4z5U.js → mediaType-BCz-hbZl.js} +2 -2
- package/dist/{mediaType-nJs-4z5U.js.map → mediaType-BCz-hbZl.js.map} +1 -1
- package/dist/{runner-DYf8dV43.js → runner-CiaiUWOE.js} +636 -71
- package/dist/runner-CiaiUWOE.js.map +1 -0
- package/dist/runner.js +1 -1
- package/dist/{server-test-COAiKxTV.js → server-test-b0hHPqXb.js} +3 -3
- package/dist/{server-test-COAiKxTV.js.map → server-test-b0hHPqXb.js.map} +1 -1
- package/dist/{tree-CF7Q8tXK.js → tree-31WAOm5k.js} +184 -23
- package/dist/tree-31WAOm5k.js.map +1 -0
- package/dist/web/assets/{core-Rhf-hNq6.js → core-FqX7NQzW.js} +1 -1
- package/dist/web/assets/engine-oniguruma-C5UP18m1.js +1 -0
- package/dist/web/assets/{highlighted-body-JLMTFACH-DcKwRTm5.js → highlighted-body-JLMTFACH-BMKFJRL-.js} +1 -1
- package/dist/web/assets/{highlights-DCtMC0YR.js → highlights-9yuGk1pg.js} +1 -1
- package/dist/web/assets/{index-BLggvVMT.js → index-C3QkKvUn.js} +233 -233
- package/dist/web/assets/index-CmllrYFx.css +1 -0
- package/dist/web/assets/langs-DfCgj44a.js +1 -0
- package/dist/web/assets/mermaid-WFAXDIZX-zTA5f8lD.js +1 -0
- package/dist/web/assets/{shiki-B3-ws3OK.js → shiki-Qa-y3xHo.js} +2 -2
- package/dist/web/assets/{stream-tokenizer-hKFrMxml.js → stream-tokenizer-VL43-ily.js} +1 -1
- package/dist/web/assets/{themes-Cvhh4Ovr.js → themes-Di4DO5zr.js} +2 -2
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/main-hdQfX9V6.js.map +0 -1
- package/dist/runner-DYf8dV43.js.map +0 -1
- package/dist/tree-CF7Q8tXK.js.map +0 -1
- package/dist/web/assets/engine-oniguruma-BbpT993F.js +0 -1
- package/dist/web/assets/index-JjF9o00P.css +0 -1
- package/dist/web/assets/langs-B2ZGl3f_.js +0 -1
- package/dist/web/assets/mermaid-WFAXDIZX-C2_MUy_a.js +0 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { n as __require, t as __commonJSMin } from "./rolldown-runtime-DegdyBIC.js";
|
|
2
|
-
import { $ as StarterSetRequest, $n as InboxItemKind, $t as FactoryAllowLargeChangeRequest, A as BrowserHostClientMessage, An as EditQueuedMessageRequest, At as LOCAL_STT_SAMPLE_RATE, B as AUTOMATION_IMPORT_LIMITS, Bt as NOTICE_BATCH_CODE, C as RemoteAccessFile, Cn as AutomationGuards, Cr as
|
|
3
|
-
import { A as isElectronRuntime, C as installClaudeSdk, D as acquireInstallLock, E as resolveNpm, M as import_websocket_server, N as wrapper_default, O as brokenInstallMessage, S as importClaudeSdkFrom, T as runNpm, _ as claudeMemoryDir, a as bufferToFormData, b as claudeSdkSource, c as refreshProcessPath, d as ImageEngineRegistry, f as createImageEngineRegistry, g as ImageGenerationError, h as resolveImageOutput, i as tryDecodeURIComponent, j as ownNodeEnv, k as findExecutable, l as defaultShell, m as imageInfo, n as readLock, o as createAdaptorServer, p as IMAGE_ASPECT_RATIOS, r as Hono, s as upgradeWebSocket, u as shellCommandLine, v as findGitRoot, w as isClaudeSdkInstalled, x as CLAUDE_SDK_VERSION, y as mainRepoRoot } from "./runner-
|
|
4
|
-
import { A as ListRootsResultSchema, C as InitializedNotificationSchema, G as isJSONRPCResultResponse, J as safeParse, K as getLiteralValue, N as LoggingLevelSchema, P as McpError, R as SUPPORTED_PROTOCOL_VERSIONS, T as LATEST_PROTOCOL_VERSION, U as isJSONRPCErrorResponse, V as isInitializeRequest, W as isJSONRPCRequest, _ as ElicitResultSchema, a as AjvJsonSchemaValidator, c as CallToolRequestSchema, f as CreateMessageResultSchema, i as assertToolsCallTaskCapability, j as ListToolsRequestSchema, l as CallToolResultSchema, m as CreateTaskResultSchema, o as Protocol, p as CreateMessageResultWithToolsSchema, q as getObjectShape, r as assertClientRequestTaskCapability, s as mergeCapabilities, t as isJsonContentType, v as EmptyResultSchema, w as JSONRPCMessageSchema, x as InitializeRequestSchema, y as ErrorCode, z as SetLevelRequestSchema } from "./mediaType-
|
|
2
|
+
import { $ as StarterSetRequest, $n as InboxItemKind, $r as ZodError, $t as FactoryAllowLargeChangeRequest, A as BrowserHostClientMessage, An as EditQueuedMessageRequest, Ar as ReasoningEffort, At as LOCAL_STT_SAMPLE_RATE, B as AUTOMATION_IMPORT_LIMITS, Br as int, Bt as NOTICE_BATCH_CODE, C as RemoteAccessFile, Cn as AutomationGuards, Cr as McpServerDiffRequest, Ct as UpdateProjectRequest, D as SignInRequest, Dn as Session, Dr as providersWith, Dt as READ_ALOUD_FULL_MAX_CHARS, E as SetNetworkPasswordResponse, En as TriagePolicy, Er as agentConfigFeatures, Et as agentEventToNewEvent, F as BrowserNavigateRequest, Fn as SpeechSummaryRequest, Ft as UpdateInstallRequest, G as AutomationFileFrontmatter, Gn as ConflictDecision, Gr as object, Gt as GithubIssue, H as DuplicateAutomationRequest, Ht as CreateGithubIssueRequest, I as BrowserNewTabRequest, In as TerminalClientMessage, Ir as array, It as UpdateStatus, J as CreateAutomationRequest, Jn as Project, Jt as GithubPr, K as AutomationGuardsPatch, Kn as StartedBy, Kt as GithubLabelRequest, L as BrowserOpenRequest, Ln as TerminalQuery, Lr as boolean, Lt as compareVersions$1, M as BrowserClientMessage, Mn as GitPushRequest, Mr as datetime, Mt as SttQuery, N as BrowserDevServerRequest, Nn as RestoreFilesRequest, Nr as _enum, Nt as TtsRequest, O as passwordProblem, On as SessionStatus, Or as AutonomyLevel, Ot as Settings, P as BrowserHistoryRequest, Pn as SessionResult, Pt as AppPointer, Q as SetAutomationEnabledRequest, Qn as InboxItem, Qt as parseGithubPrId, R as BrowserViewportRequest, Rn as RunnerCommandResults, Rt as isNewerVersion, S as PASSWORD_RULE_TEXT, Sn as AutomationAction, Sr as AgentConfigWriteRequest, St as UiClientMessage, T as SetNetworkPasswordRequest, Tn as AutomationTrigger, Tr as McpServerUpsertRequest, Tt as AGENT_EVENT_PREFIX, U as ExportAutomationsQuery, Un as SessionOptions, Ut as GithubAuthorAssociation, V as AutomationImportSource, Vt as NoticeBatchData, W as ImportAutomationsRequest, Wn as ProjectConfigFile, Wt as GithubCommentRequest, X as ListAutomationsQuery, Xn as DEFAULT_VISION_PATH, Xt as TriageIssueRequest, Y as ListAutomationRunsQuery, Yn as sortProjects, Yr as string, Yt as MergePrRequest, Z as RunAutomationRequest, Zn as FactoryLimits, Zt as githubPrId, _ as ListNoticesQuery, _n as FactorySetTypeRequest, _r as AgentConfigDeleteQuery, _t as SessionStartMeta, an as REVIEW_SUMMARY_MAX, ar as winningCommands, at as CreateProjectRequest, b as UpdateSessionRequest, bn as Artifact, br as AgentConfigLocator, bt as SteerRequest, c as UsageSettings, cn as SetAutonomyModeRequest, cr as messageFrom, ct as ForkSessionRequest, d as RunnerLock, dn as FactoryAdmitRequest, dt as LoginToolId, ei as toJSONSchema, en as FactoryDecisionRequest, et as TriageDecisionRequest, f as RunnerToServerMessage, fn as FactoryAnalysisReport, fr as McpServerTestRequest, ft as ReorderProjectsRequest, g as ListConflictsQuery, gn as FactoryReproductionReport, gr as ShelvedHook, gt as SendMessageRequest, h as ArchiveSessionRequest, hn as FactoryPlanRoundRequest, hr as HookHandler, ht as SecretKey, in as PutVisionRequest, ir as oneLineDescription, it as UpdateAutomationRequest, j as summarizeActivity, jn as GitCommitRequest, jt as LocalSttModelParams, k as ImageTestRequest, kn as DiffScope, kr as ProviderId, kt as speechExcerpt, l as SessionFileQuery, ln as isFactoryMode, lr as MCP_TEST_DEFAULT_TIMEOUT_MS, lt as ListInboxQuery, mn as FactoryFinding, mr as HookChangeRequest, mt as RespondRequest, n as killTree$1, ni as prettifyError, nn as KillSwitchRequest, nr as mergeItemDeltas, nt as UpcomingRunsQuery, o as CODEDEN_VERSION, on as ReviewVerdictSubmission, ot as FileReadQuery, p as ServerLock, pn as FactoryFileIssueRequest, pr as SkillSwitchRequest, pt as ResolveInboxRequest, q as AutomationProjectRequest, qn as WorkKey, qt as GithubListQuery, rn as LEGACY_SIZE_LIMITS, rr as ProjectCommandsQuery, rt as UpdateAutomationProjectSettingsRequest, s as UsageQuery, sn as SCOUT_UNAVAILABLE, st as FileWriteRequest, t as isAlive, tn as FactoryTuning, tr as SessionUpdatedEvent, tt as TriageReport, un as truncateReviewSummary, ut as ListSessionsQuery, v as ResolveConflictRequest, vn as ScoutState, vr as AgentConfigDiffRequest, vt as SetSecretRequest, w as SetNetworkAccessRequest, wn as AutomationRun, wr as McpServerTarget, wt as HealthResponse, x as NetworkStatus, xn as Automation, xr as AgentConfigProvider, xt as ThreadItemsQuery, y as RoleMessageRequest, yn as pipelineStep, yr as AgentConfigListQuery, yt as StartSessionRequest, z as BrowserWsQuery, zt as ConflictRecord } from "./tree-31WAOm5k.js";
|
|
3
|
+
import { A as isElectronRuntime, C as installClaudeSdk, D as acquireInstallLock, E as resolveNpm, M as import_websocket_server, N as wrapper_default, O as brokenInstallMessage, S as importClaudeSdkFrom, T as runNpm, _ as claudeMemoryDir, a as bufferToFormData, b as claudeSdkSource, c as refreshProcessPath, d as ImageEngineRegistry, f as createImageEngineRegistry, g as ImageGenerationError, h as resolveImageOutput, i as tryDecodeURIComponent, j as ownNodeEnv, k as findExecutable, l as defaultShell, m as imageInfo, n as readLock, o as createAdaptorServer, p as IMAGE_ASPECT_RATIOS, r as Hono, s as upgradeWebSocket, u as shellCommandLine, v as findGitRoot, w as isClaudeSdkInstalled, x as CLAUDE_SDK_VERSION, y as mainRepoRoot } from "./runner-CiaiUWOE.js";
|
|
4
|
+
import { A as ListRootsResultSchema, C as InitializedNotificationSchema, G as isJSONRPCResultResponse, J as safeParse, K as getLiteralValue, N as LoggingLevelSchema, P as McpError, R as SUPPORTED_PROTOCOL_VERSIONS, T as LATEST_PROTOCOL_VERSION, U as isJSONRPCErrorResponse, V as isInitializeRequest, W as isJSONRPCRequest, _ as ElicitResultSchema, a as AjvJsonSchemaValidator, c as CallToolRequestSchema, f as CreateMessageResultSchema, i as assertToolsCallTaskCapability, j as ListToolsRequestSchema, l as CallToolResultSchema, m as CreateTaskResultSchema, o as Protocol, p as CreateMessageResultWithToolsSchema, q as getObjectShape, r as assertClientRequestTaskCapability, s as mergeCapabilities, t as isJsonContentType, v as EmptyResultSchema, w as JSONRPCMessageSchema, x as InitializeRequestSchema, y as ErrorCode, z as SetLevelRequestSchema } from "./mediaType-BCz-hbZl.js";
|
|
5
5
|
import { createRequire } from "node:module";
|
|
6
6
|
import fs, { accessSync, appendFileSync, closeSync, constants, copyFileSync, cpSync, createReadStream, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
7
7
|
import { homedir, networkInterfaces, tmpdir } from "node:os";
|
|
@@ -17,6 +17,57 @@ import { DatabaseSync } from "node:sqlite";
|
|
|
17
17
|
import { createServer } from "node:net";
|
|
18
18
|
import { createRequire as createRequire$1 } from "module";
|
|
19
19
|
import { versions } from "node:process";
|
|
20
|
+
// ../../packages/contracts/src/tool-output.ts
|
|
21
|
+
/**
|
|
22
|
+
* Tool output kept in transcript pages, live updates and the browser's live transcript. The
|
|
23
|
+
* rest is fetched on demand (§6, `GET /api/sessions/:id/items/:itemId/output`).
|
|
24
|
+
*/
|
|
25
|
+
const OUTPUT_PREVIEW_CHARS = 16e3;
|
|
26
|
+
/**
|
|
27
|
+
* Most tool output the server stores on an item while it streams. Older output is dropped past
|
|
28
|
+
* this, so every chunk rewrites a bounded row however long the command runs.
|
|
29
|
+
*/
|
|
30
|
+
const STREAMED_OUTPUT_CHARS = 64e3;
|
|
31
|
+
/** Size of `text` in UTF-8 bytes, as Node's `Buffer.byteLength` counts it (works in browsers). */
|
|
32
|
+
function utf8ByteLength(text) {
|
|
33
|
+
let bytes = 0;
|
|
34
|
+
for (let i = 0; i < text.length; i++) {
|
|
35
|
+
const code = text.charCodeAt(i);
|
|
36
|
+
if (code < 128) bytes += 1;
|
|
37
|
+
else if (code < 2048) bytes += 2;
|
|
38
|
+
else if (code >= 55296 && code <= 56319 && isLowSurrogate(text.charCodeAt(i + 1))) {
|
|
39
|
+
bytes += 4;
|
|
40
|
+
i++;
|
|
41
|
+
} else bytes += 3;
|
|
42
|
+
}
|
|
43
|
+
return bytes;
|
|
44
|
+
}
|
|
45
|
+
const isLowSurrogate = (code) => code >= 56320 && code <= 57343;
|
|
46
|
+
/**
|
|
47
|
+
* Append a streamed output chunk, keeping only the last `max` characters (the end of command
|
|
48
|
+
* output is the useful part), marking the output `truncated` once anything is dropped and
|
|
49
|
+
* counting every byte seen in `totalBytes`. The work per chunk is bounded by `max` and the
|
|
50
|
+
* chunk, never by the output streamed so far. Pure.
|
|
51
|
+
*/
|
|
52
|
+
function appendToolOutput(output, chunk, max) {
|
|
53
|
+
const prev = output?.text ?? "";
|
|
54
|
+
const totalBytes = (output?.totalBytes ?? utf8ByteLength(prev)) + utf8ByteLength(chunk);
|
|
55
|
+
if (prev.length + chunk.length <= max) return {
|
|
56
|
+
...output,
|
|
57
|
+
text: prev + chunk,
|
|
58
|
+
truncated: output?.truncated ?? false,
|
|
59
|
+
totalBytes
|
|
60
|
+
};
|
|
61
|
+
const keep = max - chunk.length;
|
|
62
|
+
let text = keep > 0 ? prev.slice(prev.length - keep) + chunk : chunk.slice(chunk.length - max);
|
|
63
|
+
if (isLowSurrogate(text.charCodeAt(0))) text = text.slice(1);
|
|
64
|
+
return {
|
|
65
|
+
...output,
|
|
66
|
+
text,
|
|
67
|
+
truncated: true,
|
|
68
|
+
totalBytes
|
|
69
|
+
};
|
|
70
|
+
}
|
|
20
71
|
// ../../packages/contracts/src/vision.ts
|
|
21
72
|
const SECTIONS = [
|
|
22
73
|
[/^goals?\b/i, "goals"],
|
|
@@ -7627,7 +7678,9 @@ const turns = sqliteTable("turns", {
|
|
|
7627
7678
|
completedAt: text("completed_at"),
|
|
7628
7679
|
error: json$3("error"),
|
|
7629
7680
|
/** Usage reported for this turn (latest `usage.updated` with this turnId). */
|
|
7630
|
-
usage: json$3("usage")
|
|
7681
|
+
usage: json$3("usage"),
|
|
7682
|
+
/** The provider's fork point at the end of the turn (`forkPoint` on its end event, §9). */
|
|
7683
|
+
forkPoint: text("fork_point")
|
|
7631
7684
|
}, (t) => [primaryKey({ columns: [t.sessionId, t.id] })]);
|
|
7632
7685
|
const items = sqliteTable("items", {
|
|
7633
7686
|
sessionId: text("session_id").notNull(),
|
|
@@ -8158,11 +8211,7 @@ function applyDelta(item, delta) {
|
|
|
8158
8211
|
if (item.kind !== "tool_call") return item;
|
|
8159
8212
|
return {
|
|
8160
8213
|
...item,
|
|
8161
|
-
output:
|
|
8162
|
-
...item.output,
|
|
8163
|
-
text: (item.output?.text ?? "") + delta.text,
|
|
8164
|
-
truncated: item.output?.truncated ?? false
|
|
8165
|
-
}
|
|
8214
|
+
output: appendToolOutput(item.output, delta.text, STREAMED_OUTPUT_CHARS)
|
|
8166
8215
|
};
|
|
8167
8216
|
case "tool_input": return item;
|
|
8168
8217
|
}
|
|
@@ -9811,7 +9860,8 @@ function reduceAgentEvent(reader, sessionId, event, ts) {
|
|
|
9811
9860
|
sessionId,
|
|
9812
9861
|
status: "running",
|
|
9813
9862
|
startedAt: d.getTurn(sessionId, event.turnId)?.startedAt ?? ts,
|
|
9814
|
-
completedAt: null
|
|
9863
|
+
completedAt: null,
|
|
9864
|
+
forkPoint: null
|
|
9815
9865
|
});
|
|
9816
9866
|
if (isRootThread(d, sessionId, event.threadId)) setStatus(d, sessionId, "running", ts);
|
|
9817
9867
|
break;
|
|
@@ -9870,7 +9920,8 @@ function onTurnEnded(d, sessionId, event, ts) {
|
|
|
9870
9920
|
sessionId,
|
|
9871
9921
|
status,
|
|
9872
9922
|
completedAt: ts,
|
|
9873
|
-
error
|
|
9923
|
+
error,
|
|
9924
|
+
forkPoint: event.forkPoint ?? null
|
|
9874
9925
|
});
|
|
9875
9926
|
for (const record of d.openRequestItems(sessionId)) {
|
|
9876
9927
|
if (record.turnId !== event.turnId || !isRequestItem(record.item)) continue;
|
|
@@ -12693,8 +12744,6 @@ function mountBrowserSockets(app, services, log) {
|
|
|
12693
12744
|
return bridge;
|
|
12694
12745
|
}
|
|
12695
12746
|
// ../server/src/services/sessions/preview.ts
|
|
12696
|
-
/** Tool output kept in transcript pages and live updates; the rest is fetched on demand (§6). */
|
|
12697
|
-
const OUTPUT_PREVIEW_CHARS = 16e3;
|
|
12698
12747
|
/**
|
|
12699
12748
|
* Trim large tool outputs to their last `max` characters (the end of command output is the
|
|
12700
12749
|
* useful part) and mark them truncated with the full size. Pure.
|
|
@@ -12787,24 +12836,6 @@ function clientEvent(event, artifacts) {
|
|
|
12787
12836
|
function asDelta(e) {
|
|
12788
12837
|
return e.type === "agent.item.delta" ? e.payload : void 0;
|
|
12789
12838
|
}
|
|
12790
|
-
function deltaText(d) {
|
|
12791
|
-
return d.type === "tool_input" ? d.partialJson : d.text;
|
|
12792
|
-
}
|
|
12793
|
-
function mergeable(a, b) {
|
|
12794
|
-
if (a.itemId !== b.itemId || a.turnId !== b.turnId || a.threadId !== b.threadId) return false;
|
|
12795
|
-
if (a.delta.type !== b.delta.type) return false;
|
|
12796
|
-
if (a.delta.type === "tool_output" && b.delta.type === "tool_output") return a.delta.stream === b.delta.stream;
|
|
12797
|
-
return true;
|
|
12798
|
-
}
|
|
12799
|
-
function withText(d, text) {
|
|
12800
|
-
return d.type === "tool_input" ? {
|
|
12801
|
-
...d,
|
|
12802
|
-
partialJson: text
|
|
12803
|
-
} : {
|
|
12804
|
-
...d,
|
|
12805
|
-
text
|
|
12806
|
-
};
|
|
12807
|
-
}
|
|
12808
12839
|
/**
|
|
12809
12840
|
* Merge runs of consecutive text deltas for the same item into one event carrying the last
|
|
12810
12841
|
* seq and timestamp, so a burst of streamed tokens reaches the UI as one update. Order is
|
|
@@ -12816,11 +12847,8 @@ function coalesceEvents(events) {
|
|
|
12816
12847
|
const prev = out.at(-1);
|
|
12817
12848
|
const a = prev ? asDelta(prev) : void 0;
|
|
12818
12849
|
const b = asDelta(event);
|
|
12819
|
-
|
|
12820
|
-
|
|
12821
|
-
...b,
|
|
12822
|
-
delta: withText(b.delta, deltaText(a.delta) + deltaText(b.delta))
|
|
12823
|
-
};
|
|
12850
|
+
const merged = prev && a && b && prev.sessionId === event.sessionId ? mergeItemDeltas(a, b) : void 0;
|
|
12851
|
+
if (merged) {
|
|
12824
12852
|
out[out.length - 1] = {
|
|
12825
12853
|
...event,
|
|
12826
12854
|
payload: merged
|
|
@@ -13539,8 +13567,9 @@ function errorResponse(error, c, log) {
|
|
|
13539
13567
|
}
|
|
13540
13568
|
// ../server/src/routes/agent-config.ts
|
|
13541
13569
|
/**
|
|
13542
|
-
* `/api/agent-config` (§12.4): provider memory files, skills, subagents,
|
|
13543
|
-
* in their native files. Every overwrite and delete is backed up first;
|
|
13570
|
+
* `/api/agent-config` (§12.4): provider memory files, skills (and their switches), subagents,
|
|
13571
|
+
* MCP servers and hooks in their native files. Every overwrite and delete is backed up first;
|
|
13572
|
+
* `diff` routes preview.
|
|
13544
13573
|
*/
|
|
13545
13574
|
function agentConfigRoutes(s) {
|
|
13546
13575
|
const ac = s.agentConfig;
|
|
@@ -13556,7 +13585,7 @@ function agentConfigRoutes(s) {
|
|
|
13556
13585
|
}).post("/diff", jsonBody(AgentConfigDiffRequest), (c) => {
|
|
13557
13586
|
const { content, ...locator } = c.req.valid("json");
|
|
13558
13587
|
return c.json(ac.files.diff(locator, content));
|
|
13559
|
-
}).get("/mcp", queryOf(AgentConfigListQuery.pick({ projectId: true })), (c) => c.json({ servers: ac.mcpList(c.req.valid("query").projectId) })).put("/mcp", jsonBody(McpServerUpsertRequest), (c) => c.json(ac.mcpUpsert(c.req.valid("json")))).delete("/mcp", queryOf(McpServerTarget), (c) => c.json(ac.mcpDelete(c.req.valid("query")))).post("/mcp/diff", jsonBody(McpServerDiffRequest), (c) => c.json(ac.mcpDiff(c.req.valid("json")))).post("/mcp/test", jsonBody(McpServerTestRequest), async (c) => c.json(await ac.mcpTest(c.req.valid("json")))).get("/hooks", queryOf(AgentConfigListQuery.pick({ projectId: true })), (c) => c.json(ac.hooks.list(c.req.valid("query").projectId))).post("/hooks/diff", jsonBody(HookChangeRequest), (c) => c.json(ac.hooks.diff(c.req.valid("json")))).post("/hooks", jsonBody(HookChangeRequest), (c) => c.json(ac.hooks.apply(c.req.valid("json"))));
|
|
13588
|
+
}).get("/mcp", queryOf(AgentConfigListQuery.pick({ projectId: true })), (c) => c.json({ servers: ac.mcpList(c.req.valid("query").projectId) })).put("/mcp", jsonBody(McpServerUpsertRequest), (c) => c.json(ac.mcpUpsert(c.req.valid("json")))).delete("/mcp", queryOf(McpServerTarget), (c) => c.json(ac.mcpDelete(c.req.valid("query")))).post("/mcp/diff", jsonBody(McpServerDiffRequest), (c) => c.json(ac.mcpDiff(c.req.valid("json")))).post("/mcp/test", jsonBody(McpServerTestRequest), async (c) => c.json(await ac.mcpTest(c.req.valid("json")))).get("/hooks", queryOf(AgentConfigListQuery.pick({ projectId: true })), (c) => c.json(ac.hooks.list(c.req.valid("query").projectId))).post("/hooks/diff", jsonBody(HookChangeRequest), (c) => c.json(ac.hooks.diff(c.req.valid("json")))).post("/hooks", jsonBody(HookChangeRequest), (c) => c.json(ac.hooks.apply(c.req.valid("json")))).get("/skills", queryOf(AgentConfigListQuery.pick({ projectId: true })), (c) => c.json({ skills: ac.skills.list(c.req.valid("query").projectId) })).post("/skills/diff", jsonBody(SkillSwitchRequest), (c) => c.json(ac.skills.diff(c.req.valid("json")))).post("/skills", jsonBody(SkillSwitchRequest), (c) => c.json(ac.skills.apply(c.req.valid("json"))));
|
|
13560
13589
|
}
|
|
13561
13590
|
/**
|
|
13562
13591
|
* `GET /api/sessions/:id/agent-config/loaded` and `GET /api/sessions/:id/commands` (the
|
|
@@ -23054,17 +23083,6 @@ function conflictRoutes(s) {
|
|
|
23054
23083
|
const conflicts = s.orchestration.conflicts;
|
|
23055
23084
|
return new Hono().get("/", queryOf(ListConflictsQuery), (c) => c.json(conflicts.list(c.req.valid("query")))).get("/:id", (c) => c.json(conflicts.get(c.req.param("id")))).post("/:id/resolve", jsonBody(ResolveConflictRequest), async (c) => c.json(await conflicts.resolve(c.req.param("id"), c.req.valid("json"))));
|
|
23056
23085
|
}
|
|
23057
|
-
// ../server/src/routes/projects.ts
|
|
23058
|
-
/**
|
|
23059
|
-
* `/api/projects`: add from a folder or clone, list, reorder, update (and pin), archive, and
|
|
23060
|
-
* re-detect the folder's git checkout and GitHub remote (`refresh`, `disconnect-github`).
|
|
23061
|
-
*/
|
|
23062
|
-
function projectRoutes(s) {
|
|
23063
|
-
return new Hono().get("/", queryOf(object({ archived: _enum(["true", "false"]).optional() })), (c) => c.json(s.projects.list({ includeArchived: c.req.valid("query").archived === "true" }))).post("/", jsonBody(CreateProjectRequest), async (c) => c.json(await s.projects.create(c.req.valid("json")), 201)).put("/order", jsonBody(ReorderProjectsRequest), (c) => c.json({ projects: s.projects.reorder(c.req.valid("json").projectIds) })).get("/:id", (c) => c.json(s.projects.get(c.req.param("id")))).patch("/:id", jsonBody(UpdateProjectRequest), (c) => c.json(s.projects.update(c.req.param("id"), c.req.valid("json")))).post("/:id/refresh", async (c) => c.json(await s.redetector.refresh(c.req.param("id")))).post("/:id/disconnect-github", async (c) => c.json(await s.projects.disconnectGithub(c.req.param("id")))).delete("/:id", (c) => {
|
|
23064
|
-
s.projects.archive(c.req.param("id"));
|
|
23065
|
-
return c.json({ ok: true });
|
|
23066
|
-
});
|
|
23067
|
-
}
|
|
23068
23086
|
// ../server/src/services/files/path-guard.ts
|
|
23069
23087
|
const caseInsensitive$1 = process.platform === "win32" || process.platform === "darwin";
|
|
23070
23088
|
/** Real path of `p`, or of its nearest existing ancestor with the rest appended. */
|
|
@@ -23180,6 +23198,19 @@ function readSessionFile(root, path) {
|
|
|
23180
23198
|
truncated: stat.size > MAX_READ_BYTES$2
|
|
23181
23199
|
};
|
|
23182
23200
|
}
|
|
23201
|
+
// ../server/src/routes/projects.ts
|
|
23202
|
+
/**
|
|
23203
|
+
* `/api/projects`: add from a folder or clone, list, reorder, update (and pin), archive, and
|
|
23204
|
+
* re-detect the folder's git checkout and GitHub remote (`refresh`, `disconnect-github`).
|
|
23205
|
+
* `commands` and `files` feed the composer's `/` and `@` menus in the New session draft, before
|
|
23206
|
+
* the session and its worktree exist: they read the project folder.
|
|
23207
|
+
*/
|
|
23208
|
+
function projectRoutes(s) {
|
|
23209
|
+
return new Hono().get("/", queryOf(object({ archived: _enum(["true", "false"]).optional() })), (c) => c.json(s.projects.list({ includeArchived: c.req.valid("query").archived === "true" }))).post("/", jsonBody(CreateProjectRequest), async (c) => c.json(await s.projects.create(c.req.valid("json")), 201)).put("/order", jsonBody(ReorderProjectsRequest), (c) => c.json({ projects: s.projects.reorder(c.req.valid("json").projectIds) })).get("/:id", (c) => c.json(s.projects.get(c.req.param("id")))).patch("/:id", jsonBody(UpdateProjectRequest), (c) => c.json(s.projects.update(c.req.param("id"), c.req.valid("json")))).get("/:id/commands", queryOf(ProjectCommandsQuery), (c) => c.json(s.agentConfig.projectSlashCommands(c.req.param("id"), c.req.valid("query").provider))).get("/:id/files", (c) => c.json(listSessionFiles(s.projects.get(c.req.param("id")).path))).post("/:id/refresh", async (c) => c.json(await s.redetector.refresh(c.req.param("id")))).post("/:id/disconnect-github", async (c) => c.json(await s.projects.disconnectGithub(c.req.param("id")))).delete("/:id", (c) => {
|
|
23210
|
+
s.projects.archive(c.req.param("id"));
|
|
23211
|
+
return c.json({ ok: true });
|
|
23212
|
+
});
|
|
23213
|
+
}
|
|
23183
23214
|
// ../server/src/services/sessions/workdir.ts
|
|
23184
23215
|
/** Shown wherever a cleaned-up worktree is needed before the session is resumed. */
|
|
23185
23216
|
const WORKTREE_REMOVED_MESSAGE = "This session's worktree was cleaned up. Send the session a message to recreate it from its branch.";
|
|
@@ -23428,10 +23459,13 @@ function sessionRoutes(s) {
|
|
|
23428
23459
|
const { item } = record;
|
|
23429
23460
|
if (item.kind !== "tool_call") throw badRequest("Only tool calls have output", "no_output");
|
|
23430
23461
|
const text = item.output?.text ?? "";
|
|
23462
|
+
const totalBytes = Buffer.byteLength(text, "utf8");
|
|
23431
23463
|
const body = {
|
|
23432
23464
|
itemId: item.id,
|
|
23433
23465
|
text,
|
|
23434
|
-
totalBytes
|
|
23466
|
+
totalBytes,
|
|
23467
|
+
truncated: item.output?.truncated ?? false,
|
|
23468
|
+
outputBytes: Math.max(totalBytes, item.output?.totalBytes ?? totalBytes)
|
|
23435
23469
|
};
|
|
23436
23470
|
return c.json(body);
|
|
23437
23471
|
});
|
|
@@ -25215,13 +25249,15 @@ function unattendedAnswers(item) {
|
|
|
25215
25249
|
}
|
|
25216
25250
|
/**
|
|
25217
25251
|
* A question the provider asked through its own tool (not `ask_user`) in an unattended
|
|
25218
|
-
* session: answer it straight away with {@link UNATTENDED_ANSWER}, so the turn never waits.
|
|
25252
|
+
* session: answer it straight away with {@link UNATTENDED_ANSWER}, so the turn never waits. An
|
|
25253
|
+
* MCP server's form is left alone: its answers would go to the server as data, so
|
|
25254
|
+
* `UnwatchedApprovals` withdraws it instead, as it denies approvals.
|
|
25219
25255
|
*/
|
|
25220
25256
|
async function answerProviderQuestion(ctx, respond, e) {
|
|
25221
25257
|
if (e.type !== "agent.item.started" || !e.sessionId) return false;
|
|
25222
25258
|
const item = e.payload.item;
|
|
25223
25259
|
if (item?.kind !== "user_question" || item.deferred || item.status !== "in_progress") return false;
|
|
25224
|
-
if (!unattendedSession(ctx, e.sessionId)) return false;
|
|
25260
|
+
if (item.mcpServer || !unattendedSession(ctx, e.sessionId)) return false;
|
|
25225
25261
|
try {
|
|
25226
25262
|
await respond(e.sessionId, {
|
|
25227
25263
|
requestId: item.requestId,
|
|
@@ -26337,7 +26373,7 @@ function projectLines(s, now, indent = "") {
|
|
|
26337
26373
|
const OWN_TAG = /<(\/?codeden_(?:status|updates))/gi;
|
|
26338
26374
|
const neutralizeTags = (line) => line.replace(OWN_TAG, "‹$1");
|
|
26339
26375
|
/** A tagged block: the opening tag, the escaped lines, the closing tag. */
|
|
26340
|
-
function block(open, close, lines) {
|
|
26376
|
+
function block$1(open, close, lines) {
|
|
26341
26377
|
return [
|
|
26342
26378
|
open,
|
|
26343
26379
|
...lines.map(neutralizeTags),
|
|
@@ -26347,7 +26383,7 @@ function block(open, close, lines) {
|
|
|
26347
26383
|
/** The PM's digest for its project. */
|
|
26348
26384
|
function renderProjectDigest(s, now = Date.now()) {
|
|
26349
26385
|
const at = new Date(now).toISOString();
|
|
26350
|
-
return block(`<codeden_status project=${quote(neutralizeTags(s.project.name))} mode="${s.project.mode}" at="${at}">`, "</codeden_status>", projectLines(s, now));
|
|
26386
|
+
return block$1(`<codeden_status project=${quote(neutralizeTags(s.project.name))} mode="${s.project.mode}" at="${at}">`, "</codeden_status>", projectLines(s, now));
|
|
26351
26387
|
}
|
|
26352
26388
|
/** The orchestrator's digest: every project rolled up. */
|
|
26353
26389
|
function renderOverviewDigest(o, now = Date.now()) {
|
|
@@ -26362,7 +26398,7 @@ function renderOverviewDigest(o, now = Date.now()) {
|
|
|
26362
26398
|
lines.push("Informational PM messages:");
|
|
26363
26399
|
for (const m of o.pmMessages) lines.push(`- ${m.projectId ?? "?"}: ${quote(m.text, 300)}`);
|
|
26364
26400
|
}
|
|
26365
|
-
return block(`<codeden_status scope="all projects" at="${new Date(now).toISOString()}">`, "</codeden_status>", lines);
|
|
26401
|
+
return block$1(`<codeden_status scope="all projects" at="${new Date(now).toISOString()}">`, "</codeden_status>", lines);
|
|
26366
26402
|
}
|
|
26367
26403
|
/**
|
|
26368
26404
|
* "Updates since your last turn": one entry per notice, for the model, inside
|
|
@@ -26375,7 +26411,7 @@ function renderNotices(list, projectName) {
|
|
|
26375
26411
|
const where = n.projectId && projectName ? ` [${projectName(n.projectId)}]` : "";
|
|
26376
26412
|
lines.push(`- (${n.type})${where} ${n.title}${noticeDetail(n)}`);
|
|
26377
26413
|
}
|
|
26378
|
-
return block("<codeden_updates>", "</codeden_updates>", lines);
|
|
26414
|
+
return block$1("<codeden_updates>", "</codeden_updates>", lines);
|
|
26379
26415
|
}
|
|
26380
26416
|
function noticeDetail(n) {
|
|
26381
26417
|
switch (n.type) {
|
|
@@ -26666,6 +26702,489 @@ const orchestratorTools = [
|
|
|
26666
26702
|
askPm,
|
|
26667
26703
|
listInbox
|
|
26668
26704
|
];
|
|
26705
|
+
// ../server/src/services/automations/sources.ts
|
|
26706
|
+
const FILE_SOURCES = ["global_file", "project_file"];
|
|
26707
|
+
/** Repo-relative folder of project automation files. */
|
|
26708
|
+
const PROJECT_AUTOMATIONS_DIR = join(".codeden", "automations");
|
|
26709
|
+
/** The definition fields that, when changed in a file, update the stored automation. */
|
|
26710
|
+
function definitionKey(a) {
|
|
26711
|
+
return JSON.stringify([
|
|
26712
|
+
a.name,
|
|
26713
|
+
a.description,
|
|
26714
|
+
a.templateId,
|
|
26715
|
+
a.trigger,
|
|
26716
|
+
a.action,
|
|
26717
|
+
a.target,
|
|
26718
|
+
a.guards,
|
|
26719
|
+
a.workKey ?? null
|
|
26720
|
+
]);
|
|
26721
|
+
}
|
|
26722
|
+
/**
|
|
26723
|
+
* Automation definitions from Markdown files (§10): global ones in `~/.codeden/automations/`
|
|
26724
|
+
* (they target all projects) and each project's `.codeden/automations/` (that project only).
|
|
26725
|
+
* They are mirrored into the `automations` projection through `automation.saved` /
|
|
26726
|
+
* `automation.deleted`, keeping the enabled flag (files never enable anything). Folders are
|
|
26727
|
+
* watched, with a periodic rescan for folders created later. Built-in templates are read once.
|
|
26728
|
+
*/
|
|
26729
|
+
var AutomationSources = class {
|
|
26730
|
+
deps;
|
|
26731
|
+
paths = /* @__PURE__ */ new Map();
|
|
26732
|
+
errors = [];
|
|
26733
|
+
builtin = [];
|
|
26734
|
+
globalTemplates = [];
|
|
26735
|
+
watchers = /* @__PURE__ */ new Map();
|
|
26736
|
+
timer;
|
|
26737
|
+
rescan;
|
|
26738
|
+
constructor(deps) {
|
|
26739
|
+
this.deps = deps;
|
|
26740
|
+
const dir = findTemplatesDir(deps.templatesDir);
|
|
26741
|
+
if (dir) {
|
|
26742
|
+
const { files, errors } = readAutomationDir(dir);
|
|
26743
|
+
this.builtin = files.map((f) => toTemplate(f, "builtin"));
|
|
26744
|
+
for (const e of errors) deps.log?.(`automations: built-in template ${e.path}: ${e.message}`);
|
|
26745
|
+
} else deps.log?.("automations: built-in templates folder not found");
|
|
26746
|
+
}
|
|
26747
|
+
/** Built-in templates, then global automation files. */
|
|
26748
|
+
templates() {
|
|
26749
|
+
const builtinIds = new Set(this.builtin.map((t) => t.id));
|
|
26750
|
+
return [...this.builtin, ...this.globalTemplates.filter((t) => !builtinIds.has(t.id))];
|
|
26751
|
+
}
|
|
26752
|
+
template(id) {
|
|
26753
|
+
return this.templates().find((t) => t.id === id);
|
|
26754
|
+
}
|
|
26755
|
+
fileErrors() {
|
|
26756
|
+
return this.errors;
|
|
26757
|
+
}
|
|
26758
|
+
pathOf(automationId) {
|
|
26759
|
+
return this.paths.get(automationId) ?? null;
|
|
26760
|
+
}
|
|
26761
|
+
start() {
|
|
26762
|
+
this.reload();
|
|
26763
|
+
const every = this.deps.rescanMs ?? 6e4;
|
|
26764
|
+
if (every > 0) {
|
|
26765
|
+
this.rescan = setInterval(() => this.reload(), every);
|
|
26766
|
+
this.rescan.unref();
|
|
26767
|
+
}
|
|
26768
|
+
}
|
|
26769
|
+
stop() {
|
|
26770
|
+
if (this.timer) clearTimeout(this.timer);
|
|
26771
|
+
if (this.rescan) clearInterval(this.rescan);
|
|
26772
|
+
for (const w of this.watchers.values()) w.close();
|
|
26773
|
+
this.watchers.clear();
|
|
26774
|
+
}
|
|
26775
|
+
/** Re-read every folder and sync the projection. Returns true when anything changed. */
|
|
26776
|
+
reload() {
|
|
26777
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
26778
|
+
const errors = [];
|
|
26779
|
+
const found = [];
|
|
26780
|
+
const paths = /* @__PURE__ */ new Map();
|
|
26781
|
+
const add = (file, base) => {
|
|
26782
|
+
const { parsed } = file;
|
|
26783
|
+
found.push({
|
|
26784
|
+
...base,
|
|
26785
|
+
name: parsed.frontmatter.name,
|
|
26786
|
+
description: parsed.frontmatter.description ?? null,
|
|
26787
|
+
enabled: false,
|
|
26788
|
+
templateId: parsed.id,
|
|
26789
|
+
trigger: parsed.frontmatter.trigger,
|
|
26790
|
+
action: parsed.action,
|
|
26791
|
+
guards: parsed.guards,
|
|
26792
|
+
workKey: parsed.workKey,
|
|
26793
|
+
createdAt: now,
|
|
26794
|
+
updatedAt: now
|
|
26795
|
+
});
|
|
26796
|
+
paths.set(base.id, file.path);
|
|
26797
|
+
};
|
|
26798
|
+
const global = readAutomationDir(this.deps.globalDir);
|
|
26799
|
+
errors.push(...global.errors);
|
|
26800
|
+
this.globalTemplates = global.files.map((f) => toTemplate(f, "global_file"));
|
|
26801
|
+
for (const file of global.files) add(file, {
|
|
26802
|
+
id: `global:${file.parsed.id}`,
|
|
26803
|
+
source: "global_file",
|
|
26804
|
+
target: { type: "all_projects" }
|
|
26805
|
+
});
|
|
26806
|
+
const dirs = [this.deps.globalDir];
|
|
26807
|
+
for (const project of this.deps.projects.list()) {
|
|
26808
|
+
if (project.archivedAt) continue;
|
|
26809
|
+
const dir = join(project.path, PROJECT_AUTOMATIONS_DIR);
|
|
26810
|
+
dirs.push(dir);
|
|
26811
|
+
const local = readAutomationDir(dir);
|
|
26812
|
+
errors.push(...local.errors);
|
|
26813
|
+
for (const file of local.files) add(file, {
|
|
26814
|
+
id: `project:${project.id}:${file.parsed.id}`,
|
|
26815
|
+
source: "project_file",
|
|
26816
|
+
target: {
|
|
26817
|
+
type: "project",
|
|
26818
|
+
projectId: project.id
|
|
26819
|
+
}
|
|
26820
|
+
});
|
|
26821
|
+
}
|
|
26822
|
+
this.errors = errors;
|
|
26823
|
+
this.paths.clear();
|
|
26824
|
+
for (const [k, v] of paths) this.paths.set(k, v);
|
|
26825
|
+
this.watchDirs(dirs);
|
|
26826
|
+
return this.sync(found);
|
|
26827
|
+
}
|
|
26828
|
+
/** Store new or changed file definitions and delete those whose file is gone. */
|
|
26829
|
+
sync(found) {
|
|
26830
|
+
const existing = new Map(this.deps.db.select().from(automations).where(inArray(automations.source, [...FILE_SOURCES])).all().map((r) => {
|
|
26831
|
+
const a = rowToAutomation(r);
|
|
26832
|
+
return [a.id, a];
|
|
26833
|
+
}));
|
|
26834
|
+
const events = [];
|
|
26835
|
+
for (const next of found) {
|
|
26836
|
+
const prev = existing.get(next.id);
|
|
26837
|
+
existing.delete(next.id);
|
|
26838
|
+
if (prev && definitionKey(prev) === definitionKey(next)) continue;
|
|
26839
|
+
const automation = prev ? {
|
|
26840
|
+
...next,
|
|
26841
|
+
enabled: prev.enabled,
|
|
26842
|
+
createdAt: prev.createdAt
|
|
26843
|
+
} : next;
|
|
26844
|
+
events.push({
|
|
26845
|
+
type: "automation.saved",
|
|
26846
|
+
sessionId: null,
|
|
26847
|
+
payload: { automation }
|
|
26848
|
+
});
|
|
26849
|
+
}
|
|
26850
|
+
for (const gone of existing.keys()) events.push({
|
|
26851
|
+
type: "automation.deleted",
|
|
26852
|
+
sessionId: null,
|
|
26853
|
+
payload: { automationId: gone }
|
|
26854
|
+
});
|
|
26855
|
+
if (events.length === 0) return false;
|
|
26856
|
+
this.deps.store.append(events);
|
|
26857
|
+
this.deps.onChange?.();
|
|
26858
|
+
return true;
|
|
26859
|
+
}
|
|
26860
|
+
watchDirs(dirs) {
|
|
26861
|
+
const wanted = new Set(dirs.filter((d) => existsSync(d)));
|
|
26862
|
+
for (const [dir, watcher] of this.watchers) if (!wanted.has(dir)) {
|
|
26863
|
+
watcher.close();
|
|
26864
|
+
this.watchers.delete(dir);
|
|
26865
|
+
}
|
|
26866
|
+
for (const dir of wanted) {
|
|
26867
|
+
if (this.watchers.has(dir)) continue;
|
|
26868
|
+
try {
|
|
26869
|
+
const watcher = watch(dir, () => this.scheduleReload());
|
|
26870
|
+
watcher.on("error", () => {
|
|
26871
|
+
watcher.close();
|
|
26872
|
+
this.watchers.delete(dir);
|
|
26873
|
+
});
|
|
26874
|
+
this.watchers.set(dir, watcher);
|
|
26875
|
+
} catch (error) {
|
|
26876
|
+
this.deps.log?.(`automations: cannot watch ${dir}: ${errorMessage$1(error)}`);
|
|
26877
|
+
}
|
|
26878
|
+
}
|
|
26879
|
+
}
|
|
26880
|
+
scheduleReload() {
|
|
26881
|
+
if (this.timer) clearTimeout(this.timer);
|
|
26882
|
+
this.timer = setTimeout(() => {
|
|
26883
|
+
this.timer = void 0;
|
|
26884
|
+
try {
|
|
26885
|
+
this.reload();
|
|
26886
|
+
} catch (error) {
|
|
26887
|
+
this.deps.log?.(`automations: reload failed: ${errorMessage$1(error)}`);
|
|
26888
|
+
}
|
|
26889
|
+
}, 200);
|
|
26890
|
+
this.timer.unref();
|
|
26891
|
+
}
|
|
26892
|
+
};
|
|
26893
|
+
/** True when a project can run a template (GitHub-only templates need a GitHub project). */
|
|
26894
|
+
function templateFits(template, project) {
|
|
26895
|
+
return !template.requiresGithub || project.kind === "github";
|
|
26896
|
+
}
|
|
26897
|
+
// ../server/src/services/automations/definitions.ts
|
|
26898
|
+
/** Id of a project's starter automation created from a template. */
|
|
26899
|
+
function starterId(projectId, templateId) {
|
|
26900
|
+
return `starter:${projectId}:${templateId}`;
|
|
26901
|
+
}
|
|
26902
|
+
/** The fields that decide what an automation does, when and how often. */
|
|
26903
|
+
const BEHAVIOUR_FIELDS = [
|
|
26904
|
+
"trigger",
|
|
26905
|
+
"action",
|
|
26906
|
+
"guards"
|
|
26907
|
+
];
|
|
26908
|
+
/**
|
|
26909
|
+
* Database-stored automations (§10): create (from a template or from scratch), update,
|
|
26910
|
+
* delete, enable and disable, and the starter set offered to new projects. File-based
|
|
26911
|
+
* automations are read-only here except for their enabled switch.
|
|
26912
|
+
*/
|
|
26913
|
+
var AutomationDefinitions = class {
|
|
26914
|
+
deps;
|
|
26915
|
+
constructor(deps) {
|
|
26916
|
+
this.deps = deps;
|
|
26917
|
+
}
|
|
26918
|
+
all() {
|
|
26919
|
+
return this.deps.db.select().from(automations).all().map(rowToAutomation);
|
|
26920
|
+
}
|
|
26921
|
+
find(id) {
|
|
26922
|
+
const row = this.deps.db.select().from(automations).where(eq(automations.id, id)).get();
|
|
26923
|
+
return row ? rowToAutomation(row) : void 0;
|
|
26924
|
+
}
|
|
26925
|
+
get(id) {
|
|
26926
|
+
const a = this.find(id);
|
|
26927
|
+
if (!a) throw notFound(`No automation ${id}`, "automation_not_found");
|
|
26928
|
+
return a;
|
|
26929
|
+
}
|
|
26930
|
+
create(req, opts = {}) {
|
|
26931
|
+
const id = opts.id ?? randomUUID();
|
|
26932
|
+
const template = req.templateId ? this.template(req.templateId) : void 0;
|
|
26933
|
+
if (req.projectId) this.deps.projects.get(req.projectId);
|
|
26934
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
26935
|
+
const trigger = req.trigger ?? template?.trigger;
|
|
26936
|
+
const action = req.action ?? template?.action;
|
|
26937
|
+
const name = req.name ?? template?.name;
|
|
26938
|
+
if (!trigger || !action || !name) throw badRequest("name, trigger and action are required");
|
|
26939
|
+
const automation = Automation.parse({
|
|
26940
|
+
id,
|
|
26941
|
+
name,
|
|
26942
|
+
description: req.description !== void 0 ? req.description : template?.description ?? null,
|
|
26943
|
+
enabled: req.enabled,
|
|
26944
|
+
source: "database",
|
|
26945
|
+
templateId: template?.id ?? null,
|
|
26946
|
+
trigger,
|
|
26947
|
+
action,
|
|
26948
|
+
target: req.projectId ? {
|
|
26949
|
+
type: "project",
|
|
26950
|
+
projectId: req.projectId
|
|
26951
|
+
} : { type: "all_projects" },
|
|
26952
|
+
guards: AutomationGuards.parse({
|
|
26953
|
+
...template?.guards,
|
|
26954
|
+
...req.guards
|
|
26955
|
+
}),
|
|
26956
|
+
workKey: req.workKey ?? template?.workKey,
|
|
26957
|
+
...opts.createdBy && opts.createdBy !== "user" ? { createdBy: opts.createdBy } : {},
|
|
26958
|
+
createdAt: now,
|
|
26959
|
+
updatedAt: now
|
|
26960
|
+
});
|
|
26961
|
+
validateAutomation(automation);
|
|
26962
|
+
return this.save(automation);
|
|
26963
|
+
}
|
|
26964
|
+
update(id, req, opts = {}) {
|
|
26965
|
+
const current = this.editable(id);
|
|
26966
|
+
if (req.target?.type === "project") this.deps.projects.get(req.target.projectId);
|
|
26967
|
+
const { workKey: _drop, ...rest } = current;
|
|
26968
|
+
const workKey = req.workKey === null ? void 0 : req.workKey ?? current.workKey;
|
|
26969
|
+
const next = Automation.parse({
|
|
26970
|
+
...rest,
|
|
26971
|
+
...req.name !== void 0 ? { name: req.name } : {},
|
|
26972
|
+
...req.description !== void 0 ? { description: req.description } : {},
|
|
26973
|
+
...req.trigger ? { trigger: req.trigger } : {},
|
|
26974
|
+
...req.action ? { action: req.action } : {},
|
|
26975
|
+
...req.target ? { target: req.target } : {},
|
|
26976
|
+
guards: AutomationGuards.parse({
|
|
26977
|
+
...current.guards,
|
|
26978
|
+
...req.guards
|
|
26979
|
+
}),
|
|
26980
|
+
...workKey ? { workKey } : {},
|
|
26981
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
26982
|
+
});
|
|
26983
|
+
validateAutomation(next);
|
|
26984
|
+
const switchOff = opts.switchOffOnChange && next.enabled && BEHAVIOUR_FIELDS.some((key) => req[key] !== void 0 && !isDeepStrictEqual(current[key], next[key]));
|
|
26985
|
+
return this.save(switchOff ? {
|
|
26986
|
+
...next,
|
|
26987
|
+
enabled: false
|
|
26988
|
+
} : next);
|
|
26989
|
+
}
|
|
26990
|
+
remove(id) {
|
|
26991
|
+
this.editable(id);
|
|
26992
|
+
this.deps.store.append([{
|
|
26993
|
+
type: "automation.deleted",
|
|
26994
|
+
sessionId: null,
|
|
26995
|
+
payload: { automationId: id }
|
|
26996
|
+
}]);
|
|
26997
|
+
this.deps.schedules.removeAutomation(id);
|
|
26998
|
+
}
|
|
26999
|
+
/**
|
|
27000
|
+
* Enable or disable. With `projectId` on an automation that targets all projects, only that
|
|
27001
|
+
* project's override changes; otherwise the automation's own switch.
|
|
27002
|
+
*/
|
|
27003
|
+
setEnabled(id, enabled, projectId) {
|
|
27004
|
+
const a = this.get(id);
|
|
27005
|
+
if (projectId) this.deps.projects.get(projectId);
|
|
27006
|
+
if (projectId && a.target.type === "all_projects") {
|
|
27007
|
+
this.deps.schedules.upsert(id, projectId, { enabledOverride: enabled });
|
|
27008
|
+
return a;
|
|
27009
|
+
}
|
|
27010
|
+
if (projectId && a.target.type === "project" && a.target.projectId !== projectId) throw notFound(`Automation ${id} does not belong to that project`, "automation_not_found");
|
|
27011
|
+
if (a.enabled === enabled) return a;
|
|
27012
|
+
return this.save({
|
|
27013
|
+
...a,
|
|
27014
|
+
enabled,
|
|
27015
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
27016
|
+
});
|
|
27017
|
+
}
|
|
27018
|
+
/** Drop a project's override of an all-projects automation; it follows its own switch again. */
|
|
27019
|
+
clearOverride(id, projectId) {
|
|
27020
|
+
const a = this.get(id);
|
|
27021
|
+
this.deps.projects.get(projectId);
|
|
27022
|
+
if (a.target.type !== "all_projects") throw conflict("Only automations for all projects have per-project overrides", "no_override");
|
|
27023
|
+
this.deps.schedules.upsert(id, projectId, { enabledOverride: null });
|
|
27024
|
+
return a;
|
|
27025
|
+
}
|
|
27026
|
+
/**
|
|
27027
|
+
* Create the project's starter set (§10) from the starter templates that fit the project,
|
|
27028
|
+
* keeping existing ones; with `enable`, switch them all on.
|
|
27029
|
+
*/
|
|
27030
|
+
starterSet(projectId, enable) {
|
|
27031
|
+
const project = this.deps.projects.get(projectId);
|
|
27032
|
+
const out = [];
|
|
27033
|
+
for (const template of this.deps.sources.templates()) {
|
|
27034
|
+
if (!template.starter || template.source !== "builtin" || !templateFits(template, project)) continue;
|
|
27035
|
+
const id = starterId(projectId, template.id);
|
|
27036
|
+
const existing = this.find(id);
|
|
27037
|
+
if (existing) {
|
|
27038
|
+
out.push(enable ? this.setEnabled(id, true) : existing);
|
|
27039
|
+
continue;
|
|
27040
|
+
}
|
|
27041
|
+
out.push(this.create({
|
|
27042
|
+
templateId: template.id,
|
|
27043
|
+
projectId,
|
|
27044
|
+
enabled: enable
|
|
27045
|
+
}, { id }));
|
|
27046
|
+
}
|
|
27047
|
+
return out;
|
|
27048
|
+
}
|
|
27049
|
+
/**
|
|
27050
|
+
* An editable database copy of any automation (also file-based and built-in ones), off,
|
|
27051
|
+
* named "<name> (copy)" unless a name is given. Targets `projectId`, else the original's
|
|
27052
|
+
* target. The copy keeps the original's work key, so the two never run the same work at once.
|
|
27053
|
+
*/
|
|
27054
|
+
duplicate(id, req) {
|
|
27055
|
+
const a = this.get(id);
|
|
27056
|
+
const projectId = req.projectId ?? (a.target.type === "project" ? a.target.projectId : void 0);
|
|
27057
|
+
return this.create({
|
|
27058
|
+
projectId,
|
|
27059
|
+
name: req.name ?? `${a.name} (copy)`.slice(0, 200),
|
|
27060
|
+
description: a.description,
|
|
27061
|
+
trigger: a.trigger,
|
|
27062
|
+
action: a.action,
|
|
27063
|
+
guards: a.guards,
|
|
27064
|
+
workKey: a.workKey,
|
|
27065
|
+
enabled: false
|
|
27066
|
+
});
|
|
27067
|
+
}
|
|
27068
|
+
/**
|
|
27069
|
+
* Import shared automation files (§10): re-parse the source with the file parser, then
|
|
27070
|
+
* create each selected valid file as a database automation, always off and marked
|
|
27071
|
+
* `createdBy: import`. Nothing runs.
|
|
27072
|
+
*/
|
|
27073
|
+
importFrom(req) {
|
|
27074
|
+
if (req.projectId) this.deps.projects.get(req.projectId);
|
|
27075
|
+
const select = req.select ? new Set(req.select) : null;
|
|
27076
|
+
const items = previewImport(req.source).filter((i) => !select || select.has(i.fileName));
|
|
27077
|
+
const created = [];
|
|
27078
|
+
const failed = [];
|
|
27079
|
+
for (const item of items) {
|
|
27080
|
+
const draft = item.automation;
|
|
27081
|
+
if (!draft) {
|
|
27082
|
+
failed.push({
|
|
27083
|
+
fileName: item.fileName,
|
|
27084
|
+
error: item.error ?? "Invalid file"
|
|
27085
|
+
});
|
|
27086
|
+
continue;
|
|
27087
|
+
}
|
|
27088
|
+
created.push(this.create({
|
|
27089
|
+
projectId: req.projectId,
|
|
27090
|
+
name: draft.name,
|
|
27091
|
+
description: draft.description,
|
|
27092
|
+
trigger: draft.trigger,
|
|
27093
|
+
action: draft.action,
|
|
27094
|
+
guards: draft.guards,
|
|
27095
|
+
workKey: draft.workKey,
|
|
27096
|
+
enabled: false
|
|
27097
|
+
}, { createdBy: "import" }));
|
|
27098
|
+
}
|
|
27099
|
+
return {
|
|
27100
|
+
created,
|
|
27101
|
+
failed
|
|
27102
|
+
};
|
|
27103
|
+
}
|
|
27104
|
+
/** Parse an import without saving anything. */
|
|
27105
|
+
previewImport(req) {
|
|
27106
|
+
return previewImport(req);
|
|
27107
|
+
}
|
|
27108
|
+
template(id) {
|
|
27109
|
+
const template = this.deps.sources.template(id);
|
|
27110
|
+
if (!template) throw notFound(`No automation template ${id}`, "template_not_found");
|
|
27111
|
+
return template;
|
|
27112
|
+
}
|
|
27113
|
+
editable(id) {
|
|
27114
|
+
const a = this.get(id);
|
|
27115
|
+
if (a.source !== "database") throw conflict("This automation is defined in a file; edit the file instead", "automation_read_only");
|
|
27116
|
+
return a;
|
|
27117
|
+
}
|
|
27118
|
+
save(automation) {
|
|
27119
|
+
this.deps.store.append([{
|
|
27120
|
+
type: "automation.saved",
|
|
27121
|
+
sessionId: null,
|
|
27122
|
+
payload: { automation }
|
|
27123
|
+
}]);
|
|
27124
|
+
return automation;
|
|
27125
|
+
}
|
|
27126
|
+
};
|
|
27127
|
+
// ../server/src/services/automations/pm-change-notice.ts
|
|
27128
|
+
const MAX_VALUE = 120;
|
|
27129
|
+
/** Inbox id of the notice about a PM's change to an automation; the next change replaces it. */
|
|
27130
|
+
function pmChangeInboxId(automationId) {
|
|
27131
|
+
return `inbox:automation-changed:${automationId}`;
|
|
27132
|
+
}
|
|
27133
|
+
/** `path: before → after` for each trigger, action and guard setting that differs. */
|
|
27134
|
+
function automationChanges(before, after) {
|
|
27135
|
+
const was = /* @__PURE__ */ new Map();
|
|
27136
|
+
const now = /* @__PURE__ */ new Map();
|
|
27137
|
+
for (const key of BEHAVIOUR_FIELDS) {
|
|
27138
|
+
flatten(before[key], key, was);
|
|
27139
|
+
flatten(after[key], key, now);
|
|
27140
|
+
}
|
|
27141
|
+
const out = [];
|
|
27142
|
+
for (const path of /* @__PURE__ */ new Set([...was.keys(), ...now.keys()])) {
|
|
27143
|
+
if (was.get(path) === now.get(path)) continue;
|
|
27144
|
+
out.push(`${path}: ${shorten(was.get(path))} → ${shorten(now.get(path))}`);
|
|
27145
|
+
}
|
|
27146
|
+
return out;
|
|
27147
|
+
}
|
|
27148
|
+
function flatten(value, path, out) {
|
|
27149
|
+
if (value === void 0) return;
|
|
27150
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
27151
|
+
for (const [key, v] of Object.entries(value)) flatten(v, `${path}.${key}`, out);
|
|
27152
|
+
return;
|
|
27153
|
+
}
|
|
27154
|
+
out.set(path, JSON.stringify(value));
|
|
27155
|
+
}
|
|
27156
|
+
function shorten(text) {
|
|
27157
|
+
if (text === void 0) return "none";
|
|
27158
|
+
return text.length > MAX_VALUE ? `${text.slice(0, 119)}…` : text;
|
|
27159
|
+
}
|
|
27160
|
+
/**
|
|
27161
|
+
* The inbox item telling the user that the PM changed what an automation that was on does, so
|
|
27162
|
+
* it was switched off (§10): what changed, and that it runs again only once they switch it on.
|
|
27163
|
+
*/
|
|
27164
|
+
function pmChangeInbox(before, after, by, now) {
|
|
27165
|
+
const name = after.name.length > 80 ? `${after.name.slice(0, 79)}…` : after.name;
|
|
27166
|
+
return {
|
|
27167
|
+
id: pmChangeInboxId(after.id),
|
|
27168
|
+
kind: "message",
|
|
27169
|
+
status: "open",
|
|
27170
|
+
priority: "normal",
|
|
27171
|
+
notify: "needs_you",
|
|
27172
|
+
title: `Automation switched off: the project manager changed "${name}"`,
|
|
27173
|
+
body: ["The project manager changed what this automation does, so it was switched off. It runs again only once you switch it on in Automations.", `Changed: ${automationChanges(before, after).join("; ")}.`].join("\n"),
|
|
27174
|
+
projectId: by.projectId,
|
|
27175
|
+
sessionId: by.sessionId,
|
|
27176
|
+
threadId: null,
|
|
27177
|
+
itemId: null,
|
|
27178
|
+
requestId: null,
|
|
27179
|
+
issue: null,
|
|
27180
|
+
pr: null,
|
|
27181
|
+
automationRunId: null,
|
|
27182
|
+
createdAt: now,
|
|
27183
|
+
updatedAt: now,
|
|
27184
|
+
resolvedAt: null,
|
|
27185
|
+
resolution: null
|
|
27186
|
+
};
|
|
27187
|
+
}
|
|
26669
27188
|
// ../server/src/mcp/tools/pm/automation-edit.ts
|
|
26670
27189
|
const fields = {
|
|
26671
27190
|
name: string().trim().min(1).max(200),
|
|
@@ -26709,10 +27228,14 @@ const createAutomation = defineTool({
|
|
|
26709
27228
|
};
|
|
26710
27229
|
}
|
|
26711
27230
|
});
|
|
26712
|
-
/**
|
|
27231
|
+
/**
|
|
27232
|
+
* `update_automation` (§10): change an automation of this project created in CodeDen (not a
|
|
27233
|
+
* file). A change to what an automation that is on does (trigger, action, guards) switches it
|
|
27234
|
+
* off and tells the user, who switches it on again; automations for all projects are the user's.
|
|
27235
|
+
*/
|
|
26713
27236
|
const updateAutomation = defineTool({
|
|
26714
27237
|
name: "update_automation",
|
|
26715
|
-
description: "Change an automation of this project that lives in CodeDen (not a .md file): any of name, description, trigger, action, guards or work key. Validated like automation files.
|
|
27238
|
+
description: "Change an automation of this project that lives in CodeDen (not a .md file): any of name, description, trigger, action, guards or work key. Validated like automation files. Changing the trigger, action or guards of an automation that is on switches it OFF: tell the user, who reviews the change (it is in their inbox) and switches it on again in Automations. Renaming or a new description keeps it as it is. File-based automations are edited in their file; automations for all projects only by the user.",
|
|
26716
27239
|
roles: ["pm"],
|
|
26717
27240
|
scope: "project",
|
|
26718
27241
|
input: object({
|
|
@@ -26724,12 +27247,21 @@ const updateAutomation = defineTool({
|
|
|
26724
27247
|
guards: fields.guards.optional(),
|
|
26725
27248
|
workKey: fields.workKey.optional()
|
|
26726
27249
|
}),
|
|
26727
|
-
handler: async ({ automationId, ...patch }, { services, projectId }) => {
|
|
27250
|
+
handler: async ({ automationId, ...patch }, { services, projectId, sessionId }) => {
|
|
26728
27251
|
const current = services.automations.definitions.get(automationId);
|
|
26729
|
-
if (current.target.type === "
|
|
26730
|
-
|
|
27252
|
+
if (current.target.type === "all_projects") throw new ToolError(`Automation ${automationId} runs in all projects; only the user changes it, in Automations`);
|
|
27253
|
+
if (current.target.projectId !== projectId) throw new ToolError(`Automation ${automationId} belongs to another project`);
|
|
27254
|
+
const updated = services.automations.definitions.update(automationId, patch, { switchOffOnChange: true });
|
|
26731
27255
|
services.automations.refresh();
|
|
26732
|
-
return { updated: summary(updated) };
|
|
27256
|
+
if (!current.enabled || updated.enabled) return { updated: summary(updated) };
|
|
27257
|
+
services.inbox.put(pmChangeInbox(current, updated, {
|
|
27258
|
+
projectId,
|
|
27259
|
+
sessionId
|
|
27260
|
+
}, (/* @__PURE__ */ new Date()).toISOString()));
|
|
27261
|
+
return {
|
|
27262
|
+
updated: summary(updated),
|
|
27263
|
+
note: "Switched off because it was on and you changed what it does. The user reviews the change in their inbox and switches it on again in Automations."
|
|
27264
|
+
};
|
|
26733
27265
|
}
|
|
26734
27266
|
});
|
|
26735
27267
|
// ../server/src/mcp/tools/pm/automations.ts
|
|
@@ -28045,6 +28577,15 @@ function codexUserSkills(home) {
|
|
|
28045
28577
|
const r = homeRoot(home);
|
|
28046
28578
|
return [...skillsIn(r, join(home, ".codex", "skills"), "codex", "user"), ...skillsIn(r, join(home, ".agents", "skills"), "codex", "user")];
|
|
28047
28579
|
}
|
|
28580
|
+
/** Every provider's user skills, plus the project's (the Skills tab's switches). */
|
|
28581
|
+
function listSkills(home, project) {
|
|
28582
|
+
const r = project ? projectRoot(project) : null;
|
|
28583
|
+
return [
|
|
28584
|
+
...skillsIn(homeRoot(home), join(home, ".claude", "skills"), "claude", "user"),
|
|
28585
|
+
...codexUserSkills(home),
|
|
28586
|
+
...project && r ? [...skillsIn(r, join(project.path, ".claude", "skills"), "claude", "project"), ...skillsIn(r, join(project.path, ".agents", "skills"), "codex", "project")] : []
|
|
28587
|
+
];
|
|
28588
|
+
}
|
|
28048
28589
|
/** Dedupe by absolute path (case-insensitively on Windows and macOS), keeping the first. */
|
|
28049
28590
|
function unique(items, key) {
|
|
28050
28591
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -28969,7 +29510,7 @@ function skipSpace(text, at) {
|
|
|
28969
29510
|
return i;
|
|
28970
29511
|
}
|
|
28971
29512
|
/** End offset (exclusive) of the string starting at `at` (an opening quote). */
|
|
28972
|
-
function skipString(text, at) {
|
|
29513
|
+
function skipString$1(text, at) {
|
|
28973
29514
|
let i = at + 1;
|
|
28974
29515
|
while (i < text.length) {
|
|
28975
29516
|
const ch = text[i];
|
|
@@ -28982,14 +29523,14 @@ function skipString(text, at) {
|
|
|
28982
29523
|
/** End offset (exclusive) of the JSON value starting at `at`. */
|
|
28983
29524
|
function skipValue(text, at) {
|
|
28984
29525
|
const ch = text[at];
|
|
28985
|
-
if (ch === "\"") return skipString(text, at);
|
|
29526
|
+
if (ch === "\"") return skipString$1(text, at);
|
|
28986
29527
|
if (ch === "{" || ch === "[") {
|
|
28987
29528
|
let depth = 0;
|
|
28988
29529
|
let i = at;
|
|
28989
29530
|
while (i < text.length) {
|
|
28990
29531
|
const c = text[i];
|
|
28991
29532
|
if (c === "\"") {
|
|
28992
|
-
i = skipString(text, i);
|
|
29533
|
+
i = skipString$1(text, i);
|
|
28993
29534
|
continue;
|
|
28994
29535
|
}
|
|
28995
29536
|
if (c === "{" || c === "[") depth++;
|
|
@@ -29014,7 +29555,7 @@ function scanTopLevel(text) {
|
|
|
29014
29555
|
while (text[i] !== "}") {
|
|
29015
29556
|
if (text[i] !== "\"") throw new Error(`Unexpected character at ${i}`);
|
|
29016
29557
|
const keyStart = i;
|
|
29017
|
-
const keyEnd = skipString(text, i);
|
|
29558
|
+
const keyEnd = skipString$1(text, i);
|
|
29018
29559
|
const key = JSON.parse(text.slice(keyStart, keyEnd));
|
|
29019
29560
|
i = skipSpace(text, keyEnd);
|
|
29020
29561
|
if (text[i] !== ":") throw new Error(`Expected ':' at ${i}`);
|
|
@@ -29080,14 +29621,14 @@ function setTopLevelMember(text, key, value) {
|
|
|
29080
29621
|
return text.slice(0, last.valueEnd) + comma + member + body + text.slice(last.valueEnd);
|
|
29081
29622
|
}
|
|
29082
29623
|
// ../server/src/services/agent-config/hooks.ts
|
|
29083
|
-
const isRecord$
|
|
29084
|
-
const isGroup = (v) => isRecord$
|
|
29624
|
+
const isRecord$2 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
29625
|
+
const isGroup = (v) => isRecord$2(v) && Array.isArray(v.hooks);
|
|
29085
29626
|
/**
|
|
29086
29627
|
* A group's matcher exactly as written (whitespace can matter in a regex); missing or empty
|
|
29087
29628
|
* means "match everything".
|
|
29088
29629
|
*/
|
|
29089
29630
|
const matcherOf = (m) => typeof m === "string" && m !== "" ? m : null;
|
|
29090
|
-
const unparsable = (file, why) => new ServiceError(422, "config_unparsable", `${file} could not be parsed: ${why}`);
|
|
29631
|
+
const unparsable$2 = (file, why) => new ServiceError(422, "config_unparsable", `${file} could not be parsed: ${why}`);
|
|
29091
29632
|
/** Claude Code's settings file for a scope. */
|
|
29092
29633
|
function claudeHookFile(scope, home, project) {
|
|
29093
29634
|
if (scope === "user" || !project) return {
|
|
@@ -29149,7 +29690,7 @@ function unusableSource(f, error) {
|
|
|
29149
29690
|
error
|
|
29150
29691
|
};
|
|
29151
29692
|
}
|
|
29152
|
-
function load$
|
|
29693
|
+
function load$3(file) {
|
|
29153
29694
|
if (!existsSync(file)) return {
|
|
29154
29695
|
text: null,
|
|
29155
29696
|
doc: {}
|
|
@@ -29163,10 +29704,10 @@ function load$1(file) {
|
|
|
29163
29704
|
try {
|
|
29164
29705
|
doc = JSON.parse(text.replace(/^/, ""));
|
|
29165
29706
|
} catch (error) {
|
|
29166
|
-
throw unparsable(file, error instanceof Error ? error.message : String(error));
|
|
29707
|
+
throw unparsable$2(file, error instanceof Error ? error.message : String(error));
|
|
29167
29708
|
}
|
|
29168
|
-
if (!isRecord$
|
|
29169
|
-
if (doc.hooks !== void 0 && !isRecord$
|
|
29709
|
+
if (!isRecord$2(doc)) throw unparsable$2(file, "not a JSON object");
|
|
29710
|
+
if (doc.hooks !== void 0 && !isRecord$2(doc.hooks)) throw unparsable$2(file, "`hooks` is not an object");
|
|
29170
29711
|
return {
|
|
29171
29712
|
text,
|
|
29172
29713
|
doc
|
|
@@ -29182,7 +29723,7 @@ function readHookFile(f) {
|
|
|
29182
29723
|
};
|
|
29183
29724
|
let loaded;
|
|
29184
29725
|
try {
|
|
29185
|
-
loaded = load$
|
|
29726
|
+
loaded = load$3(f.file);
|
|
29186
29727
|
} catch (error) {
|
|
29187
29728
|
return {
|
|
29188
29729
|
source: unusableSource(f, error instanceof Error ? error.message : String(error)),
|
|
@@ -29197,7 +29738,7 @@ function readHookFile(f) {
|
|
|
29197
29738
|
};
|
|
29198
29739
|
const disableAllHooks = typeof doc.disableAllHooks === "boolean" ? { disableAllHooks: doc.disableAllHooks } : {};
|
|
29199
29740
|
const hooks = [];
|
|
29200
|
-
for (const [event, groups] of Object.entries(isRecord$
|
|
29741
|
+
for (const [event, groups] of Object.entries(isRecord$2(doc.hooks) ? doc.hooks : {})) {
|
|
29201
29742
|
if (!Array.isArray(groups)) continue;
|
|
29202
29743
|
groups.forEach((group, groupIndex) => {
|
|
29203
29744
|
if (!isGroup(group)) return;
|
|
@@ -29225,8 +29766,8 @@ function readHookFile(f) {
|
|
|
29225
29766
|
}
|
|
29226
29767
|
/** The file text before and after applying `mutate` to its `hooks` object. */
|
|
29227
29768
|
function renderHookChange(file, mutate) {
|
|
29228
|
-
const { text, doc } = load$
|
|
29229
|
-
const current = isRecord$
|
|
29769
|
+
const { text, doc } = load$3(file);
|
|
29770
|
+
const current = isRecord$2(doc.hooks) ? doc.hooks : {};
|
|
29230
29771
|
const hooks = structuredClone(current);
|
|
29231
29772
|
mutate(hooks);
|
|
29232
29773
|
const before = text ?? "";
|
|
@@ -29251,7 +29792,7 @@ function groupsOf(hooks, event, file) {
|
|
|
29251
29792
|
hooks[event] = [];
|
|
29252
29793
|
return hooks[event];
|
|
29253
29794
|
}
|
|
29254
|
-
if (!Array.isArray(existing)) throw unparsable(file, `\`hooks.${event}\` is not a list`);
|
|
29795
|
+
if (!Array.isArray(existing)) throw unparsable$2(file, `\`hooks.${event}\` is not a list`);
|
|
29255
29796
|
return existing;
|
|
29256
29797
|
}
|
|
29257
29798
|
/** Append a handler to the first group of `event` with the same matcher, or to a new group. */
|
|
@@ -29270,7 +29811,7 @@ function locate(hooks, file, t) {
|
|
|
29270
29811
|
const groups = hooks[t.event];
|
|
29271
29812
|
const group = Array.isArray(groups) ? groups[t.groupIndex] : void 0;
|
|
29272
29813
|
const current = isGroup(group) ? group.hooks[t.handlerIndex] : void 0;
|
|
29273
|
-
if (!Array.isArray(groups) || !isGroup(group) || !isRecord$
|
|
29814
|
+
if (!Array.isArray(groups) || !isGroup(group) || !isRecord$2(current)) throw notFound(`No such ${t.event} hook in ${file}; reload the list`, "hook_not_found");
|
|
29274
29815
|
if (!isDeepStrictEqual(current, t.expected)) throw conflict(`The hook changed in ${file} since it was listed; reload the list`, "hook_changed");
|
|
29275
29816
|
return {
|
|
29276
29817
|
groups,
|
|
@@ -29478,7 +30019,7 @@ var AgentConfigHooks = class {
|
|
|
29478
30019
|
};
|
|
29479
30020
|
/**
|
|
29480
30021
|
* Refuse (403 `path_outside_root`) a settings file whose real path, following links on the file
|
|
29481
|
-
* and on every existing ancestor, leaves its scope's folder.
|
|
30022
|
+
* and on every existing ancestor, leaves its scope's folder. Also used for skill switches.
|
|
29482
30023
|
*/
|
|
29483
30024
|
function guard(f) {
|
|
29484
30025
|
try {
|
|
@@ -36175,7 +36716,7 @@ function qo(e, t) {
|
|
|
36175
36716
|
return n.leadingBom ? `${i}` : i;
|
|
36176
36717
|
}
|
|
36177
36718
|
// ../server/src/services/agent-config/toml-edit.ts
|
|
36178
|
-
const isTable = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date);
|
|
36719
|
+
const isTable$1 = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date);
|
|
36179
36720
|
const bare = (key) => /^[A-Za-z0-9_-]+$/.test(key) ? key : JSON.stringify(key);
|
|
36180
36721
|
const escapeRe = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
36181
36722
|
/** `[mcp_servers.<name>]` with scalars and arrays, then one sub-table per flat map (env, headers). */
|
|
@@ -36184,11 +36725,11 @@ function serverBlock(name, server) {
|
|
|
36184
36725
|
const lines = [`[${head}]`];
|
|
36185
36726
|
const tables = [];
|
|
36186
36727
|
for (const [key, value] of Object.entries(server)) {
|
|
36187
|
-
if (!isTable(value)) {
|
|
36728
|
+
if (!isTable$1(value)) {
|
|
36188
36729
|
lines.push(qo({ [key]: value }).trim());
|
|
36189
36730
|
continue;
|
|
36190
36731
|
}
|
|
36191
|
-
if (Object.values(value).some(isTable)) return null;
|
|
36732
|
+
if (Object.values(value).some(isTable$1)) return null;
|
|
36192
36733
|
tables.push("", `[${head}.${bare(key)}]`);
|
|
36193
36734
|
for (const [k, v] of Object.entries(value)) tables.push(qo({ [k]: v }).trim());
|
|
36194
36735
|
}
|
|
@@ -36269,7 +36810,7 @@ function trimBlankRuns(lines) {
|
|
|
36269
36810
|
*/
|
|
36270
36811
|
function editTomlServer(text, name, server) {
|
|
36271
36812
|
const doc = Ko(text);
|
|
36272
|
-
const servers = isTable(doc.mcp_servers) ? { ...doc.mcp_servers } : {};
|
|
36813
|
+
const servers = isTable$1(doc.mcp_servers) ? { ...doc.mcp_servers } : {};
|
|
36273
36814
|
if (server) servers[name] = server;
|
|
36274
36815
|
else delete servers[name];
|
|
36275
36816
|
const expected = {
|
|
@@ -36279,14 +36820,14 @@ function editTomlServer(text, name, server) {
|
|
|
36279
36820
|
if (!server && Object.keys(servers).length === 0) delete expected.mcp_servers;
|
|
36280
36821
|
const edited = blockEdit(text, name, server);
|
|
36281
36822
|
if (edited !== null) try {
|
|
36282
|
-
if (isDeepStrictEqual(normalize(Ko(edited)), normalize(expected))) return edited;
|
|
36823
|
+
if (isDeepStrictEqual(normalize$1(Ko(edited)), normalize$1(expected))) return edited;
|
|
36283
36824
|
} catch {}
|
|
36284
36825
|
return Ja(text, expected);
|
|
36285
36826
|
}
|
|
36286
36827
|
/** Plain JSON view for comparisons (dates become strings, empty `mcp_servers` dropped). */
|
|
36287
|
-
function normalize(value) {
|
|
36828
|
+
function normalize$1(value) {
|
|
36288
36829
|
const plain = JSON.parse(JSON.stringify(value));
|
|
36289
|
-
if (isTable(plain.mcp_servers) && Object.keys(plain.mcp_servers).length === 0) delete plain.mcp_servers;
|
|
36830
|
+
if (isTable$1(plain.mcp_servers) && Object.keys(plain.mcp_servers).length === 0) delete plain.mcp_servers;
|
|
36290
36831
|
return plain;
|
|
36291
36832
|
}
|
|
36292
36833
|
// ../server/src/services/agent-config/mcp.ts
|
|
@@ -36308,8 +36849,8 @@ const CODEX_TRANSPORT = [
|
|
|
36308
36849
|
"bearer_token_env_var",
|
|
36309
36850
|
"enabled"
|
|
36310
36851
|
];
|
|
36311
|
-
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
36312
|
-
const strings = (v) => isRecord(v) ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, String(x)])) : void 0;
|
|
36852
|
+
const isRecord$1 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
36853
|
+
const strings = (v) => isRecord$1(v) ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, String(x)])) : void 0;
|
|
36313
36854
|
const stringList = (v) => Array.isArray(v) ? v.map((x) => String(x)) : void 0;
|
|
36314
36855
|
const omit$1 = (o, keys) => Object.fromEntries(Object.entries(o).filter(([k]) => !keys.includes(k)));
|
|
36315
36856
|
const compact = (o) => Object.fromEntries(Object.entries(o).filter(([, v]) => v !== void 0));
|
|
@@ -36376,11 +36917,11 @@ function toNative(provider, spec, existing = {}) {
|
|
|
36376
36917
|
/** `~/.claude.json` keys projects by path with forward slashes on Windows. */
|
|
36377
36918
|
function claudeProjectKey(doc, projectPath) {
|
|
36378
36919
|
const key = process.platform === "win32" ? projectPath.replace(/\\/g, "/") : projectPath;
|
|
36379
|
-
const projects = isRecord(doc.projects) ? doc.projects : {};
|
|
36920
|
+
const projects = isRecord$1(doc.projects) ? doc.projects : {};
|
|
36380
36921
|
const fold = (s) => process.platform === "linux" ? s : s.toLowerCase();
|
|
36381
36922
|
return Object.keys(projects).find((k) => fold(k) === fold(key)) ?? key;
|
|
36382
36923
|
}
|
|
36383
|
-
function load(source) {
|
|
36924
|
+
function load$2(source) {
|
|
36384
36925
|
if (!existsSync(source.file)) return {
|
|
36385
36926
|
text: null,
|
|
36386
36927
|
doc: {}
|
|
@@ -36388,7 +36929,7 @@ function load(source) {
|
|
|
36388
36929
|
const text = readFileSync(source.file, "utf8");
|
|
36389
36930
|
try {
|
|
36390
36931
|
const doc = source.format === "json" ? JSON.parse(text) : Ko(text);
|
|
36391
|
-
if (!isRecord(doc)) throw new Error("not an object");
|
|
36932
|
+
if (!isRecord$1(doc)) throw new Error("not an object");
|
|
36392
36933
|
return {
|
|
36393
36934
|
text,
|
|
36394
36935
|
doc
|
|
@@ -36401,7 +36942,7 @@ function tableAt(doc, keyPath, create) {
|
|
|
36401
36942
|
let node = doc;
|
|
36402
36943
|
for (const key of keyPath) {
|
|
36403
36944
|
const next = node[key];
|
|
36404
|
-
if (!isRecord(next)) {
|
|
36945
|
+
if (!isRecord$1(next)) {
|
|
36405
36946
|
if (!create) return void 0;
|
|
36406
36947
|
node[key] = {};
|
|
36407
36948
|
}
|
|
@@ -36411,8 +36952,8 @@ function tableAt(doc, keyPath, create) {
|
|
|
36411
36952
|
}
|
|
36412
36953
|
/** The servers defined in one source. */
|
|
36413
36954
|
function readServers(source) {
|
|
36414
|
-
const table = tableAt(load(source).doc, source.keyPath, false) ?? {};
|
|
36415
|
-
return Object.entries(table).filter(([, native]) => isRecord(native)).map(([name, native]) => ({
|
|
36955
|
+
const table = tableAt(load$2(source).doc, source.keyPath, false) ?? {};
|
|
36956
|
+
return Object.entries(table).filter(([, native]) => isRecord$1(native)).map(([name, native]) => ({
|
|
36416
36957
|
provider: source.provider,
|
|
36417
36958
|
scope: source.scope,
|
|
36418
36959
|
name,
|
|
@@ -36423,12 +36964,12 @@ function readServers(source) {
|
|
|
36423
36964
|
}
|
|
36424
36965
|
/** The new file text for an upsert (`spec`) or removal (no `spec`). Pure apart from reading. */
|
|
36425
36966
|
function renderChange(source, name, spec) {
|
|
36426
|
-
const { text, doc } = load(source);
|
|
36967
|
+
const { text, doc } = load$2(source);
|
|
36427
36968
|
const table = tableAt(doc, source.keyPath, spec !== void 0);
|
|
36428
36969
|
const current = table?.[name];
|
|
36429
36970
|
if (!spec && !(table && name in table)) throw new ServiceError(404, "server_not_found", `No MCP server named ${name} in ${source.file}`);
|
|
36430
|
-
const native = spec ? toNative(source.provider, spec, isRecord(current) ? current : {}) : void 0;
|
|
36431
|
-
if (native && isRecord(current) && isDeepStrictEqual(native, current)) return {
|
|
36971
|
+
const native = spec ? toNative(source.provider, spec, isRecord$1(current) ? current : {}) : void 0;
|
|
36972
|
+
if (native && isRecord$1(current) && isDeepStrictEqual(native, current)) return {
|
|
36432
36973
|
before: text ?? "",
|
|
36433
36974
|
after: text ?? "",
|
|
36434
36975
|
existed: text !== null
|
|
@@ -36498,7 +37039,7 @@ function sourceFor(target, home, project) {
|
|
|
36498
37039
|
format: "json",
|
|
36499
37040
|
keyPath: [
|
|
36500
37041
|
"projects",
|
|
36501
|
-
claudeProjectKey(existsSync(claudeJson) ? load({
|
|
37042
|
+
claudeProjectKey(existsSync(claudeJson) ? load$2({
|
|
36502
37043
|
file: claudeJson,
|
|
36503
37044
|
format: "json"
|
|
36504
37045
|
}).doc : {}, project.path),
|
|
@@ -36507,6 +37048,540 @@ function sourceFor(target, home, project) {
|
|
|
36507
37048
|
projectId: project.id
|
|
36508
37049
|
};
|
|
36509
37050
|
}
|
|
37051
|
+
// ../server/src/services/agent-config/skill-overrides.ts
|
|
37052
|
+
const VALUES = /* @__PURE__ */ new Set([
|
|
37053
|
+
"on",
|
|
37054
|
+
"name-only",
|
|
37055
|
+
"user-invocable-only",
|
|
37056
|
+
"off"
|
|
37057
|
+
]);
|
|
37058
|
+
const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
37059
|
+
const unparsable$1 = (file, why) => new ServiceError(422, "config_unparsable", `${file} could not be parsed: ${why}`);
|
|
37060
|
+
function load$1(file) {
|
|
37061
|
+
if (!existsSync(file)) return {
|
|
37062
|
+
text: null,
|
|
37063
|
+
doc: {},
|
|
37064
|
+
overrides: {}
|
|
37065
|
+
};
|
|
37066
|
+
const text = readFileSync(file, "utf8");
|
|
37067
|
+
if (!text.trim()) return {
|
|
37068
|
+
text,
|
|
37069
|
+
doc: {},
|
|
37070
|
+
overrides: {}
|
|
37071
|
+
};
|
|
37072
|
+
let doc;
|
|
37073
|
+
try {
|
|
37074
|
+
doc = JSON.parse(text.replace(/^/, ""));
|
|
37075
|
+
} catch (error) {
|
|
37076
|
+
throw unparsable$1(file, errorMessage$1(error));
|
|
37077
|
+
}
|
|
37078
|
+
if (!isRecord(doc)) throw unparsable$1(file, "not a JSON object");
|
|
37079
|
+
const overrides = isRecord(doc.skillOverrides) ? doc.skillOverrides : {};
|
|
37080
|
+
return {
|
|
37081
|
+
text,
|
|
37082
|
+
doc,
|
|
37083
|
+
overrides
|
|
37084
|
+
};
|
|
37085
|
+
}
|
|
37086
|
+
/**
|
|
37087
|
+
* Read a settings file's overrides. For listing, a file that can't be used sets nothing. With
|
|
37088
|
+
* `strict` (planning a switch) its error is thrown instead: 403 `path_outside_root` for a link
|
|
37089
|
+
* leading outside its folder, 422 `config_unparsable` for a file that doesn't parse. A switch
|
|
37090
|
+
* must never pick its target while a file that applies can't be read, or it could write the
|
|
37091
|
+
* wrong file.
|
|
37092
|
+
*/
|
|
37093
|
+
function readOverrides(source, strict = false) {
|
|
37094
|
+
try {
|
|
37095
|
+
guard(source);
|
|
37096
|
+
const { overrides } = load$1(source.file);
|
|
37097
|
+
const known = Object.entries(overrides).filter((e) => typeof e[1] === "string" && VALUES.has(e[1]));
|
|
37098
|
+
return {
|
|
37099
|
+
source,
|
|
37100
|
+
overrides: new Map(known)
|
|
37101
|
+
};
|
|
37102
|
+
} catch (error) {
|
|
37103
|
+
if (strict) throw error;
|
|
37104
|
+
return {
|
|
37105
|
+
source,
|
|
37106
|
+
overrides: /* @__PURE__ */ new Map(),
|
|
37107
|
+
error: errorMessage$1(error)
|
|
37108
|
+
};
|
|
37109
|
+
}
|
|
37110
|
+
}
|
|
37111
|
+
/**
|
|
37112
|
+
* Settings files from highest precedence to lowest, given them lowest first (user, project,
|
|
37113
|
+
* local…): local files keep their order (the repository-root one wins over an older one in the
|
|
37114
|
+
* working folder), then project, then user.
|
|
37115
|
+
*/
|
|
37116
|
+
function highestFirst(files) {
|
|
37117
|
+
return [...files.filter((f) => f.source.scope === "local"), ...files.filter((f) => f.source.scope !== "local").reverse()];
|
|
37118
|
+
}
|
|
37119
|
+
/** The file (highest precedence first) that decides a skill name, and its value. */
|
|
37120
|
+
function decidingOverride(highest, name) {
|
|
37121
|
+
for (const file of highest) {
|
|
37122
|
+
const value = file.overrides.get(name);
|
|
37123
|
+
if (value) return {
|
|
37124
|
+
file,
|
|
37125
|
+
value
|
|
37126
|
+
};
|
|
37127
|
+
}
|
|
37128
|
+
return null;
|
|
37129
|
+
}
|
|
37130
|
+
/** The value to write for a switch, and where (`userFile` when no file decides the name). */
|
|
37131
|
+
function planOverride(files, userFile, name, enabled) {
|
|
37132
|
+
const highest = highestFirst(files);
|
|
37133
|
+
const deciding = decidingOverride(highest, name);
|
|
37134
|
+
const target = deciding?.file.source ?? userFile;
|
|
37135
|
+
if (!enabled) return {
|
|
37136
|
+
target,
|
|
37137
|
+
value: "off"
|
|
37138
|
+
};
|
|
37139
|
+
const lower = decidingOverride(deciding ? highest.slice(highest.indexOf(deciding.file) + 1) : [], name)?.value;
|
|
37140
|
+
return {
|
|
37141
|
+
target,
|
|
37142
|
+
value: lower && lower !== "on" ? "on" : void 0
|
|
37143
|
+
};
|
|
37144
|
+
}
|
|
37145
|
+
/**
|
|
37146
|
+
* The file text before and after setting (or, with `value` undefined, removing) one skill's
|
|
37147
|
+
* entry in `skillOverrides`. Everything outside that value is kept byte for byte.
|
|
37148
|
+
*/
|
|
37149
|
+
function renderOverride(file, name, value) {
|
|
37150
|
+
const { text, overrides } = load$1(file);
|
|
37151
|
+
const entries = Object.entries(overrides);
|
|
37152
|
+
const at = entries.findIndex(([key]) => key === name);
|
|
37153
|
+
if (value === void 0) {
|
|
37154
|
+
if (at >= 0) entries.splice(at, 1);
|
|
37155
|
+
} else if (at >= 0) entries[at] = [name, value];
|
|
37156
|
+
else entries.push([name, value]);
|
|
37157
|
+
const next = Object.fromEntries(entries);
|
|
37158
|
+
const before = text ?? "";
|
|
37159
|
+
const existed = text !== null;
|
|
37160
|
+
if (isDeepStrictEqual(next, overrides)) return {
|
|
37161
|
+
before,
|
|
37162
|
+
after: before,
|
|
37163
|
+
existed,
|
|
37164
|
+
changed: false
|
|
37165
|
+
};
|
|
37166
|
+
return {
|
|
37167
|
+
before,
|
|
37168
|
+
after: text?.trim() ? setTopLevelMember(text, "skillOverrides", next) : `${JSON.stringify({ skillOverrides: next }, null, 2)}\n`,
|
|
37169
|
+
existed,
|
|
37170
|
+
changed: true
|
|
37171
|
+
};
|
|
37172
|
+
}
|
|
37173
|
+
// ../server/src/services/agent-config/toml-comments.ts
|
|
37174
|
+
/** End offset (exclusive) of a multi-line string whose body starts at `at`. */
|
|
37175
|
+
function skipMultiline(text, at, quote) {
|
|
37176
|
+
let i = at;
|
|
37177
|
+
while (i < text.length) {
|
|
37178
|
+
if (quote === "\"\"\"" && text[i] === "\\") {
|
|
37179
|
+
i += 2;
|
|
37180
|
+
continue;
|
|
37181
|
+
}
|
|
37182
|
+
if (text.startsWith(quote, i)) {
|
|
37183
|
+
let end = i + 3;
|
|
37184
|
+
while (end < text.length && end < i + 5 && text[end] === quote[0]) end++;
|
|
37185
|
+
return end;
|
|
37186
|
+
}
|
|
37187
|
+
i++;
|
|
37188
|
+
}
|
|
37189
|
+
return text.length;
|
|
37190
|
+
}
|
|
37191
|
+
/** End offset (exclusive) of a one-line string starting at the quote at `at`. */
|
|
37192
|
+
function skipString(text, at) {
|
|
37193
|
+
const quote = text[at];
|
|
37194
|
+
let i = at + 1;
|
|
37195
|
+
while (i < text.length && text[i] !== "\n") {
|
|
37196
|
+
if (quote === "\"" && text[i] === "\\") {
|
|
37197
|
+
i += 2;
|
|
37198
|
+
continue;
|
|
37199
|
+
}
|
|
37200
|
+
if (text[i] === quote) return i + 1;
|
|
37201
|
+
i++;
|
|
37202
|
+
}
|
|
37203
|
+
return i;
|
|
37204
|
+
}
|
|
37205
|
+
/**
|
|
37206
|
+
* Every comment in a TOML text, whole-line or after a value, in order. A `#` inside a basic,
|
|
37207
|
+
* literal or multi-line string is not a comment.
|
|
37208
|
+
*/
|
|
37209
|
+
function tomlComments(text) {
|
|
37210
|
+
const out = [];
|
|
37211
|
+
let i = 0;
|
|
37212
|
+
while (i < text.length) if (text.startsWith("\"\"\"", i)) i = skipMultiline(text, i + 3, "\"\"\"");
|
|
37213
|
+
else if (text.startsWith("'''", i)) i = skipMultiline(text, i + 3, "'''");
|
|
37214
|
+
else if (text[i] === "\"" || text[i] === "'") i = skipString(text, i);
|
|
37215
|
+
else if (text[i] === "#") {
|
|
37216
|
+
const newline = text.indexOf("\n", i);
|
|
37217
|
+
const end = newline < 0 ? text.length : newline;
|
|
37218
|
+
out.push({
|
|
37219
|
+
start: i,
|
|
37220
|
+
text: text.slice(i, end).replace(/\r$/, "").trimEnd()
|
|
37221
|
+
});
|
|
37222
|
+
i = end;
|
|
37223
|
+
} else i++;
|
|
37224
|
+
return out;
|
|
37225
|
+
}
|
|
37226
|
+
/** The comment that ends one line, if any (the line read on its own). */
|
|
37227
|
+
function lineComment(line) {
|
|
37228
|
+
return tomlComments(line).at(-1)?.text ?? null;
|
|
37229
|
+
}
|
|
37230
|
+
/** True when both texts hold the same comments, each as often (order and position aside). */
|
|
37231
|
+
function sameComments(before, after) {
|
|
37232
|
+
const list = (text) => tomlComments(text).map((c) => c.text).sort();
|
|
37233
|
+
const a = list(before);
|
|
37234
|
+
const b = list(after);
|
|
37235
|
+
return a.length === b.length && a.every((c, i) => c === b[i]);
|
|
37236
|
+
}
|
|
37237
|
+
// ../server/src/services/agent-config/skill-rules.ts
|
|
37238
|
+
const isTable = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && !(v instanceof Date);
|
|
37239
|
+
const fold$1 = (p) => {
|
|
37240
|
+
const abs = resolve(p);
|
|
37241
|
+
return process.platform === "linux" ? abs : abs.toLowerCase();
|
|
37242
|
+
};
|
|
37243
|
+
/** True when a rule entry selects the skill: by path when it has one, else by name. */
|
|
37244
|
+
function selects(entry, skill) {
|
|
37245
|
+
if (!isTable(entry) || typeof entry.enabled !== "boolean") return false;
|
|
37246
|
+
if (typeof entry.path === "string") return fold$1(entry.path) === fold$1(skill.path);
|
|
37247
|
+
return typeof entry.name === "string" && entry.name === skill.name;
|
|
37248
|
+
}
|
|
37249
|
+
const unparsable = (file, why) => new ServiceError(422, "config_unparsable", `${file} could not be parsed: ${why}`);
|
|
37250
|
+
function load(file) {
|
|
37251
|
+
if (!existsSync(file)) return {
|
|
37252
|
+
text: null,
|
|
37253
|
+
doc: {},
|
|
37254
|
+
rules: []
|
|
37255
|
+
};
|
|
37256
|
+
const text = readFileSync(file, "utf8");
|
|
37257
|
+
let doc;
|
|
37258
|
+
try {
|
|
37259
|
+
doc = Ko(text);
|
|
37260
|
+
} catch (error) {
|
|
37261
|
+
throw unparsable(file, errorMessage$1(error));
|
|
37262
|
+
}
|
|
37263
|
+
if (!isTable(doc)) throw unparsable(file, "not a table");
|
|
37264
|
+
if (doc.skills !== void 0 && !isTable(doc.skills)) throw unparsable(file, "`skills` is not a table");
|
|
37265
|
+
const config = isTable(doc.skills) ? doc.skills.config : void 0;
|
|
37266
|
+
if (config !== void 0 && !Array.isArray(config)) throw unparsable(file, "`skills.config` is not a list");
|
|
37267
|
+
return {
|
|
37268
|
+
text,
|
|
37269
|
+
doc,
|
|
37270
|
+
rules: config ?? []
|
|
37271
|
+
};
|
|
37272
|
+
}
|
|
37273
|
+
/** The rules in a config file; none when it is missing or can't be parsed. */
|
|
37274
|
+
function readRules(file) {
|
|
37275
|
+
try {
|
|
37276
|
+
return load(file).rules;
|
|
37277
|
+
} catch {
|
|
37278
|
+
return [];
|
|
37279
|
+
}
|
|
37280
|
+
}
|
|
37281
|
+
/** Whether the rules leave a skill on, and whether any of them selects it. */
|
|
37282
|
+
function ruleState(rules, skill) {
|
|
37283
|
+
let enabled = true;
|
|
37284
|
+
let set = false;
|
|
37285
|
+
for (const entry of rules) {
|
|
37286
|
+
if (!selects(entry, skill)) continue;
|
|
37287
|
+
enabled = entry.enabled === true;
|
|
37288
|
+
set = true;
|
|
37289
|
+
}
|
|
37290
|
+
return {
|
|
37291
|
+
enabled,
|
|
37292
|
+
set
|
|
37293
|
+
};
|
|
37294
|
+
}
|
|
37295
|
+
/**
|
|
37296
|
+
* What a switch does to the rules. Off: drop this skill's own entries of the selector used and
|
|
37297
|
+
* append one with `enabled = false`, by path for a user skill and by name for a project skill
|
|
37298
|
+
* (a project skill's path differs in every worktree). On: drop every entry that selects it.
|
|
37299
|
+
*/
|
|
37300
|
+
function ruleChange(rules, skill, byPath, enabled) {
|
|
37301
|
+
const indices = (pick) => new Set(rules.flatMap((entry, i) => pick(entry) ? [i] : []));
|
|
37302
|
+
if (enabled) return {
|
|
37303
|
+
drop: indices((entry) => selects(entry, skill)),
|
|
37304
|
+
append: null
|
|
37305
|
+
};
|
|
37306
|
+
const same = (entry) => isTable(entry) && (byPath ? typeof entry.path === "string" && fold$1(entry.path) === fold$1(skill.path) : entry.path === void 0 && entry.name === skill.name);
|
|
37307
|
+
const append = byPath ? {
|
|
37308
|
+
path: skill.path,
|
|
37309
|
+
enabled: false
|
|
37310
|
+
} : {
|
|
37311
|
+
name: skill.name,
|
|
37312
|
+
enabled: false
|
|
37313
|
+
};
|
|
37314
|
+
return {
|
|
37315
|
+
drop: indices(same),
|
|
37316
|
+
append
|
|
37317
|
+
};
|
|
37318
|
+
}
|
|
37319
|
+
/** The rule list after a change. */
|
|
37320
|
+
function applyRuleChange(rules, change) {
|
|
37321
|
+
const kept = rules.filter((_, i) => !change.drop.has(i));
|
|
37322
|
+
return change.append ? [...kept, change.append] : kept;
|
|
37323
|
+
}
|
|
37324
|
+
/** One `[[skills.config]]` block, as lines. */
|
|
37325
|
+
function block(entry) {
|
|
37326
|
+
return ["[[skills.config]]", ...Object.entries(entry).map(([k, v]) => qo({ [k]: v }).trim())];
|
|
37327
|
+
}
|
|
37328
|
+
const TABLE_HEADER = /^\s*\[/;
|
|
37329
|
+
const RULE_HEADER = /^\s*\[\[\s*skills\s*\.\s*config\s*\]\]\s*(#.*)?$/;
|
|
37330
|
+
/** Whole-line comment and blank lines: never removed by a text edit. */
|
|
37331
|
+
const KEPT = /^\s*(#.*)?$/;
|
|
37332
|
+
const isBlank = (line) => line !== void 0 && line.trim() === "";
|
|
37333
|
+
/**
|
|
37334
|
+
* Edit the `[[skills.config]]` blocks as text: a dropped entry loses its header and setting
|
|
37335
|
+
* lines, while every comment and blank line stays. A comment at the end of a dropped line stays
|
|
37336
|
+
* on a line of its own, indented like it (blank lines left doubled by the removal are
|
|
37337
|
+
* collapsed). An appended entry goes after the last remaining block, or at the end of the file.
|
|
37338
|
+
* Returns null when the entries aren't all written as such blocks (an inline array, say).
|
|
37339
|
+
*/
|
|
37340
|
+
function textEdit(text, rules, change) {
|
|
37341
|
+
const eol = text.includes("\r\n") ? "\r\n" : "\n";
|
|
37342
|
+
const lines = text === "" ? [] : text.split(/\r?\n/);
|
|
37343
|
+
const starts = lines.flatMap((line, i) => RULE_HEADER.test(line) ? [i] : []);
|
|
37344
|
+
if (starts.length !== rules.length) return null;
|
|
37345
|
+
/** Dropped lines, each with what replaces it: its own comment, or nothing. */
|
|
37346
|
+
const dropped = /* @__PURE__ */ new Map();
|
|
37347
|
+
const drop = (i) => {
|
|
37348
|
+
const line = lines[i] ?? "";
|
|
37349
|
+
const comment = lineComment(line);
|
|
37350
|
+
dropped.set(i, comment === null ? null : `${/^\s*/.exec(line)?.[0] ?? ""}${comment}`);
|
|
37351
|
+
};
|
|
37352
|
+
starts.forEach((start, k) => {
|
|
37353
|
+
if (!change.drop.has(k)) return;
|
|
37354
|
+
drop(start);
|
|
37355
|
+
for (let i = start + 1; i < lines.length && !TABLE_HEADER.test(lines[i] ?? ""); i++) if (!KEPT.test(lines[i] ?? "")) drop(i);
|
|
37356
|
+
});
|
|
37357
|
+
const out = [];
|
|
37358
|
+
let removedSinceText = false;
|
|
37359
|
+
lines.forEach((line, i) => {
|
|
37360
|
+
const replacement = dropped.get(i);
|
|
37361
|
+
if (replacement !== void 0) {
|
|
37362
|
+
removedSinceText = replacement === null;
|
|
37363
|
+
if (replacement !== null) out.push(replacement);
|
|
37364
|
+
return;
|
|
37365
|
+
}
|
|
37366
|
+
if (isBlank(line) && removedSinceText && (out.length === 0 || isBlank(out.at(-1)))) return;
|
|
37367
|
+
out.push(line);
|
|
37368
|
+
if (!isBlank(line)) removedSinceText = false;
|
|
37369
|
+
});
|
|
37370
|
+
if (!change.append) return out.join(eol);
|
|
37371
|
+
const added = block(change.append);
|
|
37372
|
+
const lastRule = out.findLastIndex((line) => RULE_HEADER.test(line));
|
|
37373
|
+
if (lastRule < 0) {
|
|
37374
|
+
while (out.length && isBlank(out.at(-1))) out.pop();
|
|
37375
|
+
return [
|
|
37376
|
+
...out,
|
|
37377
|
+
...out.length ? [""] : [],
|
|
37378
|
+
...added,
|
|
37379
|
+
""
|
|
37380
|
+
].join(eol);
|
|
37381
|
+
}
|
|
37382
|
+
let at = lastRule;
|
|
37383
|
+
for (let i = lastRule + 1; i < out.length && !TABLE_HEADER.test(out[i] ?? ""); i++) if (!KEPT.test(out[i] ?? "")) at = i;
|
|
37384
|
+
const next = out[at + 1];
|
|
37385
|
+
out.splice(at + 1, 0, "", ...added, ...next !== void 0 && !isBlank(next) ? [""] : []);
|
|
37386
|
+
return out.join(eol);
|
|
37387
|
+
}
|
|
37388
|
+
/** Plain JSON view for comparisons (dates become strings). */
|
|
37389
|
+
const plain$1 = (value) => JSON.parse(JSON.stringify(value));
|
|
37390
|
+
/** A document for comparisons: plain JSON, and an empty `[skills]` table counts as none. */
|
|
37391
|
+
function normalize(value) {
|
|
37392
|
+
const doc = plain$1(value);
|
|
37393
|
+
if (isTable(doc.skills) && Object.keys(doc.skills).length === 0) delete doc.skills;
|
|
37394
|
+
return doc;
|
|
37395
|
+
}
|
|
37396
|
+
/**
|
|
37397
|
+
* The file text before and after a switch. The `[[skills.config]]` blocks are edited as text
|
|
37398
|
+
* (`textEdit`); when the entries are written some other way, the TOML library patches the
|
|
37399
|
+
* document. Either way the result must parse to the expected document and hold exactly the
|
|
37400
|
+
* same comments, whole-line and after values alike (`sameComments`), or the switch is refused
|
|
37401
|
+
* (422 `config_edit_unsafe`) rather than lose part of the file.
|
|
37402
|
+
*/
|
|
37403
|
+
function renderRuleSwitch(file, skill, byPath, on) {
|
|
37404
|
+
const { text, doc, rules } = load(file);
|
|
37405
|
+
const before = text ?? "";
|
|
37406
|
+
const existed = text !== null;
|
|
37407
|
+
const change = ruleChange(rules, skill, byPath, on);
|
|
37408
|
+
const next = applyRuleChange(rules, change);
|
|
37409
|
+
if (isDeepStrictEqual(plain$1(next), plain$1(rules))) return {
|
|
37410
|
+
before,
|
|
37411
|
+
after: before,
|
|
37412
|
+
existed,
|
|
37413
|
+
changed: false
|
|
37414
|
+
};
|
|
37415
|
+
const skills = isTable(doc.skills) ? { ...doc.skills } : {};
|
|
37416
|
+
if (next.length) skills.config = next;
|
|
37417
|
+
else delete skills.config;
|
|
37418
|
+
const expected = normalize({
|
|
37419
|
+
...doc,
|
|
37420
|
+
skills
|
|
37421
|
+
});
|
|
37422
|
+
const safe = (candidate) => {
|
|
37423
|
+
if (candidate === null || !sameComments(before, candidate)) return false;
|
|
37424
|
+
try {
|
|
37425
|
+
return isDeepStrictEqual(normalize(Ko(candidate)), expected);
|
|
37426
|
+
} catch {
|
|
37427
|
+
return false;
|
|
37428
|
+
}
|
|
37429
|
+
};
|
|
37430
|
+
const edited = textEdit(before, rules, change);
|
|
37431
|
+
if (safe(edited)) return {
|
|
37432
|
+
before,
|
|
37433
|
+
after: edited,
|
|
37434
|
+
existed,
|
|
37435
|
+
changed: true
|
|
37436
|
+
};
|
|
37437
|
+
const patched = Ja(before, expected);
|
|
37438
|
+
if (safe(patched)) return {
|
|
37439
|
+
before,
|
|
37440
|
+
after: patched,
|
|
37441
|
+
existed,
|
|
37442
|
+
changed: true
|
|
37443
|
+
};
|
|
37444
|
+
throw new ServiceError(422, "config_edit_unsafe", `CodeDen can't change the skill settings in ${file} without losing other settings or comments; edit it by hand`);
|
|
37445
|
+
}
|
|
37446
|
+
// ../server/src/services/agent-config/skills-service.ts
|
|
37447
|
+
const fold = (p) => process.platform === "linux" ? p : p.toLowerCase();
|
|
37448
|
+
/**
|
|
37449
|
+
* Skill switches (§12.4): whether each skill is on, read from its provider's native setting,
|
|
37450
|
+
* and switching one on or off by writing that setting. Every write is previewed by `diff`,
|
|
37451
|
+
* backed up first and atomic. See `agent-config-skills.ts` in contracts for the rules.
|
|
37452
|
+
*/
|
|
37453
|
+
var AgentConfigSkills = class {
|
|
37454
|
+
deps;
|
|
37455
|
+
constructor(deps) {
|
|
37456
|
+
this.deps = deps;
|
|
37457
|
+
}
|
|
37458
|
+
/** The user's skills, plus the project's, with their state for the project's checkout. */
|
|
37459
|
+
list(projectId) {
|
|
37460
|
+
const project = projectId ? this.deps.project(projectId) : null;
|
|
37461
|
+
return this.withState(listSkills(this.deps.home, project), this.pageContext(project));
|
|
37462
|
+
}
|
|
37463
|
+
/** The state of the skills a session in `cwd` finds (What's loaded, the `/` menu). */
|
|
37464
|
+
forSession(skills, cwd, gitRoot, projectId) {
|
|
37465
|
+
const context = claudeSessionHookFiles(this.deps.home, cwd, gitRoot, projectId);
|
|
37466
|
+
return this.withState(skills, context);
|
|
37467
|
+
}
|
|
37468
|
+
diff(req) {
|
|
37469
|
+
const plan = this.plan(req);
|
|
37470
|
+
const { before, after, existed } = plan.render();
|
|
37471
|
+
return {
|
|
37472
|
+
...unifiedDiff(plan.file.file, existed, before, after),
|
|
37473
|
+
scope: plan.scope
|
|
37474
|
+
};
|
|
37475
|
+
}
|
|
37476
|
+
apply(req) {
|
|
37477
|
+
const plan = this.plan(req);
|
|
37478
|
+
const { after, existed, changed } = plan.render();
|
|
37479
|
+
if (!changed) return {
|
|
37480
|
+
file: null,
|
|
37481
|
+
backupPath: null,
|
|
37482
|
+
skills: this.list(req.listProjectId)
|
|
37483
|
+
};
|
|
37484
|
+
const backupPath = existed ? this.deps.backup(plan.file.file) : null;
|
|
37485
|
+
mkdirSync(dirname(plan.file.file), { recursive: true });
|
|
37486
|
+
guard(plan.file);
|
|
37487
|
+
atomicWrite(plan.file.file, after);
|
|
37488
|
+
return {
|
|
37489
|
+
file: plan.file.file,
|
|
37490
|
+
backupPath,
|
|
37491
|
+
skills: this.list(req.listProjectId)
|
|
37492
|
+
};
|
|
37493
|
+
}
|
|
37494
|
+
withState(skills, context) {
|
|
37495
|
+
const claude = highestFirst(context.map((f) => readOverrides(f)));
|
|
37496
|
+
const codexRules = this.codexRules();
|
|
37497
|
+
return skills.map((skill) => {
|
|
37498
|
+
if (skill.provider === "codex") {
|
|
37499
|
+
const state = ruleState(codexRules, {
|
|
37500
|
+
name: skill.name,
|
|
37501
|
+
path: skill.file.absolutePath
|
|
37502
|
+
});
|
|
37503
|
+
return {
|
|
37504
|
+
...skill,
|
|
37505
|
+
enabled: state.enabled,
|
|
37506
|
+
setIn: state.set ? this.codexConfig() : null
|
|
37507
|
+
};
|
|
37508
|
+
}
|
|
37509
|
+
const deciding = decidingOverride(claude, skill.name);
|
|
37510
|
+
const value = deciding?.value;
|
|
37511
|
+
return {
|
|
37512
|
+
...skill,
|
|
37513
|
+
enabled: value !== "off",
|
|
37514
|
+
...value === "name-only" || value === "user-invocable-only" ? { visibility: value } : {},
|
|
37515
|
+
setIn: deciding?.file.source.file ?? null
|
|
37516
|
+
};
|
|
37517
|
+
});
|
|
37518
|
+
}
|
|
37519
|
+
/** Claude's settings files for the Agents page: the user's, plus the project's. */
|
|
37520
|
+
pageContext(project) {
|
|
37521
|
+
const home = this.deps.home;
|
|
37522
|
+
const user = claudeHookFile("user", home, null);
|
|
37523
|
+
if (!project) return [user];
|
|
37524
|
+
return [
|
|
37525
|
+
user,
|
|
37526
|
+
claudeHookFile("project", home, project),
|
|
37527
|
+
claudeHookFile("local", home, project)
|
|
37528
|
+
];
|
|
37529
|
+
}
|
|
37530
|
+
codexConfig() {
|
|
37531
|
+
return join(this.deps.home, ".codex", "config.toml");
|
|
37532
|
+
}
|
|
37533
|
+
/** Codex's rules for listing; none when config.toml leads outside home or doesn't parse. */
|
|
37534
|
+
codexRules() {
|
|
37535
|
+
try {
|
|
37536
|
+
guard({
|
|
37537
|
+
root: this.deps.home,
|
|
37538
|
+
file: this.codexConfig()
|
|
37539
|
+
});
|
|
37540
|
+
} catch {
|
|
37541
|
+
return [];
|
|
37542
|
+
}
|
|
37543
|
+
return readRules(this.codexConfig());
|
|
37544
|
+
}
|
|
37545
|
+
/**
|
|
37546
|
+
* What a switch will do, with every input checked first: the skill must be one listed for
|
|
37547
|
+
* `listProjectId` (404 `skill_not_found`); every settings file that applies must be readable
|
|
37548
|
+
* and inside its folder (422 `config_unparsable`, 403 `path_outside_root`), so the target is
|
|
37549
|
+
* never chosen around a file that can't be read; and the file written must stay inside its
|
|
37550
|
+
* folder.
|
|
37551
|
+
*/
|
|
37552
|
+
plan(req) {
|
|
37553
|
+
if (!agentConfigFeatures[req.provider].skillSwitches) throw badRequest(`CodeDen does not switch ${req.provider} skills`, "skills_unsupported");
|
|
37554
|
+
const project = req.listProjectId ? this.deps.project(req.listProjectId) : null;
|
|
37555
|
+
const { provider, enabled, listProjectId: _, ...locator } = req;
|
|
37556
|
+
const absolutePath = fold(this.deps.resolve(locator).absolutePath);
|
|
37557
|
+
const skill = listSkills(this.deps.home, project).find((s) => s.provider === provider && fold(s.file.absolutePath) === absolutePath);
|
|
37558
|
+
if (!skill) throw notFound(`No ${provider} skill at ${req.path}; reload the list`, "skill_not_found");
|
|
37559
|
+
if (provider === "codex") {
|
|
37560
|
+
const file = {
|
|
37561
|
+
root: this.deps.home,
|
|
37562
|
+
file: this.codexConfig()
|
|
37563
|
+
};
|
|
37564
|
+
guard(file);
|
|
37565
|
+
const target = {
|
|
37566
|
+
name: skill.name,
|
|
37567
|
+
path: skill.file.absolutePath
|
|
37568
|
+
};
|
|
37569
|
+
const byPath = skill.scope === "user";
|
|
37570
|
+
return {
|
|
37571
|
+
file,
|
|
37572
|
+
scope: "user",
|
|
37573
|
+
render: () => renderRuleSwitch(file.file, target, byPath, enabled)
|
|
37574
|
+
};
|
|
37575
|
+
}
|
|
37576
|
+
const { target, value } = planOverride(this.pageContext(project).map((f) => readOverrides(f, true)), claudeHookFile("user", this.deps.home, null), skill.name, enabled);
|
|
37577
|
+
guard(target);
|
|
37578
|
+
return {
|
|
37579
|
+
file: target,
|
|
37580
|
+
scope: target.scope,
|
|
37581
|
+
render: () => renderOverride(target.file, skill.name, value)
|
|
37582
|
+
};
|
|
37583
|
+
}
|
|
37584
|
+
};
|
|
36510
37585
|
// ../server/src/services/agent-config/slash-commands.ts
|
|
36511
37586
|
/**
|
|
36512
37587
|
* The composer's `/` menu (§13): the skills and custom commands that apply to a session's
|
|
@@ -36620,13 +37695,14 @@ function reportedCommands(db, sessionId) {
|
|
|
36620
37695
|
const MCP_TEST_SLACK_MS = 5e3;
|
|
36621
37696
|
/**
|
|
36622
37697
|
* Agent configuration (§12.4): discovery of each provider's memory files, skills, subagents,
|
|
36623
|
-
* MCP servers and hooks; edits in the native files with backups and diff previews;
|
|
36624
|
-
* "what's loaded" view of a session.
|
|
37698
|
+
* MCP servers and hooks; edits in the native files with backups and diff previews; skill
|
|
37699
|
+
* switches; and the "what's loaded" view of a session.
|
|
36625
37700
|
*/
|
|
36626
37701
|
var AgentConfigService = class {
|
|
36627
37702
|
deps;
|
|
36628
37703
|
files;
|
|
36629
37704
|
hooks;
|
|
37705
|
+
skills;
|
|
36630
37706
|
constructor(deps) {
|
|
36631
37707
|
this.deps = deps;
|
|
36632
37708
|
this.files = new AgentConfigFiles((l) => this.rootDir(l), deps.backupDir);
|
|
@@ -36636,6 +37712,12 @@ var AgentConfigService = class {
|
|
|
36636
37712
|
backup: (file) => this.files.backup(file),
|
|
36637
37713
|
shelf: new HookShelf(deps.disabledHooksFile)
|
|
36638
37714
|
});
|
|
37715
|
+
this.skills = new AgentConfigSkills({
|
|
37716
|
+
home: deps.home,
|
|
37717
|
+
project: (id) => this.project(id),
|
|
37718
|
+
backup: (file) => this.files.backup(file),
|
|
37719
|
+
resolve: (locator) => this.files.resolve(locator)
|
|
37720
|
+
});
|
|
36639
37721
|
}
|
|
36640
37722
|
list(query) {
|
|
36641
37723
|
const project = query.projectId ? this.project(query.projectId) : null;
|
|
@@ -36741,31 +37823,58 @@ var AgentConfigService = class {
|
|
|
36741
37823
|
}
|
|
36742
37824
|
/**
|
|
36743
37825
|
* The composer's `/` menu for a session (§13): the skills that apply to its working folder
|
|
36744
|
-
* (as What's loaded finds them) and its provider's custom
|
|
36745
|
-
* has reported a non-empty list of what it runs for the
|
|
36746
|
-
* that list (`mergeSlashCommands`).
|
|
37826
|
+
* (as What's loaded finds them, without those switched off) and its provider's custom
|
|
37827
|
+
* command files; once the provider has reported a non-empty list of what it runs for the
|
|
37828
|
+
* session (built-in ones included), that list (`mergeSlashCommands`).
|
|
36747
37829
|
* Role sessions get what applies to their own folder: the orchestrator, outside any
|
|
36748
37830
|
* project, only user-level entries.
|
|
36749
37831
|
*/
|
|
36750
37832
|
slashCommands(sessionId) {
|
|
36751
37833
|
const { session, gitRoot, files } = this.discover(sessionId);
|
|
36752
|
-
|
|
36753
|
-
return mergeSlashCommands([...files.skills.map(skillCommand), ...custom], this.deps.reportedCommands(sessionId), session.provider);
|
|
37834
|
+
return mergeSlashCommands(this.discoveredCommands(session.provider, session.cwd, gitRoot, files.skills), this.deps.reportedCommands(sessionId), session.provider);
|
|
36754
37835
|
}
|
|
36755
|
-
/**
|
|
37836
|
+
/**
|
|
37837
|
+
* The `/` menu of a session that does not exist yet (the New session draft): what `provider`
|
|
37838
|
+
* would load in the project folder. No provider has reported a list, so the files are the list.
|
|
37839
|
+
*/
|
|
37840
|
+
projectSlashCommands(projectId, provider) {
|
|
37841
|
+
const project = this.project(projectId);
|
|
37842
|
+
const { gitRoot, files } = this.discoverIn(provider, project.path, project);
|
|
37843
|
+
return mergeSlashCommands(this.discoveredCommands(provider, project.path, gitRoot, files.skills), null, provider);
|
|
37844
|
+
}
|
|
37845
|
+
/** Skills that are switched on, then the provider's custom command files, in precedence order. */
|
|
37846
|
+
discoveredCommands(provider, cwd, gitRoot, skills) {
|
|
37847
|
+
const custom = provider === "claude" ? loadedClaudeCommands(this.deps.home, cwd, gitRoot) : [];
|
|
37848
|
+
return [...skills.filter((s) => s.enabled).map(skillCommand), ...custom];
|
|
37849
|
+
}
|
|
37850
|
+
/**
|
|
37851
|
+
* A session's working folder, project and the native files its provider loads there, with
|
|
37852
|
+
* each skill's switch state for that folder.
|
|
37853
|
+
*/
|
|
36756
37854
|
discover(sessionId) {
|
|
36757
37855
|
const session = this.deps.repo.get(sessionId);
|
|
36758
37856
|
const project = session.projectId ? this.project(session.projectId) : null;
|
|
36759
|
-
const gitRoot = findGitRoot(session.cwd);
|
|
36760
|
-
const home = this.deps.home;
|
|
36761
37857
|
return {
|
|
36762
37858
|
session,
|
|
36763
37859
|
project,
|
|
37860
|
+
...this.discoverIn(session.provider, session.cwd, project)
|
|
37861
|
+
};
|
|
37862
|
+
}
|
|
37863
|
+
/** The native files `provider` loads in `cwd` (skills with their switch state), and the git root. */
|
|
37864
|
+
discoverIn(provider, cwd, project) {
|
|
37865
|
+
const gitRoot = findGitRoot(cwd);
|
|
37866
|
+
const home = this.deps.home;
|
|
37867
|
+
const found = provider === "codex" ? loadedCodex(home, cwd, gitRoot, project) : provider === "claude" ? loadedClaude(home, cwd, gitRoot, project) : {
|
|
37868
|
+
memory: [],
|
|
37869
|
+
skills: [],
|
|
37870
|
+
subagents: []
|
|
37871
|
+
};
|
|
37872
|
+
const skills = this.skills.forSession(found.skills, cwd, gitRoot, project?.id ?? null);
|
|
37873
|
+
return {
|
|
36764
37874
|
gitRoot,
|
|
36765
|
-
files:
|
|
36766
|
-
|
|
36767
|
-
skills
|
|
36768
|
-
subagents: []
|
|
37875
|
+
files: {
|
|
37876
|
+
...found,
|
|
37877
|
+
skills
|
|
36769
37878
|
}
|
|
36770
37879
|
};
|
|
36771
37880
|
}
|
|
@@ -36834,418 +37943,6 @@ var AgentConfigService = class {
|
|
|
36834
37943
|
];
|
|
36835
37944
|
}
|
|
36836
37945
|
};
|
|
36837
|
-
// ../server/src/services/automations/sources.ts
|
|
36838
|
-
const FILE_SOURCES = ["global_file", "project_file"];
|
|
36839
|
-
/** Repo-relative folder of project automation files. */
|
|
36840
|
-
const PROJECT_AUTOMATIONS_DIR = join(".codeden", "automations");
|
|
36841
|
-
/** The definition fields that, when changed in a file, update the stored automation. */
|
|
36842
|
-
function definitionKey(a) {
|
|
36843
|
-
return JSON.stringify([
|
|
36844
|
-
a.name,
|
|
36845
|
-
a.description,
|
|
36846
|
-
a.templateId,
|
|
36847
|
-
a.trigger,
|
|
36848
|
-
a.action,
|
|
36849
|
-
a.target,
|
|
36850
|
-
a.guards,
|
|
36851
|
-
a.workKey ?? null
|
|
36852
|
-
]);
|
|
36853
|
-
}
|
|
36854
|
-
/**
|
|
36855
|
-
* Automation definitions from Markdown files (§10): global ones in `~/.codeden/automations/`
|
|
36856
|
-
* (they target all projects) and each project's `.codeden/automations/` (that project only).
|
|
36857
|
-
* They are mirrored into the `automations` projection through `automation.saved` /
|
|
36858
|
-
* `automation.deleted`, keeping the enabled flag (files never enable anything). Folders are
|
|
36859
|
-
* watched, with a periodic rescan for folders created later. Built-in templates are read once.
|
|
36860
|
-
*/
|
|
36861
|
-
var AutomationSources = class {
|
|
36862
|
-
deps;
|
|
36863
|
-
paths = /* @__PURE__ */ new Map();
|
|
36864
|
-
errors = [];
|
|
36865
|
-
builtin = [];
|
|
36866
|
-
globalTemplates = [];
|
|
36867
|
-
watchers = /* @__PURE__ */ new Map();
|
|
36868
|
-
timer;
|
|
36869
|
-
rescan;
|
|
36870
|
-
constructor(deps) {
|
|
36871
|
-
this.deps = deps;
|
|
36872
|
-
const dir = findTemplatesDir(deps.templatesDir);
|
|
36873
|
-
if (dir) {
|
|
36874
|
-
const { files, errors } = readAutomationDir(dir);
|
|
36875
|
-
this.builtin = files.map((f) => toTemplate(f, "builtin"));
|
|
36876
|
-
for (const e of errors) deps.log?.(`automations: built-in template ${e.path}: ${e.message}`);
|
|
36877
|
-
} else deps.log?.("automations: built-in templates folder not found");
|
|
36878
|
-
}
|
|
36879
|
-
/** Built-in templates, then global automation files. */
|
|
36880
|
-
templates() {
|
|
36881
|
-
const builtinIds = new Set(this.builtin.map((t) => t.id));
|
|
36882
|
-
return [...this.builtin, ...this.globalTemplates.filter((t) => !builtinIds.has(t.id))];
|
|
36883
|
-
}
|
|
36884
|
-
template(id) {
|
|
36885
|
-
return this.templates().find((t) => t.id === id);
|
|
36886
|
-
}
|
|
36887
|
-
fileErrors() {
|
|
36888
|
-
return this.errors;
|
|
36889
|
-
}
|
|
36890
|
-
pathOf(automationId) {
|
|
36891
|
-
return this.paths.get(automationId) ?? null;
|
|
36892
|
-
}
|
|
36893
|
-
start() {
|
|
36894
|
-
this.reload();
|
|
36895
|
-
const every = this.deps.rescanMs ?? 6e4;
|
|
36896
|
-
if (every > 0) {
|
|
36897
|
-
this.rescan = setInterval(() => this.reload(), every);
|
|
36898
|
-
this.rescan.unref();
|
|
36899
|
-
}
|
|
36900
|
-
}
|
|
36901
|
-
stop() {
|
|
36902
|
-
if (this.timer) clearTimeout(this.timer);
|
|
36903
|
-
if (this.rescan) clearInterval(this.rescan);
|
|
36904
|
-
for (const w of this.watchers.values()) w.close();
|
|
36905
|
-
this.watchers.clear();
|
|
36906
|
-
}
|
|
36907
|
-
/** Re-read every folder and sync the projection. Returns true when anything changed. */
|
|
36908
|
-
reload() {
|
|
36909
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
36910
|
-
const errors = [];
|
|
36911
|
-
const found = [];
|
|
36912
|
-
const paths = /* @__PURE__ */ new Map();
|
|
36913
|
-
const add = (file, base) => {
|
|
36914
|
-
const { parsed } = file;
|
|
36915
|
-
found.push({
|
|
36916
|
-
...base,
|
|
36917
|
-
name: parsed.frontmatter.name,
|
|
36918
|
-
description: parsed.frontmatter.description ?? null,
|
|
36919
|
-
enabled: false,
|
|
36920
|
-
templateId: parsed.id,
|
|
36921
|
-
trigger: parsed.frontmatter.trigger,
|
|
36922
|
-
action: parsed.action,
|
|
36923
|
-
guards: parsed.guards,
|
|
36924
|
-
workKey: parsed.workKey,
|
|
36925
|
-
createdAt: now,
|
|
36926
|
-
updatedAt: now
|
|
36927
|
-
});
|
|
36928
|
-
paths.set(base.id, file.path);
|
|
36929
|
-
};
|
|
36930
|
-
const global = readAutomationDir(this.deps.globalDir);
|
|
36931
|
-
errors.push(...global.errors);
|
|
36932
|
-
this.globalTemplates = global.files.map((f) => toTemplate(f, "global_file"));
|
|
36933
|
-
for (const file of global.files) add(file, {
|
|
36934
|
-
id: `global:${file.parsed.id}`,
|
|
36935
|
-
source: "global_file",
|
|
36936
|
-
target: { type: "all_projects" }
|
|
36937
|
-
});
|
|
36938
|
-
const dirs = [this.deps.globalDir];
|
|
36939
|
-
for (const project of this.deps.projects.list()) {
|
|
36940
|
-
if (project.archivedAt) continue;
|
|
36941
|
-
const dir = join(project.path, PROJECT_AUTOMATIONS_DIR);
|
|
36942
|
-
dirs.push(dir);
|
|
36943
|
-
const local = readAutomationDir(dir);
|
|
36944
|
-
errors.push(...local.errors);
|
|
36945
|
-
for (const file of local.files) add(file, {
|
|
36946
|
-
id: `project:${project.id}:${file.parsed.id}`,
|
|
36947
|
-
source: "project_file",
|
|
36948
|
-
target: {
|
|
36949
|
-
type: "project",
|
|
36950
|
-
projectId: project.id
|
|
36951
|
-
}
|
|
36952
|
-
});
|
|
36953
|
-
}
|
|
36954
|
-
this.errors = errors;
|
|
36955
|
-
this.paths.clear();
|
|
36956
|
-
for (const [k, v] of paths) this.paths.set(k, v);
|
|
36957
|
-
this.watchDirs(dirs);
|
|
36958
|
-
return this.sync(found);
|
|
36959
|
-
}
|
|
36960
|
-
/** Store new or changed file definitions and delete those whose file is gone. */
|
|
36961
|
-
sync(found) {
|
|
36962
|
-
const existing = new Map(this.deps.db.select().from(automations).where(inArray(automations.source, [...FILE_SOURCES])).all().map((r) => {
|
|
36963
|
-
const a = rowToAutomation(r);
|
|
36964
|
-
return [a.id, a];
|
|
36965
|
-
}));
|
|
36966
|
-
const events = [];
|
|
36967
|
-
for (const next of found) {
|
|
36968
|
-
const prev = existing.get(next.id);
|
|
36969
|
-
existing.delete(next.id);
|
|
36970
|
-
if (prev && definitionKey(prev) === definitionKey(next)) continue;
|
|
36971
|
-
const automation = prev ? {
|
|
36972
|
-
...next,
|
|
36973
|
-
enabled: prev.enabled,
|
|
36974
|
-
createdAt: prev.createdAt
|
|
36975
|
-
} : next;
|
|
36976
|
-
events.push({
|
|
36977
|
-
type: "automation.saved",
|
|
36978
|
-
sessionId: null,
|
|
36979
|
-
payload: { automation }
|
|
36980
|
-
});
|
|
36981
|
-
}
|
|
36982
|
-
for (const gone of existing.keys()) events.push({
|
|
36983
|
-
type: "automation.deleted",
|
|
36984
|
-
sessionId: null,
|
|
36985
|
-
payload: { automationId: gone }
|
|
36986
|
-
});
|
|
36987
|
-
if (events.length === 0) return false;
|
|
36988
|
-
this.deps.store.append(events);
|
|
36989
|
-
this.deps.onChange?.();
|
|
36990
|
-
return true;
|
|
36991
|
-
}
|
|
36992
|
-
watchDirs(dirs) {
|
|
36993
|
-
const wanted = new Set(dirs.filter((d) => existsSync(d)));
|
|
36994
|
-
for (const [dir, watcher] of this.watchers) if (!wanted.has(dir)) {
|
|
36995
|
-
watcher.close();
|
|
36996
|
-
this.watchers.delete(dir);
|
|
36997
|
-
}
|
|
36998
|
-
for (const dir of wanted) {
|
|
36999
|
-
if (this.watchers.has(dir)) continue;
|
|
37000
|
-
try {
|
|
37001
|
-
const watcher = watch(dir, () => this.scheduleReload());
|
|
37002
|
-
watcher.on("error", () => {
|
|
37003
|
-
watcher.close();
|
|
37004
|
-
this.watchers.delete(dir);
|
|
37005
|
-
});
|
|
37006
|
-
this.watchers.set(dir, watcher);
|
|
37007
|
-
} catch (error) {
|
|
37008
|
-
this.deps.log?.(`automations: cannot watch ${dir}: ${errorMessage$1(error)}`);
|
|
37009
|
-
}
|
|
37010
|
-
}
|
|
37011
|
-
}
|
|
37012
|
-
scheduleReload() {
|
|
37013
|
-
if (this.timer) clearTimeout(this.timer);
|
|
37014
|
-
this.timer = setTimeout(() => {
|
|
37015
|
-
this.timer = void 0;
|
|
37016
|
-
try {
|
|
37017
|
-
this.reload();
|
|
37018
|
-
} catch (error) {
|
|
37019
|
-
this.deps.log?.(`automations: reload failed: ${errorMessage$1(error)}`);
|
|
37020
|
-
}
|
|
37021
|
-
}, 200);
|
|
37022
|
-
this.timer.unref();
|
|
37023
|
-
}
|
|
37024
|
-
};
|
|
37025
|
-
/** True when a project can run a template (GitHub-only templates need a GitHub project). */
|
|
37026
|
-
function templateFits(template, project) {
|
|
37027
|
-
return !template.requiresGithub || project.kind === "github";
|
|
37028
|
-
}
|
|
37029
|
-
// ../server/src/services/automations/definitions.ts
|
|
37030
|
-
/** Id of a project's starter automation created from a template. */
|
|
37031
|
-
function starterId(projectId, templateId) {
|
|
37032
|
-
return `starter:${projectId}:${templateId}`;
|
|
37033
|
-
}
|
|
37034
|
-
/**
|
|
37035
|
-
* Database-stored automations (§10): create (from a template or from scratch), update,
|
|
37036
|
-
* delete, enable and disable, and the starter set offered to new projects. File-based
|
|
37037
|
-
* automations are read-only here except for their enabled switch.
|
|
37038
|
-
*/
|
|
37039
|
-
var AutomationDefinitions = class {
|
|
37040
|
-
deps;
|
|
37041
|
-
constructor(deps) {
|
|
37042
|
-
this.deps = deps;
|
|
37043
|
-
}
|
|
37044
|
-
all() {
|
|
37045
|
-
return this.deps.db.select().from(automations).all().map(rowToAutomation);
|
|
37046
|
-
}
|
|
37047
|
-
find(id) {
|
|
37048
|
-
const row = this.deps.db.select().from(automations).where(eq(automations.id, id)).get();
|
|
37049
|
-
return row ? rowToAutomation(row) : void 0;
|
|
37050
|
-
}
|
|
37051
|
-
get(id) {
|
|
37052
|
-
const a = this.find(id);
|
|
37053
|
-
if (!a) throw notFound(`No automation ${id}`, "automation_not_found");
|
|
37054
|
-
return a;
|
|
37055
|
-
}
|
|
37056
|
-
create(req, opts = {}) {
|
|
37057
|
-
const id = opts.id ?? randomUUID();
|
|
37058
|
-
const template = req.templateId ? this.template(req.templateId) : void 0;
|
|
37059
|
-
if (req.projectId) this.deps.projects.get(req.projectId);
|
|
37060
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
37061
|
-
const trigger = req.trigger ?? template?.trigger;
|
|
37062
|
-
const action = req.action ?? template?.action;
|
|
37063
|
-
const name = req.name ?? template?.name;
|
|
37064
|
-
if (!trigger || !action || !name) throw badRequest("name, trigger and action are required");
|
|
37065
|
-
const automation = Automation.parse({
|
|
37066
|
-
id,
|
|
37067
|
-
name,
|
|
37068
|
-
description: req.description !== void 0 ? req.description : template?.description ?? null,
|
|
37069
|
-
enabled: req.enabled,
|
|
37070
|
-
source: "database",
|
|
37071
|
-
templateId: template?.id ?? null,
|
|
37072
|
-
trigger,
|
|
37073
|
-
action,
|
|
37074
|
-
target: req.projectId ? {
|
|
37075
|
-
type: "project",
|
|
37076
|
-
projectId: req.projectId
|
|
37077
|
-
} : { type: "all_projects" },
|
|
37078
|
-
guards: AutomationGuards.parse({
|
|
37079
|
-
...template?.guards,
|
|
37080
|
-
...req.guards
|
|
37081
|
-
}),
|
|
37082
|
-
workKey: req.workKey ?? template?.workKey,
|
|
37083
|
-
...opts.createdBy && opts.createdBy !== "user" ? { createdBy: opts.createdBy } : {},
|
|
37084
|
-
createdAt: now,
|
|
37085
|
-
updatedAt: now
|
|
37086
|
-
});
|
|
37087
|
-
validateAutomation(automation);
|
|
37088
|
-
return this.save(automation);
|
|
37089
|
-
}
|
|
37090
|
-
update(id, req) {
|
|
37091
|
-
const current = this.editable(id);
|
|
37092
|
-
if (req.target?.type === "project") this.deps.projects.get(req.target.projectId);
|
|
37093
|
-
const { workKey: _drop, ...rest } = current;
|
|
37094
|
-
const workKey = req.workKey === null ? void 0 : req.workKey ?? current.workKey;
|
|
37095
|
-
const next = Automation.parse({
|
|
37096
|
-
...rest,
|
|
37097
|
-
...req.name !== void 0 ? { name: req.name } : {},
|
|
37098
|
-
...req.description !== void 0 ? { description: req.description } : {},
|
|
37099
|
-
...req.trigger ? { trigger: req.trigger } : {},
|
|
37100
|
-
...req.action ? { action: req.action } : {},
|
|
37101
|
-
...req.target ? { target: req.target } : {},
|
|
37102
|
-
guards: AutomationGuards.parse({
|
|
37103
|
-
...current.guards,
|
|
37104
|
-
...req.guards
|
|
37105
|
-
}),
|
|
37106
|
-
...workKey ? { workKey } : {},
|
|
37107
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
37108
|
-
});
|
|
37109
|
-
validateAutomation(next);
|
|
37110
|
-
return this.save(next);
|
|
37111
|
-
}
|
|
37112
|
-
remove(id) {
|
|
37113
|
-
this.editable(id);
|
|
37114
|
-
this.deps.store.append([{
|
|
37115
|
-
type: "automation.deleted",
|
|
37116
|
-
sessionId: null,
|
|
37117
|
-
payload: { automationId: id }
|
|
37118
|
-
}]);
|
|
37119
|
-
this.deps.schedules.removeAutomation(id);
|
|
37120
|
-
}
|
|
37121
|
-
/**
|
|
37122
|
-
* Enable or disable. With `projectId` on an automation that targets all projects, only that
|
|
37123
|
-
* project's override changes; otherwise the automation's own switch.
|
|
37124
|
-
*/
|
|
37125
|
-
setEnabled(id, enabled, projectId) {
|
|
37126
|
-
const a = this.get(id);
|
|
37127
|
-
if (projectId) this.deps.projects.get(projectId);
|
|
37128
|
-
if (projectId && a.target.type === "all_projects") {
|
|
37129
|
-
this.deps.schedules.upsert(id, projectId, { enabledOverride: enabled });
|
|
37130
|
-
return a;
|
|
37131
|
-
}
|
|
37132
|
-
if (projectId && a.target.type === "project" && a.target.projectId !== projectId) throw notFound(`Automation ${id} does not belong to that project`, "automation_not_found");
|
|
37133
|
-
if (a.enabled === enabled) return a;
|
|
37134
|
-
return this.save({
|
|
37135
|
-
...a,
|
|
37136
|
-
enabled,
|
|
37137
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
37138
|
-
});
|
|
37139
|
-
}
|
|
37140
|
-
/** Drop a project's override of an all-projects automation; it follows its own switch again. */
|
|
37141
|
-
clearOverride(id, projectId) {
|
|
37142
|
-
const a = this.get(id);
|
|
37143
|
-
this.deps.projects.get(projectId);
|
|
37144
|
-
if (a.target.type !== "all_projects") throw conflict("Only automations for all projects have per-project overrides", "no_override");
|
|
37145
|
-
this.deps.schedules.upsert(id, projectId, { enabledOverride: null });
|
|
37146
|
-
return a;
|
|
37147
|
-
}
|
|
37148
|
-
/**
|
|
37149
|
-
* Create the project's starter set (§10) from the starter templates that fit the project,
|
|
37150
|
-
* keeping existing ones; with `enable`, switch them all on.
|
|
37151
|
-
*/
|
|
37152
|
-
starterSet(projectId, enable) {
|
|
37153
|
-
const project = this.deps.projects.get(projectId);
|
|
37154
|
-
const out = [];
|
|
37155
|
-
for (const template of this.deps.sources.templates()) {
|
|
37156
|
-
if (!template.starter || template.source !== "builtin" || !templateFits(template, project)) continue;
|
|
37157
|
-
const id = starterId(projectId, template.id);
|
|
37158
|
-
const existing = this.find(id);
|
|
37159
|
-
if (existing) {
|
|
37160
|
-
out.push(enable ? this.setEnabled(id, true) : existing);
|
|
37161
|
-
continue;
|
|
37162
|
-
}
|
|
37163
|
-
out.push(this.create({
|
|
37164
|
-
templateId: template.id,
|
|
37165
|
-
projectId,
|
|
37166
|
-
enabled: enable
|
|
37167
|
-
}, { id }));
|
|
37168
|
-
}
|
|
37169
|
-
return out;
|
|
37170
|
-
}
|
|
37171
|
-
/**
|
|
37172
|
-
* An editable database copy of any automation (also file-based and built-in ones), off,
|
|
37173
|
-
* named "<name> (copy)" unless a name is given. Targets `projectId`, else the original's
|
|
37174
|
-
* target. The copy keeps the original's work key, so the two never run the same work at once.
|
|
37175
|
-
*/
|
|
37176
|
-
duplicate(id, req) {
|
|
37177
|
-
const a = this.get(id);
|
|
37178
|
-
const projectId = req.projectId ?? (a.target.type === "project" ? a.target.projectId : void 0);
|
|
37179
|
-
return this.create({
|
|
37180
|
-
projectId,
|
|
37181
|
-
name: req.name ?? `${a.name} (copy)`.slice(0, 200),
|
|
37182
|
-
description: a.description,
|
|
37183
|
-
trigger: a.trigger,
|
|
37184
|
-
action: a.action,
|
|
37185
|
-
guards: a.guards,
|
|
37186
|
-
workKey: a.workKey,
|
|
37187
|
-
enabled: false
|
|
37188
|
-
});
|
|
37189
|
-
}
|
|
37190
|
-
/**
|
|
37191
|
-
* Import shared automation files (§10): re-parse the source with the file parser, then
|
|
37192
|
-
* create each selected valid file as a database automation, always off and marked
|
|
37193
|
-
* `createdBy: import`. Nothing runs.
|
|
37194
|
-
*/
|
|
37195
|
-
importFrom(req) {
|
|
37196
|
-
if (req.projectId) this.deps.projects.get(req.projectId);
|
|
37197
|
-
const select = req.select ? new Set(req.select) : null;
|
|
37198
|
-
const items = previewImport(req.source).filter((i) => !select || select.has(i.fileName));
|
|
37199
|
-
const created = [];
|
|
37200
|
-
const failed = [];
|
|
37201
|
-
for (const item of items) {
|
|
37202
|
-
const draft = item.automation;
|
|
37203
|
-
if (!draft) {
|
|
37204
|
-
failed.push({
|
|
37205
|
-
fileName: item.fileName,
|
|
37206
|
-
error: item.error ?? "Invalid file"
|
|
37207
|
-
});
|
|
37208
|
-
continue;
|
|
37209
|
-
}
|
|
37210
|
-
created.push(this.create({
|
|
37211
|
-
projectId: req.projectId,
|
|
37212
|
-
name: draft.name,
|
|
37213
|
-
description: draft.description,
|
|
37214
|
-
trigger: draft.trigger,
|
|
37215
|
-
action: draft.action,
|
|
37216
|
-
guards: draft.guards,
|
|
37217
|
-
workKey: draft.workKey,
|
|
37218
|
-
enabled: false
|
|
37219
|
-
}, { createdBy: "import" }));
|
|
37220
|
-
}
|
|
37221
|
-
return {
|
|
37222
|
-
created,
|
|
37223
|
-
failed
|
|
37224
|
-
};
|
|
37225
|
-
}
|
|
37226
|
-
/** Parse an import without saving anything. */
|
|
37227
|
-
previewImport(req) {
|
|
37228
|
-
return previewImport(req);
|
|
37229
|
-
}
|
|
37230
|
-
template(id) {
|
|
37231
|
-
const template = this.deps.sources.template(id);
|
|
37232
|
-
if (!template) throw notFound(`No automation template ${id}`, "template_not_found");
|
|
37233
|
-
return template;
|
|
37234
|
-
}
|
|
37235
|
-
editable(id) {
|
|
37236
|
-
const a = this.get(id);
|
|
37237
|
-
if (a.source !== "database") throw conflict("This automation is defined in a file; edit the file instead", "automation_read_only");
|
|
37238
|
-
return a;
|
|
37239
|
-
}
|
|
37240
|
-
save(automation) {
|
|
37241
|
-
this.deps.store.append([{
|
|
37242
|
-
type: "automation.saved",
|
|
37243
|
-
sessionId: null,
|
|
37244
|
-
payload: { automation }
|
|
37245
|
-
}]);
|
|
37246
|
-
return automation;
|
|
37247
|
-
}
|
|
37248
|
-
};
|
|
37249
37946
|
// ../server/src/services/sessions/conflicts.ts
|
|
37250
37947
|
/** Session statuses that count as running or queued work for conflict detection (§8.4). */
|
|
37251
37948
|
const ACTIVE_SESSION_STATUSES = [
|
|
@@ -39105,6 +39802,8 @@ const DONE_STAGES = [
|
|
|
39105
39802
|
var FactoryStore = class {
|
|
39106
39803
|
db;
|
|
39107
39804
|
events;
|
|
39805
|
+
/** When each project's loop last ticked (see `saveTick`); not persisted. */
|
|
39806
|
+
lastTicks = /* @__PURE__ */ new Map();
|
|
39108
39807
|
constructor(db, events) {
|
|
39109
39808
|
this.db = db;
|
|
39110
39809
|
this.events = events;
|
|
@@ -39134,7 +39833,17 @@ var FactoryStore = class {
|
|
|
39134
39833
|
updatedAt: now.toISOString()
|
|
39135
39834
|
};
|
|
39136
39835
|
}
|
|
39836
|
+
/**
|
|
39837
|
+
* Save the loop; writes only when something besides its stamps (`lastTickAt`, `updatedAt`)
|
|
39838
|
+
* changed. The log is never pruned (§6) and every loop event refreshes the open factory
|
|
39839
|
+
* panels, while an idle factory ticks every 30 s.
|
|
39840
|
+
*/
|
|
39137
39841
|
saveLoop(loop) {
|
|
39842
|
+
const current = this.loop(loop.projectId);
|
|
39843
|
+
if (current && sameLoop(current, loop)) return {
|
|
39844
|
+
...loop,
|
|
39845
|
+
updatedAt: current.updatedAt
|
|
39846
|
+
};
|
|
39138
39847
|
const next = {
|
|
39139
39848
|
...loop,
|
|
39140
39849
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -39146,6 +39855,21 @@ var FactoryStore = class {
|
|
|
39146
39855
|
}]);
|
|
39147
39856
|
return next;
|
|
39148
39857
|
}
|
|
39858
|
+
/**
|
|
39859
|
+
* End a tick: remember when it ran and save the loop if the tick changed it. A tick that
|
|
39860
|
+
* changed nothing saves nothing, so the time is kept in memory for the panel's "last check".
|
|
39861
|
+
*/
|
|
39862
|
+
saveTick(loop, at) {
|
|
39863
|
+
this.lastTicks.set(loop.projectId, at);
|
|
39864
|
+
return this.saveLoop({
|
|
39865
|
+
...loop,
|
|
39866
|
+
lastTickAt: at
|
|
39867
|
+
});
|
|
39868
|
+
}
|
|
39869
|
+
/** When the project's loop last ticked in this process (none yet after a restart). */
|
|
39870
|
+
lastTick(projectId) {
|
|
39871
|
+
return this.lastTicks.get(projectId);
|
|
39872
|
+
}
|
|
39149
39873
|
item(id) {
|
|
39150
39874
|
const item = this.db.select().from(factoryItems).where(eq(factoryItems.id, id)).get()?.item;
|
|
39151
39875
|
return item ? withItemDefaults(item) : void 0;
|
|
@@ -39258,6 +39982,11 @@ function withItemDefaults(item) {
|
|
|
39258
39982
|
...item
|
|
39259
39983
|
};
|
|
39260
39984
|
}
|
|
39985
|
+
/** Same loop apart from the stamps a save sets; key order and `undefined` fields don't count. */
|
|
39986
|
+
function sameLoop(a, b) {
|
|
39987
|
+
const content = ({ lastTickAt: _tick, updatedAt: _saved, ...rest }) => JSON.parse(JSON.stringify(rest));
|
|
39988
|
+
return isDeepStrictEqual(content(a), content(b));
|
|
39989
|
+
}
|
|
39261
39990
|
const TUNING_DEFAULTS = FactoryTuning.parse({});
|
|
39262
39991
|
/** A loop as read from the projection: newer tuning knobs and the scout filled in. */
|
|
39263
39992
|
function upgradeLoop(loop) {
|
|
@@ -41526,11 +42255,9 @@ async function runTick(ctx, projectId, hooks) {
|
|
|
41526
42255
|
state: deriveState(ctx, projectId, loop)
|
|
41527
42256
|
}, now);
|
|
41528
42257
|
}
|
|
42258
|
+
/** Ticks that change nothing append nothing (the status shows the tick time from memory). */
|
|
41529
42259
|
function save(ctx, loop, now) {
|
|
41530
|
-
return ctx.store.
|
|
41531
|
-
...loop,
|
|
41532
|
-
lastTickAt: now.toISOString()
|
|
41533
|
-
});
|
|
42260
|
+
return ctx.store.saveTick(loop, now.toISOString());
|
|
41534
42261
|
}
|
|
41535
42262
|
/** The loop state the panel shows: the most advanced stage with work in it. */
|
|
41536
42263
|
function deriveState(ctx, projectId, loop) {
|
|
@@ -42087,7 +42814,7 @@ async function buildStatus(ctx, projectId) {
|
|
|
42087
42814
|
scout: project.autonomyMode === "dark_factory" ? scoutStatus(ctx, project, loop, spend) : null,
|
|
42088
42815
|
visionExists: visionOf(project).exists,
|
|
42089
42816
|
lastDigest: ctx.store.digests(projectId, 1)[0] ?? null,
|
|
42090
|
-
lastTickAt: loop.lastTickAt
|
|
42817
|
+
lastTickAt: ctx.store.lastTick(projectId) ?? loop.lastTickAt
|
|
42091
42818
|
};
|
|
42092
42819
|
}
|
|
42093
42820
|
// ../server/src/services/factory/factory-service.ts
|
|
@@ -51470,6 +52197,20 @@ var SessionRepo = class {
|
|
|
51470
52197
|
turnCount(sessionId) {
|
|
51471
52198
|
return this.db.select({ n: sql`count(*)` }).from(turns).where(eq(turns.sessionId, sessionId)).get()?.n ?? 0;
|
|
51472
52199
|
}
|
|
52200
|
+
/** One of the session's turns, or null when it has none with that id. */
|
|
52201
|
+
turn(sessionId, turnId) {
|
|
52202
|
+
const row = this.db.select().from(turns).where(and(eq(turns.sessionId, sessionId), eq(turns.id, turnId))).get();
|
|
52203
|
+
return row ? rowToTurn(row) : null;
|
|
52204
|
+
}
|
|
52205
|
+
/** The index of the checkpoint written when a turn ended (§9), or null when there is none. */
|
|
52206
|
+
turnCheckpoint(sessionId, turnId) {
|
|
52207
|
+
return this.db.select({ index: checkpoints.index }).from(checkpoints).where(and(eq(checkpoints.sessionId, sessionId), eq(checkpoints.turnId, turnId))).orderBy(desc(checkpoints.index)).limit(1).get()?.index ?? null;
|
|
52208
|
+
}
|
|
52209
|
+
/** The ids of the session's root-thread turns, oldest first (by start); turn N is at N - 1. */
|
|
52210
|
+
rootTurnIds(session) {
|
|
52211
|
+
if (!session.rootThreadId) return [];
|
|
52212
|
+
return this.db.select({ id: turns.id }).from(turns).where(and(eq(turns.sessionId, session.id), eq(turns.threadId, session.rootThreadId))).orderBy(asc(turns.startedAt), asc(sql`rowid`)).all().map((r) => r.id);
|
|
52213
|
+
}
|
|
51473
52214
|
/** Follow-ups of the latest root-thread turn that produced some (§7.5). */
|
|
51474
52215
|
latestFollowups(session) {
|
|
51475
52216
|
if (!session.rootThreadId) return null;
|
|
@@ -51499,6 +52240,55 @@ function readProjectConfig(projectPath) {
|
|
|
51499
52240
|
function projectDefaultProvider(project) {
|
|
51500
52241
|
return project.defaultProvider ?? readProjectConfig(project.path)?.defaultProvider ?? null;
|
|
51501
52242
|
}
|
|
52243
|
+
// ../server/src/services/sessions/fork-turn.ts
|
|
52244
|
+
/** Runner error code (from the provider): the conversation no longer holds the fork point. */
|
|
52245
|
+
const FORK_POINT_MISSING = "fork_point_missing";
|
|
52246
|
+
/**
|
|
52247
|
+
* Check that `source` can be forked at `turnId`, before asking the runner anything: a turn of
|
|
52248
|
+
* its own root thread that has ended, and (unless it is the latest) one whose end recorded a
|
|
52249
|
+
* fork point and a checkpoint. Throws `ServiceError`s.
|
|
52250
|
+
*/
|
|
52251
|
+
function forkTurn(repo, source, turnId) {
|
|
52252
|
+
const turn = repo.turn(source.id, turnId);
|
|
52253
|
+
if (!turn) throw notFound("The session has no such turn", "turn_not_found");
|
|
52254
|
+
if (turn.threadId !== source.rootThreadId) throw badRequest("Only the session's own turns can be forked from", "fork_unsupported");
|
|
52255
|
+
if (turn.status === "running") throw conflict("Wait for the turn to finish before forking from it", "turn_running");
|
|
52256
|
+
const rootTurns = repo.rootTurnIds(source);
|
|
52257
|
+
const turnNumber = rootTurns.indexOf(turnId) + 1;
|
|
52258
|
+
if (rootTurns.at(-1) === turnId) return {
|
|
52259
|
+
turnId,
|
|
52260
|
+
turnNumber,
|
|
52261
|
+
latest: true
|
|
52262
|
+
};
|
|
52263
|
+
if (!turn.forkPoint) throw conflict("This turn can't be forked from: the provider didn't record where its conversation ended", "fork_point_unavailable");
|
|
52264
|
+
const checkpoint = repo.turnCheckpoint(source.id, turnId);
|
|
52265
|
+
if (checkpoint === null) throw conflict("CodeDen has no snapshot of the code at the end of this turn", "no_checkpoint");
|
|
52266
|
+
return {
|
|
52267
|
+
turnId,
|
|
52268
|
+
turnNumber,
|
|
52269
|
+
latest: false,
|
|
52270
|
+
upTo: turn.forkPoint,
|
|
52271
|
+
checkpoint
|
|
52272
|
+
};
|
|
52273
|
+
}
|
|
52274
|
+
/**
|
|
52275
|
+
* The checkpoint of an earlier turn, once the runner confirms it still has it (`indexes` from
|
|
52276
|
+
* its settled list). Throws 409 `no_checkpoint` when it doesn't.
|
|
52277
|
+
*/
|
|
52278
|
+
function confirmedCheckpoint(at, indexes) {
|
|
52279
|
+
if (!indexes.includes(at.checkpoint)) throw conflict("CodeDen has no snapshot of the code at the end of this turn", "no_checkpoint");
|
|
52280
|
+
return at.checkpoint;
|
|
52281
|
+
}
|
|
52282
|
+
/**
|
|
52283
|
+
* A provider that no longer finds the fork point in the conversation (it was replaced or
|
|
52284
|
+
* deleted since the turn ended) answers 409 `fork_point_missing`; anything else is left to
|
|
52285
|
+
* the usual runner error mapping.
|
|
52286
|
+
*/
|
|
52287
|
+
function forkPointMissing(error) {
|
|
52288
|
+
if (runnerErrorCode(error) !== FORK_POINT_MISSING) return null;
|
|
52289
|
+
const detail = error instanceof Error ? ` (${error.message})` : "";
|
|
52290
|
+
return conflict(`This turn can't be forked from any more: its conversation changed since it ended${detail}`, FORK_POINT_MISSING);
|
|
52291
|
+
}
|
|
51502
52292
|
// ../server/src/services/sessions/fork.ts
|
|
51503
52293
|
const BUSY$1 = /* @__PURE__ */ new Set(["running", "waiting"]);
|
|
51504
52294
|
const NOT_STARTED$1 = /* @__PURE__ */ new Set([
|
|
@@ -51542,13 +52332,19 @@ function forkTitle(source) {
|
|
|
51542
52332
|
* 2. a copy of its conversation, made now by the provider (`provider.fork`).
|
|
51543
52333
|
* The source is only read. A turn that started meanwhile, even one that has already finished,
|
|
51544
52334
|
* would make the two disagree, so that fails the fork (`sourceMoved`; the unused copy is left
|
|
51545
|
-
* behind, never resumed).
|
|
52335
|
+
* behind, never resumed).
|
|
52336
|
+
*
|
|
52337
|
+
* With `req.turnId` of an earlier turn ("Fork from here"), the code is that turn's checkpoint
|
|
52338
|
+
* and the copy ends with that turn (`upTo`, its fork point). Neither changes once the turn has
|
|
52339
|
+
* ended, so later turns don't affect it. The latest turn forks as above. Throws `ServiceError`s.
|
|
51546
52340
|
*/
|
|
51547
52341
|
async function planFork(deps, sourceId, req) {
|
|
51548
52342
|
const { repo, runner } = deps;
|
|
51549
52343
|
const source = repo.get(sourceId);
|
|
51550
52344
|
const provider = runner.providers().find((p) => p.id === source.provider);
|
|
51551
52345
|
const project = forkProject(deps.projects, source, provider);
|
|
52346
|
+
const at = req.turnId ? forkTurn(repo, source, req.turnId) : null;
|
|
52347
|
+
const earlier = at && !at.latest ? at : null;
|
|
51552
52348
|
const turns = repo.turnCount(source.id);
|
|
51553
52349
|
const { checkpoints } = await runner.request({
|
|
51554
52350
|
name: "checkpoint.list",
|
|
@@ -51556,18 +52352,25 @@ async function planFork(deps, sourceId, req) {
|
|
|
51556
52352
|
sessionId: source.id,
|
|
51557
52353
|
settled: true
|
|
51558
52354
|
}, SETTLE_TIMEOUT_MS).catch(rethrow);
|
|
51559
|
-
const checkpoint = checkpoints.at(-1)?.index ?? null;
|
|
52355
|
+
const checkpoint = earlier ? confirmedCheckpoint(earlier, checkpoints.map((c) => c.index)) : checkpoints.at(-1)?.index ?? null;
|
|
51560
52356
|
const { resumeToken } = await runner.request({
|
|
51561
52357
|
name: "provider.fork",
|
|
51562
52358
|
providerId: source.provider,
|
|
51563
52359
|
resumeToken: source.resumeToken,
|
|
51564
|
-
cwd: project.path
|
|
51565
|
-
|
|
51566
|
-
|
|
51567
|
-
|
|
52360
|
+
cwd: project.path,
|
|
52361
|
+
...earlier ? { upTo: earlier.upTo } : {}
|
|
52362
|
+
}, FORK_TIMEOUT_MS).catch((error) => {
|
|
52363
|
+
throw forkPointMissing(error) ?? toServiceError$1(error);
|
|
52364
|
+
});
|
|
52365
|
+
if (!earlier) {
|
|
52366
|
+
const late = sourceMoved(repo, source, turns, provider);
|
|
52367
|
+
if (late) throw late;
|
|
52368
|
+
}
|
|
51568
52369
|
return forkPlan(source, project, {
|
|
51569
52370
|
checkpoint,
|
|
51570
|
-
resumeToken
|
|
52371
|
+
resumeToken,
|
|
52372
|
+
turnId: at?.turnId,
|
|
52373
|
+
turnNumber: at?.turnNumber
|
|
51571
52374
|
}, req);
|
|
51572
52375
|
}
|
|
51573
52376
|
/**
|
|
@@ -51596,10 +52399,11 @@ function forkProject(projects, source, provider) {
|
|
|
51596
52399
|
* effort, permissions, plan mode, work and issue, started by the user, with its own copy of the
|
|
51597
52400
|
* conversation (`resumeToken`). Its worktree branches from the source's branch and starts from
|
|
51598
52401
|
* `checkpoint`; its pull request targets the source's base branch. There is no §8.4 conflict
|
|
51599
|
-
* check: the fork is a deliberate, visible duplicate (`forkedFrom
|
|
52402
|
+
* check: the fork is a deliberate, visible duplicate (`forkedFrom`, with the turn it was taken
|
|
52403
|
+
* at when one was named).
|
|
51600
52404
|
*/
|
|
51601
52405
|
function forkPlan(source, project, origin, req) {
|
|
51602
|
-
const { checkpoint } = origin;
|
|
52406
|
+
const { checkpoint, turnId, turnNumber } = origin;
|
|
51603
52407
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
51604
52408
|
const session = {
|
|
51605
52409
|
id: randomUUID(),
|
|
@@ -51630,7 +52434,9 @@ function forkPlan(source, project, origin, req) {
|
|
|
51630
52434
|
rootThreadId: null,
|
|
51631
52435
|
forkedFrom: {
|
|
51632
52436
|
sessionId: source.id,
|
|
51633
|
-
checkpoint
|
|
52437
|
+
checkpoint,
|
|
52438
|
+
...turnId ? { turnId } : {},
|
|
52439
|
+
...turnId && turnNumber ? { turnNumber } : {}
|
|
51634
52440
|
},
|
|
51635
52441
|
createdAt: now,
|
|
51636
52442
|
updatedAt: now,
|
|
@@ -51988,8 +52794,8 @@ var SessionService = class {
|
|
|
51988
52794
|
}
|
|
51989
52795
|
/**
|
|
51990
52796
|
* Fork an agent session (§9): a new session with a copy of its conversation, in a new
|
|
51991
|
-
* worktree started from its latest checkpoint
|
|
51992
|
-
* background.
|
|
52797
|
+
* worktree started from its latest checkpoint, or from a named turn's ("Fork from here",
|
|
52798
|
+
* `fork.ts`). The worktree is made in the background.
|
|
51993
52799
|
*/
|
|
51994
52800
|
async fork(id, req) {
|
|
51995
52801
|
const { repo, projects, runner } = this.deps;
|
|
@@ -52303,14 +53109,16 @@ function isUnwatched(session) {
|
|
|
52303
53109
|
return session.automationRunId !== null || session.startedBy.kind === "automation" || session.startedBy.kind === "pm";
|
|
52304
53110
|
}
|
|
52305
53111
|
/**
|
|
52306
|
-
* Approvals nobody is there to answer (§10.1, §11.2). Nothing here ever
|
|
52307
|
-
*
|
|
52308
|
-
*
|
|
53112
|
+
* Approvals, and MCP servers' forms, nobody is there to answer (§10.1, §11.2). Nothing here ever
|
|
53113
|
+
* approves or fills in a form: an approval is denied, with guidance, and a form is withdrawn
|
|
53114
|
+
* (`cancel`, no data sent), so the agent carries on instead of waiting forever (fail closed).
|
|
53115
|
+
* - The PM and the factory's sessions in a dark factory are answered at once, as their questions
|
|
52309
53116
|
* are answered at once.
|
|
52310
53117
|
* - Sessions started by an automation, a PM or the factory otherwise wait
|
|
52311
|
-
* {@link UNWATCHED_APPROVAL_WAIT_MS} for the user
|
|
52312
|
-
*
|
|
52313
|
-
* Sessions the user started wait for the user, however long.
|
|
53118
|
+
* {@link UNWATCHED_APPROVAL_WAIT_MS} for the user. A scheduled run then finishes instead of
|
|
53119
|
+
* blocking the next ones.
|
|
53120
|
+
* Sessions the user started wait for the user, however long. The agent's own questions are left
|
|
53121
|
+
* to the factory's rules (`answerProviderQuestion`).
|
|
52314
53122
|
*/
|
|
52315
53123
|
var UnwatchedApprovals = class {
|
|
52316
53124
|
deps;
|
|
@@ -52319,10 +53127,10 @@ var UnwatchedApprovals = class {
|
|
|
52319
53127
|
constructor(deps) {
|
|
52320
53128
|
this.deps = deps;
|
|
52321
53129
|
}
|
|
52322
|
-
/** Watch new
|
|
53130
|
+
/** Watch new requests, and pick up the ones already open (a restart keeps their age). */
|
|
52323
53131
|
start() {
|
|
52324
53132
|
this.unsubscribe ??= this.deps.store.subscribe((events) => this.onEvents(events));
|
|
52325
|
-
for (const item of this.deps.inbox.list()) if (item.kind === "approval") this.track(item);
|
|
53133
|
+
for (const item of this.deps.inbox.list()) if (item.kind === "approval" || item.kind === "question") this.track(item);
|
|
52326
53134
|
}
|
|
52327
53135
|
stop() {
|
|
52328
53136
|
this.unsubscribe?.();
|
|
@@ -52334,7 +53142,7 @@ var UnwatchedApprovals = class {
|
|
|
52334
53142
|
for (const e of events) {
|
|
52335
53143
|
if (e.type !== "inbox.created" && e.type !== "inbox.updated") continue;
|
|
52336
53144
|
const { item } = e.payload;
|
|
52337
|
-
if (item.kind !== "approval") continue;
|
|
53145
|
+
if (item.kind !== "approval" && item.kind !== "question") continue;
|
|
52338
53146
|
if (item.status !== "open") {
|
|
52339
53147
|
clearTimeout(this.timers.get(item.id));
|
|
52340
53148
|
this.timers.delete(item.id);
|
|
@@ -52344,10 +53152,11 @@ var UnwatchedApprovals = class {
|
|
|
52344
53152
|
track(item) {
|
|
52345
53153
|
const { sessionId, requestId } = item;
|
|
52346
53154
|
if (item.status !== "open" || !sessionId || !requestId || this.timers.has(item.id)) return;
|
|
53155
|
+
if (item.kind === "question" && !this.isMcpForm(item)) return;
|
|
52347
53156
|
const session = this.deps.repo.find(sessionId)?.session;
|
|
52348
53157
|
if (!session) return;
|
|
52349
53158
|
if (unattendedSession(this.deps, sessionId)) {
|
|
52350
|
-
this.
|
|
53159
|
+
this.close(item, UNATTENDED_APPROVAL);
|
|
52351
53160
|
return;
|
|
52352
53161
|
}
|
|
52353
53162
|
if (!isUnwatched(session)) return;
|
|
@@ -52355,26 +53164,33 @@ var UnwatchedApprovals = class {
|
|
|
52355
53164
|
const age = (this.deps.now?.() ?? Date.now()) - Date.parse(item.createdAt);
|
|
52356
53165
|
const timer = setTimeout(() => {
|
|
52357
53166
|
this.timers.delete(item.id);
|
|
52358
|
-
this.
|
|
53167
|
+
this.close(item, noAnswerDenial(waitMs));
|
|
52359
53168
|
}, Math.max(0, waitMs - (Number.isFinite(age) ? age : 0)));
|
|
52360
53169
|
timer.unref?.();
|
|
52361
53170
|
this.timers.set(item.id, timer);
|
|
52362
53171
|
}
|
|
52363
|
-
|
|
53172
|
+
/** A question that is an MCP server's form (its transcript item names the server). */
|
|
53173
|
+
isMcpForm(item) {
|
|
53174
|
+
if (!item.sessionId || !item.itemId) return false;
|
|
53175
|
+
const mirrored = this.deps.repo.item(item.sessionId, item.itemId)?.item;
|
|
53176
|
+
return mirrored?.kind === "user_question" && mirrored.mcpServer !== void 0;
|
|
53177
|
+
}
|
|
53178
|
+
/** Deny an approval with `message`, or withdraw a form: never approve, never invent data. */
|
|
53179
|
+
async close(item, message) {
|
|
52364
53180
|
if (this.deps.inbox.find(item.id)?.status !== "open") return;
|
|
52365
53181
|
if (!item.sessionId || !item.requestId) return;
|
|
52366
53182
|
try {
|
|
52367
53183
|
await this.deps.respond(item.sessionId, {
|
|
52368
53184
|
requestId: item.requestId,
|
|
52369
|
-
response: {
|
|
53185
|
+
response: item.kind === "approval" ? {
|
|
52370
53186
|
kind: "approval",
|
|
52371
53187
|
decision: "deny",
|
|
52372
53188
|
message,
|
|
52373
53189
|
decidedBy: "policy"
|
|
52374
|
-
}
|
|
53190
|
+
} : { kind: "cancel" }
|
|
52375
53191
|
});
|
|
52376
53192
|
} catch (error) {
|
|
52377
|
-
this.deps.log?.(`approvals:
|
|
53193
|
+
this.deps.log?.(`approvals: close ${item.id}: ${errorMessage$1(error)}`);
|
|
52378
53194
|
}
|
|
52379
53195
|
}
|
|
52380
53196
|
};
|
|
@@ -57544,4 +58360,4 @@ main().then((code) => process.exit(code), (error) => {
|
|
|
57544
58360
|
});
|
|
57545
58361
|
export {};
|
|
57546
58362
|
|
|
57547
|
-
//# sourceMappingURL=main-
|
|
58363
|
+
//# sourceMappingURL=main-Bx58CdV4.js.map
|