whipped 0.8.1 → 0.9.1
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 +1296 -311
- package/dist/mcp-server.js +150 -2
- package/dist/migrations/014_companion_sessions.sql +34 -0
- package/dist/migrations/015_companion_worktree_mode.sql +45 -0
- package/dist/migrations/016_companion_plans.sql +22 -0
- package/dist/migrations/017_companion_saved_plans.sql +23 -0
- package/dist/migrations/018_generalize_plan_session_ref.sql +29 -0
- package/dist/migrations/019_rename_plan_to_canvas.sql +20 -0
- package/dist/web-ui/assets/abnfDiagram-VRR7QNED-CMDqE9Fd.js +119 -0
- package/dist/web-ui/assets/arc-Dbpy-AQ8.js +131 -0
- package/dist/web-ui/assets/architectureDiagram-ZJ3FMSHR-BKEqdC5c.js +8821 -0
- package/dist/web-ui/assets/blockDiagram-677ZJIJ3-CTsEohS4.js +3801 -0
- package/dist/web-ui/assets/c4Diagram-LMCZKHZV-Dn6mvxwq.js +2479 -0
- package/dist/web-ui/assets/channel-CGIzRjBJ.js +7 -0
- package/dist/web-ui/assets/chunk-2Q5K7J3B-DYVO9tIE.js +17 -0
- package/dist/web-ui/assets/chunk-32BRIVSS-CqBj7LwD.js +116 -0
- package/dist/web-ui/assets/chunk-5VM5RSS4-D6as2qUK.js +19 -0
- package/dist/web-ui/assets/chunk-EX3LRPZG-Daf_kmKB.js +1996 -0
- package/dist/web-ui/assets/chunk-JWPE2WC7-Ab-zf2K1.js +17 -0
- package/dist/web-ui/assets/chunk-MOJQB5TN-DnYfTlwW.js +855 -0
- package/dist/web-ui/assets/chunk-RYQCIY6F-BkiNDZ_4.js +476 -0
- package/dist/web-ui/assets/chunk-V7JOEXUC-DCb_30mT.js +2022 -0
- package/dist/web-ui/assets/chunk-VR4S4FIN-CO86AkST.js +25 -0
- package/dist/web-ui/assets/chunk-XXDRQBXY--g2YuB3U.js +13 -0
- package/dist/web-ui/assets/classDiagram-OUVF2IWQ-C25Jck2H.js +24 -0
- package/dist/web-ui/assets/classDiagram-v2-EOCWNBFH-C25Jck2H.js +24 -0
- package/dist/web-ui/assets/cose-bilkent-JH36ORCC-DhhXza2J.js +4943 -0
- package/dist/web-ui/assets/cynefin-VYW2F7L2-CaR1DgV9.js +31527 -0
- package/dist/web-ui/assets/cynefinDiagram-TSTJHNR4-CLGnTJf1.js +454 -0
- package/dist/web-ui/assets/cytoscape.esm-CaQ7Fomf.js +30346 -0
- package/dist/web-ui/assets/dagre-VKFMJZFB-DitV5zjf.js +526 -0
- package/dist/web-ui/assets/defaultLocale-B2RvLBDe.js +206 -0
- package/dist/web-ui/assets/diagram-FQU43EPY-C1rGhyyl.js +636 -0
- package/dist/web-ui/assets/diagram-G47NLZAW-CTQhjAQX.js +858 -0
- package/dist/web-ui/assets/diagram-NH7WQ7WH-DI8N7xbl.js +212 -0
- package/dist/web-ui/assets/diagram-OA4YK3LP-WcmQFHPD.js +492 -0
- package/dist/web-ui/assets/diagram-WEI45ONY-P5J7jo04.js +309 -0
- package/dist/web-ui/assets/ebnfDiagram-CCIWWBDH-VgG6WhIs.js +139 -0
- package/dist/web-ui/assets/erDiagram-Q63AITRT-aQJn3J15.js +1238 -0
- package/dist/web-ui/assets/flowDiagram-23GEKE2U-mdFXB92B.js +2353 -0
- package/dist/web-ui/assets/ganttDiagram-NO4QXBWP-CqDgijmY.js +3733 -0
- package/dist/web-ui/assets/geist-cyrillic-ext-wght-normal-DjL33-gN.woff2 +0 -0
- package/dist/web-ui/assets/geist-cyrillic-wght-normal-BEAKL7Jp.woff2 +0 -0
- package/dist/web-ui/assets/geist-latin-ext-wght-normal-DC-KSUi6.woff2 +0 -0
- package/dist/web-ui/assets/geist-latin-wght-normal-BgDaEnEv.woff2 +0 -0
- package/dist/web-ui/assets/geist-mono-cyrillic-ext-wght-normal-I4S5GZfc.woff2 +0 -0
- package/dist/web-ui/assets/geist-mono-cyrillic-wght-normal-BmXc_FBt.woff2 +0 -0
- package/dist/web-ui/assets/geist-mono-latin-ext-wght-normal-DrnZ1wKl.woff2 +0 -0
- package/dist/web-ui/assets/geist-mono-latin-wght-normal-B_7UjwxQ.woff2 +0 -0
- package/dist/web-ui/assets/geist-mono-symbols2-wght-normal-GZpp1pK2.woff2 +0 -0
- package/dist/web-ui/assets/geist-mono-vietnamese-wght-normal-D8KDMBhC.woff2 +0 -0
- package/dist/web-ui/assets/geist-vietnamese-wght-normal-6IgcOCM7.woff2 +0 -0
- package/dist/web-ui/assets/gitGraphDiagram-IHSO6WYX-8juiaoTk.js +1385 -0
- package/dist/web-ui/assets/graph-BMLV0goG.js +2042 -0
- package/dist/web-ui/assets/{index-CRXPsGTP.css → index-DPjATOCj.css} +800 -1207
- package/dist/web-ui/assets/{index-BMFVAmy4.js → index-DddtVpjm.js} +41479 -39640
- package/dist/web-ui/assets/infoDiagram-FWYZ7A6U-C2g8E3ea.js +32 -0
- package/dist/web-ui/assets/init-ZxktEp_H.js +16 -0
- package/dist/web-ui/assets/ishikawaDiagram-FXEZZL3T-cBRjKZAE.js +967 -0
- package/dist/web-ui/assets/journeyDiagram-5HDEW3XC-BvGisWzY.js +1256 -0
- package/dist/web-ui/assets/kanban-definition-HUTT4EX6-xCU5FVAS.js +1055 -0
- package/dist/web-ui/assets/katex-CqNtglxf.js +14499 -0
- package/dist/web-ui/assets/layout-BNmRhaUB.js +2359 -0
- package/dist/web-ui/assets/linear-uVfTbk22.js +340 -0
- package/dist/web-ui/assets/map-BEO0Bu8q.js +298 -0
- package/dist/web-ui/assets/mermaid.core-D-SdXkuv.js +26639 -0
- package/dist/web-ui/assets/mindmap-definition-LN4V7U3C-BXMrLpcc.js +1183 -0
- package/dist/web-ui/assets/ordinal-DSZU4PqD.js +76 -0
- package/dist/web-ui/assets/pegDiagram-2B236MQR-4QY6zfTY.js +127 -0
- package/dist/web-ui/assets/pieDiagram-ENE6RG2P-CvA8hnwZ.js +318 -0
- package/dist/web-ui/assets/quadrantDiagram-ABIIQ3AL-b9LyRoDu.js +1341 -0
- package/dist/web-ui/assets/railroadDiagram-RFXS5EU6-xnbYx8zt.js +93 -0
- package/dist/web-ui/assets/requirementDiagram-TGXJPOKE-DXgeFZvD.js +1205 -0
- package/dist/web-ui/assets/sankeyDiagram-HTMAVEWB-N3WPRpVR.js +1264 -0
- package/dist/web-ui/assets/sequenceDiagram-DBY2YBRQ-CasLOrw_.js +4523 -0
- package/dist/web-ui/assets/sizeCapture-X5ZJPWSS-DlBvxVbP.js +64 -0
- package/dist/web-ui/assets/stateDiagram-2N3HPSRC-fp4Rfa7y.js +453 -0
- package/dist/web-ui/assets/stateDiagram-v2-6OUMAXLB-B1Sbo4u9.js +23 -0
- package/dist/web-ui/assets/swimlanes-5IMT3BWC-D8woP0NL.js +8575 -0
- package/dist/web-ui/assets/swimlanesDiagram-G3AALYLV-BkAvTJ1E.js +21 -0
- package/dist/web-ui/assets/timeline-definition-FHXFAJF6-Bri3dfoP.js +1606 -0
- package/dist/web-ui/assets/vennDiagram-L72KCM5P-DTZlIjiw.js +2523 -0
- package/dist/web-ui/assets/wardleyDiagram-EHGQE667-CCyt_RTI.js +978 -0
- package/dist/web-ui/assets/xychartDiagram-FW5EYKEG-DtQR47sr.js +1972 -0
- package/dist/web-ui/index.html +2 -2
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3529,7 +3529,7 @@ function isResumableSessionState(state) {
|
|
|
3529
3529
|
function normalizeTag(raw2) {
|
|
3530
3530
|
return raw2.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
3531
3531
|
}
|
|
3532
|
-
var ASSISTANT_AGENT_PREFIX, runtimeAgentIdSchema, effortLevelSchema, agentModelChoiceSchema, workflowSlotTypeSchema, tierLevelSchema, LEVEL_ORDER, modelPairSchema, pairSelectionModeSchema, SLOT_TOOL_IDS, slotToolSchema, slotModelConfigSchema, cardModelConfigSchema, promptValueSchema, EMPTY_INLINE_PROMPT, workflowSlotSchema, DEFAULT_MODEL_PAIR, DEFAULT_SLOT_MODEL_FIELDS, workflowSchema, DEFAULT_WORKFLOW, DEFAULT_STORY_WORKFLOW, DEFAULT_GIT_INSTRUCTIONS, runtimeBoardColumnIdSchema, BOARD_COLUMNS, reviewActorSchema, reviewIssueSchema, reviewAttachmentSchema, runtimeReviewCommentSchema, runtimeActivityEntrySchema, runtimeTaskSessionStateSchema, runtimeTerminalSessionEntrySchema, runtimeCardPrioritySchema, cardTypeSchema, runtimePrMetaSchema, runtimeBoardCardSchema, runtimeBoardColumnSchema, runtimeBoardDataSchema, notificationSoundsConfigSchema, runtimeGlobalConfigSchema, runtimeGithubConfigSchema, runtimeWorktreeCopyEntrySchema, runtimeWorktreeSetupSchema, runtimeProjectSecretSchema, runtimeProjectConfigSchema, runtimeWorkspaceStateResponseSchema, runtimeWorkspaceStateSaveRequestSchema, runtimeVisualElementSchema, runtimeVisualCommentSchema, runtimeCardCreateRequestSchema, runtimeBulkCardImportItemSchema, runtimeBulkCardsCreateRequestSchema, runtimeCardMoveRequestSchema, runtimeCardUpdateRequestSchema, memoryScopeSchema, memoryTypeSchema, memorySourceTypeSchema, memoryStatusSchema, runtimeMemoryOriginAgentSchema, runtimeMemorySchema, recurringScheduleKindSchema, recurringScheduleSchema, recurringRunStatusSchema, recurringRunTriggerSchema, recurringAgentRunSchema, recurringAgentSchema, recurringAgentCreateRequestSchema, recurringAgentUpdateRequestSchema, projectFolderSchema, topLevelItemSchema, projectsLayoutSchema, runtimeProjectSchema;
|
|
3532
|
+
var ASSISTANT_AGENT_PREFIX, runtimeAgentIdSchema, effortLevelSchema, agentModelChoiceSchema, DEFAULT_AGENT_MODEL_CHOICE, workflowSlotTypeSchema, tierLevelSchema, LEVEL_ORDER, modelPairSchema, pairSelectionModeSchema, SLOT_TOOL_IDS, slotToolSchema, slotModelConfigSchema, cardModelConfigSchema, promptValueSchema, EMPTY_INLINE_PROMPT, workflowSlotSchema, DEFAULT_MODEL_PAIR, DEFAULT_SLOT_MODEL_FIELDS, workflowSchema, DEFAULT_WORKFLOW, DEFAULT_STORY_WORKFLOW, DEFAULT_GIT_INSTRUCTIONS, runtimeBoardColumnIdSchema, BOARD_COLUMNS, reviewActorSchema, reviewIssueSchema, reviewAttachmentSchema, runtimeReviewCommentSchema, runtimeActivityEntrySchema, runtimeTaskSessionStateSchema, runtimeTerminalSessionEntrySchema, runtimeCardPrioritySchema, cardTypeSchema, runtimePrMetaSchema, runtimeBoardCardSchema, runtimeBoardColumnSchema, runtimeBoardDataSchema, notificationSoundsConfigSchema, runtimeGlobalConfigSchema, runtimeGithubConfigSchema, runtimeWorktreeCopyEntrySchema, runtimeWorktreeSetupSchema, runtimeProjectSecretSchema, runtimeProjectConfigSchema, runtimeWorkspaceStateResponseSchema, runtimeWorkspaceStateSaveRequestSchema, runtimeVisualElementSchema, runtimeVisualCommentSchema, runtimeCardCreateRequestSchema, runtimeBulkCardImportItemSchema, runtimeBulkCardsCreateRequestSchema, runtimeCardMoveRequestSchema, runtimeCardUpdateRequestSchema, memoryScopeSchema, memoryTypeSchema, memorySourceTypeSchema, memoryStatusSchema, runtimeMemoryOriginAgentSchema, runtimeMemorySchema, recurringScheduleKindSchema, recurringScheduleSchema, recurringRunStatusSchema, recurringRunTriggerSchema, recurringAgentRunSchema, recurringAgentSchema, recurringAgentCreateRequestSchema, recurringAgentUpdateRequestSchema, companionSessionStatusSchema, companionSessionSchema, companionSessionCreateRequestSchema, choiceOptionSchema, REQUIRED_FIELD_DESCRIPTION, questionLeafInputSchema, questionInputSchema, canvasBlockSchema, canvasDocumentSchema, companionSavedCanvasSchema, projectFolderSchema, topLevelItemSchema, projectsLayoutSchema, runtimeProjectSchema;
|
|
3533
3533
|
var init_api_contract = __esm({
|
|
3534
3534
|
"src/core/api-contract.ts"() {
|
|
3535
3535
|
"use strict";
|
|
@@ -3541,6 +3541,7 @@ var init_api_contract = __esm({
|
|
|
3541
3541
|
model: z.string().nullable().optional(),
|
|
3542
3542
|
effort: effortLevelSchema.nullable().optional()
|
|
3543
3543
|
});
|
|
3544
|
+
DEFAULT_AGENT_MODEL_CHOICE = { agentId: "claude", model: null, effort: null };
|
|
3544
3545
|
workflowSlotTypeSchema = z.enum(["dev", "review", "plan", "orch"]);
|
|
3545
3546
|
tierLevelSchema = z.enum(["minimal", "low", "medium", "high", "max"]);
|
|
3546
3547
|
LEVEL_ORDER = ["minimal", "low", "medium", "high", "max"];
|
|
@@ -4090,6 +4091,104 @@ Do NOT include:
|
|
|
4090
4091
|
enabled: z.boolean().optional(),
|
|
4091
4092
|
journal: z.string().optional()
|
|
4092
4093
|
});
|
|
4094
|
+
companionSessionStatusSchema = z.enum(["installing", "running", "stopped", "merged", "discarded"]);
|
|
4095
|
+
companionSessionSchema = z.object({
|
|
4096
|
+
id: z.string(),
|
|
4097
|
+
name: z.string(),
|
|
4098
|
+
useWorktree: z.boolean(),
|
|
4099
|
+
baseRef: z.string(),
|
|
4100
|
+
branchName: z.string().nullable(),
|
|
4101
|
+
worktreePath: z.string().nullable(),
|
|
4102
|
+
workflowId: z.string().nullable(),
|
|
4103
|
+
seedPrompt: z.string().default(""),
|
|
4104
|
+
agentId: runtimeAgentIdSchema,
|
|
4105
|
+
model: z.string().nullable(),
|
|
4106
|
+
effort: effortLevelSchema.nullable(),
|
|
4107
|
+
status: companionSessionStatusSchema.default("stopped"),
|
|
4108
|
+
savedCanvasId: z.string().nullable(),
|
|
4109
|
+
createdAt: z.number(),
|
|
4110
|
+
updatedAt: z.number()
|
|
4111
|
+
});
|
|
4112
|
+
companionSessionCreateRequestSchema = z.object({
|
|
4113
|
+
name: z.string().optional(),
|
|
4114
|
+
useWorktree: z.boolean().default(true),
|
|
4115
|
+
baseRef: z.string().min(1),
|
|
4116
|
+
branchName: z.string().optional(),
|
|
4117
|
+
workflowId: z.string().optional(),
|
|
4118
|
+
model: agentModelChoiceSchema.optional(),
|
|
4119
|
+
savedCanvasId: z.string().optional()
|
|
4120
|
+
});
|
|
4121
|
+
choiceOptionSchema = z.object({
|
|
4122
|
+
value: z.string(),
|
|
4123
|
+
label: z.string(),
|
|
4124
|
+
description: z.string().optional()
|
|
4125
|
+
});
|
|
4126
|
+
REQUIRED_FIELD_DESCRIPTION = 'Signal only \u2014 not enforced by the panel, the developer can send/approve without answering. If this comes back "(not answered)" and you still need it, ask again in your next canvas version.';
|
|
4127
|
+
questionLeafInputSchema = z.discriminatedUnion("kind", [
|
|
4128
|
+
z.object({
|
|
4129
|
+
kind: z.literal("single_choice"),
|
|
4130
|
+
name: z.string(),
|
|
4131
|
+
label: z.string().optional(),
|
|
4132
|
+
options: z.array(choiceOptionSchema),
|
|
4133
|
+
allowOther: z.boolean().optional(),
|
|
4134
|
+
required: z.boolean().optional().describe(REQUIRED_FIELD_DESCRIPTION)
|
|
4135
|
+
}),
|
|
4136
|
+
z.object({
|
|
4137
|
+
kind: z.literal("multi_choice"),
|
|
4138
|
+
name: z.string(),
|
|
4139
|
+
label: z.string().optional(),
|
|
4140
|
+
options: z.array(choiceOptionSchema),
|
|
4141
|
+
allowOther: z.boolean().optional(),
|
|
4142
|
+
required: z.boolean().optional().describe(REQUIRED_FIELD_DESCRIPTION)
|
|
4143
|
+
}),
|
|
4144
|
+
z.object({
|
|
4145
|
+
kind: z.literal("text"),
|
|
4146
|
+
name: z.string(),
|
|
4147
|
+
label: z.string().optional(),
|
|
4148
|
+
placeholder: z.string().optional(),
|
|
4149
|
+
multiline: z.boolean().optional(),
|
|
4150
|
+
required: z.boolean().optional().describe(REQUIRED_FIELD_DESCRIPTION)
|
|
4151
|
+
})
|
|
4152
|
+
]);
|
|
4153
|
+
questionInputSchema = z.discriminatedUnion("kind", [
|
|
4154
|
+
...questionLeafInputSchema.options,
|
|
4155
|
+
z.object({ kind: z.literal("composite"), parts: z.array(questionLeafInputSchema) })
|
|
4156
|
+
]);
|
|
4157
|
+
canvasBlockSchema = z.discriminatedUnion("type", [
|
|
4158
|
+
z.object({ id: z.string(), type: z.literal("markdown"), body: z.string() }),
|
|
4159
|
+
// Rendered via dangerouslySetInnerHTML, unsanitized — same trust boundary as
|
|
4160
|
+
// the markdown block's rehype-raw pass-through (the agent is the user's own
|
|
4161
|
+
// coding agent, not untrusted third-party input).
|
|
4162
|
+
z.object({
|
|
4163
|
+
id: z.string(),
|
|
4164
|
+
type: z.literal("html"),
|
|
4165
|
+
body: z.string().describe(
|
|
4166
|
+
`Raw HTML rendered via dangerouslySetInnerHTML at runtime. Use this whenever the developer wants to see UI, layout, or visual design \u2014 a dashboard, a page structure, a component arrangement \u2014 since markdown can only describe that in prose while an actual html mockup shows it. It is NOT run through the app's build-time Tailwind compiler, so Tailwind utility classes (e.g. class="grid grid-cols-3 gap-4") produce no CSS and render unstyled \u2014 style the mockup with inline style="..." attributes, or a <style> block scoped to unique ids/classes you define within the same body.`
|
|
4167
|
+
)
|
|
4168
|
+
}),
|
|
4169
|
+
z.object({
|
|
4170
|
+
id: z.string(),
|
|
4171
|
+
type: z.literal("diagram"),
|
|
4172
|
+
format: z.literal("mermaid"),
|
|
4173
|
+
source: z.string(),
|
|
4174
|
+
caption: z.string().optional()
|
|
4175
|
+
}),
|
|
4176
|
+
z.object({ id: z.string(), type: z.literal("question"), prompt: z.string(), input: questionInputSchema })
|
|
4177
|
+
]);
|
|
4178
|
+
canvasDocumentSchema = z.object({
|
|
4179
|
+
version: z.number(),
|
|
4180
|
+
createdAt: z.number(),
|
|
4181
|
+
blocks: z.array(canvasBlockSchema)
|
|
4182
|
+
});
|
|
4183
|
+
companionSavedCanvasSchema = z.object({
|
|
4184
|
+
id: z.string(),
|
|
4185
|
+
workspaceId: z.string(),
|
|
4186
|
+
title: z.string(),
|
|
4187
|
+
blocks: z.array(canvasBlockSchema),
|
|
4188
|
+
sourceSessionId: z.string().nullable(),
|
|
4189
|
+
createdAt: z.number(),
|
|
4190
|
+
updatedAt: z.number()
|
|
4191
|
+
});
|
|
4093
4192
|
projectFolderSchema = z.object({
|
|
4094
4193
|
id: z.string(),
|
|
4095
4194
|
name: z.string(),
|
|
@@ -13623,9 +13722,9 @@ function installGracefulShutdownHandlers(options) {
|
|
|
13623
13722
|
init_logger();
|
|
13624
13723
|
|
|
13625
13724
|
// src/server/runtime-server.ts
|
|
13626
|
-
import { existsSync as
|
|
13725
|
+
import { existsSync as existsSync15, readFileSync as readFileSync8 } from "node:fs";
|
|
13627
13726
|
import { createServer } from "node:http";
|
|
13628
|
-
import { join as
|
|
13727
|
+
import { join as join23 } from "node:path";
|
|
13629
13728
|
import { fileURLToPath as fileURLToPath5 } from "node:url";
|
|
13630
13729
|
import * as nodePty3 from "node-pty";
|
|
13631
13730
|
|
|
@@ -13670,6 +13769,7 @@ var HOOKS_DIR = join9(WHIPPED_HOME_DIR, "hooks");
|
|
|
13670
13769
|
var CLAUDE_TASK_SETTINGS_PATH = join9(HOOKS_DIR, "claude-task-settings.json");
|
|
13671
13770
|
var CLAUDE_ASSISTANT_MCP_CONFIG_PATH = join9(HOOKS_DIR, "claude-assistant-mcp-config.json");
|
|
13672
13771
|
var CLAUDE_REVIEW_MCP_CONFIG_PATH = join9(HOOKS_DIR, "claude-review-mcp-config.json");
|
|
13772
|
+
var CLAUDE_COMPANION_SETTINGS_PATH = join9(HOOKS_DIR, "claude-companion-settings.json");
|
|
13673
13773
|
var HOOK_TASK_ID_ENV = "WHIPPED_HOOK_TASK_ID";
|
|
13674
13774
|
var HOOK_WORKSPACE_ID_ENV = "WHIPPED_HOOK_WORKSPACE_ID";
|
|
13675
13775
|
function getServerPort(serverUrl) {
|
|
@@ -13696,10 +13796,18 @@ async function writeClaudeTaskHookSettings(serverPort) {
|
|
|
13696
13796
|
await mkdir(HOOKS_DIR, { recursive: true });
|
|
13697
13797
|
await writeFile(CLAUDE_TASK_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
13698
13798
|
}
|
|
13699
|
-
function
|
|
13799
|
+
async function writeClaudeCompanionSettings() {
|
|
13800
|
+
const settings = {
|
|
13801
|
+
permissions: { deny: ["EnterPlanMode", "ExitPlanMode"] }
|
|
13802
|
+
};
|
|
13803
|
+
await mkdir(HOOKS_DIR, { recursive: true });
|
|
13804
|
+
await writeFile(CLAUDE_COMPANION_SETTINGS_PATH, JSON.stringify(settings, null, 2));
|
|
13805
|
+
}
|
|
13806
|
+
function buildMcpRoleArgs(role, recurringAgentId, companionSessionId) {
|
|
13700
13807
|
const args = [];
|
|
13701
13808
|
if (role) args.push(`--role=${role}`);
|
|
13702
13809
|
if (recurringAgentId) args.push(`--recurring-agent-id=${recurringAgentId}`);
|
|
13810
|
+
if (companionSessionId) args.push(`--companion-session-id=${companionSessionId}`);
|
|
13703
13811
|
return args;
|
|
13704
13812
|
}
|
|
13705
13813
|
function buildWhippedMcpServerSpec(mcp, serverUrl, workspaceId, agentId, extraArgs = []) {
|
|
@@ -20484,7 +20592,7 @@ init_workspace_state();
|
|
|
20484
20592
|
var import_tree_kill2 = __toESM(require_tree_kill(), 1);
|
|
20485
20593
|
import { existsSync as existsSync10 } from "node:fs";
|
|
20486
20594
|
import { cp, link, mkdir as mkdir3, stat as stat2, symlink, unlink as unlink2 } from "node:fs/promises";
|
|
20487
|
-
import { dirname as dirname6, join as
|
|
20595
|
+
import { dirname as dirname6, join as join18, resolve as resolve2 } from "node:path";
|
|
20488
20596
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
20489
20597
|
import * as nodePty2 from "node-pty";
|
|
20490
20598
|
init_api_contract();
|
|
@@ -20508,13 +20616,320 @@ function resolvePromptText(prompt, repoPath) {
|
|
|
20508
20616
|
|
|
20509
20617
|
// src/daemon/scheduler.ts
|
|
20510
20618
|
init_task_id();
|
|
20619
|
+
|
|
20620
|
+
// src/state/companion-canvases-store.ts
|
|
20621
|
+
init_task_id();
|
|
20622
|
+
init_db();
|
|
20623
|
+
var RECENT_CANVASES_LIMIT = 20;
|
|
20624
|
+
function safeJsonParse2(raw2, fallback) {
|
|
20625
|
+
try {
|
|
20626
|
+
return JSON.parse(raw2);
|
|
20627
|
+
} catch {
|
|
20628
|
+
return fallback;
|
|
20629
|
+
}
|
|
20630
|
+
}
|
|
20631
|
+
function canvasFromRow(row) {
|
|
20632
|
+
return {
|
|
20633
|
+
version: row.version,
|
|
20634
|
+
createdAt: row.created_at,
|
|
20635
|
+
blocks: safeJsonParse2(row.blocks_json, [])
|
|
20636
|
+
};
|
|
20637
|
+
}
|
|
20638
|
+
function nextVersion(sessionId) {
|
|
20639
|
+
const row = getDb().prepare("SELECT MAX(version) as max FROM companion_canvases WHERE session_id = ?").get(sessionId);
|
|
20640
|
+
return (row?.max ?? 0) + 1;
|
|
20641
|
+
}
|
|
20642
|
+
function createCompanionCanvas(sessionId, workspaceId, blocks) {
|
|
20643
|
+
const id = `cpv_${generateTaskId()}`;
|
|
20644
|
+
const version = nextVersion(sessionId);
|
|
20645
|
+
const createdAt = Date.now();
|
|
20646
|
+
getDb().prepare(
|
|
20647
|
+
"INSERT INTO companion_canvases (id, session_id, workspace_id, version, blocks_json, created_at) VALUES (?, ?, ?, ?, ?, ?)"
|
|
20648
|
+
).run(id, sessionId, workspaceId, version, JSON.stringify(blocks), createdAt);
|
|
20649
|
+
return { version, createdAt, blocks };
|
|
20650
|
+
}
|
|
20651
|
+
function listCompanionCanvases(sessionId) {
|
|
20652
|
+
const rows = getDb().prepare("SELECT * FROM companion_canvases WHERE session_id = ? ORDER BY version DESC LIMIT ?").all(sessionId, RECENT_CANVASES_LIMIT);
|
|
20653
|
+
return rows.map(canvasFromRow);
|
|
20654
|
+
}
|
|
20655
|
+
function deleteCompanionCanvasesForSession(sessionId) {
|
|
20656
|
+
getDb().prepare("DELETE FROM companion_canvases WHERE session_id = ?").run(sessionId);
|
|
20657
|
+
}
|
|
20658
|
+
|
|
20659
|
+
// src/state/companion-saved-canvases-store.ts
|
|
20660
|
+
init_task_id();
|
|
20661
|
+
init_db();
|
|
20662
|
+
function safeJsonParse3(raw2, fallback) {
|
|
20663
|
+
try {
|
|
20664
|
+
return JSON.parse(raw2);
|
|
20665
|
+
} catch {
|
|
20666
|
+
return fallback;
|
|
20667
|
+
}
|
|
20668
|
+
}
|
|
20669
|
+
function savedCanvasFromRow(row) {
|
|
20670
|
+
return {
|
|
20671
|
+
id: row.id,
|
|
20672
|
+
workspaceId: row.workspace_id,
|
|
20673
|
+
title: row.title,
|
|
20674
|
+
blocks: safeJsonParse3(row.blocks_json, []),
|
|
20675
|
+
sourceSessionId: row.source_session_id,
|
|
20676
|
+
createdAt: row.created_at,
|
|
20677
|
+
updatedAt: row.updated_at
|
|
20678
|
+
};
|
|
20679
|
+
}
|
|
20680
|
+
function listCompanionSavedCanvases(workspaceId) {
|
|
20681
|
+
const rows = getDb().prepare("SELECT * FROM companion_saved_canvases WHERE workspace_id = ? ORDER BY updated_at DESC").all(workspaceId);
|
|
20682
|
+
return rows.map(savedCanvasFromRow);
|
|
20683
|
+
}
|
|
20684
|
+
function getCompanionSavedCanvas(id) {
|
|
20685
|
+
const row = getDb().prepare("SELECT * FROM companion_saved_canvases WHERE id = ?").get(id);
|
|
20686
|
+
return row ? savedCanvasFromRow(row) : null;
|
|
20687
|
+
}
|
|
20688
|
+
function createCompanionSavedCanvas(workspaceId, input) {
|
|
20689
|
+
const id = `csp_${generateTaskId()}`;
|
|
20690
|
+
const now = Date.now();
|
|
20691
|
+
getDb().prepare(
|
|
20692
|
+
`INSERT INTO companion_saved_canvases
|
|
20693
|
+
(id, workspace_id, title, blocks_json, source_session_id, created_at, updated_at)
|
|
20694
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
20695
|
+
).run(id, workspaceId, input.title, JSON.stringify(input.blocks), input.sourceSessionId, now, now);
|
|
20696
|
+
const created = getCompanionSavedCanvas(id);
|
|
20697
|
+
if (!created) throw new Error("createCompanionSavedCanvas: row vanished after insert");
|
|
20698
|
+
return created;
|
|
20699
|
+
}
|
|
20700
|
+
function updateCompanionSavedCanvas(id, input) {
|
|
20701
|
+
getDb().prepare("UPDATE companion_saved_canvases SET title = ?, blocks_json = ?, updated_at = ? WHERE id = ?").run(input.title, JSON.stringify(input.blocks), Date.now(), id);
|
|
20702
|
+
return getCompanionSavedCanvas(id);
|
|
20703
|
+
}
|
|
20704
|
+
function deleteCompanionSavedCanvas(id) {
|
|
20705
|
+
getDb().prepare("DELETE FROM companion_saved_canvases WHERE id = ?").run(id);
|
|
20706
|
+
}
|
|
20707
|
+
function findCompanionSavedCanvasBySourceSession(sessionId) {
|
|
20708
|
+
const row = getDb().prepare("SELECT * FROM companion_saved_canvases WHERE source_session_id = ? ORDER BY updated_at DESC LIMIT 1").get(sessionId);
|
|
20709
|
+
return row ? savedCanvasFromRow(row) : null;
|
|
20710
|
+
}
|
|
20711
|
+
|
|
20712
|
+
// src/state/companion-sessions-store.ts
|
|
20713
|
+
init_task_id();
|
|
20714
|
+
init_db();
|
|
20715
|
+
function sessionFromRow(row) {
|
|
20716
|
+
return {
|
|
20717
|
+
id: row.id,
|
|
20718
|
+
name: row.name,
|
|
20719
|
+
useWorktree: row.use_worktree === 1,
|
|
20720
|
+
baseRef: row.base_ref,
|
|
20721
|
+
branchName: row.branch_name,
|
|
20722
|
+
worktreePath: row.worktree_path,
|
|
20723
|
+
workflowId: row.workflow_id,
|
|
20724
|
+
seedPrompt: row.seed_prompt,
|
|
20725
|
+
agentId: row.agent_id,
|
|
20726
|
+
model: row.model,
|
|
20727
|
+
effort: row.effort,
|
|
20728
|
+
status: row.status,
|
|
20729
|
+
savedCanvasId: row.saved_canvas_id,
|
|
20730
|
+
createdAt: row.created_at,
|
|
20731
|
+
updatedAt: row.updated_at
|
|
20732
|
+
};
|
|
20733
|
+
}
|
|
20734
|
+
function listCompanionSessions(workspaceId) {
|
|
20735
|
+
const rows = getDb().prepare("SELECT * FROM companion_sessions WHERE workspace_id = ? ORDER BY created_at DESC").all(workspaceId);
|
|
20736
|
+
return rows.map(sessionFromRow);
|
|
20737
|
+
}
|
|
20738
|
+
function getCompanionSession(id) {
|
|
20739
|
+
const row = getDb().prepare("SELECT * FROM companion_sessions WHERE id = ?").get(id);
|
|
20740
|
+
return row ? sessionFromRow(row) : null;
|
|
20741
|
+
}
|
|
20742
|
+
function createCompanionSession(workspaceId, input) {
|
|
20743
|
+
const id = `cs_${generateTaskId()}`;
|
|
20744
|
+
const now = Date.now();
|
|
20745
|
+
getDb().prepare(
|
|
20746
|
+
`INSERT INTO companion_sessions
|
|
20747
|
+
(id, workspace_id, name, use_worktree, base_ref, branch_name, worktree_path, workflow_id, seed_prompt,
|
|
20748
|
+
agent_id, model, effort, status, saved_canvas_id, created_at, updated_at)
|
|
20749
|
+
VALUES (?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, ?, 'stopped', ?, ?, ?)`
|
|
20750
|
+
).run(
|
|
20751
|
+
id,
|
|
20752
|
+
workspaceId,
|
|
20753
|
+
input.name,
|
|
20754
|
+
input.useWorktree ? 1 : 0,
|
|
20755
|
+
input.baseRef,
|
|
20756
|
+
input.branchName,
|
|
20757
|
+
input.workflowId,
|
|
20758
|
+
input.seedPrompt,
|
|
20759
|
+
input.agentId,
|
|
20760
|
+
input.model,
|
|
20761
|
+
input.effort,
|
|
20762
|
+
input.savedCanvasId,
|
|
20763
|
+
now,
|
|
20764
|
+
now
|
|
20765
|
+
);
|
|
20766
|
+
const created = getCompanionSession(id);
|
|
20767
|
+
if (!created) throw new Error("createCompanionSession: row vanished after insert");
|
|
20768
|
+
return created;
|
|
20769
|
+
}
|
|
20770
|
+
function setCompanionSessionSavedCanvasId(id, savedCanvasId) {
|
|
20771
|
+
getDb().prepare("UPDATE companion_sessions SET saved_canvas_id = ?, updated_at = ? WHERE id = ?").run(savedCanvasId, Date.now(), id);
|
|
20772
|
+
}
|
|
20773
|
+
function setCompanionSessionWorktreePath(id, worktreePath) {
|
|
20774
|
+
getDb().prepare("UPDATE companion_sessions SET worktree_path = ?, updated_at = ? WHERE id = ?").run(worktreePath, Date.now(), id);
|
|
20775
|
+
}
|
|
20776
|
+
function setCompanionSessionStatus(id, status) {
|
|
20777
|
+
getDb().prepare("UPDATE companion_sessions SET status = ?, updated_at = ? WHERE id = ?").run(status, Date.now(), id);
|
|
20778
|
+
}
|
|
20779
|
+
function deleteCompanionSession(id) {
|
|
20780
|
+
getDb().prepare("DELETE FROM companion_sessions WHERE id = ?").run(id);
|
|
20781
|
+
}
|
|
20782
|
+
function resetStaleCompanionSessions(workspaceId) {
|
|
20783
|
+
const res = getDb().prepare(
|
|
20784
|
+
"UPDATE companion_sessions SET status = 'stopped', updated_at = ? WHERE workspace_id = ? AND status IN ('running', 'installing')"
|
|
20785
|
+
).run(Date.now(), workspaceId);
|
|
20786
|
+
return res.changes;
|
|
20787
|
+
}
|
|
20788
|
+
|
|
20789
|
+
// src/daemon/scheduler.ts
|
|
20511
20790
|
init_workspace_state();
|
|
20512
20791
|
|
|
20513
|
-
// src/daemon/
|
|
20792
|
+
// src/daemon/companion-agent.ts
|
|
20793
|
+
init_api_contract();
|
|
20794
|
+
|
|
20795
|
+
// src/daemon/canvas-mode-prompt.ts
|
|
20796
|
+
function serializeCanvasBlocksForPrompt(blocks) {
|
|
20797
|
+
return blocks.map((block) => {
|
|
20798
|
+
if (block.type === "markdown") return block.body;
|
|
20799
|
+
if (block.type === "html") return `\`\`\`html
|
|
20800
|
+
${block.body}
|
|
20801
|
+
\`\`\``;
|
|
20802
|
+
if (block.type === "diagram") return `\`\`\`mermaid
|
|
20803
|
+
${block.source}
|
|
20804
|
+
\`\`\``;
|
|
20805
|
+
const input = block.input;
|
|
20806
|
+
const options = input.kind === "single_choice" || input.kind === "multi_choice" ? `
|
|
20807
|
+
Options: ${input.options.map((o) => o.label).join(", ")}` : "";
|
|
20808
|
+
return `Q: ${block.prompt}${options}`;
|
|
20809
|
+
}).join("\n\n");
|
|
20810
|
+
}
|
|
20811
|
+
function buildCanvasModeGuidance() {
|
|
20812
|
+
return `Call \`whipped_show_canvas\` at most once per turn. Never call it twice in a row before the developer has replied \u2014 each call appends a new version to their canvas, so back-to-back calls show up as clutter, not a revision. If you want to reconsider before sending, do that thinking first and make one call with the version you're actually confident in.
|
|
20813
|
+
|
|
20814
|
+
Pick the right block type for what you're conveying: markdown for reasoning, steps, findings, and options; an \`html\` block whenever the developer wants to see UI, layout, or visual design \u2014 a dashboard, a page structure, a component arrangement. Don't default to describing a layout in prose when they asked to see it \u2014 build an actual mockup (divs, flexbox/grid, realistic spacing and colors) so they're looking at an approximation of the real thing, not reading about it. \`html\` blocks are injected into the page at runtime via \`dangerouslySetInnerHTML\` \u2014 they are NOT compiled by the app's build-time Tailwind setup, so Tailwind utility classes in that HTML (e.g. \`class="grid grid-cols-3 gap-4"\`) produce no CSS and render unstyled. That's a styling detail, not a reason to avoid html blocks \u2014 style mockups with inline \`style="..."\` attributes, or a \`<style>\` block scoped to unique ids/classes you define in that same block's body.
|
|
20815
|
+
|
|
20816
|
+
A question can be marked \`required\`, but that's a signal to you, not something the UI enforces \u2014 the developer can send feedback (or approve) without answering one, e.g. because they'd rather just leave a comment than pick from options that don't fit. The message you get back states every question explicitly, either with an answer or "(not answered)" \u2014 never silently omitted. If a required question comes back "(not answered)" and it's still something you need to know, ask it again in your next canvas version rather than assuming it's resolved. And if a comment on a question block says the options don't fit (wrong choices, missing one they want, etc.), revise, add, or remove options in your next version accordingly instead of re-asking the same broken question verbatim.
|
|
20817
|
+
|
|
20818
|
+
When the developer approves a canvas, they'll be offered the option to save it to the project's reusable canvas library. If they do, you'll be asked to consolidate everything proposed across every version pushed in this session into ONE final, coherent canvas and save it via the \`whipped_save_canvas\` tool. Beyond that one prompted moment, also call \`whipped_save_canvas\` proactively whenever you finish a meaningful chunk of work \u2014 describe what's done explicitly in the blocks (not just what's left), so that if this session's canvas is ever resumed later, its state accurately reflects progress. If this session already has a saved canvas (you resumed from one, or already saved once), calling the tool again updates that same canvas in place rather than creating a duplicate \u2014 you don't need to track which case applies, the tool handles it.`;
|
|
20819
|
+
}
|
|
20820
|
+
|
|
20821
|
+
// src/git/git-diff-utils.ts
|
|
20514
20822
|
import { spawnSync as spawnSync5 } from "node:child_process";
|
|
20515
|
-
import {
|
|
20516
|
-
import { readdir, readFile as readFile2, stat, unlink } from "node:fs/promises";
|
|
20823
|
+
import { readFileSync as readFileSync6 } from "node:fs";
|
|
20517
20824
|
import { join as join16 } from "node:path";
|
|
20825
|
+
function git4(args, cwd) {
|
|
20826
|
+
return spawnSync5("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).stdout?.trim() ?? "";
|
|
20827
|
+
}
|
|
20828
|
+
function readFileSafe(filePath) {
|
|
20829
|
+
try {
|
|
20830
|
+
return readFileSync6(filePath, "utf-8");
|
|
20831
|
+
} catch {
|
|
20832
|
+
return "";
|
|
20833
|
+
}
|
|
20834
|
+
}
|
|
20835
|
+
function getGitStat(worktreePath, baseRef) {
|
|
20836
|
+
const parts = [
|
|
20837
|
+
git4(["diff", "--stat", `${baseRef}...HEAD`], worktreePath),
|
|
20838
|
+
git4(["diff", "--stat", "--cached"], worktreePath),
|
|
20839
|
+
git4(["diff", "--stat"], worktreePath)
|
|
20840
|
+
].filter(Boolean);
|
|
20841
|
+
const newUntracked = git4(["ls-files", "--others", "--exclude-standard"], worktreePath).split("\n").map((f2) => f2.trim()).filter(Boolean);
|
|
20842
|
+
if (newUntracked.length > 0) {
|
|
20843
|
+
parts.push(`New files:
|
|
20844
|
+
${newUntracked.map((f2) => ` ${f2}`).join("\n")}`);
|
|
20845
|
+
}
|
|
20846
|
+
return parts.join("\n") || "(no changes detected \u2014 agent may not have committed yet)";
|
|
20847
|
+
}
|
|
20848
|
+
function getGitFullDiff(worktreePath, baseRef) {
|
|
20849
|
+
const sections = [];
|
|
20850
|
+
const diffParts = [
|
|
20851
|
+
git4(["diff", "-U15", `${baseRef}...HEAD`], worktreePath),
|
|
20852
|
+
git4(["diff", "-U15", "--cached"], worktreePath),
|
|
20853
|
+
git4(["diff", "-U15"], worktreePath)
|
|
20854
|
+
].filter(Boolean);
|
|
20855
|
+
if (diffParts.length > 0) {
|
|
20856
|
+
sections.push(`\`\`\`diff
|
|
20857
|
+
${diffParts.join("\n")}
|
|
20858
|
+
\`\`\``);
|
|
20859
|
+
}
|
|
20860
|
+
const newUntracked = git4(["ls-files", "--others", "--exclude-standard"], worktreePath).split("\n").map((f2) => f2.trim()).filter(Boolean);
|
|
20861
|
+
if (newUntracked.length > 0) {
|
|
20862
|
+
const newFileContents = [];
|
|
20863
|
+
for (const file of newUntracked) {
|
|
20864
|
+
const content = readFileSafe(join16(worktreePath, file));
|
|
20865
|
+
const ext = file.split(".").pop() ?? "";
|
|
20866
|
+
newFileContents.push(content ? `### ${file}
|
|
20867
|
+
\`\`\`${ext}
|
|
20868
|
+
${content}
|
|
20869
|
+
\`\`\`` : `### ${file} (unreadable)`);
|
|
20870
|
+
}
|
|
20871
|
+
sections.push(`New files (full content):
|
|
20872
|
+
|
|
20873
|
+
${newFileContents.join("\n\n")}`);
|
|
20874
|
+
}
|
|
20875
|
+
return sections.join("\n\n");
|
|
20876
|
+
}
|
|
20877
|
+
function getGitHeadSha(worktreePath) {
|
|
20878
|
+
return git4(["rev-parse", "HEAD"], worktreePath);
|
|
20879
|
+
}
|
|
20880
|
+
var DIFF_MAX_BUFFER = 4 * 1024 * 1024;
|
|
20881
|
+
function buildWorktreeDiff(worktreePath, baseRef) {
|
|
20882
|
+
const mergeBaseResult = spawnSync5("git", ["merge-base", baseRef, "HEAD"], {
|
|
20883
|
+
cwd: worktreePath,
|
|
20884
|
+
encoding: "utf-8"
|
|
20885
|
+
});
|
|
20886
|
+
if (mergeBaseResult.status !== 0) {
|
|
20887
|
+
return { diff: null, error: mergeBaseResult.stderr?.trim() || "Failed to resolve merge base" };
|
|
20888
|
+
}
|
|
20889
|
+
const diffResult = spawnSync5("git", ["diff", mergeBaseResult.stdout.trim(), "--no-color", "-U3"], {
|
|
20890
|
+
cwd: worktreePath,
|
|
20891
|
+
encoding: "utf-8",
|
|
20892
|
+
maxBuffer: DIFF_MAX_BUFFER
|
|
20893
|
+
});
|
|
20894
|
+
if (diffResult.status !== 0 && diffResult.stderr) {
|
|
20895
|
+
return { diff: null, error: diffResult.stderr.trim() };
|
|
20896
|
+
}
|
|
20897
|
+
const untrackedResult = spawnSync5("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
20898
|
+
cwd: worktreePath,
|
|
20899
|
+
encoding: "utf-8"
|
|
20900
|
+
});
|
|
20901
|
+
const untrackedDiffs = (untrackedResult.stdout ?? "").split("\n").map((f2) => f2.trim()).filter(Boolean).map((file) => {
|
|
20902
|
+
const header = `diff --git a/${file} b/${file}
|
|
20903
|
+
new file mode 100644
|
|
20904
|
+
--- /dev/null
|
|
20905
|
+
+++ b/${file}`;
|
|
20906
|
+
const content = readFileSafe(join16(worktreePath, file));
|
|
20907
|
+
if (!content) return header;
|
|
20908
|
+
const lines = content.split("\n");
|
|
20909
|
+
const addedLines = lines.map((l) => `+${l}`).join("\n");
|
|
20910
|
+
return `${header}
|
|
20911
|
+
@@ -0,0 +1,${lines.length} @@
|
|
20912
|
+
${addedLines}`;
|
|
20913
|
+
});
|
|
20914
|
+
const diff = [diffResult.stdout, ...untrackedDiffs].filter((s2) => s2?.trim()).join("\n");
|
|
20915
|
+
const behindResult = spawnSync5("git", ["rev-list", "--count", `HEAD..${baseRef}`], {
|
|
20916
|
+
cwd: worktreePath,
|
|
20917
|
+
encoding: "utf-8"
|
|
20918
|
+
});
|
|
20919
|
+
const baseBehindCount = parseInt(behindResult.stdout?.trim() ?? "0", 10) || 0;
|
|
20920
|
+
return { diff, error: null, baseBehindCount };
|
|
20921
|
+
}
|
|
20922
|
+
var INLINE_DIFF_LIMIT = 8e3;
|
|
20923
|
+
function formatDiffBlock(fullDiff, baseRef, header = "Git diff") {
|
|
20924
|
+
if (fullDiff.length <= INLINE_DIFF_LIMIT) return `${header}:
|
|
20925
|
+
${fullDiff}`;
|
|
20926
|
+
return `Large changeset (${fullDiff.length.toLocaleString()} chars). Use \`git diff ${baseRef}...HEAD\` and read individual files to explore.`;
|
|
20927
|
+
}
|
|
20928
|
+
|
|
20929
|
+
// src/daemon/review-pipeline.ts
|
|
20930
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
20931
|
+
import { readdir, readFile as readFile2, stat, unlink } from "node:fs/promises";
|
|
20932
|
+
import { join as join17 } from "node:path";
|
|
20518
20933
|
init_runtime_config();
|
|
20519
20934
|
init_api_contract();
|
|
20520
20935
|
init_logger();
|
|
@@ -20799,7 +21214,7 @@ function getSlotTriggerWord(type) {
|
|
|
20799
21214
|
}
|
|
20800
21215
|
var SCREENSHOT_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "webp"]);
|
|
20801
21216
|
async function attachBrowserArtifacts(workspaceId, card, result, since) {
|
|
20802
|
-
const dir =
|
|
21217
|
+
const dir = join17(ATTACHMENTS_DIR, card.id);
|
|
20803
21218
|
let entries;
|
|
20804
21219
|
try {
|
|
20805
21220
|
entries = await readdir(dir);
|
|
@@ -20812,7 +21227,7 @@ async function attachBrowserArtifacts(workspaceId, card, result, since) {
|
|
|
20812
21227
|
for (const name of entries) {
|
|
20813
21228
|
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
20814
21229
|
if (!SCREENSHOT_EXTENSIONS.has(ext)) continue;
|
|
20815
|
-
const filePath =
|
|
21230
|
+
const filePath = join17(dir, name);
|
|
20816
21231
|
try {
|
|
20817
21232
|
const info2 = await stat(filePath);
|
|
20818
21233
|
if (!info2.isFile() || info2.mtimeMs < since) continue;
|
|
@@ -21077,80 +21492,25 @@ function runAgentOnce(agentId, prompt, cwd, workspaceId, streamId, stateHub, reg
|
|
|
21077
21492
|
unregisterProcess = registerLiveProcess(streamId, proc);
|
|
21078
21493
|
});
|
|
21079
21494
|
}
|
|
21080
|
-
function
|
|
21081
|
-
return
|
|
21082
|
-
}
|
|
21083
|
-
function readFileSafe(filePath) {
|
|
21084
|
-
try {
|
|
21085
|
-
return readFileSync6(filePath, "utf-8");
|
|
21086
|
-
} catch {
|
|
21087
|
-
return "";
|
|
21088
|
-
}
|
|
21495
|
+
function attachmentLines(attachments) {
|
|
21496
|
+
return attachments.map((a, i) => `- [Attachment #${i + 1}] ${a.name}: ${a.path}`).join("\n");
|
|
21089
21497
|
}
|
|
21090
|
-
function
|
|
21498
|
+
function formatComment(c, opts) {
|
|
21499
|
+
const typeLabel = c.type === "human" ? "Human Feedback" : c.type === "visual-comment" ? "Visual Feedback" : c.type.replace(/[-_]/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
21500
|
+
const actorId = c.actor.id;
|
|
21501
|
+
const statusLabel = c.status?.toUpperCase() ?? "";
|
|
21502
|
+
const hasMustFix = c.issues?.some((i) => i.severity === "blocking" || i.severity === "warning") ?? false;
|
|
21503
|
+
const failedRound = c.status === "fail" || hasMustFix;
|
|
21504
|
+
const mustFix = failedRound && !opts.stripMustFix ? " \u26A0 MUST FIX BEFORE PROCEEDING" : "";
|
|
21091
21505
|
const parts = [
|
|
21092
|
-
|
|
21093
|
-
|
|
21094
|
-
|
|
21095
|
-
|
|
21096
|
-
|
|
21097
|
-
|
|
21098
|
-
|
|
21099
|
-
|
|
21100
|
-
}
|
|
21101
|
-
return parts.join("\n") || "(no changes detected \u2014 agent may not have committed yet)";
|
|
21102
|
-
}
|
|
21103
|
-
function getGitFullDiff(worktreePath, baseRef) {
|
|
21104
|
-
const sections = [];
|
|
21105
|
-
const diffParts = [
|
|
21106
|
-
git4(["diff", "-U15", `${baseRef}...HEAD`], worktreePath),
|
|
21107
|
-
git4(["diff", "-U15", "--cached"], worktreePath),
|
|
21108
|
-
git4(["diff", "-U15"], worktreePath)
|
|
21109
|
-
].filter(Boolean);
|
|
21110
|
-
if (diffParts.length > 0) {
|
|
21111
|
-
sections.push(`\`\`\`diff
|
|
21112
|
-
${diffParts.join("\n")}
|
|
21113
|
-
\`\`\``);
|
|
21114
|
-
}
|
|
21115
|
-
const newUntracked = git4(["ls-files", "--others", "--exclude-standard"], worktreePath).split("\n").map((f2) => f2.trim()).filter(Boolean);
|
|
21116
|
-
if (newUntracked.length > 0) {
|
|
21117
|
-
const newFileContents = [];
|
|
21118
|
-
for (const file of newUntracked) {
|
|
21119
|
-
const content = readFileSafe(join16(worktreePath, file));
|
|
21120
|
-
const ext = file.split(".").pop() ?? "";
|
|
21121
|
-
newFileContents.push(content ? `### ${file}
|
|
21122
|
-
\`\`\`${ext}
|
|
21123
|
-
${content}
|
|
21124
|
-
\`\`\`` : `### ${file} (unreadable)`);
|
|
21125
|
-
}
|
|
21126
|
-
sections.push(`New files (full content):
|
|
21127
|
-
|
|
21128
|
-
${newFileContents.join("\n\n")}`);
|
|
21129
|
-
}
|
|
21130
|
-
return sections.join("\n\n");
|
|
21131
|
-
}
|
|
21132
|
-
function getGitHeadSha(worktreePath) {
|
|
21133
|
-
return git4(["rev-parse", "HEAD"], worktreePath);
|
|
21134
|
-
}
|
|
21135
|
-
function attachmentLines(attachments) {
|
|
21136
|
-
return attachments.map((a, i) => `- [Attachment #${i + 1}] ${a.name}: ${a.path}`).join("\n");
|
|
21137
|
-
}
|
|
21138
|
-
function formatComment(c, opts) {
|
|
21139
|
-
const typeLabel = c.type === "human" ? "Human Feedback" : c.type === "visual-comment" ? "Visual Feedback" : c.type.replace(/[-_]/g, " ").replace(/\b\w/g, (l) => l.toUpperCase());
|
|
21140
|
-
const actorId = c.actor.id;
|
|
21141
|
-
const statusLabel = c.status?.toUpperCase() ?? "";
|
|
21142
|
-
const hasMustFix = c.issues?.some((i) => i.severity === "blocking" || i.severity === "warning") ?? false;
|
|
21143
|
-
const failedRound = c.status === "fail" || hasMustFix;
|
|
21144
|
-
const mustFix = failedRound && !opts.stripMustFix ? " \u26A0 MUST FIX BEFORE PROCEEDING" : "";
|
|
21145
|
-
const parts = [
|
|
21146
|
-
`${opts.headingLevel} ${typeLabel} \xB7 ${actorId}${statusLabel ? ` \xB7 ${statusLabel}` : ""}${mustFix}`
|
|
21147
|
-
];
|
|
21148
|
-
if (c.summary) parts.push(c.summary);
|
|
21149
|
-
if (c.issues?.length) {
|
|
21150
|
-
for (const issue of c.issues) {
|
|
21151
|
-
const loc = issue.file ? `${issue.file}${issue.line != null ? `:${issue.line}` : ""}` : "";
|
|
21152
|
-
parts.push(`- [${issue.severity}]${loc ? ` ${loc}` : ""} \u2014 ${issue.message}`);
|
|
21153
|
-
}
|
|
21506
|
+
`${opts.headingLevel} ${typeLabel} \xB7 ${actorId}${statusLabel ? ` \xB7 ${statusLabel}` : ""}${mustFix}`
|
|
21507
|
+
];
|
|
21508
|
+
if (c.summary) parts.push(c.summary);
|
|
21509
|
+
if (c.issues?.length) {
|
|
21510
|
+
for (const issue of c.issues) {
|
|
21511
|
+
const loc = issue.file ? `${issue.file}${issue.line != null ? `:${issue.line}` : ""}` : "";
|
|
21512
|
+
parts.push(`- [${issue.severity}]${loc ? ` ${loc}` : ""} \u2014 ${issue.message}`);
|
|
21513
|
+
}
|
|
21154
21514
|
}
|
|
21155
21515
|
if (c.attachments?.length) {
|
|
21156
21516
|
parts.push(`Attached files (use Read tool to view):
|
|
@@ -21257,12 +21617,6 @@ ${sections.join("\n\n---\n\n")}`,
|
|
|
21257
21617
|
files: []
|
|
21258
21618
|
};
|
|
21259
21619
|
}
|
|
21260
|
-
var INLINE_DIFF_LIMIT = 8e3;
|
|
21261
|
-
function formatDiffBlock(fullDiff, baseRef, header = "Git diff") {
|
|
21262
|
-
if (fullDiff.length <= INLINE_DIFF_LIMIT) return `${header}:
|
|
21263
|
-
${fullDiff}`;
|
|
21264
|
-
return `Large changeset (${fullDiff.length.toLocaleString()} chars). Use \`git diff ${baseRef}...HEAD\` and read individual files to explore.`;
|
|
21265
|
-
}
|
|
21266
21620
|
var FOLLOWUP_REVIEW_FOCUS = `**This is a follow-up review.** An earlier version of this work already passed review in a prior round \u2014 only the \`## New Feedback\` / \`## Current Iteration\` items triggered this run. Re-review ONLY: (1) whether those items were addressed in the changes below, and (2) whether the new changes introduced a regression or broke a caller (grep callers of anything touched to confirm). Do NOT re-litigate previously-approved code or raise issues unrelated to this round's feedback.`;
|
|
21267
21621
|
function renderReviewDiff(stat3, fullDiff, baseRef, scope) {
|
|
21268
21622
|
if (!scope.useIncremental) {
|
|
@@ -21764,6 +22118,64 @@ function tryParseAgentJson(output) {
|
|
|
21764
22118
|
}
|
|
21765
22119
|
}
|
|
21766
22120
|
|
|
22121
|
+
// src/daemon/companion-agent.ts
|
|
22122
|
+
function buildCompanionAgentSystemPrompt(workspaceId, repoPath, worktreePath, baseRef, secrets, systemPrompt, gitInstructions, seedPrompt, resumedCanvas) {
|
|
22123
|
+
const effectiveGitInstructions = gitInstructions?.trim() || DEFAULT_GIT_INSTRUCTIONS;
|
|
22124
|
+
const fullDiff = getGitFullDiff(worktreePath, baseRef);
|
|
22125
|
+
const worktreeSection = fullDiff ? `## Current worktree state (vs ${baseRef})
|
|
22126
|
+
${getGitStat(worktreePath, baseRef)}
|
|
22127
|
+
|
|
22128
|
+
## Diff (vs ${baseRef})
|
|
22129
|
+
${formatDiffBlock(fullDiff, baseRef, "Git diff")}` : `## Worktree state
|
|
22130
|
+
|
|
22131
|
+
The worktree is clean and branched from \`${baseRef}\` \u2014 there is no diff yet. Skip \`git diff\` and start working.`;
|
|
22132
|
+
const parts = [
|
|
22133
|
+
`You are the Companion agent for the project at \`${repoPath}\`.
|
|
22134
|
+
|
|
22135
|
+
You are a direct, chat-driven pairing session with a developer. Unlike the ticket pipeline's dev agent, you are not working through a queued task with an automated reviewer downstream \u2014 the developer is talking to you live and steering the work turn by turn. You have full write access to the code in your current working directory, which is an isolated git worktree branched from \`${baseRef}\`.
|
|
22136
|
+
|
|
22137
|
+
Work incrementally and check in with the developer as you go rather than disappearing to complete a large scope autonomously. When you commit, follow the project's git conventions (see "## Git conventions" below) \u2014 do not commit until the developer asks you to, unless they've told you to commit as you go.`,
|
|
22138
|
+
worktreeSection
|
|
22139
|
+
];
|
|
22140
|
+
parts.push(`## Sharing a canvas with the developer
|
|
22141
|
+
|
|
22142
|
+
When the developer asks you to "plan" something, wants to see a report, findings, or a set of questions answered \u2014 or you want to lay out an approach before starting \u2014 use the \`whipped_show_canvas\` MCP tool. That's what "plan" (and requests like it) means in this session: push it to the canvas, don't just describe it in chat. Do NOT use any other built-in planning mode you might have; always push through this tool instead, even for what would normally trigger that. Push markdown, raw HTML, mermaid diagrams, and interactive questions \u2014 instead of writing a long response as a chat message \u2014 whenever you want structured feedback. The developer's answers, comments, and notes come back as a normal follow-up message in this conversation \u2014 there is no separate response channel, so treat it exactly like something they typed.
|
|
22143
|
+
|
|
22144
|
+
${buildCanvasModeGuidance()}`);
|
|
22145
|
+
if (resumedCanvas) {
|
|
22146
|
+
parts.push(`## Resuming a saved canvas
|
|
22147
|
+
|
|
22148
|
+
This session was started from a previously saved canvas titled "${resumedCanvas.title}". Its content is shown in full below \u2014 the developer can already see this in their canvas as version 1, but you cannot read it back, so this is the only place you'll see it. Treat it as the current state of the work: continue from here rather than re-planning from scratch, and call \`whipped_save_canvas\` again as you make further progress so the saved canvas stays in sync with what's actually done.
|
|
22149
|
+
|
|
22150
|
+
${serializeCanvasBlocksForPrompt(resumedCanvas.blocks)}`);
|
|
22151
|
+
}
|
|
22152
|
+
if (seedPrompt?.trim()) parts.push(`## Project-specific instructions
|
|
22153
|
+
|
|
22154
|
+
${seedPrompt.trim()}`);
|
|
22155
|
+
parts.push(`## Memory
|
|
22156
|
+
|
|
22157
|
+
This project has its own persistent memory. The \`whipped_save_memory\` / \`whipped_update_memory\` MCP tools ARE this project's memory \u2014 do NOT use your own notes, scratch files, CLAUDE.md, or any other memory system for durable facts.
|
|
22158
|
+
|
|
22159
|
+
When you are asked to "remember", "save to memory", "note for next time" \u2014 or you hit a cross-cutting convention, an architecture decision, a non-obvious repo-wide gotcha, or a correction the developer made \u2014 record it in memory. Do NOT record what is already in the code or schema (endpoint request/response shapes, query params, column lists, field names, colour classes, per-page layout): if your note would cite the file where the truth lives, the file is the memory \u2014 skip it. Keep each entry to one focused fact in 1-3 sentences.
|
|
22160
|
+
|
|
22161
|
+
Before recording, check the memory list injected above (each entry shows its \`[id]\`) and \`whipped_search_memory\`. If what you're recording **contradicts, reverses, supersedes, corrects, or is a near-duplicate of** an existing memory, call \`whipped_update_memory\` with that memory's id and overwrite it \u2014 do NOT create a second, conflicting entry.
|
|
22162
|
+
|
|
22163
|
+
Scope a memory \`project\` for facts specific to this repo, or \`global\` for things that apply across all the user's projects (style/preferences).`);
|
|
22164
|
+
const secretsSection = buildSecretsSection(secrets);
|
|
22165
|
+
if (secretsSection) parts.push(secretsSection);
|
|
22166
|
+
if (systemPrompt?.trim()) parts.push(`## Project context
|
|
22167
|
+
|
|
22168
|
+
${systemPrompt.trim()}`);
|
|
22169
|
+
parts.push(`## Git conventions
|
|
22170
|
+
|
|
22171
|
+
${effectiveGitInstructions}`);
|
|
22172
|
+
const memContext = buildMemoryContext(workspaceId);
|
|
22173
|
+
const text = parts.join("\n\n");
|
|
22174
|
+
return memContext ? `${memContext}
|
|
22175
|
+
|
|
22176
|
+
${text}` : text;
|
|
22177
|
+
}
|
|
22178
|
+
|
|
21767
22179
|
// src/daemon/scheduler.ts
|
|
21768
22180
|
var FAST_EXIT_THRESHOLD_MS = 8e3;
|
|
21769
22181
|
var MAX_RECENT_BUFFERS = 100;
|
|
@@ -21774,6 +22186,7 @@ var TaskScheduler = class {
|
|
|
21774
22186
|
options;
|
|
21775
22187
|
running = /* @__PURE__ */ new Map();
|
|
21776
22188
|
assistantSessions = /* @__PURE__ */ new Map();
|
|
22189
|
+
companionSessions = /* @__PURE__ */ new Map();
|
|
21777
22190
|
// Keep the last output buffer around after a task exits so the terminal
|
|
21778
22191
|
// can still restore when the user opens it for a completed/awaiting-review task.
|
|
21779
22192
|
recentBuffers = /* @__PURE__ */ new Map();
|
|
@@ -21829,7 +22242,7 @@ var TaskScheduler = class {
|
|
|
21829
22242
|
get assistantAgentTaskId() {
|
|
21830
22243
|
return `${ASSISTANT_AGENT_PREFIX}${this.options.workspaceId}`;
|
|
21831
22244
|
}
|
|
21832
|
-
async startAssistantAgent() {
|
|
22245
|
+
async startAssistantAgent(override, savedCanvasId) {
|
|
21833
22246
|
const { workspaceId, repoPath, serverUrl, stateHub, defaultAgent } = this.options;
|
|
21834
22247
|
const taskId = this.assistantAgentTaskId;
|
|
21835
22248
|
const existing = this.assistantSessions.get(taskId);
|
|
@@ -21841,11 +22254,18 @@ var TaskScheduler = class {
|
|
|
21841
22254
|
stateHub.clearTerminalBuffer(workspaceId, taskId);
|
|
21842
22255
|
const prompt = "";
|
|
21843
22256
|
const projectConfig = await loadProjectConfig(workspaceId);
|
|
21844
|
-
const assistantModel = projectConfig.assistantModel;
|
|
22257
|
+
const assistantModel = override ?? projectConfig.assistantModel;
|
|
21845
22258
|
const agentId = assistantModel?.agentId ?? defaultAgent;
|
|
21846
22259
|
const secrets = projectConfig.secrets ?? [];
|
|
21847
22260
|
const secretsEnv = buildSecretsEnv(secrets);
|
|
21848
|
-
const
|
|
22261
|
+
const savedCanvas = savedCanvasId ? getCompanionSavedCanvas(savedCanvasId) : null;
|
|
22262
|
+
if (savedCanvas) createCompanionCanvas(taskId, workspaceId, savedCanvas.blocks);
|
|
22263
|
+
const assistantSystemPrompt = buildAssistantAgentSystemPrompt(
|
|
22264
|
+
repoPath,
|
|
22265
|
+
secrets,
|
|
22266
|
+
projectConfig.systemPrompt,
|
|
22267
|
+
savedCanvas ? { title: savedCanvas.title, blocks: savedCanvas.blocks } : void 0
|
|
22268
|
+
);
|
|
21849
22269
|
const memContext = buildMemoryContext(workspaceId);
|
|
21850
22270
|
const appendSystemPrompt = memContext ? `${memContext}
|
|
21851
22271
|
|
|
@@ -21896,6 +22316,9 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
|
|
|
21896
22316
|
...agentId === "cursor" ? { [CURSOR_CONFIG_DIR_ENV]: getCursorConfigDir(taskId) } : {}
|
|
21897
22317
|
},
|
|
21898
22318
|
mcpConfigPath: agentId === "claude" ? CLAUDE_ASSISTANT_MCP_CONFIG_PATH : void 0,
|
|
22319
|
+
// Denies Claude's own native plan-mode tools — see writeClaudeCompanionSettings
|
|
22320
|
+
// — so "plan" always means whipped_show_canvas, same as the companion agent.
|
|
22321
|
+
hookSettingsPath: agentId === "claude" ? CLAUDE_COMPANION_SETTINGS_PATH : void 0,
|
|
21899
22322
|
mcpServer: agentId === "codex" ? buildWhippedMcpServerSpec(
|
|
21900
22323
|
getMcpServerPath(),
|
|
21901
22324
|
serverUrl,
|
|
@@ -21932,6 +22355,234 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
|
|
|
21932
22355
|
isAssistantAgentRunning() {
|
|
21933
22356
|
return this.assistantSessions.has(this.assistantAgentTaskId);
|
|
21934
22357
|
}
|
|
22358
|
+
// Creates the worktree (or resolves the main-repo checkout) synchronously and
|
|
22359
|
+
// returns quickly — install + agent spawn happen in the background via
|
|
22360
|
+
// launchCompanionAgent so callers (the create-session API request) aren't
|
|
22361
|
+
// blocked on a potentially long install command.
|
|
22362
|
+
async startCompanionAgent(session) {
|
|
22363
|
+
const { workspaceId, repoPath, stateHub } = this.options;
|
|
22364
|
+
const taskId = session.id;
|
|
22365
|
+
const existing = this.companionSessions.get(taskId);
|
|
22366
|
+
if (existing) existing.process.kill();
|
|
22367
|
+
this.recentBuffers.delete(taskId);
|
|
22368
|
+
stateHub.clearTerminalBuffer(workspaceId, taskId);
|
|
22369
|
+
if (session.useWorktree) {
|
|
22370
|
+
const worktree = createWorktree(taskId, repoPath, session.baseRef, session.branchName ?? void 0);
|
|
22371
|
+
setCompanionSessionWorktreePath(taskId, worktree.path);
|
|
22372
|
+
setCompanionSessionStatus(taskId, worktree.isNew ? "installing" : "running");
|
|
22373
|
+
void this.launchCompanionAgent(session, worktree.path, worktree.isNew);
|
|
22374
|
+
} else {
|
|
22375
|
+
setCompanionSessionWorktreePath(taskId, repoPath);
|
|
22376
|
+
setCompanionSessionStatus(taskId, "running");
|
|
22377
|
+
void this.launchCompanionAgent(session, repoPath, false);
|
|
22378
|
+
}
|
|
22379
|
+
}
|
|
22380
|
+
async launchCompanionAgent(session, cwd, isNewWorktree) {
|
|
22381
|
+
const { workspaceId, repoPath, serverUrl, stateHub } = this.options;
|
|
22382
|
+
const taskId = session.id;
|
|
22383
|
+
const agentId = session.agentId;
|
|
22384
|
+
const projectConfig = await loadProjectConfig(workspaceId);
|
|
22385
|
+
const secrets = projectConfig.secrets ?? [];
|
|
22386
|
+
const secretsEnv = buildSecretsEnv(secrets);
|
|
22387
|
+
if (isNewWorktree && projectConfig.worktreeSetup) {
|
|
22388
|
+
await this.runCompanionInstall(taskId, workspaceId, repoPath, cwd, projectConfig.worktreeSetup);
|
|
22389
|
+
if (this.isShuttingDown) return;
|
|
22390
|
+
if (this.manuallyStoppedInstalls.delete(taskId)) {
|
|
22391
|
+
await removeWorktreeAsync(taskId, repoPath, session.branchName ?? void 0);
|
|
22392
|
+
setCompanionSessionWorktreePath(taskId, null);
|
|
22393
|
+
setCompanionSessionStatus(taskId, "stopped");
|
|
22394
|
+
return;
|
|
22395
|
+
}
|
|
22396
|
+
}
|
|
22397
|
+
setCompanionSessionStatus(taskId, "running");
|
|
22398
|
+
const resumedCanvas = session.savedCanvasId ? getCompanionSavedCanvas(session.savedCanvasId) : null;
|
|
22399
|
+
const appendSystemPrompt = buildCompanionAgentSystemPrompt(
|
|
22400
|
+
workspaceId,
|
|
22401
|
+
repoPath,
|
|
22402
|
+
cwd,
|
|
22403
|
+
session.baseRef,
|
|
22404
|
+
secrets,
|
|
22405
|
+
projectConfig.systemPrompt,
|
|
22406
|
+
projectConfig.gitInstructions,
|
|
22407
|
+
session.seedPrompt,
|
|
22408
|
+
resumedCanvas ? { title: resumedCanvas.title, blocks: resumedCanvas.blocks } : void 0
|
|
22409
|
+
);
|
|
22410
|
+
const mcpConfigPath = !isPluginConfigAgent(agentId) && agentId !== "cursor" ? getMcpConfigPath(taskId) : void 0;
|
|
22411
|
+
if (agentId === "claude") {
|
|
22412
|
+
await writeClaudeMcpConfig(
|
|
22413
|
+
getMcpServerPath(),
|
|
22414
|
+
serverUrl,
|
|
22415
|
+
workspaceId,
|
|
22416
|
+
agentId,
|
|
22417
|
+
mcpConfigPath,
|
|
22418
|
+
void 0,
|
|
22419
|
+
buildMcpRoleArgs("companion", void 0, taskId)
|
|
22420
|
+
).catch((err) => {
|
|
22421
|
+
logger.warn({ err }, "[scheduler] Failed to write companion agent MCP config");
|
|
22422
|
+
});
|
|
22423
|
+
} else if (isPluginConfigAgent(agentId)) {
|
|
22424
|
+
const mcpSpec = buildWhippedMcpServerSpec(
|
|
22425
|
+
getMcpServerPath(),
|
|
22426
|
+
serverUrl,
|
|
22427
|
+
workspaceId,
|
|
22428
|
+
agentId,
|
|
22429
|
+
buildMcpRoleArgs("companion", void 0, taskId)
|
|
22430
|
+
);
|
|
22431
|
+
await writePluginAgentFiles(agentId, taskId, getServerPort(serverUrl), mcpSpec, { appendSystemPrompt }).catch(
|
|
22432
|
+
(err) => {
|
|
22433
|
+
logger.warn({ err }, `[scheduler] Failed to write ${agentId} companion agent files`);
|
|
22434
|
+
}
|
|
22435
|
+
);
|
|
22436
|
+
} else if (agentId === "cursor") {
|
|
22437
|
+
const mcpSpec = buildWhippedMcpServerSpec(
|
|
22438
|
+
getMcpServerPath(),
|
|
22439
|
+
serverUrl,
|
|
22440
|
+
workspaceId,
|
|
22441
|
+
agentId,
|
|
22442
|
+
buildMcpRoleArgs("companion", void 0, taskId)
|
|
22443
|
+
);
|
|
22444
|
+
await writeCursorConfigFiles(taskId, getServerPort(serverUrl), mcpSpec).catch((err) => {
|
|
22445
|
+
logger.warn({ err }, "[scheduler] Failed to write cursor companion agent config");
|
|
22446
|
+
});
|
|
22447
|
+
}
|
|
22448
|
+
let resolveExit;
|
|
22449
|
+
const exitPromise = new Promise((resolve5) => {
|
|
22450
|
+
resolveExit = resolve5;
|
|
22451
|
+
});
|
|
22452
|
+
const companionTask = {
|
|
22453
|
+
taskId,
|
|
22454
|
+
streamId: taskId,
|
|
22455
|
+
// companion session uses taskId as its stream (single persistent session)
|
|
22456
|
+
agentId,
|
|
22457
|
+
exitPromise,
|
|
22458
|
+
process: spawnAgent({
|
|
22459
|
+
agentId,
|
|
22460
|
+
prompt: "",
|
|
22461
|
+
cwd,
|
|
22462
|
+
env: {
|
|
22463
|
+
...secretsEnv,
|
|
22464
|
+
...buildTaskHookEnv(taskId, workspaceId),
|
|
22465
|
+
WHIPPED_SLOT: "companion",
|
|
22466
|
+
...pluginAgentConfigDirEnv(agentId, taskId),
|
|
22467
|
+
...agentId === "cursor" ? { [CURSOR_CONFIG_DIR_ENV]: getCursorConfigDir(taskId) } : {}
|
|
22468
|
+
},
|
|
22469
|
+
mcpConfigPath: agentId === "claude" ? mcpConfigPath : void 0,
|
|
22470
|
+
// Denies Claude's own native plan-mode tools for this session — see
|
|
22471
|
+
// writeClaudeCompanionSettings — so "plan" always means whipped_show_canvas.
|
|
22472
|
+
hookSettingsPath: agentId === "claude" ? CLAUDE_COMPANION_SETTINGS_PATH : void 0,
|
|
22473
|
+
mcpServer: agentId === "codex" ? buildWhippedMcpServerSpec(
|
|
22474
|
+
getMcpServerPath(),
|
|
22475
|
+
serverUrl,
|
|
22476
|
+
workspaceId,
|
|
22477
|
+
agentId,
|
|
22478
|
+
buildMcpRoleArgs("companion", void 0, taskId)
|
|
22479
|
+
) : void 0,
|
|
22480
|
+
model: session.model ?? null,
|
|
22481
|
+
effort: session.effort ?? null,
|
|
22482
|
+
appendSystemPrompt: isPluginConfigAgent(agentId) ? void 0 : appendSystemPrompt,
|
|
22483
|
+
onOutput: (data) => {
|
|
22484
|
+
companionTask.outputBuffer += data;
|
|
22485
|
+
stateHub.broadcastTerminalOutput(workspaceId, taskId, data);
|
|
22486
|
+
},
|
|
22487
|
+
onExit: () => {
|
|
22488
|
+
this.setRecentBuffer(taskId, companionTask.outputBuffer);
|
|
22489
|
+
this.companionSessions.delete(taskId);
|
|
22490
|
+
setCompanionSessionStatus(taskId, "stopped");
|
|
22491
|
+
resolveExit();
|
|
22492
|
+
}
|
|
22493
|
+
}),
|
|
22494
|
+
startedAt: Date.now(),
|
|
22495
|
+
outputBuffer: ""
|
|
22496
|
+
};
|
|
22497
|
+
this.companionSessions.set(taskId, companionTask);
|
|
22498
|
+
}
|
|
22499
|
+
// Copies/links worktreeSetup's configured files then runs its install command,
|
|
22500
|
+
// mirroring the card dev-agent's worktree setup step (launchDevAgent above) but
|
|
22501
|
+
// streamed straight into the companion session's own terminal (taskId) instead
|
|
22502
|
+
// of a separate install stream row, since companion sessions have no such table.
|
|
22503
|
+
async runCompanionInstall(taskId, workspaceId, repoPath, worktreePath, worktreeSetup) {
|
|
22504
|
+
const { stateHub } = this.options;
|
|
22505
|
+
const { filesToCopy, installCommand } = worktreeSetup;
|
|
22506
|
+
for (const entry of filesToCopy) {
|
|
22507
|
+
const src = join18(repoPath, entry.path);
|
|
22508
|
+
if (!existsSync10(src)) continue;
|
|
22509
|
+
const dst = join18(worktreePath, entry.path);
|
|
22510
|
+
await mkdir3(dirname6(dst), { recursive: true });
|
|
22511
|
+
try {
|
|
22512
|
+
if (entry.symlink) {
|
|
22513
|
+
await shareIntoWorktree(src, dst);
|
|
22514
|
+
} else {
|
|
22515
|
+
await cp(src, dst, { recursive: true, dereference: true });
|
|
22516
|
+
}
|
|
22517
|
+
} catch (err) {
|
|
22518
|
+
logger.warn(
|
|
22519
|
+
{ err },
|
|
22520
|
+
`[scheduler] companion worktree setup: failed to ${entry.symlink ? "link" : "copy"} ${entry.path}`
|
|
22521
|
+
);
|
|
22522
|
+
}
|
|
22523
|
+
}
|
|
22524
|
+
const installCmd = installCommand.trim();
|
|
22525
|
+
if (!installCmd) return;
|
|
22526
|
+
let buffer = "";
|
|
22527
|
+
const emit = (data) => {
|
|
22528
|
+
buffer += data;
|
|
22529
|
+
this.recentBuffers.set(taskId, buffer);
|
|
22530
|
+
stateHub.broadcastTerminalOutput(workspaceId, taskId, data);
|
|
22531
|
+
};
|
|
22532
|
+
emit(`\x1B[1;36m$ ${installCmd}\x1B[0m\r
|
|
22533
|
+
`);
|
|
22534
|
+
const exitCode = await new Promise((resolveExit) => {
|
|
22535
|
+
const [shell, shellArgs] = getShellInvocation(installCmd);
|
|
22536
|
+
const proc = nodePty2.spawn(shell, shellArgs, {
|
|
22537
|
+
name: "xterm-256color",
|
|
22538
|
+
cols: 220,
|
|
22539
|
+
rows: 50,
|
|
22540
|
+
cwd: worktreePath,
|
|
22541
|
+
env: { ...process.env, REPO_PATH: repoPath, TERM: "xterm-256color" }
|
|
22542
|
+
});
|
|
22543
|
+
this.runningInstalls.set(taskId, proc);
|
|
22544
|
+
proc.onData(emit);
|
|
22545
|
+
proc.onExit(({ exitCode: exitCode2 }) => resolveExit(exitCode2 ?? 0));
|
|
22546
|
+
});
|
|
22547
|
+
this.runningInstalls.delete(taskId);
|
|
22548
|
+
if (this.isShuttingDown) return;
|
|
22549
|
+
if (this.manuallyStoppedInstalls.has(taskId)) {
|
|
22550
|
+
emit("\r\n\x1B[1;33mInstall stopped\x1B[0m\r\n");
|
|
22551
|
+
return;
|
|
22552
|
+
}
|
|
22553
|
+
if (exitCode !== 0) {
|
|
22554
|
+
logger.error(`[scheduler] Install command failed (code ${exitCode}) for companion session ${taskId}`);
|
|
22555
|
+
emit(`\r
|
|
22556
|
+
\x1B[1;31mInstall command failed (code ${exitCode}) \u2014 proceeding anyway\x1B[0m\r
|
|
22557
|
+
`);
|
|
22558
|
+
} else {
|
|
22559
|
+
emit("\r\n\x1B[1;32mInstall complete\x1B[0m\r\n");
|
|
22560
|
+
}
|
|
22561
|
+
}
|
|
22562
|
+
// Kills the session's process (or its still-running install command) and waits
|
|
22563
|
+
// for it to actually exit (bounded by a timeout) before returning — callers
|
|
22564
|
+
// that are about to delete or merge the worktree on disk must await this
|
|
22565
|
+
// first, since attemptMerge/removeWorktreeAsync can otherwise race a still-live
|
|
22566
|
+
// process whose cwd is inside that worktree.
|
|
22567
|
+
async stopCompanionAgent(sessionId) {
|
|
22568
|
+
const installProc = this.runningInstalls.get(sessionId);
|
|
22569
|
+
if (installProc) {
|
|
22570
|
+
this.manuallyStoppedInstalls.add(sessionId);
|
|
22571
|
+
this.runningInstalls.delete(sessionId);
|
|
22572
|
+
killInstallProcess(installProc);
|
|
22573
|
+
}
|
|
22574
|
+
const session = this.companionSessions.get(sessionId);
|
|
22575
|
+
if (session) {
|
|
22576
|
+
const exitPromise = session.exitPromise ?? Promise.resolve();
|
|
22577
|
+
session.process.kill();
|
|
22578
|
+
await Promise.race([exitPromise, new Promise((resolve5) => setTimeout(resolve5, 5e3))]);
|
|
22579
|
+
}
|
|
22580
|
+
this.companionSessions.delete(sessionId);
|
|
22581
|
+
setCompanionSessionStatus(sessionId, "stopped");
|
|
22582
|
+
}
|
|
22583
|
+
isCompanionAgentRunning(sessionId) {
|
|
22584
|
+
return this.companionSessions.has(sessionId);
|
|
22585
|
+
}
|
|
21935
22586
|
get activeCount() {
|
|
21936
22587
|
return this.running.size;
|
|
21937
22588
|
}
|
|
@@ -22120,9 +22771,9 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
|
|
|
22120
22771
|
const copied = [];
|
|
22121
22772
|
const linked = [];
|
|
22122
22773
|
for (const entry of filesToCopy) {
|
|
22123
|
-
const src =
|
|
22774
|
+
const src = join18(repoPath, entry.path);
|
|
22124
22775
|
if (!existsSync10(src)) continue;
|
|
22125
|
-
const dst =
|
|
22776
|
+
const dst = join18(worktree.path, entry.path);
|
|
22126
22777
|
await mkdir3(dirname6(dst), { recursive: true });
|
|
22127
22778
|
try {
|
|
22128
22779
|
if (entry.symlink) {
|
|
@@ -22542,6 +23193,8 @@ ${devSystemPromptResult.text}`;
|
|
|
22542
23193
|
}
|
|
22543
23194
|
const assistantSession = this.assistantSessions.get(streamId);
|
|
22544
23195
|
if (assistantSession) return assistantSession.outputBuffer;
|
|
23196
|
+
const companionSession = this.companionSessions.get(streamId);
|
|
23197
|
+
if (companionSession) return companionSession.outputBuffer;
|
|
22545
23198
|
return this.recentBuffers.get(streamId) ?? null;
|
|
22546
23199
|
}
|
|
22547
23200
|
resizeTerminal(streamId, cols, rows) {
|
|
@@ -22558,7 +23211,7 @@ ${devSystemPromptResult.text}`;
|
|
|
22558
23211
|
for (const task of this.running.values()) {
|
|
22559
23212
|
if (task.streamId === streamId) return task.process;
|
|
22560
23213
|
}
|
|
22561
|
-
return this.assistantSessions.get(streamId)?.process ?? this.liveProcesses.get(streamId);
|
|
23214
|
+
return this.assistantSessions.get(streamId)?.process ?? this.companionSessions.get(streamId)?.process ?? this.liveProcesses.get(streamId);
|
|
22562
23215
|
}
|
|
22563
23216
|
async handleHookEvent(event, taskId) {
|
|
22564
23217
|
const { workspaceId, stateHub } = this.options;
|
|
@@ -22744,6 +23397,10 @@ ${devSystemPromptResult.text}`;
|
|
|
22744
23397
|
}
|
|
22745
23398
|
this.runningInstalls.clear();
|
|
22746
23399
|
this.stopAssistantAgent();
|
|
23400
|
+
for (const [, task] of this.companionSessions) {
|
|
23401
|
+
task.process.kill();
|
|
23402
|
+
}
|
|
23403
|
+
this.companionSessions.clear();
|
|
22747
23404
|
}
|
|
22748
23405
|
// Call before stopAll() during graceful shutdown so onExit handlers bail out
|
|
22749
23406
|
// and do not overwrite the failed/todo state written by cleanupStaleTasks().
|
|
@@ -22789,8 +23446,15 @@ function getMcpServerPath() {
|
|
|
22789
23446
|
args: [resolve2(thisDir, "mcp-server.js")]
|
|
22790
23447
|
};
|
|
22791
23448
|
}
|
|
22792
|
-
function buildAssistantAgentSystemPrompt(repoPath, secrets = [], systemPrompt) {
|
|
23449
|
+
function buildAssistantAgentSystemPrompt(repoPath, secrets = [], systemPrompt, resumedCanvas) {
|
|
22793
23450
|
const secretsSection = buildSecretsSection(secrets);
|
|
23451
|
+
const resumedCanvasSection = resumedCanvas ? `
|
|
23452
|
+
|
|
23453
|
+
# Resuming a saved canvas
|
|
23454
|
+
|
|
23455
|
+
This conversation was started from a previously saved canvas titled "${resumedCanvas.title}". Its content is shown in full below \u2014 the developer can already see this in their canvas as version 1, but you cannot read it back, so this is the only place you'll see it. Treat it as the current state of the discussion: continue from here rather than re-planning from scratch, and call \`whipped_save_canvas\` again as things progress so the saved canvas stays in sync.
|
|
23456
|
+
|
|
23457
|
+
${serializeCanvasBlocksForPrompt(resumedCanvas.blocks)}` : "";
|
|
22794
23458
|
return `You are the Assistant for the project at \`${repoPath}\`.
|
|
22795
23459
|
|
|
22796
23460
|
You are a conversational project assistant. You can discuss the project, help plan work, answer questions about the codebase, workflows, and board state, and help the developer decide what to build. You also have full control over the Kanban board and workflows via MCP tools.
|
|
@@ -22824,6 +23488,10 @@ You are a conversational project assistant. You can discuss the project, help pl
|
|
|
22824
23488
|
- \`kanban_get_workflows\` \u2014 list all workflows (task and story/orch) with their agent slots, model tiers, tools, and prompts
|
|
22825
23489
|
- \`kanban_upsert_workflow\` \u2014 create or fully replace a workflow (pass complete workflow object)
|
|
22826
23490
|
|
|
23491
|
+
## Canvas
|
|
23492
|
+
- \`whipped_show_canvas\` \u2014 push a structured, interactive canvas (markdown, HTML mockups, mermaid diagrams, questions) \u2014 for a plan, a report, findings, or anything else worth showing rather than describing in chat
|
|
23493
|
+
- \`whipped_save_canvas\` \u2014 consolidate the conversation's canvas versions into one and save it to the reusable canvas library
|
|
23494
|
+
|
|
22827
23495
|
## Memory
|
|
22828
23496
|
- \`whipped_search_memory\` \u2014 search durable project + global memory before re-discovering how something works
|
|
22829
23497
|
- \`whipped_get_memory\` \u2014 fetch one memory's full content by id
|
|
@@ -22832,6 +23500,12 @@ You are a conversational project assistant. You can discuss the project, help pl
|
|
|
22832
23500
|
|
|
22833
23501
|
The Memory section injected above this prompt lists existing memories with their \`[id]\`. When the developer asks you to "remember" something, or states a durable preference/decision, save it. Before saving, check the injected list and \`whipped_search_memory\`; if it contradicts or supersedes an existing entry, \`whipped_update_memory\` that id instead of creating a duplicate.
|
|
22834
23502
|
|
|
23503
|
+
# Sharing a canvas
|
|
23504
|
+
|
|
23505
|
+
When you want to lay out an approach, a UI mockup, or gather structured feedback before creating tickets \u2014 or the developer asks you to "plan" something, wants a report, or wants a set of questions answered \u2014 use the \`whipped_show_canvas\` MCP tool instead of writing a long response as a chat message. Do NOT use any other built-in planning mode you might have; always push it through this tool instead, even for what would normally trigger that. The developer's answers, comments, and notes come back as a normal follow-up message in this conversation \u2014 there is no separate response channel, so treat it exactly like something they typed.
|
|
23506
|
+
|
|
23507
|
+
${buildCanvasModeGuidance()}${resumedCanvasSection}
|
|
23508
|
+
|
|
22835
23509
|
# Card types
|
|
22836
23510
|
|
|
22837
23511
|
**task** \u2014 a normal development ticket. Runs an optional plan slot, then dev, then any number of review slots (based on workflow).
|
|
@@ -25379,6 +26053,7 @@ var errorHandler2 = (err, c) => {
|
|
|
25379
26053
|
};
|
|
25380
26054
|
|
|
25381
26055
|
// src/api/routes/agent.ts
|
|
26056
|
+
init_api_contract();
|
|
25382
26057
|
import { z as z3 } from "zod";
|
|
25383
26058
|
|
|
25384
26059
|
// node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/utils/cookie.js
|
|
@@ -25676,8 +26351,8 @@ var zv = (target, schema) => zValidator(target, schema, (result) => {
|
|
|
25676
26351
|
});
|
|
25677
26352
|
|
|
25678
26353
|
// src/api/services/agent-service.ts
|
|
25679
|
-
var startAgentSession = async (scheduler) => ({
|
|
25680
|
-
taskId: await scheduler.startAssistantAgent()
|
|
26354
|
+
var startAgentSession = async (scheduler, override, savedCanvasId) => ({
|
|
26355
|
+
taskId: await scheduler.startAssistantAgent(override, savedCanvasId)
|
|
25681
26356
|
});
|
|
25682
26357
|
var stopAgentSession = async (scheduler) => {
|
|
25683
26358
|
scheduler?.stopAssistantAgent();
|
|
@@ -25691,21 +26366,26 @@ var getAgentSessionStatus = async (scheduler) => {
|
|
|
25691
26366
|
};
|
|
25692
26367
|
|
|
25693
26368
|
// src/api/routes/agent.ts
|
|
26369
|
+
var startSessionBodySchema = z3.object({
|
|
26370
|
+
workspaceId: z3.string(),
|
|
26371
|
+
override: agentModelChoiceSchema.optional(),
|
|
26372
|
+
savedCanvasId: z3.string().optional()
|
|
26373
|
+
});
|
|
25694
26374
|
var agentController = new Hono2().get("/session", zv("query", z3.object({ workspaceId: z3.string() })), async (c) => {
|
|
25695
26375
|
const ctx = c.var.ctx;
|
|
25696
26376
|
const { workspaceId } = c.req.valid("query");
|
|
25697
26377
|
return c.json(await getAgentSessionStatus(ctx.getScheduler(workspaceId)));
|
|
25698
|
-
}).post("/session", zv("json",
|
|
26378
|
+
}).post("/session", zv("json", startSessionBodySchema), async (c) => {
|
|
25699
26379
|
const ctx = c.var.ctx;
|
|
25700
|
-
const { workspaceId } = c.req.valid("json");
|
|
26380
|
+
const { workspaceId, override, savedCanvasId } = c.req.valid("json");
|
|
25701
26381
|
const scheduler = ctx.getScheduler(workspaceId);
|
|
25702
26382
|
if (!scheduler) {
|
|
25703
26383
|
await ctx.ensureWorkspace(workspaceId);
|
|
25704
26384
|
const retried = ctx.getScheduler(workspaceId);
|
|
25705
26385
|
if (!retried) throw NotFoundError("Workspace");
|
|
25706
|
-
return c.json(await startAgentSession(retried));
|
|
26386
|
+
return c.json(await startAgentSession(retried, override, savedCanvasId));
|
|
25707
26387
|
}
|
|
25708
|
-
return c.json(await startAgentSession(scheduler));
|
|
26388
|
+
return c.json(await startAgentSession(scheduler, override, savedCanvasId));
|
|
25709
26389
|
}).delete("/session", zv("query", z3.object({ workspaceId: z3.string() })), async (c) => {
|
|
25710
26390
|
const ctx = c.var.ctx;
|
|
25711
26391
|
const { workspaceId } = c.req.valid("query");
|
|
@@ -26251,57 +26931,11 @@ var getDiffService = async (workspaceId, cardId) => {
|
|
|
26251
26931
|
const card = board.cards[cardId];
|
|
26252
26932
|
if (!card) throw NotFoundError("Card");
|
|
26253
26933
|
const worktreePath = getWorktreePath(resolveWorktreeOwnerId(cardId, board.cards));
|
|
26254
|
-
const { existsSync:
|
|
26255
|
-
if (!
|
|
26934
|
+
const { existsSync: existsSync16 } = await import("node:fs");
|
|
26935
|
+
if (!existsSync16(worktreePath)) {
|
|
26256
26936
|
return { diff: null, error: "No worktree \u2014 agent has not started yet" };
|
|
26257
26937
|
}
|
|
26258
|
-
|
|
26259
|
-
cwd: worktreePath,
|
|
26260
|
-
encoding: "utf-8",
|
|
26261
|
-
maxBuffer: 4 * 1024 * 1024
|
|
26262
|
-
});
|
|
26263
|
-
if (committedResult.status !== 0 && committedResult.stderr) {
|
|
26264
|
-
return { diff: null, error: committedResult.stderr.trim() };
|
|
26265
|
-
}
|
|
26266
|
-
const stagedResult = spawnSync6("git", ["diff", "--cached", "--no-color", "-U3"], {
|
|
26267
|
-
cwd: worktreePath,
|
|
26268
|
-
encoding: "utf-8",
|
|
26269
|
-
maxBuffer: 4 * 1024 * 1024
|
|
26270
|
-
});
|
|
26271
|
-
const unstagedResult = spawnSync6("git", ["diff", "--no-color", "-U3"], {
|
|
26272
|
-
cwd: worktreePath,
|
|
26273
|
-
encoding: "utf-8",
|
|
26274
|
-
maxBuffer: 4 * 1024 * 1024
|
|
26275
|
-
});
|
|
26276
|
-
const untrackedResult = spawnSync6("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
26277
|
-
cwd: worktreePath,
|
|
26278
|
-
encoding: "utf-8"
|
|
26279
|
-
});
|
|
26280
|
-
const untrackedFiles = (untrackedResult.stdout ?? "").split("\n").map((f2) => f2.trim()).filter(Boolean);
|
|
26281
|
-
const { readFileSync: readFileSync9 } = await import("node:fs");
|
|
26282
|
-
const untrackedDiffs = untrackedFiles.map((file) => {
|
|
26283
|
-
try {
|
|
26284
|
-
const content = readFileSync9(`${worktreePath}/${file}`, "utf-8");
|
|
26285
|
-
const lines = content.split("\n");
|
|
26286
|
-
const addedLines = lines.map((l, _i) => `+${l}`).join("\n");
|
|
26287
|
-
const hunkHeader = `@@ -0,0 +1,${lines.length} @@`;
|
|
26288
|
-
return `diff --git a/${file} b/${file}
|
|
26289
|
-
new file mode 100644
|
|
26290
|
-
--- /dev/null
|
|
26291
|
-
+++ b/${file}
|
|
26292
|
-
${hunkHeader}
|
|
26293
|
-
${addedLines}`;
|
|
26294
|
-
} catch {
|
|
26295
|
-
return null;
|
|
26296
|
-
}
|
|
26297
|
-
}).filter((d) => d !== null);
|
|
26298
|
-
const diff = [committedResult.stdout, stagedResult.stdout, unstagedResult.stdout, ...untrackedDiffs].filter((s2) => s2?.trim()).join("\n");
|
|
26299
|
-
const behindResult = spawnSync6("git", ["rev-list", "--count", `HEAD..${card.baseRef}`], {
|
|
26300
|
-
cwd: worktreePath,
|
|
26301
|
-
encoding: "utf-8"
|
|
26302
|
-
});
|
|
26303
|
-
const baseBehindCount = parseInt(behindResult.stdout?.trim() ?? "0", 10) || 0;
|
|
26304
|
-
return { diff, error: null, baseBehindCount };
|
|
26938
|
+
return buildWorktreeDiff(worktreePath, card.baseRef);
|
|
26305
26939
|
};
|
|
26306
26940
|
var getCommitsService = async (workspaceId, cardId) => {
|
|
26307
26941
|
const workspaces = await listWorkspaces();
|
|
@@ -26311,8 +26945,8 @@ var getCommitsService = async (workspaceId, cardId) => {
|
|
|
26311
26945
|
const card = board.cards[cardId];
|
|
26312
26946
|
if (!card) throw NotFoundError("Card");
|
|
26313
26947
|
const worktreePath = getWorktreePath(resolveWorktreeOwnerId(cardId, board.cards));
|
|
26314
|
-
const { existsSync:
|
|
26315
|
-
if (!
|
|
26948
|
+
const { existsSync: existsSync16 } = await import("node:fs");
|
|
26949
|
+
if (!existsSync16(worktreePath)) return { commits: [] };
|
|
26316
26950
|
const result = spawnSync6("git", ["log", "--pretty=format:%H%x00%h%x00%s%x00%an%x00%ai", `${card.baseRef}..HEAD`], {
|
|
26317
26951
|
cwd: worktreePath,
|
|
26318
26952
|
encoding: "utf-8"
|
|
@@ -26333,8 +26967,8 @@ var getDiffForCommitService = async (workspaceId, cardId, commitHash) => {
|
|
|
26333
26967
|
if (!card) throw NotFoundError("Card");
|
|
26334
26968
|
if (!/^[0-9a-f]{4,64}$/i.test(commitHash)) return { diff: null, error: "Invalid commit hash" };
|
|
26335
26969
|
const worktreePath = getWorktreePath(resolveWorktreeOwnerId(cardId, board.cards));
|
|
26336
|
-
const { existsSync:
|
|
26337
|
-
if (!
|
|
26970
|
+
const { existsSync: existsSync16 } = await import("node:fs");
|
|
26971
|
+
if (!existsSync16(worktreePath)) return { diff: null, error: "No worktree" };
|
|
26338
26972
|
const result = spawnSync6("git", ["show", commitHash, "--format=", "--patch", "--no-color", "-U3"], {
|
|
26339
26973
|
cwd: worktreePath,
|
|
26340
26974
|
encoding: "utf-8",
|
|
@@ -26582,6 +27216,330 @@ var cardsController = new Hono2().post("/", zv("json", runtimeCardCreateRequestS
|
|
|
26582
27216
|
}
|
|
26583
27217
|
);
|
|
26584
27218
|
|
|
27219
|
+
// src/api/routes/companion-saved-canvases.ts
|
|
27220
|
+
import { z as z6 } from "zod";
|
|
27221
|
+
|
|
27222
|
+
// src/api/services/companion-saved-canvases-service.ts
|
|
27223
|
+
async function listCompanionSavedCanvasesEntry(workspaceId) {
|
|
27224
|
+
return { canvases: listCompanionSavedCanvases(workspaceId) };
|
|
27225
|
+
}
|
|
27226
|
+
async function deleteCompanionSavedCanvasEntry(id) {
|
|
27227
|
+
deleteCompanionSavedCanvas(id);
|
|
27228
|
+
}
|
|
27229
|
+
async function clearCompanionCanvasesEntry(sessionId) {
|
|
27230
|
+
deleteCompanionCanvasesForSession(sessionId);
|
|
27231
|
+
}
|
|
27232
|
+
async function saveCompanionCanvasEntry(sessionId, workspaceId, title, blocks) {
|
|
27233
|
+
const session = getCompanionSession(sessionId);
|
|
27234
|
+
if (session) {
|
|
27235
|
+
if (session.savedCanvasId) {
|
|
27236
|
+
const updated = updateCompanionSavedCanvas(session.savedCanvasId, { title, blocks });
|
|
27237
|
+
if (updated) return updated;
|
|
27238
|
+
}
|
|
27239
|
+
const created = createCompanionSavedCanvas(workspaceId, { title, blocks, sourceSessionId: sessionId });
|
|
27240
|
+
setCompanionSessionSavedCanvasId(sessionId, created.id);
|
|
27241
|
+
return created;
|
|
27242
|
+
}
|
|
27243
|
+
const existing = findCompanionSavedCanvasBySourceSession(sessionId);
|
|
27244
|
+
if (existing) {
|
|
27245
|
+
const updated = updateCompanionSavedCanvas(existing.id, { title, blocks });
|
|
27246
|
+
if (updated) return updated;
|
|
27247
|
+
}
|
|
27248
|
+
return createCompanionSavedCanvas(workspaceId, { title, blocks, sourceSessionId: sessionId });
|
|
27249
|
+
}
|
|
27250
|
+
|
|
27251
|
+
// src/api/routes/companion-saved-canvases.ts
|
|
27252
|
+
var companionSavedCanvasesController = new Hono2().get("/", zv("query", z6.object({ workspaceId: z6.string() })), async (c) => {
|
|
27253
|
+
const { workspaceId } = c.req.valid("query");
|
|
27254
|
+
return c.json(await listCompanionSavedCanvasesEntry(workspaceId));
|
|
27255
|
+
}).delete("/:id", zv("param", z6.object({ id: z6.string() })), async (c) => {
|
|
27256
|
+
const { id } = c.req.valid("param");
|
|
27257
|
+
await deleteCompanionSavedCanvasEntry(id);
|
|
27258
|
+
return c.json({ ok: true });
|
|
27259
|
+
});
|
|
27260
|
+
|
|
27261
|
+
// src/api/routes/companion-sessions.ts
|
|
27262
|
+
init_api_contract();
|
|
27263
|
+
import { z as z7 } from "zod";
|
|
27264
|
+
|
|
27265
|
+
// src/api/services/companion-canvases-service.ts
|
|
27266
|
+
var createCompanionCanvasEntry = async (sessionId, workspaceId, blocks) => {
|
|
27267
|
+
return createCompanionCanvas(sessionId, workspaceId, blocks);
|
|
27268
|
+
};
|
|
27269
|
+
var listCompanionCanvasesEntry = async (sessionId) => {
|
|
27270
|
+
return { canvases: listCompanionCanvases(sessionId) };
|
|
27271
|
+
};
|
|
27272
|
+
|
|
27273
|
+
// src/api/services/companion-diff-service.ts
|
|
27274
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
27275
|
+
import { existsSync as existsSync11 } from "node:fs";
|
|
27276
|
+
var MAX_BUFFER = 4 * 1024 * 1024;
|
|
27277
|
+
var getCompanionDiffService = async (id) => {
|
|
27278
|
+
const session = getCompanionSession(id);
|
|
27279
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27280
|
+
if (!session.worktreePath || !existsSync11(session.worktreePath)) {
|
|
27281
|
+
return { diff: null, error: "No worktree \u2014 agent has not started yet" };
|
|
27282
|
+
}
|
|
27283
|
+
return buildWorktreeDiff(session.worktreePath, session.baseRef);
|
|
27284
|
+
};
|
|
27285
|
+
var getCompanionCommitsService = async (id) => {
|
|
27286
|
+
const session = getCompanionSession(id);
|
|
27287
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27288
|
+
if (!session.worktreePath || !existsSync11(session.worktreePath)) return { commits: [] };
|
|
27289
|
+
const result = spawnSync7("git", ["log", "--pretty=format:%H%x00%h%x00%s%x00%an%x00%ai", `${session.baseRef}..HEAD`], {
|
|
27290
|
+
cwd: session.worktreePath,
|
|
27291
|
+
encoding: "utf-8"
|
|
27292
|
+
});
|
|
27293
|
+
if (result.status !== 0 || !result.stdout?.trim()) return { commits: [] };
|
|
27294
|
+
const commits = result.stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
27295
|
+
const [hash = "", shortHash = "", message = "", author = "", date2 = ""] = line.split("\0");
|
|
27296
|
+
return { hash, shortHash, message, author, date: date2 };
|
|
27297
|
+
});
|
|
27298
|
+
return { commits };
|
|
27299
|
+
};
|
|
27300
|
+
var getCompanionDiffForCommitService = async (id, commitHash) => {
|
|
27301
|
+
const session = getCompanionSession(id);
|
|
27302
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27303
|
+
if (!/^[0-9a-f]{4,64}$/i.test(commitHash)) return { diff: null, error: "Invalid commit hash" };
|
|
27304
|
+
if (!session.worktreePath || !existsSync11(session.worktreePath)) return { diff: null, error: "No worktree" };
|
|
27305
|
+
const result = spawnSync7("git", ["show", commitHash, "--format=", "--patch", "--no-color", "-U3"], {
|
|
27306
|
+
cwd: session.worktreePath,
|
|
27307
|
+
encoding: "utf-8",
|
|
27308
|
+
maxBuffer: MAX_BUFFER
|
|
27309
|
+
});
|
|
27310
|
+
if (result.status !== 0) return { diff: null, error: result.stderr?.trim() || "git show failed" };
|
|
27311
|
+
return { diff: result.stdout, error: null };
|
|
27312
|
+
};
|
|
27313
|
+
|
|
27314
|
+
// src/api/services/companion-merge-service.ts
|
|
27315
|
+
init_workspace_state();
|
|
27316
|
+
async function commitAndMergeCompanionService(id, repoPath, commitMessage, scheduler) {
|
|
27317
|
+
const session = getCompanionSession(id);
|
|
27318
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27319
|
+
if (!session.useWorktree || !session.branchName) {
|
|
27320
|
+
throw BadRequestError("This session works directly in the main repo \u2014 there's nothing to merge");
|
|
27321
|
+
}
|
|
27322
|
+
if (!session.worktreePath) throw BadRequestError("Session has no worktree to merge");
|
|
27323
|
+
await scheduler?.stopCompanionAgent(id);
|
|
27324
|
+
const dirty = await isWorktreeDirty(session.worktreePath);
|
|
27325
|
+
if (dirty) {
|
|
27326
|
+
if (!commitMessage) return { status: "needs_commit" };
|
|
27327
|
+
await commitWorktree(session.worktreePath, commitMessage);
|
|
27328
|
+
}
|
|
27329
|
+
const result = attemptMerge(repoPath, id, session.branchName);
|
|
27330
|
+
setCompanionSessionWorktreePath(id, null);
|
|
27331
|
+
if (result.dirtyBase) {
|
|
27332
|
+
setCompanionSessionStatus(id, "stopped");
|
|
27333
|
+
return { status: "dirty_base" };
|
|
27334
|
+
}
|
|
27335
|
+
if (!result.ok) {
|
|
27336
|
+
abortMerge(repoPath);
|
|
27337
|
+
setCompanionSessionStatus(id, "stopped");
|
|
27338
|
+
return { status: "conflict", conflictedFiles: result.conflictedFiles };
|
|
27339
|
+
}
|
|
27340
|
+
setCompanionSessionStatus(id, "merged");
|
|
27341
|
+
return { status: "merged" };
|
|
27342
|
+
}
|
|
27343
|
+
async function commitAndPRCompanionService(id, workspaceId, commitMessage, title, description, baseRefOverride) {
|
|
27344
|
+
const session = getCompanionSession(id);
|
|
27345
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27346
|
+
if (!session.worktreePath) throw BadRequestError("Session has no worktree");
|
|
27347
|
+
const projectConfig = await loadProjectConfig(workspaceId);
|
|
27348
|
+
const token = projectConfig.secrets?.find((s2) => s2.key === "GITHUB_TOKEN")?.value;
|
|
27349
|
+
if (!token) return { status: "no_token" };
|
|
27350
|
+
const dirty = await isWorktreeDirty(session.worktreePath);
|
|
27351
|
+
if (dirty) {
|
|
27352
|
+
if (!commitMessage) return { status: "needs_commit" };
|
|
27353
|
+
await commitWorktree(session.worktreePath, commitMessage);
|
|
27354
|
+
}
|
|
27355
|
+
const branch = session.branchName ?? getCurrentBranch(session.worktreePath);
|
|
27356
|
+
if (!branch) throw BadRequestError("Could not determine the current branch to push");
|
|
27357
|
+
await pushBranch(session.worktreePath, branch);
|
|
27358
|
+
const prUrl = await createGithubPR(
|
|
27359
|
+
session.worktreePath,
|
|
27360
|
+
title,
|
|
27361
|
+
description,
|
|
27362
|
+
baseRefOverride || session.baseRef,
|
|
27363
|
+
token
|
|
27364
|
+
);
|
|
27365
|
+
return { status: "pr_created", prUrl };
|
|
27366
|
+
}
|
|
27367
|
+
|
|
27368
|
+
// src/api/services/companion-service.ts
|
|
27369
|
+
init_api_contract();
|
|
27370
|
+
init_workspace_state();
|
|
27371
|
+
async function listCompanionSessionsEntry(workspaceId) {
|
|
27372
|
+
return listCompanionSessions(workspaceId);
|
|
27373
|
+
}
|
|
27374
|
+
function getCompanionSessionEntry(id) {
|
|
27375
|
+
return getCompanionSession(id);
|
|
27376
|
+
}
|
|
27377
|
+
async function createCompanionSessionEntry(workspaceId, repoPath, req, scheduler) {
|
|
27378
|
+
const useWorktree = req.useWorktree;
|
|
27379
|
+
const branchName = req.branchName?.trim() || null;
|
|
27380
|
+
if (useWorktree && !branchName) {
|
|
27381
|
+
throw BadRequestError("Branch name is required when using an isolated worktree");
|
|
27382
|
+
}
|
|
27383
|
+
const projectConfig = await loadProjectConfig(workspaceId);
|
|
27384
|
+
const workflow = req.workflowId ? projectConfig.workflows.find((w2) => w2.id === req.workflowId) : void 0;
|
|
27385
|
+
const devSlot = workflow?.slots.find((s2) => s2.type === "dev");
|
|
27386
|
+
const seedPrompt = devSlot ? resolvePromptText(devSlot.prompt, repoPath) : "";
|
|
27387
|
+
const suggestedPair = devSlot?.pairs[0];
|
|
27388
|
+
const model = req.model ?? (suggestedPair ? { agentId: suggestedPair.binary, model: suggestedPair.model, effort: suggestedPair.effort } : DEFAULT_AGENT_MODEL_CHOICE);
|
|
27389
|
+
const savedCanvas = req.savedCanvasId ? getCompanionSavedCanvas(req.savedCanvasId) : null;
|
|
27390
|
+
const name = req.name?.trim() || savedCanvas?.title || (useWorktree ? branchName : "Main repo session");
|
|
27391
|
+
const session = createCompanionSession(workspaceId, {
|
|
27392
|
+
name,
|
|
27393
|
+
useWorktree,
|
|
27394
|
+
baseRef: req.baseRef,
|
|
27395
|
+
branchName: useWorktree ? branchName : null,
|
|
27396
|
+
workflowId: workflow?.id ?? null,
|
|
27397
|
+
seedPrompt,
|
|
27398
|
+
agentId: model.agentId ?? "claude",
|
|
27399
|
+
model: model.model ?? null,
|
|
27400
|
+
effort: model.effort ?? null,
|
|
27401
|
+
savedCanvasId: savedCanvas?.id ?? null
|
|
27402
|
+
});
|
|
27403
|
+
if (savedCanvas) createCompanionCanvas(session.id, workspaceId, savedCanvas.blocks);
|
|
27404
|
+
await scheduler.startCompanionAgent(session);
|
|
27405
|
+
return getCompanionSession(session.id) ?? session;
|
|
27406
|
+
}
|
|
27407
|
+
async function stopCompanionSessionEntry(id, scheduler) {
|
|
27408
|
+
if (!getCompanionSession(id)) throw NotFoundError("Companion session");
|
|
27409
|
+
await scheduler?.stopCompanionAgent(id);
|
|
27410
|
+
}
|
|
27411
|
+
async function discardCompanionSessionEntry(id, repoPath, scheduler) {
|
|
27412
|
+
const session = getCompanionSession(id);
|
|
27413
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27414
|
+
await scheduler?.stopCompanionAgent(id);
|
|
27415
|
+
if (session.useWorktree && session.worktreePath) {
|
|
27416
|
+
await removeWorktreeAsync(id, repoPath, session.branchName ?? void 0);
|
|
27417
|
+
}
|
|
27418
|
+
deleteCompanionSession(id);
|
|
27419
|
+
}
|
|
27420
|
+
|
|
27421
|
+
// src/api/routes/companion-sessions.ts
|
|
27422
|
+
var companionSessionsController = new Hono2().get("/", zv("query", z7.object({ workspaceId: z7.string() })), async (c) => {
|
|
27423
|
+
const { workspaceId } = c.req.valid("query");
|
|
27424
|
+
return c.json(await listCompanionSessionsEntry(workspaceId));
|
|
27425
|
+
}).get("/:id", zv("param", z7.object({ id: z7.string() })), async (c) => {
|
|
27426
|
+
const { id } = c.req.valid("param");
|
|
27427
|
+
const session = getCompanionSessionEntry(id);
|
|
27428
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27429
|
+
return c.json(session);
|
|
27430
|
+
}).post("/", zv("json", companionSessionCreateRequestSchema.extend({ workspaceId: z7.string() })), async (c) => {
|
|
27431
|
+
const ctx = c.var.ctx;
|
|
27432
|
+
const { workspaceId, ...req } = c.req.valid("json");
|
|
27433
|
+
const ws = await ctx.ensureWorkspace(workspaceId);
|
|
27434
|
+
const scheduler = ctx.getScheduler(workspaceId);
|
|
27435
|
+
if (!scheduler) throw NotFoundError("Workspace");
|
|
27436
|
+
const session = await createCompanionSessionEntry(workspaceId, ws.repoPath, req, scheduler);
|
|
27437
|
+
return c.json(session);
|
|
27438
|
+
}).delete(
|
|
27439
|
+
"/:id",
|
|
27440
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27441
|
+
zv("query", z7.object({ workspaceId: z7.string() })),
|
|
27442
|
+
async (c) => {
|
|
27443
|
+
const ctx = c.var.ctx;
|
|
27444
|
+
const { id } = c.req.valid("param");
|
|
27445
|
+
const { workspaceId } = c.req.valid("query");
|
|
27446
|
+
await stopCompanionSessionEntry(id, ctx.getScheduler(workspaceId));
|
|
27447
|
+
return c.json({ ok: true });
|
|
27448
|
+
}
|
|
27449
|
+
).post(
|
|
27450
|
+
"/:id/discard",
|
|
27451
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27452
|
+
zv("json", z7.object({ workspaceId: z7.string() })),
|
|
27453
|
+
async (c) => {
|
|
27454
|
+
const ctx = c.var.ctx;
|
|
27455
|
+
const { id } = c.req.valid("param");
|
|
27456
|
+
const { workspaceId } = c.req.valid("json");
|
|
27457
|
+
const ws = await ctx.ensureWorkspace(workspaceId);
|
|
27458
|
+
await discardCompanionSessionEntry(id, ws.repoPath, ctx.getScheduler(workspaceId));
|
|
27459
|
+
return c.json({ ok: true });
|
|
27460
|
+
}
|
|
27461
|
+
).get("/:id/diff", zv("param", z7.object({ id: z7.string() })), async (c) => {
|
|
27462
|
+
const { id } = c.req.valid("param");
|
|
27463
|
+
return c.json(await getCompanionDiffService(id));
|
|
27464
|
+
}).get("/:id/commits", zv("param", z7.object({ id: z7.string() })), async (c) => {
|
|
27465
|
+
const { id } = c.req.valid("param");
|
|
27466
|
+
return c.json(await getCompanionCommitsService(id));
|
|
27467
|
+
}).get(
|
|
27468
|
+
"/:id/diff-for-commit",
|
|
27469
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27470
|
+
zv("query", z7.object({ commitHash: z7.string() })),
|
|
27471
|
+
async (c) => {
|
|
27472
|
+
const { id } = c.req.valid("param");
|
|
27473
|
+
const { commitHash } = c.req.valid("query");
|
|
27474
|
+
return c.json(await getCompanionDiffForCommitService(id, commitHash));
|
|
27475
|
+
}
|
|
27476
|
+
).post(
|
|
27477
|
+
"/:id/commit-and-merge",
|
|
27478
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27479
|
+
zv("json", z7.object({ workspaceId: z7.string(), commitMessage: z7.string().optional() })),
|
|
27480
|
+
async (c) => {
|
|
27481
|
+
const ctx = c.var.ctx;
|
|
27482
|
+
const { id } = c.req.valid("param");
|
|
27483
|
+
const { workspaceId, commitMessage } = c.req.valid("json");
|
|
27484
|
+
const ws = await ctx.ensureWorkspace(workspaceId);
|
|
27485
|
+
const result = await commitAndMergeCompanionService(
|
|
27486
|
+
id,
|
|
27487
|
+
ws.repoPath,
|
|
27488
|
+
commitMessage,
|
|
27489
|
+
ctx.getScheduler(workspaceId)
|
|
27490
|
+
);
|
|
27491
|
+
return c.json(result);
|
|
27492
|
+
}
|
|
27493
|
+
).post(
|
|
27494
|
+
"/:id/commit-and-pr",
|
|
27495
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27496
|
+
zv(
|
|
27497
|
+
"json",
|
|
27498
|
+
z7.object({
|
|
27499
|
+
workspaceId: z7.string(),
|
|
27500
|
+
commitMessage: z7.string().optional(),
|
|
27501
|
+
title: z7.string(),
|
|
27502
|
+
description: z7.string(),
|
|
27503
|
+
baseRef: z7.string().optional()
|
|
27504
|
+
})
|
|
27505
|
+
),
|
|
27506
|
+
async (c) => {
|
|
27507
|
+
const { id } = c.req.valid("param");
|
|
27508
|
+
const { workspaceId, commitMessage, title, description, baseRef } = c.req.valid("json");
|
|
27509
|
+
const result = await commitAndPRCompanionService(id, workspaceId, commitMessage, title, description, baseRef);
|
|
27510
|
+
return c.json(result);
|
|
27511
|
+
}
|
|
27512
|
+
).post(
|
|
27513
|
+
"/:id/canvas",
|
|
27514
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27515
|
+
zv("json", z7.object({ workspaceId: z7.string(), blocks: z7.array(canvasBlockSchema) })),
|
|
27516
|
+
async (c) => {
|
|
27517
|
+
const ctx = c.var.ctx;
|
|
27518
|
+
const { id } = c.req.valid("param");
|
|
27519
|
+
const { workspaceId, blocks } = c.req.valid("json");
|
|
27520
|
+
const canvas = await createCompanionCanvasEntry(id, workspaceId, blocks);
|
|
27521
|
+
ctx.stateHub.broadcastCompanionCanvasUpdate(workspaceId, id, canvas);
|
|
27522
|
+
return c.json(canvas);
|
|
27523
|
+
}
|
|
27524
|
+
).get("/:id/canvases", zv("param", z7.object({ id: z7.string() })), async (c) => {
|
|
27525
|
+
const { id } = c.req.valid("param");
|
|
27526
|
+
return c.json(await listCompanionCanvasesEntry(id));
|
|
27527
|
+
}).delete("/:id/canvases", zv("param", z7.object({ id: z7.string() })), async (c) => {
|
|
27528
|
+
const { id } = c.req.valid("param");
|
|
27529
|
+
await clearCompanionCanvasesEntry(id);
|
|
27530
|
+
return c.json({ ok: true });
|
|
27531
|
+
}).post(
|
|
27532
|
+
"/:id/save-canvas",
|
|
27533
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27534
|
+
zv("json", z7.object({ workspaceId: z7.string(), title: z7.string(), blocks: z7.array(canvasBlockSchema) })),
|
|
27535
|
+
async (c) => {
|
|
27536
|
+
const { id } = c.req.valid("param");
|
|
27537
|
+
const { workspaceId, title, blocks } = c.req.valid("json");
|
|
27538
|
+
const saved = await saveCompanionCanvasEntry(id, workspaceId, title, blocks);
|
|
27539
|
+
return c.json(saved);
|
|
27540
|
+
}
|
|
27541
|
+
);
|
|
27542
|
+
|
|
26585
27543
|
// src/api/routes/config.ts
|
|
26586
27544
|
init_api_contract();
|
|
26587
27545
|
|
|
@@ -26605,20 +27563,20 @@ var configController = new Hono2().get("/", async (c) => {
|
|
|
26605
27563
|
});
|
|
26606
27564
|
|
|
26607
27565
|
// src/api/routes/fs.ts
|
|
26608
|
-
import { z as
|
|
27566
|
+
import { z as z8 } from "zod";
|
|
26609
27567
|
|
|
26610
27568
|
// src/api/services/fs-service.ts
|
|
26611
27569
|
init_runtime_config();
|
|
26612
|
-
import { spawnSync as
|
|
26613
|
-
import { existsSync as
|
|
27570
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
27571
|
+
import { existsSync as existsSync13, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
26614
27572
|
import { homedir as homedir4 } from "node:os";
|
|
26615
|
-
import { dirname as dirname7, join as
|
|
27573
|
+
import { dirname as dirname7, join as join20, resolve as resolve3 } from "node:path";
|
|
26616
27574
|
|
|
26617
27575
|
// src/core/terminal-apps.ts
|
|
26618
|
-
import { spawnSync as
|
|
26619
|
-
import { existsSync as
|
|
27576
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
27577
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
26620
27578
|
import { homedir as homedir3 } from "node:os";
|
|
26621
|
-
import { join as
|
|
27579
|
+
import { join as join19 } from "node:path";
|
|
26622
27580
|
var MACOS_TERMINALS = [
|
|
26623
27581
|
{ bundle: "Terminal", label: "Terminal" },
|
|
26624
27582
|
{ bundle: "iTerm", label: "iTerm" },
|
|
@@ -26650,13 +27608,13 @@ function appExists(bundle) {
|
|
|
26650
27608
|
`/Applications/${bundle}.app`,
|
|
26651
27609
|
`/System/Applications/${bundle}.app`,
|
|
26652
27610
|
`/System/Applications/Utilities/${bundle}.app`,
|
|
26653
|
-
|
|
27611
|
+
join19(homedir3(), "Applications", `${bundle}.app`)
|
|
26654
27612
|
];
|
|
26655
|
-
return paths.some((p2) =>
|
|
27613
|
+
return paths.some((p2) => existsSync12(p2));
|
|
26656
27614
|
}
|
|
26657
27615
|
function binaryExists(bin) {
|
|
26658
27616
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
26659
|
-
const r =
|
|
27617
|
+
const r = spawnSync8(finder, [bin], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" });
|
|
26660
27618
|
return r.status === 0 && r.stdout.trim().length > 0;
|
|
26661
27619
|
}
|
|
26662
27620
|
function listTerminalApps() {
|
|
@@ -26671,26 +27629,26 @@ function listTerminalApps() {
|
|
|
26671
27629
|
function openTerminalAt(path2, preferredId) {
|
|
26672
27630
|
if (process.platform === "darwin") {
|
|
26673
27631
|
const bundle = preferredId && appExists(preferredId) ? preferredId : "Terminal";
|
|
26674
|
-
|
|
27632
|
+
spawnSync8("open", ["-a", bundle, path2], { stdio: "ignore" });
|
|
26675
27633
|
return;
|
|
26676
27634
|
}
|
|
26677
27635
|
if (process.platform === "win32") {
|
|
26678
27636
|
const id = preferredId && WINDOWS_TERMINALS.some((t) => t.id === preferredId && binaryExists(t.check)) ? preferredId : "cmd";
|
|
26679
27637
|
if (id === "wt") {
|
|
26680
|
-
|
|
27638
|
+
spawnSync8("wt", ["-d", path2], { stdio: "ignore" });
|
|
26681
27639
|
} else if (id === "powershell") {
|
|
26682
|
-
|
|
27640
|
+
spawnSync8("cmd", ["/c", "start", "powershell", "-NoExit", "-Command", `Set-Location -Path '${path2}'`], {
|
|
26683
27641
|
stdio: "ignore"
|
|
26684
27642
|
});
|
|
26685
27643
|
} else {
|
|
26686
|
-
|
|
27644
|
+
spawnSync8("cmd", ["/c", "start", "cmd", "/K", `cd /D "${path2}"`], { stdio: "ignore" });
|
|
26687
27645
|
}
|
|
26688
27646
|
return;
|
|
26689
27647
|
}
|
|
26690
27648
|
const bin = preferredId && binaryExists(preferredId) ? preferredId : LINUX_TERMINALS.find((t) => binaryExists(t.bin))?.bin;
|
|
26691
27649
|
if (!bin) return;
|
|
26692
27650
|
const args = linuxLaunchArgs(bin, path2);
|
|
26693
|
-
|
|
27651
|
+
spawnSync8(bin, args, { stdio: "ignore" });
|
|
26694
27652
|
}
|
|
26695
27653
|
function linuxLaunchArgs(bin, path2) {
|
|
26696
27654
|
switch (bin) {
|
|
@@ -26716,7 +27674,7 @@ function linuxLaunchArgs(bin, path2) {
|
|
|
26716
27674
|
// src/api/services/fs-service.ts
|
|
26717
27675
|
var openPath = (path2) => {
|
|
26718
27676
|
const cmd = process.platform === "win32" ? "explorer" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
26719
|
-
|
|
27677
|
+
spawnSync9(cmd, [path2], { stdio: "ignore" });
|
|
26720
27678
|
return { ok: true };
|
|
26721
27679
|
};
|
|
26722
27680
|
var listTerminals = async () => listTerminalApps();
|
|
@@ -26727,15 +27685,15 @@ var openTerminal = async (path2) => {
|
|
|
26727
27685
|
};
|
|
26728
27686
|
var listDir = async (path2, includeFiles, showHidden) => {
|
|
26729
27687
|
let target = resolve3(path2 || homedir4());
|
|
26730
|
-
while (target !== dirname7(target) && !(
|
|
27688
|
+
while (target !== dirname7(target) && !(existsSync13(target) && statSync(target).isDirectory())) {
|
|
26731
27689
|
target = dirname7(target);
|
|
26732
27690
|
}
|
|
26733
27691
|
const parent = dirname7(target);
|
|
26734
27692
|
const visible = (name) => showHidden || !name.startsWith(".");
|
|
26735
27693
|
try {
|
|
26736
27694
|
const entries = readdirSync2(target, { withFileTypes: true });
|
|
26737
|
-
const dirs = entries.filter((e) => e.isDirectory() && visible(e.name)).map((e) => ({ name: e.name, path:
|
|
26738
|
-
const files = includeFiles ? entries.filter((e) => e.isFile() && visible(e.name)).map((e) => ({ name: e.name, path:
|
|
27695
|
+
const dirs = entries.filter((e) => e.isDirectory() && visible(e.name)).map((e) => ({ name: e.name, path: join20(target, e.name) })).sort((a, b2) => a.name.localeCompare(b2.name));
|
|
27696
|
+
const files = includeFiles ? entries.filter((e) => e.isFile() && visible(e.name)).map((e) => ({ name: e.name, path: join20(target, e.name) })).sort((a, b2) => a.name.localeCompare(b2.name)) : [];
|
|
26739
27697
|
return { current: target, parent: parent !== target ? parent : null, dirs, files };
|
|
26740
27698
|
} catch {
|
|
26741
27699
|
return { current: target, parent: parent !== target ? parent : null, dirs: [], files: [] };
|
|
@@ -26743,22 +27701,22 @@ var listDir = async (path2, includeFiles, showHidden) => {
|
|
|
26743
27701
|
};
|
|
26744
27702
|
|
|
26745
27703
|
// src/api/routes/fs.ts
|
|
26746
|
-
var fsController = new Hono2().post("/open", zv("json",
|
|
27704
|
+
var fsController = new Hono2().post("/open", zv("json", z8.object({ path: z8.string() })), async (c) => {
|
|
26747
27705
|
const { path: path2 } = c.req.valid("json");
|
|
26748
27706
|
return c.json(openPath(path2));
|
|
26749
27707
|
}).get("/terminals", async (c) => {
|
|
26750
27708
|
return c.json(await listTerminals());
|
|
26751
|
-
}).post("/open-terminal", zv("json",
|
|
27709
|
+
}).post("/open-terminal", zv("json", z8.object({ path: z8.string() })), async (c) => {
|
|
26752
27710
|
const { path: path2 } = c.req.valid("json");
|
|
26753
27711
|
return c.json(await openTerminal(path2));
|
|
26754
27712
|
}).get(
|
|
26755
27713
|
"/list-dir",
|
|
26756
27714
|
zv(
|
|
26757
27715
|
"query",
|
|
26758
|
-
|
|
26759
|
-
path:
|
|
26760
|
-
includeFiles:
|
|
26761
|
-
showHidden:
|
|
27716
|
+
z8.object({
|
|
27717
|
+
path: z8.string().optional().default(""),
|
|
27718
|
+
includeFiles: z8.coerce.boolean().optional(),
|
|
27719
|
+
showHidden: z8.coerce.boolean().optional()
|
|
26762
27720
|
})
|
|
26763
27721
|
),
|
|
26764
27722
|
async (c) => {
|
|
@@ -26769,7 +27727,7 @@ var fsController = new Hono2().post("/open", zv("json", z6.object({ path: z6.str
|
|
|
26769
27727
|
|
|
26770
27728
|
// src/api/routes/memory.ts
|
|
26771
27729
|
init_api_contract();
|
|
26772
|
-
import { z as
|
|
27730
|
+
import { z as z9 } from "zod";
|
|
26773
27731
|
|
|
26774
27732
|
// src/api/services/memory-service.ts
|
|
26775
27733
|
var listMemoryEntries = async (filter) => listMemories(filter);
|
|
@@ -26801,9 +27759,9 @@ var memoryController = new Hono2().get(
|
|
|
26801
27759
|
"/",
|
|
26802
27760
|
zv(
|
|
26803
27761
|
"query",
|
|
26804
|
-
|
|
27762
|
+
z9.object({
|
|
26805
27763
|
scope: memoryScopeSchema,
|
|
26806
|
-
workspaceId:
|
|
27764
|
+
workspaceId: z9.string().optional(),
|
|
26807
27765
|
status: memoryStatusSchema.optional()
|
|
26808
27766
|
})
|
|
26809
27767
|
),
|
|
@@ -26817,33 +27775,33 @@ var memoryController = new Hono2().get(
|
|
|
26817
27775
|
})
|
|
26818
27776
|
);
|
|
26819
27777
|
}
|
|
26820
|
-
).get("/search", zv("query",
|
|
27778
|
+
).get("/search", zv("query", z9.object({ query: z9.string(), workspaceId: z9.string().optional() })), async (c) => {
|
|
26821
27779
|
const input = c.req.valid("query");
|
|
26822
27780
|
return c.json(await searchMemoryEntries(input.query, input.workspaceId ?? null));
|
|
26823
|
-
}).get("/for-card", zv("query",
|
|
27781
|
+
}).get("/for-card", zv("query", z9.object({ cardId: z9.string() })), async (c) => {
|
|
26824
27782
|
const input = c.req.valid("query");
|
|
26825
27783
|
return c.json(await listMemoryEntriesForCard(input.cardId));
|
|
26826
|
-
}).get("/tags", async (c) => c.json(await listTagsEntries())).get("/workspace-tags", zv("query",
|
|
27784
|
+
}).get("/tags", async (c) => c.json(await listTagsEntries())).get("/workspace-tags", zv("query", z9.object({ workspaceId: z9.string() })), async (c) => {
|
|
26827
27785
|
const { workspaceId } = c.req.valid("query");
|
|
26828
27786
|
return c.json(await getWorkspaceTagsEntry(workspaceId));
|
|
26829
|
-
}).get("/:id", zv("param",
|
|
27787
|
+
}).get("/:id", zv("param", z9.object({ id: z9.string() })), async (c) => {
|
|
26830
27788
|
const { id } = c.req.valid("param");
|
|
26831
27789
|
return c.json(await getMemoryEntry(id));
|
|
26832
27790
|
}).post(
|
|
26833
27791
|
"/propose",
|
|
26834
27792
|
zv(
|
|
26835
27793
|
"json",
|
|
26836
|
-
|
|
27794
|
+
z9.object({
|
|
26837
27795
|
scope: memoryScopeSchema,
|
|
26838
|
-
workspaceId:
|
|
26839
|
-
originWorkspaceId:
|
|
27796
|
+
workspaceId: z9.string().optional(),
|
|
27797
|
+
originWorkspaceId: z9.string().optional(),
|
|
26840
27798
|
type: memoryTypeSchema,
|
|
26841
|
-
title:
|
|
26842
|
-
content:
|
|
27799
|
+
title: z9.string().min(1),
|
|
27800
|
+
content: z9.string().min(1),
|
|
26843
27801
|
sourceType: memorySourceTypeSchema.default("task_lesson"),
|
|
26844
|
-
importance:
|
|
26845
|
-
tags:
|
|
26846
|
-
originCardId:
|
|
27802
|
+
importance: z9.number().int().min(1).max(3).optional(),
|
|
27803
|
+
tags: z9.array(z9.string()).optional(),
|
|
27804
|
+
originCardId: z9.string().optional(),
|
|
26847
27805
|
originAgent: runtimeMemoryOriginAgentSchema.optional()
|
|
26848
27806
|
})
|
|
26849
27807
|
),
|
|
@@ -26875,12 +27833,12 @@ var memoryController = new Hono2().get(
|
|
|
26875
27833
|
"/propose-update",
|
|
26876
27834
|
zv(
|
|
26877
27835
|
"json",
|
|
26878
|
-
|
|
26879
|
-
id:
|
|
27836
|
+
z9.object({
|
|
27837
|
+
id: z9.string(),
|
|
26880
27838
|
type: memoryTypeSchema.optional(),
|
|
26881
|
-
title:
|
|
26882
|
-
content:
|
|
26883
|
-
importance:
|
|
27839
|
+
title: z9.string().min(1).optional(),
|
|
27840
|
+
content: z9.string().min(1).optional(),
|
|
27841
|
+
importance: z9.number().int().min(1).max(3).optional(),
|
|
26884
27842
|
sourceType: memorySourceTypeSchema.default("task_lesson")
|
|
26885
27843
|
})
|
|
26886
27844
|
),
|
|
@@ -26894,16 +27852,16 @@ var memoryController = new Hono2().get(
|
|
|
26894
27852
|
"/",
|
|
26895
27853
|
zv(
|
|
26896
27854
|
"json",
|
|
26897
|
-
|
|
27855
|
+
z9.object({
|
|
26898
27856
|
scope: memoryScopeSchema,
|
|
26899
|
-
workspaceId:
|
|
26900
|
-
originWorkspaceId:
|
|
27857
|
+
workspaceId: z9.string().optional(),
|
|
27858
|
+
originWorkspaceId: z9.string().optional(),
|
|
26901
27859
|
type: memoryTypeSchema,
|
|
26902
|
-
title:
|
|
26903
|
-
content:
|
|
26904
|
-
importance:
|
|
26905
|
-
tags:
|
|
26906
|
-
boundWorkspaceIds:
|
|
27860
|
+
title: z9.string().min(1),
|
|
27861
|
+
content: z9.string().min(1),
|
|
27862
|
+
importance: z9.number().int().min(1).max(3).optional(),
|
|
27863
|
+
tags: z9.array(z9.string()).optional(),
|
|
27864
|
+
boundWorkspaceIds: z9.array(z9.string()).optional()
|
|
26907
27865
|
})
|
|
26908
27866
|
),
|
|
26909
27867
|
async (c) => {
|
|
@@ -26932,14 +27890,14 @@ var memoryController = new Hono2().get(
|
|
|
26932
27890
|
}
|
|
26933
27891
|
).patch(
|
|
26934
27892
|
"/:id",
|
|
26935
|
-
zv("param",
|
|
27893
|
+
zv("param", z9.object({ id: z9.string() })),
|
|
26936
27894
|
zv(
|
|
26937
27895
|
"json",
|
|
26938
|
-
|
|
27896
|
+
z9.object({
|
|
26939
27897
|
type: memoryTypeSchema.optional(),
|
|
26940
|
-
title:
|
|
26941
|
-
content:
|
|
26942
|
-
importance:
|
|
27898
|
+
title: z9.string().min(1).optional(),
|
|
27899
|
+
content: z9.string().min(1).optional(),
|
|
27900
|
+
importance: z9.number().int().min(1).max(3).optional()
|
|
26943
27901
|
})
|
|
26944
27902
|
),
|
|
26945
27903
|
async (c) => {
|
|
@@ -26948,19 +27906,19 @@ var memoryController = new Hono2().get(
|
|
|
26948
27906
|
if (!updated) throw NotFoundError("Memory");
|
|
26949
27907
|
return c.json(updated);
|
|
26950
27908
|
}
|
|
26951
|
-
).post("/:id/approve", zv("param",
|
|
27909
|
+
).post("/:id/approve", zv("param", z9.object({ id: z9.string() })), async (c) => {
|
|
26952
27910
|
const { id } = c.req.valid("param");
|
|
26953
27911
|
const approved = await approveMemoryEntry(id);
|
|
26954
27912
|
if (!approved) throw NotFoundError("Memory");
|
|
26955
27913
|
return c.json(approved);
|
|
26956
|
-
}).delete("/:id", zv("param",
|
|
27914
|
+
}).delete("/:id", zv("param", z9.object({ id: z9.string() })), async (c) => {
|
|
26957
27915
|
const { id } = c.req.valid("param");
|
|
26958
27916
|
await removeMemoryEntry(id);
|
|
26959
27917
|
return c.json({ ok: true });
|
|
26960
27918
|
}).patch(
|
|
26961
27919
|
"/:id/tags",
|
|
26962
|
-
zv("param",
|
|
26963
|
-
zv("json",
|
|
27920
|
+
zv("param", z9.object({ id: z9.string() })),
|
|
27921
|
+
zv("json", z9.object({ tags: z9.array(z9.string()) })),
|
|
26964
27922
|
async (c) => {
|
|
26965
27923
|
const { id } = c.req.valid("param");
|
|
26966
27924
|
const { tags } = c.req.valid("json");
|
|
@@ -26970,8 +27928,8 @@ var memoryController = new Hono2().get(
|
|
|
26970
27928
|
}
|
|
26971
27929
|
).patch(
|
|
26972
27930
|
"/:id/bindings",
|
|
26973
|
-
zv("param",
|
|
26974
|
-
zv("json",
|
|
27931
|
+
zv("param", z9.object({ id: z9.string() })),
|
|
27932
|
+
zv("json", z9.object({ workspaceIds: z9.array(z9.string()) })),
|
|
26975
27933
|
async (c) => {
|
|
26976
27934
|
const { id } = c.req.valid("param");
|
|
26977
27935
|
const { workspaceIds } = c.req.valid("json");
|
|
@@ -26979,7 +27937,7 @@ var memoryController = new Hono2().get(
|
|
|
26979
27937
|
if (!updated) throw NotFoundError("Memory");
|
|
26980
27938
|
return c.json(updated);
|
|
26981
27939
|
}
|
|
26982
|
-
).put("/workspace-tags", zv("json",
|
|
27940
|
+
).put("/workspace-tags", zv("json", z9.object({ workspaceId: z9.string(), tags: z9.array(z9.string()) })), async (c) => {
|
|
26983
27941
|
const { workspaceId, tags } = c.req.valid("json");
|
|
26984
27942
|
await setWorkspaceTagsEntry(workspaceId, tags);
|
|
26985
27943
|
return c.json({ ok: true });
|
|
@@ -26987,7 +27945,7 @@ var memoryController = new Hono2().get(
|
|
|
26987
27945
|
|
|
26988
27946
|
// src/api/routes/project-config.ts
|
|
26989
27947
|
init_api_contract();
|
|
26990
|
-
import { z as
|
|
27948
|
+
import { z as z10 } from "zod";
|
|
26991
27949
|
|
|
26992
27950
|
// src/api/services/project-config-service.ts
|
|
26993
27951
|
init_workspace_state();
|
|
@@ -27007,21 +27965,21 @@ var setSystemPrompt = async (workspaceId, prompt) => {
|
|
|
27007
27965
|
};
|
|
27008
27966
|
|
|
27009
27967
|
// src/api/routes/project-config.ts
|
|
27010
|
-
var projectConfigController = new Hono2().get("/", zv("query",
|
|
27968
|
+
var projectConfigController = new Hono2().get("/", zv("query", z10.object({ workspaceId: z10.string() })), async (c) => {
|
|
27011
27969
|
return c.json(await getProjectConfig(c.req.valid("query").workspaceId));
|
|
27012
|
-
}).put("/", zv("json",
|
|
27970
|
+
}).put("/", zv("json", z10.object({ workspaceId: z10.string(), config: runtimeProjectConfigSchema })), async (c) => {
|
|
27013
27971
|
const ctx = c.var.ctx;
|
|
27014
27972
|
const { workspaceId, config } = c.req.valid("json");
|
|
27015
27973
|
await saveProjectConfig2(workspaceId, config);
|
|
27016
27974
|
ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
|
|
27017
27975
|
return c.json({ ok: true });
|
|
27018
|
-
}).post("/git-instructions", zv("json",
|
|
27976
|
+
}).post("/git-instructions", zv("json", z10.object({ workspaceId: z10.string(), instructions: z10.string() })), async (c) => {
|
|
27019
27977
|
const ctx = c.var.ctx;
|
|
27020
27978
|
const { workspaceId, instructions } = c.req.valid("json");
|
|
27021
27979
|
const { cleared } = await setGitInstructions(workspaceId, instructions);
|
|
27022
27980
|
ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
|
|
27023
27981
|
return c.json({ ok: true, cleared });
|
|
27024
|
-
}).post("/system-prompt", zv("json",
|
|
27982
|
+
}).post("/system-prompt", zv("json", z10.object({ workspaceId: z10.string(), prompt: z10.string() })), async (c) => {
|
|
27025
27983
|
const ctx = c.var.ctx;
|
|
27026
27984
|
const { workspaceId, prompt } = c.req.valid("json");
|
|
27027
27985
|
const { cleared } = await setSystemPrompt(workspaceId, prompt);
|
|
@@ -27031,12 +27989,12 @@ var projectConfigController = new Hono2().get("/", zv("query", z8.object({ works
|
|
|
27031
27989
|
|
|
27032
27990
|
// src/api/routes/projects.ts
|
|
27033
27991
|
init_api_contract();
|
|
27034
|
-
import { z as
|
|
27992
|
+
import { z as z11 } from "zod";
|
|
27035
27993
|
|
|
27036
27994
|
// src/api/services/projects-service.ts
|
|
27037
27995
|
init_runtime_config();
|
|
27038
27996
|
init_api_contract();
|
|
27039
|
-
import { spawnSync as
|
|
27997
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
27040
27998
|
init_logger();
|
|
27041
27999
|
|
|
27042
28000
|
// src/state/projects-layout.ts
|
|
@@ -27119,7 +28077,7 @@ var checkProjectPath = async (repoPath) => {
|
|
|
27119
28077
|
branches: []
|
|
27120
28078
|
};
|
|
27121
28079
|
}
|
|
27122
|
-
const r =
|
|
28080
|
+
const r = spawnSync10("git", ["rev-parse", "--git-dir"], {
|
|
27123
28081
|
cwd: repoPath,
|
|
27124
28082
|
encoding: "utf-8",
|
|
27125
28083
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -27135,12 +28093,12 @@ var checkProjectPath = async (repoPath) => {
|
|
|
27135
28093
|
remote: null,
|
|
27136
28094
|
branches: []
|
|
27137
28095
|
};
|
|
27138
|
-
const branchR =
|
|
28096
|
+
const branchR = spawnSync10("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
27139
28097
|
cwd: repoPath,
|
|
27140
28098
|
encoding: "utf-8",
|
|
27141
28099
|
stdio: ["ignore", "pipe", "pipe"]
|
|
27142
28100
|
});
|
|
27143
|
-
const remoteR =
|
|
28101
|
+
const remoteR = spawnSync10("git", ["remote", "get-url", "origin"], {
|
|
27144
28102
|
cwd: repoPath,
|
|
27145
28103
|
encoding: "utf-8",
|
|
27146
28104
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -27159,7 +28117,7 @@ var addProject = async (repoPath, initialConfig) => {
|
|
|
27159
28117
|
} catch {
|
|
27160
28118
|
throw BadRequestError(`Path does not exist: ${repoPath}`);
|
|
27161
28119
|
}
|
|
27162
|
-
const r =
|
|
28120
|
+
const r = spawnSync10("git", ["rev-parse", "--git-dir"], {
|
|
27163
28121
|
cwd: repoPath,
|
|
27164
28122
|
encoding: "utf-8",
|
|
27165
28123
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -27191,7 +28149,7 @@ var removeProject = async (workspaceId) => {
|
|
|
27191
28149
|
const cards = boardCards;
|
|
27192
28150
|
enqueueCleanup2(async () => {
|
|
27193
28151
|
const { rm: rm3 } = await import("node:fs/promises");
|
|
27194
|
-
const { join:
|
|
28152
|
+
const { join: join24 } = await import("node:path");
|
|
27195
28153
|
for (const [cardId, card] of Object.entries(cards)) {
|
|
27196
28154
|
if (resolveWorktreeOwnerId(cardId, cards) === cardId) {
|
|
27197
28155
|
await removeWorktreeAsync(cardId, repoPath, card.branchName).catch((err) => {
|
|
@@ -27199,11 +28157,11 @@ var removeProject = async (workspaceId) => {
|
|
|
27199
28157
|
});
|
|
27200
28158
|
}
|
|
27201
28159
|
}
|
|
27202
|
-
await rm3(
|
|
28160
|
+
await rm3(join24(WORKSPACES_DIR, workspaceId), { recursive: true, force: true }).catch((err) => {
|
|
27203
28161
|
logger.warn(`[cleanup:project:${workspaceId}] workspace dir failed: ${String(err)}`);
|
|
27204
28162
|
});
|
|
27205
28163
|
for (const cardId of Object.keys(cards)) {
|
|
27206
|
-
await rm3(
|
|
28164
|
+
await rm3(join24(ATTACHMENTS_DIR, cardId), { recursive: true, force: true }).catch(() => {
|
|
27207
28165
|
});
|
|
27208
28166
|
}
|
|
27209
28167
|
logger.info(`[cleanup:project:${workspaceId}] done`);
|
|
@@ -27217,14 +28175,14 @@ var projectsController = new Hono2().get("/", async (c) => {
|
|
|
27217
28175
|
return c.json(await listProjects());
|
|
27218
28176
|
}).get("/layout", (c) => {
|
|
27219
28177
|
return c.json(getProjectsLayout());
|
|
27220
|
-
}).get("/check-path", zv("query",
|
|
28178
|
+
}).get("/check-path", zv("query", z11.object({ repoPath: z11.string() })), async (c) => {
|
|
27221
28179
|
return c.json(await checkProjectPath(c.req.valid("query").repoPath));
|
|
27222
28180
|
}).post(
|
|
27223
28181
|
"/",
|
|
27224
28182
|
zv(
|
|
27225
28183
|
"json",
|
|
27226
|
-
|
|
27227
|
-
repoPath:
|
|
28184
|
+
z11.object({
|
|
28185
|
+
repoPath: z11.string().min(1),
|
|
27228
28186
|
initialConfig: runtimeProjectConfigSchema.partial().optional()
|
|
27229
28187
|
})
|
|
27230
28188
|
),
|
|
@@ -27251,7 +28209,7 @@ var projectsController = new Hono2().get("/", async (c) => {
|
|
|
27251
28209
|
|
|
27252
28210
|
// src/api/routes/recurring-agents.ts
|
|
27253
28211
|
init_api_contract();
|
|
27254
|
-
import { z as
|
|
28212
|
+
import { z as z12 } from "zod";
|
|
27255
28213
|
|
|
27256
28214
|
// src/api/services/recurring-agents-service.ts
|
|
27257
28215
|
function listRecurringAgentsEntry(workspaceId) {
|
|
@@ -27276,20 +28234,20 @@ function setRecurringAgentJournalEntry(id, journal) {
|
|
|
27276
28234
|
}
|
|
27277
28235
|
|
|
27278
28236
|
// src/api/routes/recurring-agents.ts
|
|
27279
|
-
var recurringAgentsController = new Hono2().get("/", zv("query",
|
|
28237
|
+
var recurringAgentsController = new Hono2().get("/", zv("query", z12.object({ workspaceId: z12.string() })), async (c) => {
|
|
27280
28238
|
const { workspaceId } = c.req.valid("query");
|
|
27281
28239
|
return c.json(listRecurringAgentsEntry(workspaceId));
|
|
27282
|
-
}).get("/:id", zv("param",
|
|
28240
|
+
}).get("/:id", zv("param", z12.object({ id: z12.string() })), async (c) => {
|
|
27283
28241
|
const { id } = c.req.valid("param");
|
|
27284
28242
|
const agent = getRecurringAgentEntry(id);
|
|
27285
28243
|
if (!agent) throw NotFoundError("Recurring agent");
|
|
27286
28244
|
return c.json(agent);
|
|
27287
|
-
}).post("/", zv("json", recurringAgentCreateRequestSchema.extend({ workspaceId:
|
|
28245
|
+
}).post("/", zv("json", recurringAgentCreateRequestSchema.extend({ workspaceId: z12.string() })), async (c) => {
|
|
27288
28246
|
const { workspaceId, ...req } = c.req.valid("json");
|
|
27289
28247
|
return c.json(createRecurringAgentEntry(workspaceId, req));
|
|
27290
28248
|
}).patch(
|
|
27291
28249
|
"/:id",
|
|
27292
|
-
zv("param",
|
|
28250
|
+
zv("param", z12.object({ id: z12.string() })),
|
|
27293
28251
|
zv("json", recurringAgentUpdateRequestSchema.omit({ id: true })),
|
|
27294
28252
|
async (c) => {
|
|
27295
28253
|
const { id } = c.req.valid("param");
|
|
@@ -27299,8 +28257,8 @@ var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ wo
|
|
|
27299
28257
|
}
|
|
27300
28258
|
).post(
|
|
27301
28259
|
"/:id/journal",
|
|
27302
|
-
zv("param",
|
|
27303
|
-
zv("json",
|
|
28260
|
+
zv("param", z12.object({ id: z12.string() })),
|
|
28261
|
+
zv("json", z12.object({ journal: z12.string() })),
|
|
27304
28262
|
async (c) => {
|
|
27305
28263
|
const { id } = c.req.valid("param");
|
|
27306
28264
|
const { journal } = c.req.valid("json");
|
|
@@ -27310,8 +28268,8 @@ var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ wo
|
|
|
27310
28268
|
}
|
|
27311
28269
|
).post(
|
|
27312
28270
|
"/:id/run",
|
|
27313
|
-
zv("param",
|
|
27314
|
-
zv("query",
|
|
28271
|
+
zv("param", z12.object({ id: z12.string() })),
|
|
28272
|
+
zv("query", z12.object({ workspaceId: z12.string() })),
|
|
27315
28273
|
async (c) => {
|
|
27316
28274
|
const { id } = c.req.valid("param");
|
|
27317
28275
|
const { workspaceId } = c.req.valid("query");
|
|
@@ -27320,14 +28278,14 @@ var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ wo
|
|
|
27320
28278
|
const started = await scheduler?.runNow(id) ?? false;
|
|
27321
28279
|
return c.json({ started });
|
|
27322
28280
|
}
|
|
27323
|
-
).delete("/:id", zv("param",
|
|
28281
|
+
).delete("/:id", zv("param", z12.object({ id: z12.string() })), async (c) => {
|
|
27324
28282
|
const { id } = c.req.valid("param");
|
|
27325
28283
|
deleteRecurringAgentEntry(id);
|
|
27326
28284
|
return c.json({ ok: true });
|
|
27327
28285
|
});
|
|
27328
28286
|
|
|
27329
28287
|
// src/api/routes/run.ts
|
|
27330
|
-
import { z as
|
|
28288
|
+
import { z as z13 } from "zod";
|
|
27331
28289
|
|
|
27332
28290
|
// src/api/services/run-service.ts
|
|
27333
28291
|
init_workspace_state();
|
|
@@ -27343,15 +28301,19 @@ var resolveCardCwd = async (workspaceId, cardId, repoPath) => {
|
|
|
27343
28301
|
if (!card) return null;
|
|
27344
28302
|
return card.worktreePath ?? repoPath;
|
|
27345
28303
|
};
|
|
28304
|
+
var resolveCompanionSessionCwd = (sessionId) => {
|
|
28305
|
+
const session = getCompanionSession(sessionId);
|
|
28306
|
+
return session?.worktreePath ?? null;
|
|
28307
|
+
};
|
|
27346
28308
|
|
|
27347
28309
|
// src/api/routes/run.ts
|
|
27348
|
-
var runController = new Hono2().get("/status", zv("query",
|
|
28310
|
+
var runController = new Hono2().get("/status", zv("query", z13.object({ workspaceId: z13.string() })), (c) => {
|
|
27349
28311
|
const ctx = c.var.ctx;
|
|
27350
28312
|
const { workspaceId } = c.req.valid("query");
|
|
27351
28313
|
const session = ctx.getRunSession(workspaceId);
|
|
27352
28314
|
if (!session) return c.json({ cardId: null, status: "stopped", errorMessage: void 0 });
|
|
27353
28315
|
return c.json({ cardId: session.cardId, status: session.status, errorMessage: session.errorMessage });
|
|
27354
|
-
}).post("/start", zv("json",
|
|
28316
|
+
}).post("/start", zv("json", z13.object({ workspaceId: z13.string(), cardId: z13.string() })), async (c) => {
|
|
27355
28317
|
const ctx = c.var.ctx;
|
|
27356
28318
|
const { workspaceId, cardId } = c.req.valid("json");
|
|
27357
28319
|
const ws = await ctx.ensureWorkspace(workspaceId);
|
|
@@ -27363,7 +28325,18 @@ var runController = new Hono2().get("/status", zv("query", z11.object({ workspac
|
|
|
27363
28325
|
if (cwd === null) throw NotFoundError("Card");
|
|
27364
28326
|
ctx.startRun(workspaceId, cardId, command, cwd);
|
|
27365
28327
|
return c.json({ ok: true });
|
|
27366
|
-
}).post("/start-
|
|
28328
|
+
}).post("/start-companion", zv("json", z13.object({ workspaceId: z13.string(), sessionId: z13.string() })), async (c) => {
|
|
28329
|
+
const ctx = c.var.ctx;
|
|
28330
|
+
const { workspaceId, sessionId } = c.req.valid("json");
|
|
28331
|
+
const command = await resolveStartCommand(workspaceId);
|
|
28332
|
+
if (!command) {
|
|
28333
|
+
throw PreconditionFailedError("No start command configured. Add one in Settings \u2192 Environment.");
|
|
28334
|
+
}
|
|
28335
|
+
const cwd = resolveCompanionSessionCwd(sessionId);
|
|
28336
|
+
if (cwd === null) throw NotFoundError("Companion session");
|
|
28337
|
+
ctx.startRun(workspaceId, sessionId, command, cwd);
|
|
28338
|
+
return c.json({ ok: true });
|
|
28339
|
+
}).post("/start-base", zv("json", z13.object({ workspaceId: z13.string() })), async (c) => {
|
|
27367
28340
|
const ctx = c.var.ctx;
|
|
27368
28341
|
const { workspaceId } = c.req.valid("json");
|
|
27369
28342
|
const ws = await ctx.ensureWorkspace(workspaceId);
|
|
@@ -27373,7 +28346,7 @@ var runController = new Hono2().get("/status", zv("query", z11.object({ workspac
|
|
|
27373
28346
|
}
|
|
27374
28347
|
ctx.startRun(workspaceId, null, command, ws.repoPath);
|
|
27375
28348
|
return c.json({ ok: true });
|
|
27376
|
-
}).post("/stop", zv("json",
|
|
28349
|
+
}).post("/stop", zv("json", z13.object({ workspaceId: z13.string() })), (c) => {
|
|
27377
28350
|
const ctx = c.var.ctx;
|
|
27378
28351
|
const { workspaceId } = c.req.valid("json");
|
|
27379
28352
|
ctx.stopRun(workspaceId);
|
|
@@ -27381,7 +28354,7 @@ var runController = new Hono2().get("/status", zv("query", z11.object({ workspac
|
|
|
27381
28354
|
});
|
|
27382
28355
|
|
|
27383
28356
|
// src/api/routes/slack.ts
|
|
27384
|
-
import { z as
|
|
28357
|
+
import { z as z14 } from "zod";
|
|
27385
28358
|
|
|
27386
28359
|
// src/api/services/slack-service.ts
|
|
27387
28360
|
init_runtime_config();
|
|
@@ -27428,20 +28401,20 @@ var createApp = async (appConfigToken, publicUrl, botName) => {
|
|
|
27428
28401
|
var slackController = new Hono2().post("/resetApp", async (c) => {
|
|
27429
28402
|
await resetApp();
|
|
27430
28403
|
return c.json({ ok: true });
|
|
27431
|
-
}).post("/updateSigningSecret", zv("json",
|
|
28404
|
+
}).post("/updateSigningSecret", zv("json", z14.object({ signingSecret: z14.string().min(1) })), async (c) => {
|
|
27432
28405
|
await updateSigningSecret(c.req.valid("json").signingSecret);
|
|
27433
28406
|
return c.json({ ok: true });
|
|
27434
28407
|
}).post(
|
|
27435
28408
|
"/importCredentials",
|
|
27436
28409
|
zv(
|
|
27437
28410
|
"json",
|
|
27438
|
-
|
|
27439
|
-
slackAppId:
|
|
27440
|
-
slackClientId:
|
|
27441
|
-
slackClientSecret:
|
|
27442
|
-
slackSigningSecret:
|
|
27443
|
-
slackOauthAuthorizeUrl:
|
|
27444
|
-
slackPublicUrl:
|
|
28411
|
+
z14.object({
|
|
28412
|
+
slackAppId: z14.string(),
|
|
28413
|
+
slackClientId: z14.string(),
|
|
28414
|
+
slackClientSecret: z14.string(),
|
|
28415
|
+
slackSigningSecret: z14.string(),
|
|
28416
|
+
slackOauthAuthorizeUrl: z14.string(),
|
|
28417
|
+
slackPublicUrl: z14.string()
|
|
27445
28418
|
})
|
|
27446
28419
|
),
|
|
27447
28420
|
async (c) => {
|
|
@@ -27450,7 +28423,7 @@ var slackController = new Hono2().post("/resetApp", async (c) => {
|
|
|
27450
28423
|
}
|
|
27451
28424
|
).post(
|
|
27452
28425
|
"/createApp",
|
|
27453
|
-
zv("json",
|
|
28426
|
+
zv("json", z14.object({ appConfigToken: z14.string(), publicUrl: z14.string(), botName: z14.string().default("Whipped") })),
|
|
27454
28427
|
async (c) => {
|
|
27455
28428
|
const { appConfigToken, publicUrl, botName } = c.req.valid("json");
|
|
27456
28429
|
return c.json(await createApp(appConfigToken, publicUrl, botName));
|
|
@@ -27458,27 +28431,27 @@ var slackController = new Hono2().post("/resetApp", async (c) => {
|
|
|
27458
28431
|
);
|
|
27459
28432
|
|
|
27460
28433
|
// src/api/routes/terminal.ts
|
|
27461
|
-
import { z as
|
|
28434
|
+
import { z as z15 } from "zod";
|
|
27462
28435
|
|
|
27463
28436
|
// src/api/services/terminal-service.ts
|
|
27464
28437
|
var toBufferResponse = (buffer) => ({ data: buffer ?? "" });
|
|
27465
28438
|
|
|
27466
28439
|
// src/api/routes/terminal.ts
|
|
27467
|
-
var terminalController = new Hono2().get("/buffer", zv("query",
|
|
28440
|
+
var terminalController = new Hono2().get("/buffer", zv("query", z15.object({ workspaceId: z15.string(), taskId: z15.string() })), (c) => {
|
|
27468
28441
|
const ctx = c.var.ctx;
|
|
27469
28442
|
const { workspaceId, taskId } = c.req.valid("query");
|
|
27470
28443
|
const buf = ctx.getScheduler(workspaceId)?.getOutputBuffer(taskId);
|
|
27471
28444
|
return c.json(toBufferResponse(buf));
|
|
27472
28445
|
}).post(
|
|
27473
28446
|
"/resize",
|
|
27474
|
-
zv("json",
|
|
28447
|
+
zv("json", z15.object({ workspaceId: z15.string(), taskId: z15.string(), cols: z15.number(), rows: z15.number() })),
|
|
27475
28448
|
(c) => {
|
|
27476
28449
|
const ctx = c.var.ctx;
|
|
27477
28450
|
const { workspaceId, taskId, cols, rows } = c.req.valid("json");
|
|
27478
28451
|
ctx.getScheduler(workspaceId)?.resizeTerminal(taskId, cols, rows);
|
|
27479
28452
|
return c.json({ ok: true });
|
|
27480
28453
|
}
|
|
27481
|
-
).post("/input", zv("json",
|
|
28454
|
+
).post("/input", zv("json", z15.object({ workspaceId: z15.string(), taskId: z15.string(), data: z15.string() })), (c) => {
|
|
27482
28455
|
const ctx = c.var.ctx;
|
|
27483
28456
|
const { workspaceId, taskId, data } = c.req.valid("json");
|
|
27484
28457
|
ctx.getScheduler(workspaceId)?.writeToTerminal(taskId, data);
|
|
@@ -27486,7 +28459,7 @@ var terminalController = new Hono2().get("/buffer", zv("query", z13.object({ wor
|
|
|
27486
28459
|
});
|
|
27487
28460
|
|
|
27488
28461
|
// src/api/routes/tunnel.ts
|
|
27489
|
-
import { z as
|
|
28462
|
+
import { z as z16 } from "zod";
|
|
27490
28463
|
|
|
27491
28464
|
// src/api/services/tunnel-service.ts
|
|
27492
28465
|
init_runtime_config();
|
|
@@ -27495,12 +28468,12 @@ init_runtime_config();
|
|
|
27495
28468
|
import { execFile as execFile9, spawn as spawn7 } from "node:child_process";
|
|
27496
28469
|
import { mkdir as mkdir4, writeFile as writeFile3, readFile as readFile3, access, unlink as unlink4 } from "node:fs/promises";
|
|
27497
28470
|
import { homedir as homedir5 } from "node:os";
|
|
27498
|
-
import { join as
|
|
28471
|
+
import { join as join21 } from "node:path";
|
|
27499
28472
|
import { promisify as promisify9 } from "node:util";
|
|
27500
28473
|
init_runtime_config();
|
|
27501
28474
|
init_logger();
|
|
27502
28475
|
var execFileAsync7 = promisify9(execFile9);
|
|
27503
|
-
var CLOUDFLARED_DIR =
|
|
28476
|
+
var CLOUDFLARED_DIR = join21(homedir5(), ".cloudflared");
|
|
27504
28477
|
async function checkCloudflaredInstalled() {
|
|
27505
28478
|
try {
|
|
27506
28479
|
const { stdout } = await execFileAsync7("cloudflared", ["--version"]);
|
|
@@ -27512,7 +28485,7 @@ async function checkCloudflaredInstalled() {
|
|
|
27512
28485
|
}
|
|
27513
28486
|
async function checkCloudflaredAuth() {
|
|
27514
28487
|
try {
|
|
27515
|
-
await access(
|
|
28488
|
+
await access(join21(CLOUDFLARED_DIR, "cert.pem"));
|
|
27516
28489
|
return true;
|
|
27517
28490
|
} catch {
|
|
27518
28491
|
return false;
|
|
@@ -27523,7 +28496,7 @@ async function openCloudflaredLogin(force = false) {
|
|
|
27523
28496
|
if (alreadyLoggedIn && !force) return { alreadyLoggedIn: true };
|
|
27524
28497
|
if (force) {
|
|
27525
28498
|
try {
|
|
27526
|
-
await unlink4(
|
|
28499
|
+
await unlink4(join21(CLOUDFLARED_DIR, "cert.pem"));
|
|
27527
28500
|
} catch {
|
|
27528
28501
|
}
|
|
27529
28502
|
}
|
|
@@ -27592,7 +28565,7 @@ async function createTunnel(tunnelName) {
|
|
|
27592
28565
|
}
|
|
27593
28566
|
}
|
|
27594
28567
|
async function writeTunnelConfig(tunnelId, tunnelName, domain) {
|
|
27595
|
-
const credentialsFile =
|
|
28568
|
+
const credentialsFile = join21(CLOUDFLARED_DIR, `${tunnelId}.json`);
|
|
27596
28569
|
const config = [
|
|
27597
28570
|
`tunnel: ${tunnelId}`,
|
|
27598
28571
|
`credentials-file: ${credentialsFile}`,
|
|
@@ -27603,12 +28576,12 @@ async function writeTunnelConfig(tunnelId, tunnelName, domain) {
|
|
|
27603
28576
|
` - service: http_status:404`
|
|
27604
28577
|
].join("\n");
|
|
27605
28578
|
await mkdir4(CLOUDFLARED_DIR, { recursive: true });
|
|
27606
|
-
await writeFile3(
|
|
28579
|
+
await writeFile3(join21(CLOUDFLARED_DIR, "config.yml"), config, "utf-8");
|
|
27607
28580
|
logger.info(`[tunnel-setup] Wrote ~/.cloudflared/config.yml for tunnel ${tunnelName}`);
|
|
27608
28581
|
}
|
|
27609
28582
|
async function readTunnelConfig() {
|
|
27610
28583
|
try {
|
|
27611
|
-
const raw2 = await readFile3(
|
|
28584
|
+
const raw2 = await readFile3(join21(CLOUDFLARED_DIR, "config.yml"), "utf-8");
|
|
27612
28585
|
const tunnelMatch = raw2.match(/^tunnel:\s*(.+)$/m);
|
|
27613
28586
|
const hostnameMatch = raw2.match(/hostname:\s*(.+)$/m);
|
|
27614
28587
|
return {
|
|
@@ -27657,9 +28630,9 @@ var resetTunnel = async () => {
|
|
|
27657
28630
|
await updateGlobalConfig({ tunnelId: void 0, tunnelDomain: void 0, autoStartTunnel: false });
|
|
27658
28631
|
const { unlink: unlink5 } = await import("node:fs/promises");
|
|
27659
28632
|
const { homedir: homedir6 } = await import("node:os");
|
|
27660
|
-
const { join:
|
|
28633
|
+
const { join: join24 } = await import("node:path");
|
|
27661
28634
|
try {
|
|
27662
|
-
await unlink5(
|
|
28635
|
+
await unlink5(join24(homedir6(), ".cloudflared", "config.yml"));
|
|
27663
28636
|
} catch {
|
|
27664
28637
|
}
|
|
27665
28638
|
};
|
|
@@ -27671,9 +28644,9 @@ var tunnelController = new Hono2().get("/checkCloudflared", async (c) => {
|
|
|
27671
28644
|
return c.json(await getTunnelConfig());
|
|
27672
28645
|
}).get("/tunnelStatus", async (c) => {
|
|
27673
28646
|
return c.json(await getTunnelStatus());
|
|
27674
|
-
}).post("/cloudflaredLogin", zv("json",
|
|
28647
|
+
}).post("/cloudflaredLogin", zv("json", z16.object({ force: z16.boolean().default(false) })), async (c) => {
|
|
27675
28648
|
return c.json(await cloudflaredLogin(c.req.valid("json").force));
|
|
27676
|
-
}).post("/createTunnel", zv("json",
|
|
28649
|
+
}).post("/createTunnel", zv("json", z16.object({ domain: z16.string() })), async (c) => {
|
|
27677
28650
|
return c.json(await createTunnel2(c.req.valid("json").domain));
|
|
27678
28651
|
}).post("/startTunnel", async (c) => {
|
|
27679
28652
|
return c.json(await startTunnel());
|
|
@@ -27686,11 +28659,11 @@ var tunnelController = new Hono2().get("/checkCloudflared", async (c) => {
|
|
|
27686
28659
|
|
|
27687
28660
|
// src/api/routes/workflows.ts
|
|
27688
28661
|
init_api_contract();
|
|
27689
|
-
import { z as
|
|
28662
|
+
import { z as z17 } from "zod";
|
|
27690
28663
|
|
|
27691
28664
|
// src/api/services/workflows-service.ts
|
|
27692
28665
|
init_workspace_state();
|
|
27693
|
-
import { existsSync as
|
|
28666
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
27694
28667
|
import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
|
|
27695
28668
|
import { dirname as dirname8, isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
|
|
27696
28669
|
var resolvePromptPath = async (workspaceId, requestedPath) => {
|
|
@@ -27735,22 +28708,22 @@ var writePromptFile = async (workspaceId, path2, content) => {
|
|
|
27735
28708
|
};
|
|
27736
28709
|
var readPromptFile = async (workspaceId, path2) => {
|
|
27737
28710
|
const targetPath = await resolvePromptPath(workspaceId, path2);
|
|
27738
|
-
if (!
|
|
28711
|
+
if (!existsSync14(targetPath)) return { content: "", exists: false };
|
|
27739
28712
|
const content = await readFile4(targetPath, "utf-8");
|
|
27740
28713
|
return { content, exists: true };
|
|
27741
28714
|
};
|
|
27742
28715
|
|
|
27743
28716
|
// src/api/routes/workflows.ts
|
|
27744
|
-
var workflowsController = new Hono2().get("/", zv("query",
|
|
28717
|
+
var workflowsController = new Hono2().get("/", zv("query", z17.object({ workspaceId: z17.string() })), async (c) => {
|
|
27745
28718
|
const { workspaceId } = c.req.valid("query");
|
|
27746
28719
|
return c.json(await listWorkflows(workspaceId));
|
|
27747
|
-
}).post("/", zv("json",
|
|
28720
|
+
}).post("/", zv("json", z17.object({ workspaceId: z17.string(), workflow: workflowSchema })), async (c) => {
|
|
27748
28721
|
const ctx = c.var.ctx;
|
|
27749
28722
|
const { workspaceId, workflow } = c.req.valid("json");
|
|
27750
28723
|
const result = await upsertWorkflow(workspaceId, workflow);
|
|
27751
28724
|
ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
|
|
27752
28725
|
return c.json(result);
|
|
27753
|
-
}).delete("/:workflowId", zv("query",
|
|
28726
|
+
}).delete("/:workflowId", zv("query", z17.object({ workspaceId: z17.string() })), async (c) => {
|
|
27754
28727
|
const ctx = c.var.ctx;
|
|
27755
28728
|
const { workspaceId } = c.req.valid("query");
|
|
27756
28729
|
const workflowId = c.req.param("workflowId");
|
|
@@ -27759,23 +28732,23 @@ var workflowsController = new Hono2().get("/", zv("query", z15.object({ workspac
|
|
|
27759
28732
|
return c.json(result);
|
|
27760
28733
|
}).post(
|
|
27761
28734
|
"/prompt-file",
|
|
27762
|
-
zv("json",
|
|
28735
|
+
zv("json", z17.object({ workspaceId: z17.string(), path: z17.string().min(1), content: z17.string() })),
|
|
27763
28736
|
async (c) => {
|
|
27764
28737
|
const { workspaceId, path: path2, content } = c.req.valid("json");
|
|
27765
28738
|
return c.json(await writePromptFile(workspaceId, path2, content));
|
|
27766
28739
|
}
|
|
27767
|
-
).get("/prompt-file", zv("query",
|
|
28740
|
+
).get("/prompt-file", zv("query", z17.object({ workspaceId: z17.string(), path: z17.string().min(1) })), async (c) => {
|
|
27768
28741
|
const { workspaceId, path: path2 } = c.req.valid("query");
|
|
27769
28742
|
return c.json(await readPromptFile(workspaceId, path2));
|
|
27770
28743
|
});
|
|
27771
28744
|
|
|
27772
28745
|
// src/api/routes/workspace.ts
|
|
27773
28746
|
init_api_contract();
|
|
27774
|
-
import { z as
|
|
28747
|
+
import { z as z18 } from "zod";
|
|
27775
28748
|
|
|
27776
28749
|
// src/api/services/workspace-service.ts
|
|
27777
28750
|
init_workspace_state();
|
|
27778
|
-
import { spawnSync as
|
|
28751
|
+
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
27779
28752
|
var loadStateForWorkspace = async (workspaceId) => {
|
|
27780
28753
|
const workspaces = await listWorkspaces();
|
|
27781
28754
|
const ws = workspaces.find((w2) => w2.workspaceId === workspaceId);
|
|
@@ -27785,12 +28758,12 @@ var loadStateForWorkspace = async (workspaceId) => {
|
|
|
27785
28758
|
var loadStateForContext = async (workspaceId, repoPath) => loadWorkspaceState(workspaceId, repoPath);
|
|
27786
28759
|
var saveState = async (workspaceId, request2) => saveWorkspaceState(workspaceId, request2);
|
|
27787
28760
|
var listRootFiles = (repoPath) => {
|
|
27788
|
-
const ignored =
|
|
28761
|
+
const ignored = spawnSync11(
|
|
27789
28762
|
"git",
|
|
27790
28763
|
["ls-files", "--others", "--ignored", "--exclude-standard", "--directory", "--no-empty-directory"],
|
|
27791
28764
|
{ cwd: repoPath, encoding: "utf-8" }
|
|
27792
28765
|
);
|
|
27793
|
-
const untracked =
|
|
28766
|
+
const untracked = spawnSync11("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
27794
28767
|
cwd: repoPath,
|
|
27795
28768
|
encoding: "utf-8"
|
|
27796
28769
|
});
|
|
@@ -27799,7 +28772,7 @@ var listRootFiles = (repoPath) => {
|
|
|
27799
28772
|
};
|
|
27800
28773
|
|
|
27801
28774
|
// src/api/routes/workspace.ts
|
|
27802
|
-
var workspaceController = new Hono2().get("/state", zv("query",
|
|
28775
|
+
var workspaceController = new Hono2().get("/state", zv("query", z18.object({ workspaceId: z18.string().optional() })), async (c) => {
|
|
27803
28776
|
const ctx = c.var.ctx;
|
|
27804
28777
|
const { workspaceId } = c.req.valid("query");
|
|
27805
28778
|
if (workspaceId) {
|
|
@@ -27807,10 +28780,10 @@ var workspaceController = new Hono2().get("/state", zv("query", z16.object({ wor
|
|
|
27807
28780
|
}
|
|
27808
28781
|
if (!ctx.currentWorkspaceId || !ctx.currentRepoPath) throw BadRequestError("No workspace context");
|
|
27809
28782
|
return c.json(await loadStateForContext(ctx.currentWorkspaceId, ctx.currentRepoPath));
|
|
27810
|
-
}).post("/save", zv("json", runtimeWorkspaceStateSaveRequestSchema.extend({ workspaceId:
|
|
28783
|
+
}).post("/save", zv("json", runtimeWorkspaceStateSaveRequestSchema.extend({ workspaceId: z18.string() })), async (c) => {
|
|
27811
28784
|
const { workspaceId, board, revision } = c.req.valid("json");
|
|
27812
28785
|
return c.json(await saveState(workspaceId, { board, revision }));
|
|
27813
|
-
}).get("/root-files", zv("query",
|
|
28786
|
+
}).get("/root-files", zv("query", z18.object({ workspaceId: z18.string() })), async (c) => {
|
|
27814
28787
|
const ctx = c.var.ctx;
|
|
27815
28788
|
const { workspaceId } = c.req.valid("query");
|
|
27816
28789
|
const ws = await ctx.ensureWorkspace(workspaceId);
|
|
@@ -27822,7 +28795,7 @@ function createApiApp(ctx) {
|
|
|
27822
28795
|
const app = new Hono2().basePath("/api").use("*", async (c, next) => {
|
|
27823
28796
|
c.set("ctx", ctx);
|
|
27824
28797
|
await next();
|
|
27825
|
-
}).get("/health", (c) => c.json({ ok: true })).route("/auth", authController).route("/agent", agentController).route("/agents", agentsController).route("/cards", cardsController).route("/config", configController).route("/fs", fsController).route("/memory", memoryController).route("/project-config", projectConfigController).route("/projects", projectsController).route("/recurring-agents", recurringAgentsController).route("/run", runController).route("/slack", slackController).route("/terminal", terminalController).route("/tunnel", tunnelController).route("/workflows", workflowsController).route("/workspace", workspaceController);
|
|
28798
|
+
}).get("/health", (c) => c.json({ ok: true })).route("/auth", authController).route("/agent", agentController).route("/agents", agentsController).route("/cards", cardsController).route("/companion-saved-canvases", companionSavedCanvasesController).route("/companion-sessions", companionSessionsController).route("/config", configController).route("/fs", fsController).route("/memory", memoryController).route("/project-config", projectConfigController).route("/projects", projectsController).route("/recurring-agents", recurringAgentsController).route("/run", runController).route("/slack", slackController).route("/terminal", terminalController).route("/tunnel", tunnelController).route("/workflows", workflowsController).route("/workspace", workspaceController);
|
|
27826
28799
|
app.onError(errorHandler2);
|
|
27827
28800
|
return app;
|
|
27828
28801
|
}
|
|
@@ -27932,6 +28905,9 @@ var RuntimeStateHub = class {
|
|
|
27932
28905
|
broadcastRunSessionChange(workspaceId, cardId, status, errorMessage) {
|
|
27933
28906
|
this.broadcastToWorkspace(workspaceId, { type: "run_session_changed", cardId, status, errorMessage });
|
|
27934
28907
|
}
|
|
28908
|
+
broadcastCompanionCanvasUpdate(workspaceId, sessionId, canvas) {
|
|
28909
|
+
this.broadcastToWorkspace(workspaceId, { type: "companion_canvas_updated", sessionId, canvas });
|
|
28910
|
+
}
|
|
27935
28911
|
broadcastToWorkspace(workspaceId, event) {
|
|
27936
28912
|
const clientIds = this.workspaceClients.get(workspaceId);
|
|
27937
28913
|
if (!clientIds) return;
|
|
@@ -27953,8 +28929,8 @@ var RuntimeStateHub = class {
|
|
|
27953
28929
|
// src/core/update-check.ts
|
|
27954
28930
|
init_paths();
|
|
27955
28931
|
import { readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
|
|
27956
|
-
import { join as
|
|
27957
|
-
var CACHE_FILE =
|
|
28932
|
+
import { join as join22 } from "node:path";
|
|
28933
|
+
var CACHE_FILE = join22(WHIPPED_HOME_DIR, "update-check.json");
|
|
27958
28934
|
var REGISTRY_URL = "https://registry.npmjs.org/whipped/latest";
|
|
27959
28935
|
var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1e3;
|
|
27960
28936
|
function readCache() {
|
|
@@ -28013,6 +28989,10 @@ async function cleanupStaleTasks(workspaceId, hub) {
|
|
|
28013
28989
|
if (staleRecurring > 0) {
|
|
28014
28990
|
logger.info(`[server] Cleared ${staleRecurring} stale recurring-agent run(s) for ${workspaceId}`);
|
|
28015
28991
|
}
|
|
28992
|
+
const staleCompanions = resetStaleCompanionSessions(workspaceId);
|
|
28993
|
+
if (staleCompanions > 0) {
|
|
28994
|
+
logger.info(`[server] Reset ${staleCompanions} stale companion session(s) for ${workspaceId}`);
|
|
28995
|
+
}
|
|
28016
28996
|
for (const card of Object.values(board.cards)) {
|
|
28017
28997
|
if (card.terminalSessions?.some((s2) => s2.endedAt === void 0)) {
|
|
28018
28998
|
await closeAllOpenTerminalSessions(workspaceId, card.id, now);
|
|
@@ -28241,9 +29221,9 @@ async function createRuntimeServer(options) {
|
|
|
28241
29221
|
};
|
|
28242
29222
|
}
|
|
28243
29223
|
const apiApp = createApiApp(createContext());
|
|
28244
|
-
const webUiDistPath =
|
|
28245
|
-
const webUiIndexPath =
|
|
28246
|
-
const hasWebUi =
|
|
29224
|
+
const webUiDistPath = join23(__dirname2, "web-ui");
|
|
29225
|
+
const webUiIndexPath = join23(webUiDistPath, "index.html");
|
|
29226
|
+
const hasWebUi = existsSync15(webUiIndexPath);
|
|
28247
29227
|
const httpServer = createServer(async (req, res) => {
|
|
28248
29228
|
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
28249
29229
|
if (url.pathname === "/api/slack/oauth-callback") {
|
|
@@ -28476,7 +29456,7 @@ async function createRuntimeServer(options) {
|
|
|
28476
29456
|
res.end("Bad request");
|
|
28477
29457
|
return;
|
|
28478
29458
|
}
|
|
28479
|
-
const filePath =
|
|
29459
|
+
const filePath = join23(ATTACHMENTS_DIR, cardId, filename);
|
|
28480
29460
|
const { readFile: readFile5 } = await import("node:fs/promises");
|
|
28481
29461
|
try {
|
|
28482
29462
|
const data = await readFile5(filePath);
|
|
@@ -28551,8 +29531,8 @@ async function createRuntimeServer(options) {
|
|
|
28551
29531
|
return;
|
|
28552
29532
|
}
|
|
28553
29533
|
if (hasWebUi) {
|
|
28554
|
-
const filePath = url.pathname === "/" || !url.pathname.includes(".") ? webUiIndexPath :
|
|
28555
|
-
if (
|
|
29534
|
+
const filePath = url.pathname === "/" || !url.pathname.includes(".") ? webUiIndexPath : join23(webUiDistPath, url.pathname);
|
|
29535
|
+
if (existsSync15(filePath)) {
|
|
28556
29536
|
const content = readFileSync8(filePath);
|
|
28557
29537
|
res.writeHead(200, { "Content-Type": getContentType(filePath) });
|
|
28558
29538
|
res.end(content);
|
|
@@ -28632,15 +29612,17 @@ async function createRuntimeServer(options) {
|
|
|
28632
29612
|
if (activeBuffer !== null) {
|
|
28633
29613
|
if (activeBuffer && ws.readyState === 1) ws.send(prepareBufferForReplay(activeBuffer));
|
|
28634
29614
|
} else {
|
|
28635
|
-
|
|
28636
|
-
|
|
28637
|
-
|
|
28638
|
-
|
|
28639
|
-
|
|
28640
|
-
|
|
28641
|
-
|
|
28642
|
-
|
|
28643
|
-
|
|
29615
|
+
loadTerminalBuffer(workspaceId, taskId).then((diskSnapshot) => {
|
|
29616
|
+
if (diskSnapshot) {
|
|
29617
|
+
if (ws.readyState === 1) ws.send(prepareBufferForReplay(diskSnapshot));
|
|
29618
|
+
return;
|
|
29619
|
+
}
|
|
29620
|
+
const hubSnapshot = stateHub.getTerminalBuffer(workspaceId, taskId);
|
|
29621
|
+
if (hubSnapshot && ws.readyState === 1) ws.send(prepareBufferForReplay(hubSnapshot));
|
|
29622
|
+
}).catch(() => {
|
|
29623
|
+
const hubSnapshot = stateHub.getTerminalBuffer(workspaceId, taskId);
|
|
29624
|
+
if (hubSnapshot && ws.readyState === 1) ws.send(prepareBufferForReplay(hubSnapshot));
|
|
29625
|
+
});
|
|
28644
29626
|
}
|
|
28645
29627
|
ws.on("message", (raw2) => {
|
|
28646
29628
|
const text = raw2.toString();
|
|
@@ -28720,6 +29702,9 @@ async function createRuntimeServer(options) {
|
|
|
28720
29702
|
await writeClaudeTaskHookSettings(port).catch((err) => {
|
|
28721
29703
|
logger.warn("[server] Failed to write claude hook settings:", err);
|
|
28722
29704
|
});
|
|
29705
|
+
await writeClaudeCompanionSettings().catch((err) => {
|
|
29706
|
+
logger.warn("[server] Failed to write claude companion settings:", err);
|
|
29707
|
+
});
|
|
28723
29708
|
if (_globalConfig.autoStartTunnel) tunnelManager.start();
|
|
28724
29709
|
scheduleUpdateChecks(VERSION10, (latestVersion) => {
|
|
28725
29710
|
stateHub.broadcastUpdateAvailable(latestVersion);
|
|
@@ -28770,7 +29755,7 @@ process.on("uncaughtException", (err) => {
|
|
|
28770
29755
|
if (err.code === "EPIPE" || err.code === "ECONNRESET") return;
|
|
28771
29756
|
throw err;
|
|
28772
29757
|
});
|
|
28773
|
-
var VERSION9 = true ? "0.
|
|
29758
|
+
var VERSION9 = true ? "0.9.1" : "0.0.0-dev";
|
|
28774
29759
|
async function isPortAvailable(port, host) {
|
|
28775
29760
|
return new Promise((resolve5) => {
|
|
28776
29761
|
const probe = createServer2();
|