whipped 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1294 -259
- 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-RwOyl_Kz.js +119 -0
- package/dist/web-ui/assets/arc-DmDBHE0H.js +131 -0
- package/dist/web-ui/assets/architectureDiagram-ZJ3FMSHR-RTwadm6J.js +8821 -0
- package/dist/web-ui/assets/blockDiagram-677ZJIJ3-Dvv5uMUE.js +3801 -0
- package/dist/web-ui/assets/c4Diagram-LMCZKHZV-bvr9R9cD.js +2479 -0
- package/dist/web-ui/assets/channel-BGhlETgZ.js +7 -0
- package/dist/web-ui/assets/chunk-2Q5K7J3B-BRq-Qbau.js +17 -0
- package/dist/web-ui/assets/chunk-32BRIVSS-Dy1BUZGx.js +116 -0
- package/dist/web-ui/assets/chunk-5VM5RSS4-DCUiIwIc.js +19 -0
- package/dist/web-ui/assets/chunk-EX3LRPZG-Cg_Vtzwz.js +1996 -0
- package/dist/web-ui/assets/chunk-JWPE2WC7-BW4n_ZhH.js +17 -0
- package/dist/web-ui/assets/chunk-MOJQB5TN-BykRa615.js +855 -0
- package/dist/web-ui/assets/chunk-RYQCIY6F-D4F7oV1d.js +476 -0
- package/dist/web-ui/assets/chunk-V7JOEXUC-DD4mm30h.js +2022 -0
- package/dist/web-ui/assets/chunk-VR4S4FIN-Fgvcluvk.js +25 -0
- package/dist/web-ui/assets/chunk-XXDRQBXY-C4FVmO5r.js +13 -0
- package/dist/web-ui/assets/classDiagram-OUVF2IWQ-DD4KIYF1.js +24 -0
- package/dist/web-ui/assets/classDiagram-v2-EOCWNBFH-DD4KIYF1.js +24 -0
- package/dist/web-ui/assets/cose-bilkent-JH36ORCC-ekFwvYt9.js +4943 -0
- package/dist/web-ui/assets/cynefin-VYW2F7L2-DTNV7gvQ.js +31527 -0
- package/dist/web-ui/assets/cynefinDiagram-TSTJHNR4-koYialeC.js +454 -0
- package/dist/web-ui/assets/cytoscape.esm-CaQ7Fomf.js +30346 -0
- package/dist/web-ui/assets/dagre-VKFMJZFB-Do43FV3o.js +526 -0
- package/dist/web-ui/assets/defaultLocale-B2RvLBDe.js +206 -0
- package/dist/web-ui/assets/diagram-FQU43EPY-D0STdny6.js +636 -0
- package/dist/web-ui/assets/diagram-G47NLZAW-D9g6BdZT.js +858 -0
- package/dist/web-ui/assets/diagram-NH7WQ7WH-zLW6CAmi.js +212 -0
- package/dist/web-ui/assets/diagram-OA4YK3LP-CA5tvsYw.js +492 -0
- package/dist/web-ui/assets/diagram-WEI45ONY-CLmYUHR0.js +309 -0
- package/dist/web-ui/assets/ebnfDiagram-CCIWWBDH-KkHubBI6.js +139 -0
- package/dist/web-ui/assets/erDiagram-Q63AITRT-BJna2u1n.js +1238 -0
- package/dist/web-ui/assets/flowDiagram-23GEKE2U-DVJKalah.js +2353 -0
- package/dist/web-ui/assets/ganttDiagram-NO4QXBWP-D9HwNV4u.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-B7wnoO0J.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-CuGz83Sg.js → index-DZ7I8r_C.js} +41629 -39831
- package/dist/web-ui/assets/infoDiagram-FWYZ7A6U-BY6XoiF8.js +32 -0
- package/dist/web-ui/assets/init-ZxktEp_H.js +16 -0
- package/dist/web-ui/assets/ishikawaDiagram-FXEZZL3T-BaZVnO8j.js +967 -0
- package/dist/web-ui/assets/journeyDiagram-5HDEW3XC-CUA6DUAQ.js +1256 -0
- package/dist/web-ui/assets/kanban-definition-HUTT4EX6-5W5tiWrd.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-CfvGIyDE.js +340 -0
- package/dist/web-ui/assets/map-BEO0Bu8q.js +298 -0
- package/dist/web-ui/assets/mermaid.core-BRk3IzY2.js +26639 -0
- package/dist/web-ui/assets/mindmap-definition-LN4V7U3C-2GmLg6ou.js +1183 -0
- package/dist/web-ui/assets/ordinal-DSZU4PqD.js +76 -0
- package/dist/web-ui/assets/pegDiagram-2B236MQR-gTEdrkJg.js +127 -0
- package/dist/web-ui/assets/pieDiagram-ENE6RG2P-CYXjIhqC.js +318 -0
- package/dist/web-ui/assets/quadrantDiagram-ABIIQ3AL-BStRZxwf.js +1341 -0
- package/dist/web-ui/assets/railroadDiagram-RFXS5EU6-btveDRG2.js +93 -0
- package/dist/web-ui/assets/requirementDiagram-TGXJPOKE-Cy_155rE.js +1205 -0
- package/dist/web-ui/assets/sankeyDiagram-HTMAVEWB-Chtvw3_G.js +1264 -0
- package/dist/web-ui/assets/sequenceDiagram-DBY2YBRQ-DDuMVEX1.js +4523 -0
- package/dist/web-ui/assets/sizeCapture-X5ZJPWSS-Bylf0o6J.js +64 -0
- package/dist/web-ui/assets/stateDiagram-2N3HPSRC-DIzLeR5G.js +453 -0
- package/dist/web-ui/assets/stateDiagram-v2-6OUMAXLB-zG_WjT1-.js +23 -0
- package/dist/web-ui/assets/swimlanes-5IMT3BWC-TKaCmVta.js +8575 -0
- package/dist/web-ui/assets/swimlanesDiagram-G3AALYLV-C5eB3qqS.js +21 -0
- package/dist/web-ui/assets/timeline-definition-FHXFAJF6-CC5Ujpcu.js +1606 -0
- package/dist/web-ui/assets/vennDiagram-L72KCM5P-DUSVXSYT.js +2523 -0
- package/dist/web-ui/assets/wardleyDiagram-EHGQE667-CoP9xn89.js +978 -0
- package/dist/web-ui/assets/xychartDiagram-FW5EYKEG-B9FqP_kk.js +1972 -0
- package/dist/web-ui/index.html +2 -2
- package/package.json +14 -16
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 readFileSync9 } 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,278 @@ 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 INLINE_DIFF_LIMIT = 8e3;
|
|
20881
|
+
function formatDiffBlock(fullDiff, baseRef, header = "Git diff") {
|
|
20882
|
+
if (fullDiff.length <= INLINE_DIFF_LIMIT) return `${header}:
|
|
20883
|
+
${fullDiff}`;
|
|
20884
|
+
return `Large changeset (${fullDiff.length.toLocaleString()} chars). Use \`git diff ${baseRef}...HEAD\` and read individual files to explore.`;
|
|
20885
|
+
}
|
|
20886
|
+
|
|
20887
|
+
// src/daemon/review-pipeline.ts
|
|
20888
|
+
import { existsSync as existsSync9 } from "node:fs";
|
|
20889
|
+
import { readdir, readFile as readFile2, stat, unlink } from "node:fs/promises";
|
|
20890
|
+
import { join as join17 } from "node:path";
|
|
20518
20891
|
init_runtime_config();
|
|
20519
20892
|
init_api_contract();
|
|
20520
20893
|
init_logger();
|
|
@@ -20799,7 +21172,7 @@ function getSlotTriggerWord(type) {
|
|
|
20799
21172
|
}
|
|
20800
21173
|
var SCREENSHOT_EXTENSIONS = /* @__PURE__ */ new Set(["png", "jpg", "jpeg", "webp"]);
|
|
20801
21174
|
async function attachBrowserArtifacts(workspaceId, card, result, since) {
|
|
20802
|
-
const dir =
|
|
21175
|
+
const dir = join17(ATTACHMENTS_DIR, card.id);
|
|
20803
21176
|
let entries;
|
|
20804
21177
|
try {
|
|
20805
21178
|
entries = await readdir(dir);
|
|
@@ -20812,7 +21185,7 @@ async function attachBrowserArtifacts(workspaceId, card, result, since) {
|
|
|
20812
21185
|
for (const name of entries) {
|
|
20813
21186
|
const ext = name.split(".").pop()?.toLowerCase() ?? "";
|
|
20814
21187
|
if (!SCREENSHOT_EXTENSIONS.has(ext)) continue;
|
|
20815
|
-
const filePath =
|
|
21188
|
+
const filePath = join17(dir, name);
|
|
20816
21189
|
try {
|
|
20817
21190
|
const info2 = await stat(filePath);
|
|
20818
21191
|
if (!info2.isFile() || info2.mtimeMs < since) continue;
|
|
@@ -21077,61 +21450,6 @@ function runAgentOnce(agentId, prompt, cwd, workspaceId, streamId, stateHub, reg
|
|
|
21077
21450
|
unregisterProcess = registerLiveProcess(streamId, proc);
|
|
21078
21451
|
});
|
|
21079
21452
|
}
|
|
21080
|
-
function git4(args, cwd) {
|
|
21081
|
-
return spawnSync5("git", args, { cwd, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).stdout?.trim() ?? "";
|
|
21082
|
-
}
|
|
21083
|
-
function readFileSafe(filePath) {
|
|
21084
|
-
try {
|
|
21085
|
-
return readFileSync6(filePath, "utf-8");
|
|
21086
|
-
} catch {
|
|
21087
|
-
return "";
|
|
21088
|
-
}
|
|
21089
|
-
}
|
|
21090
|
-
function getGitStat(worktreePath, baseRef) {
|
|
21091
|
-
const parts = [
|
|
21092
|
-
git4(["diff", "--stat", `${baseRef}...HEAD`], worktreePath),
|
|
21093
|
-
git4(["diff", "--stat", "--cached"], worktreePath),
|
|
21094
|
-
git4(["diff", "--stat"], worktreePath)
|
|
21095
|
-
].filter(Boolean);
|
|
21096
|
-
const newUntracked = git4(["ls-files", "--others", "--exclude-standard"], worktreePath).split("\n").map((f2) => f2.trim()).filter(Boolean);
|
|
21097
|
-
if (newUntracked.length > 0) {
|
|
21098
|
-
parts.push(`New files:
|
|
21099
|
-
${newUntracked.map((f2) => ` ${f2}`).join("\n")}`);
|
|
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
21453
|
function attachmentLines(attachments) {
|
|
21136
21454
|
return attachments.map((a, i) => `- [Attachment #${i + 1}] ${a.name}: ${a.path}`).join("\n");
|
|
21137
21455
|
}
|
|
@@ -21257,12 +21575,6 @@ ${sections.join("\n\n---\n\n")}`,
|
|
|
21257
21575
|
files: []
|
|
21258
21576
|
};
|
|
21259
21577
|
}
|
|
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
21578
|
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
21579
|
function renderReviewDiff(stat3, fullDiff, baseRef, scope) {
|
|
21268
21580
|
if (!scope.useIncremental) {
|
|
@@ -21764,16 +22076,75 @@ function tryParseAgentJson(output) {
|
|
|
21764
22076
|
}
|
|
21765
22077
|
}
|
|
21766
22078
|
|
|
21767
|
-
// src/daemon/
|
|
21768
|
-
|
|
21769
|
-
|
|
21770
|
-
|
|
21771
|
-
|
|
21772
|
-
|
|
22079
|
+
// src/daemon/companion-agent.ts
|
|
22080
|
+
function buildCompanionAgentSystemPrompt(workspaceId, repoPath, worktreePath, baseRef, secrets, systemPrompt, gitInstructions, seedPrompt, resumedCanvas) {
|
|
22081
|
+
const effectiveGitInstructions = gitInstructions?.trim() || DEFAULT_GIT_INSTRUCTIONS;
|
|
22082
|
+
const fullDiff = getGitFullDiff(worktreePath, baseRef);
|
|
22083
|
+
const worktreeSection = fullDiff ? `## Current worktree state (vs ${baseRef})
|
|
22084
|
+
${getGitStat(worktreePath, baseRef)}
|
|
22085
|
+
|
|
22086
|
+
## Diff (vs ${baseRef})
|
|
22087
|
+
${formatDiffBlock(fullDiff, baseRef, "Git diff")}` : `## Worktree state
|
|
22088
|
+
|
|
22089
|
+
The worktree is clean and branched from \`${baseRef}\` \u2014 there is no diff yet. Skip \`git diff\` and start working.`;
|
|
22090
|
+
const parts = [
|
|
22091
|
+
`You are the Companion agent for the project at \`${repoPath}\`.
|
|
22092
|
+
|
|
22093
|
+
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}\`.
|
|
22094
|
+
|
|
22095
|
+
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.`,
|
|
22096
|
+
worktreeSection
|
|
22097
|
+
];
|
|
22098
|
+
parts.push(`## Sharing a canvas with the developer
|
|
22099
|
+
|
|
22100
|
+
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.
|
|
22101
|
+
|
|
22102
|
+
${buildCanvasModeGuidance()}`);
|
|
22103
|
+
if (resumedCanvas) {
|
|
22104
|
+
parts.push(`## Resuming a saved canvas
|
|
22105
|
+
|
|
22106
|
+
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.
|
|
22107
|
+
|
|
22108
|
+
${serializeCanvasBlocksForPrompt(resumedCanvas.blocks)}`);
|
|
22109
|
+
}
|
|
22110
|
+
if (seedPrompt?.trim()) parts.push(`## Project-specific instructions
|
|
22111
|
+
|
|
22112
|
+
${seedPrompt.trim()}`);
|
|
22113
|
+
parts.push(`## Memory
|
|
22114
|
+
|
|
22115
|
+
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.
|
|
22116
|
+
|
|
22117
|
+
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.
|
|
22118
|
+
|
|
22119
|
+
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.
|
|
22120
|
+
|
|
22121
|
+
Scope a memory \`project\` for facts specific to this repo, or \`global\` for things that apply across all the user's projects (style/preferences).`);
|
|
22122
|
+
const secretsSection = buildSecretsSection(secrets);
|
|
22123
|
+
if (secretsSection) parts.push(secretsSection);
|
|
22124
|
+
if (systemPrompt?.trim()) parts.push(`## Project context
|
|
22125
|
+
|
|
22126
|
+
${systemPrompt.trim()}`);
|
|
22127
|
+
parts.push(`## Git conventions
|
|
22128
|
+
|
|
22129
|
+
${effectiveGitInstructions}`);
|
|
22130
|
+
const memContext = buildMemoryContext(workspaceId);
|
|
22131
|
+
const text = parts.join("\n\n");
|
|
22132
|
+
return memContext ? `${memContext}
|
|
22133
|
+
|
|
22134
|
+
${text}` : text;
|
|
22135
|
+
}
|
|
22136
|
+
|
|
22137
|
+
// src/daemon/scheduler.ts
|
|
22138
|
+
var FAST_EXIT_THRESHOLD_MS = 8e3;
|
|
22139
|
+
var MAX_RECENT_BUFFERS = 100;
|
|
22140
|
+
var TaskScheduler = class {
|
|
22141
|
+
constructor(options) {
|
|
22142
|
+
this.options = options;
|
|
21773
22143
|
}
|
|
21774
22144
|
options;
|
|
21775
22145
|
running = /* @__PURE__ */ new Map();
|
|
21776
22146
|
assistantSessions = /* @__PURE__ */ new Map();
|
|
22147
|
+
companionSessions = /* @__PURE__ */ new Map();
|
|
21777
22148
|
// Keep the last output buffer around after a task exits so the terminal
|
|
21778
22149
|
// can still restore when the user opens it for a completed/awaiting-review task.
|
|
21779
22150
|
recentBuffers = /* @__PURE__ */ new Map();
|
|
@@ -21829,7 +22200,7 @@ var TaskScheduler = class {
|
|
|
21829
22200
|
get assistantAgentTaskId() {
|
|
21830
22201
|
return `${ASSISTANT_AGENT_PREFIX}${this.options.workspaceId}`;
|
|
21831
22202
|
}
|
|
21832
|
-
async startAssistantAgent() {
|
|
22203
|
+
async startAssistantAgent(override, savedCanvasId) {
|
|
21833
22204
|
const { workspaceId, repoPath, serverUrl, stateHub, defaultAgent } = this.options;
|
|
21834
22205
|
const taskId = this.assistantAgentTaskId;
|
|
21835
22206
|
const existing = this.assistantSessions.get(taskId);
|
|
@@ -21841,11 +22212,18 @@ var TaskScheduler = class {
|
|
|
21841
22212
|
stateHub.clearTerminalBuffer(workspaceId, taskId);
|
|
21842
22213
|
const prompt = "";
|
|
21843
22214
|
const projectConfig = await loadProjectConfig(workspaceId);
|
|
21844
|
-
const assistantModel = projectConfig.assistantModel;
|
|
22215
|
+
const assistantModel = override ?? projectConfig.assistantModel;
|
|
21845
22216
|
const agentId = assistantModel?.agentId ?? defaultAgent;
|
|
21846
22217
|
const secrets = projectConfig.secrets ?? [];
|
|
21847
22218
|
const secretsEnv = buildSecretsEnv(secrets);
|
|
21848
|
-
const
|
|
22219
|
+
const savedCanvas = savedCanvasId ? getCompanionSavedCanvas(savedCanvasId) : null;
|
|
22220
|
+
if (savedCanvas) createCompanionCanvas(taskId, workspaceId, savedCanvas.blocks);
|
|
22221
|
+
const assistantSystemPrompt = buildAssistantAgentSystemPrompt(
|
|
22222
|
+
repoPath,
|
|
22223
|
+
secrets,
|
|
22224
|
+
projectConfig.systemPrompt,
|
|
22225
|
+
savedCanvas ? { title: savedCanvas.title, blocks: savedCanvas.blocks } : void 0
|
|
22226
|
+
);
|
|
21849
22227
|
const memContext = buildMemoryContext(workspaceId);
|
|
21850
22228
|
const appendSystemPrompt = memContext ? `${memContext}
|
|
21851
22229
|
|
|
@@ -21896,6 +22274,9 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
|
|
|
21896
22274
|
...agentId === "cursor" ? { [CURSOR_CONFIG_DIR_ENV]: getCursorConfigDir(taskId) } : {}
|
|
21897
22275
|
},
|
|
21898
22276
|
mcpConfigPath: agentId === "claude" ? CLAUDE_ASSISTANT_MCP_CONFIG_PATH : void 0,
|
|
22277
|
+
// Denies Claude's own native plan-mode tools — see writeClaudeCompanionSettings
|
|
22278
|
+
// — so "plan" always means whipped_show_canvas, same as the companion agent.
|
|
22279
|
+
hookSettingsPath: agentId === "claude" ? CLAUDE_COMPANION_SETTINGS_PATH : void 0,
|
|
21899
22280
|
mcpServer: agentId === "codex" ? buildWhippedMcpServerSpec(
|
|
21900
22281
|
getMcpServerPath(),
|
|
21901
22282
|
serverUrl,
|
|
@@ -21932,6 +22313,234 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
|
|
|
21932
22313
|
isAssistantAgentRunning() {
|
|
21933
22314
|
return this.assistantSessions.has(this.assistantAgentTaskId);
|
|
21934
22315
|
}
|
|
22316
|
+
// Creates the worktree (or resolves the main-repo checkout) synchronously and
|
|
22317
|
+
// returns quickly — install + agent spawn happen in the background via
|
|
22318
|
+
// launchCompanionAgent so callers (the create-session API request) aren't
|
|
22319
|
+
// blocked on a potentially long install command.
|
|
22320
|
+
async startCompanionAgent(session) {
|
|
22321
|
+
const { workspaceId, repoPath, stateHub } = this.options;
|
|
22322
|
+
const taskId = session.id;
|
|
22323
|
+
const existing = this.companionSessions.get(taskId);
|
|
22324
|
+
if (existing) existing.process.kill();
|
|
22325
|
+
this.recentBuffers.delete(taskId);
|
|
22326
|
+
stateHub.clearTerminalBuffer(workspaceId, taskId);
|
|
22327
|
+
if (session.useWorktree) {
|
|
22328
|
+
const worktree = createWorktree(taskId, repoPath, session.baseRef, session.branchName ?? void 0);
|
|
22329
|
+
setCompanionSessionWorktreePath(taskId, worktree.path);
|
|
22330
|
+
setCompanionSessionStatus(taskId, worktree.isNew ? "installing" : "running");
|
|
22331
|
+
void this.launchCompanionAgent(session, worktree.path, worktree.isNew);
|
|
22332
|
+
} else {
|
|
22333
|
+
setCompanionSessionWorktreePath(taskId, repoPath);
|
|
22334
|
+
setCompanionSessionStatus(taskId, "running");
|
|
22335
|
+
void this.launchCompanionAgent(session, repoPath, false);
|
|
22336
|
+
}
|
|
22337
|
+
}
|
|
22338
|
+
async launchCompanionAgent(session, cwd, isNewWorktree) {
|
|
22339
|
+
const { workspaceId, repoPath, serverUrl, stateHub } = this.options;
|
|
22340
|
+
const taskId = session.id;
|
|
22341
|
+
const agentId = session.agentId;
|
|
22342
|
+
const projectConfig = await loadProjectConfig(workspaceId);
|
|
22343
|
+
const secrets = projectConfig.secrets ?? [];
|
|
22344
|
+
const secretsEnv = buildSecretsEnv(secrets);
|
|
22345
|
+
if (isNewWorktree && projectConfig.worktreeSetup) {
|
|
22346
|
+
await this.runCompanionInstall(taskId, workspaceId, repoPath, cwd, projectConfig.worktreeSetup);
|
|
22347
|
+
if (this.isShuttingDown) return;
|
|
22348
|
+
if (this.manuallyStoppedInstalls.delete(taskId)) {
|
|
22349
|
+
await removeWorktreeAsync(taskId, repoPath, session.branchName ?? void 0);
|
|
22350
|
+
setCompanionSessionWorktreePath(taskId, null);
|
|
22351
|
+
setCompanionSessionStatus(taskId, "stopped");
|
|
22352
|
+
return;
|
|
22353
|
+
}
|
|
22354
|
+
}
|
|
22355
|
+
setCompanionSessionStatus(taskId, "running");
|
|
22356
|
+
const resumedCanvas = session.savedCanvasId ? getCompanionSavedCanvas(session.savedCanvasId) : null;
|
|
22357
|
+
const appendSystemPrompt = buildCompanionAgentSystemPrompt(
|
|
22358
|
+
workspaceId,
|
|
22359
|
+
repoPath,
|
|
22360
|
+
cwd,
|
|
22361
|
+
session.baseRef,
|
|
22362
|
+
secrets,
|
|
22363
|
+
projectConfig.systemPrompt,
|
|
22364
|
+
projectConfig.gitInstructions,
|
|
22365
|
+
session.seedPrompt,
|
|
22366
|
+
resumedCanvas ? { title: resumedCanvas.title, blocks: resumedCanvas.blocks } : void 0
|
|
22367
|
+
);
|
|
22368
|
+
const mcpConfigPath = !isPluginConfigAgent(agentId) && agentId !== "cursor" ? getMcpConfigPath(taskId) : void 0;
|
|
22369
|
+
if (agentId === "claude") {
|
|
22370
|
+
await writeClaudeMcpConfig(
|
|
22371
|
+
getMcpServerPath(),
|
|
22372
|
+
serverUrl,
|
|
22373
|
+
workspaceId,
|
|
22374
|
+
agentId,
|
|
22375
|
+
mcpConfigPath,
|
|
22376
|
+
void 0,
|
|
22377
|
+
buildMcpRoleArgs("companion", void 0, taskId)
|
|
22378
|
+
).catch((err) => {
|
|
22379
|
+
logger.warn({ err }, "[scheduler] Failed to write companion agent MCP config");
|
|
22380
|
+
});
|
|
22381
|
+
} else if (isPluginConfigAgent(agentId)) {
|
|
22382
|
+
const mcpSpec = buildWhippedMcpServerSpec(
|
|
22383
|
+
getMcpServerPath(),
|
|
22384
|
+
serverUrl,
|
|
22385
|
+
workspaceId,
|
|
22386
|
+
agentId,
|
|
22387
|
+
buildMcpRoleArgs("companion", void 0, taskId)
|
|
22388
|
+
);
|
|
22389
|
+
await writePluginAgentFiles(agentId, taskId, getServerPort(serverUrl), mcpSpec, { appendSystemPrompt }).catch(
|
|
22390
|
+
(err) => {
|
|
22391
|
+
logger.warn({ err }, `[scheduler] Failed to write ${agentId} companion agent files`);
|
|
22392
|
+
}
|
|
22393
|
+
);
|
|
22394
|
+
} else if (agentId === "cursor") {
|
|
22395
|
+
const mcpSpec = buildWhippedMcpServerSpec(
|
|
22396
|
+
getMcpServerPath(),
|
|
22397
|
+
serverUrl,
|
|
22398
|
+
workspaceId,
|
|
22399
|
+
agentId,
|
|
22400
|
+
buildMcpRoleArgs("companion", void 0, taskId)
|
|
22401
|
+
);
|
|
22402
|
+
await writeCursorConfigFiles(taskId, getServerPort(serverUrl), mcpSpec).catch((err) => {
|
|
22403
|
+
logger.warn({ err }, "[scheduler] Failed to write cursor companion agent config");
|
|
22404
|
+
});
|
|
22405
|
+
}
|
|
22406
|
+
let resolveExit;
|
|
22407
|
+
const exitPromise = new Promise((resolve5) => {
|
|
22408
|
+
resolveExit = resolve5;
|
|
22409
|
+
});
|
|
22410
|
+
const companionTask = {
|
|
22411
|
+
taskId,
|
|
22412
|
+
streamId: taskId,
|
|
22413
|
+
// companion session uses taskId as its stream (single persistent session)
|
|
22414
|
+
agentId,
|
|
22415
|
+
exitPromise,
|
|
22416
|
+
process: spawnAgent({
|
|
22417
|
+
agentId,
|
|
22418
|
+
prompt: "",
|
|
22419
|
+
cwd,
|
|
22420
|
+
env: {
|
|
22421
|
+
...secretsEnv,
|
|
22422
|
+
...buildTaskHookEnv(taskId, workspaceId),
|
|
22423
|
+
WHIPPED_SLOT: "companion",
|
|
22424
|
+
...pluginAgentConfigDirEnv(agentId, taskId),
|
|
22425
|
+
...agentId === "cursor" ? { [CURSOR_CONFIG_DIR_ENV]: getCursorConfigDir(taskId) } : {}
|
|
22426
|
+
},
|
|
22427
|
+
mcpConfigPath: agentId === "claude" ? mcpConfigPath : void 0,
|
|
22428
|
+
// Denies Claude's own native plan-mode tools for this session — see
|
|
22429
|
+
// writeClaudeCompanionSettings — so "plan" always means whipped_show_canvas.
|
|
22430
|
+
hookSettingsPath: agentId === "claude" ? CLAUDE_COMPANION_SETTINGS_PATH : void 0,
|
|
22431
|
+
mcpServer: agentId === "codex" ? buildWhippedMcpServerSpec(
|
|
22432
|
+
getMcpServerPath(),
|
|
22433
|
+
serverUrl,
|
|
22434
|
+
workspaceId,
|
|
22435
|
+
agentId,
|
|
22436
|
+
buildMcpRoleArgs("companion", void 0, taskId)
|
|
22437
|
+
) : void 0,
|
|
22438
|
+
model: session.model ?? null,
|
|
22439
|
+
effort: session.effort ?? null,
|
|
22440
|
+
appendSystemPrompt: isPluginConfigAgent(agentId) ? void 0 : appendSystemPrompt,
|
|
22441
|
+
onOutput: (data) => {
|
|
22442
|
+
companionTask.outputBuffer += data;
|
|
22443
|
+
stateHub.broadcastTerminalOutput(workspaceId, taskId, data);
|
|
22444
|
+
},
|
|
22445
|
+
onExit: () => {
|
|
22446
|
+
this.setRecentBuffer(taskId, companionTask.outputBuffer);
|
|
22447
|
+
this.companionSessions.delete(taskId);
|
|
22448
|
+
setCompanionSessionStatus(taskId, "stopped");
|
|
22449
|
+
resolveExit();
|
|
22450
|
+
}
|
|
22451
|
+
}),
|
|
22452
|
+
startedAt: Date.now(),
|
|
22453
|
+
outputBuffer: ""
|
|
22454
|
+
};
|
|
22455
|
+
this.companionSessions.set(taskId, companionTask);
|
|
22456
|
+
}
|
|
22457
|
+
// Copies/links worktreeSetup's configured files then runs its install command,
|
|
22458
|
+
// mirroring the card dev-agent's worktree setup step (launchDevAgent above) but
|
|
22459
|
+
// streamed straight into the companion session's own terminal (taskId) instead
|
|
22460
|
+
// of a separate install stream row, since companion sessions have no such table.
|
|
22461
|
+
async runCompanionInstall(taskId, workspaceId, repoPath, worktreePath, worktreeSetup) {
|
|
22462
|
+
const { stateHub } = this.options;
|
|
22463
|
+
const { filesToCopy, installCommand } = worktreeSetup;
|
|
22464
|
+
for (const entry of filesToCopy) {
|
|
22465
|
+
const src = join18(repoPath, entry.path);
|
|
22466
|
+
if (!existsSync10(src)) continue;
|
|
22467
|
+
const dst = join18(worktreePath, entry.path);
|
|
22468
|
+
await mkdir3(dirname6(dst), { recursive: true });
|
|
22469
|
+
try {
|
|
22470
|
+
if (entry.symlink) {
|
|
22471
|
+
await shareIntoWorktree(src, dst);
|
|
22472
|
+
} else {
|
|
22473
|
+
await cp(src, dst, { recursive: true, dereference: true });
|
|
22474
|
+
}
|
|
22475
|
+
} catch (err) {
|
|
22476
|
+
logger.warn(
|
|
22477
|
+
{ err },
|
|
22478
|
+
`[scheduler] companion worktree setup: failed to ${entry.symlink ? "link" : "copy"} ${entry.path}`
|
|
22479
|
+
);
|
|
22480
|
+
}
|
|
22481
|
+
}
|
|
22482
|
+
const installCmd = installCommand.trim();
|
|
22483
|
+
if (!installCmd) return;
|
|
22484
|
+
let buffer = "";
|
|
22485
|
+
const emit = (data) => {
|
|
22486
|
+
buffer += data;
|
|
22487
|
+
this.recentBuffers.set(taskId, buffer);
|
|
22488
|
+
stateHub.broadcastTerminalOutput(workspaceId, taskId, data);
|
|
22489
|
+
};
|
|
22490
|
+
emit(`\x1B[1;36m$ ${installCmd}\x1B[0m\r
|
|
22491
|
+
`);
|
|
22492
|
+
const exitCode = await new Promise((resolveExit) => {
|
|
22493
|
+
const [shell, shellArgs] = getShellInvocation(installCmd);
|
|
22494
|
+
const proc = nodePty2.spawn(shell, shellArgs, {
|
|
22495
|
+
name: "xterm-256color",
|
|
22496
|
+
cols: 220,
|
|
22497
|
+
rows: 50,
|
|
22498
|
+
cwd: worktreePath,
|
|
22499
|
+
env: { ...process.env, REPO_PATH: repoPath, TERM: "xterm-256color" }
|
|
22500
|
+
});
|
|
22501
|
+
this.runningInstalls.set(taskId, proc);
|
|
22502
|
+
proc.onData(emit);
|
|
22503
|
+
proc.onExit(({ exitCode: exitCode2 }) => resolveExit(exitCode2 ?? 0));
|
|
22504
|
+
});
|
|
22505
|
+
this.runningInstalls.delete(taskId);
|
|
22506
|
+
if (this.isShuttingDown) return;
|
|
22507
|
+
if (this.manuallyStoppedInstalls.has(taskId)) {
|
|
22508
|
+
emit("\r\n\x1B[1;33mInstall stopped\x1B[0m\r\n");
|
|
22509
|
+
return;
|
|
22510
|
+
}
|
|
22511
|
+
if (exitCode !== 0) {
|
|
22512
|
+
logger.error(`[scheduler] Install command failed (code ${exitCode}) for companion session ${taskId}`);
|
|
22513
|
+
emit(`\r
|
|
22514
|
+
\x1B[1;31mInstall command failed (code ${exitCode}) \u2014 proceeding anyway\x1B[0m\r
|
|
22515
|
+
`);
|
|
22516
|
+
} else {
|
|
22517
|
+
emit("\r\n\x1B[1;32mInstall complete\x1B[0m\r\n");
|
|
22518
|
+
}
|
|
22519
|
+
}
|
|
22520
|
+
// Kills the session's process (or its still-running install command) and waits
|
|
22521
|
+
// for it to actually exit (bounded by a timeout) before returning — callers
|
|
22522
|
+
// that are about to delete or merge the worktree on disk must await this
|
|
22523
|
+
// first, since attemptMerge/removeWorktreeAsync can otherwise race a still-live
|
|
22524
|
+
// process whose cwd is inside that worktree.
|
|
22525
|
+
async stopCompanionAgent(sessionId) {
|
|
22526
|
+
const installProc = this.runningInstalls.get(sessionId);
|
|
22527
|
+
if (installProc) {
|
|
22528
|
+
this.manuallyStoppedInstalls.add(sessionId);
|
|
22529
|
+
this.runningInstalls.delete(sessionId);
|
|
22530
|
+
killInstallProcess(installProc);
|
|
22531
|
+
}
|
|
22532
|
+
const session = this.companionSessions.get(sessionId);
|
|
22533
|
+
if (session) {
|
|
22534
|
+
const exitPromise = session.exitPromise ?? Promise.resolve();
|
|
22535
|
+
session.process.kill();
|
|
22536
|
+
await Promise.race([exitPromise, new Promise((resolve5) => setTimeout(resolve5, 5e3))]);
|
|
22537
|
+
}
|
|
22538
|
+
this.companionSessions.delete(sessionId);
|
|
22539
|
+
setCompanionSessionStatus(sessionId, "stopped");
|
|
22540
|
+
}
|
|
22541
|
+
isCompanionAgentRunning(sessionId) {
|
|
22542
|
+
return this.companionSessions.has(sessionId);
|
|
22543
|
+
}
|
|
21935
22544
|
get activeCount() {
|
|
21936
22545
|
return this.running.size;
|
|
21937
22546
|
}
|
|
@@ -22120,9 +22729,9 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
|
|
|
22120
22729
|
const copied = [];
|
|
22121
22730
|
const linked = [];
|
|
22122
22731
|
for (const entry of filesToCopy) {
|
|
22123
|
-
const src =
|
|
22732
|
+
const src = join18(repoPath, entry.path);
|
|
22124
22733
|
if (!existsSync10(src)) continue;
|
|
22125
|
-
const dst =
|
|
22734
|
+
const dst = join18(worktree.path, entry.path);
|
|
22126
22735
|
await mkdir3(dirname6(dst), { recursive: true });
|
|
22127
22736
|
try {
|
|
22128
22737
|
if (entry.symlink) {
|
|
@@ -22542,6 +23151,8 @@ ${devSystemPromptResult.text}`;
|
|
|
22542
23151
|
}
|
|
22543
23152
|
const assistantSession = this.assistantSessions.get(streamId);
|
|
22544
23153
|
if (assistantSession) return assistantSession.outputBuffer;
|
|
23154
|
+
const companionSession = this.companionSessions.get(streamId);
|
|
23155
|
+
if (companionSession) return companionSession.outputBuffer;
|
|
22545
23156
|
return this.recentBuffers.get(streamId) ?? null;
|
|
22546
23157
|
}
|
|
22547
23158
|
resizeTerminal(streamId, cols, rows) {
|
|
@@ -22558,7 +23169,7 @@ ${devSystemPromptResult.text}`;
|
|
|
22558
23169
|
for (const task of this.running.values()) {
|
|
22559
23170
|
if (task.streamId === streamId) return task.process;
|
|
22560
23171
|
}
|
|
22561
|
-
return this.assistantSessions.get(streamId)?.process ?? this.liveProcesses.get(streamId);
|
|
23172
|
+
return this.assistantSessions.get(streamId)?.process ?? this.companionSessions.get(streamId)?.process ?? this.liveProcesses.get(streamId);
|
|
22562
23173
|
}
|
|
22563
23174
|
async handleHookEvent(event, taskId) {
|
|
22564
23175
|
const { workspaceId, stateHub } = this.options;
|
|
@@ -22744,6 +23355,10 @@ ${devSystemPromptResult.text}`;
|
|
|
22744
23355
|
}
|
|
22745
23356
|
this.runningInstalls.clear();
|
|
22746
23357
|
this.stopAssistantAgent();
|
|
23358
|
+
for (const [, task] of this.companionSessions) {
|
|
23359
|
+
task.process.kill();
|
|
23360
|
+
}
|
|
23361
|
+
this.companionSessions.clear();
|
|
22747
23362
|
}
|
|
22748
23363
|
// Call before stopAll() during graceful shutdown so onExit handlers bail out
|
|
22749
23364
|
// and do not overwrite the failed/todo state written by cleanupStaleTasks().
|
|
@@ -22789,8 +23404,15 @@ function getMcpServerPath() {
|
|
|
22789
23404
|
args: [resolve2(thisDir, "mcp-server.js")]
|
|
22790
23405
|
};
|
|
22791
23406
|
}
|
|
22792
|
-
function buildAssistantAgentSystemPrompt(repoPath, secrets = [], systemPrompt) {
|
|
23407
|
+
function buildAssistantAgentSystemPrompt(repoPath, secrets = [], systemPrompt, resumedCanvas) {
|
|
22793
23408
|
const secretsSection = buildSecretsSection(secrets);
|
|
23409
|
+
const resumedCanvasSection = resumedCanvas ? `
|
|
23410
|
+
|
|
23411
|
+
# Resuming a saved canvas
|
|
23412
|
+
|
|
23413
|
+
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.
|
|
23414
|
+
|
|
23415
|
+
${serializeCanvasBlocksForPrompt(resumedCanvas.blocks)}` : "";
|
|
22794
23416
|
return `You are the Assistant for the project at \`${repoPath}\`.
|
|
22795
23417
|
|
|
22796
23418
|
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 +23446,10 @@ You are a conversational project assistant. You can discuss the project, help pl
|
|
|
22824
23446
|
- \`kanban_get_workflows\` \u2014 list all workflows (task and story/orch) with their agent slots, model tiers, tools, and prompts
|
|
22825
23447
|
- \`kanban_upsert_workflow\` \u2014 create or fully replace a workflow (pass complete workflow object)
|
|
22826
23448
|
|
|
23449
|
+
## Canvas
|
|
23450
|
+
- \`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
|
|
23451
|
+
- \`whipped_save_canvas\` \u2014 consolidate the conversation's canvas versions into one and save it to the reusable canvas library
|
|
23452
|
+
|
|
22827
23453
|
## Memory
|
|
22828
23454
|
- \`whipped_search_memory\` \u2014 search durable project + global memory before re-discovering how something works
|
|
22829
23455
|
- \`whipped_get_memory\` \u2014 fetch one memory's full content by id
|
|
@@ -22832,6 +23458,12 @@ You are a conversational project assistant. You can discuss the project, help pl
|
|
|
22832
23458
|
|
|
22833
23459
|
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
23460
|
|
|
23461
|
+
# Sharing a canvas
|
|
23462
|
+
|
|
23463
|
+
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.
|
|
23464
|
+
|
|
23465
|
+
${buildCanvasModeGuidance()}${resumedCanvasSection}
|
|
23466
|
+
|
|
22835
23467
|
# Card types
|
|
22836
23468
|
|
|
22837
23469
|
**task** \u2014 a normal development ticket. Runs an optional plan slot, then dev, then any number of review slots (based on workflow).
|
|
@@ -25379,6 +26011,7 @@ var errorHandler2 = (err, c) => {
|
|
|
25379
26011
|
};
|
|
25380
26012
|
|
|
25381
26013
|
// src/api/routes/agent.ts
|
|
26014
|
+
init_api_contract();
|
|
25382
26015
|
import { z as z3 } from "zod";
|
|
25383
26016
|
|
|
25384
26017
|
// node_modules/.pnpm/hono@4.12.23/node_modules/hono/dist/utils/cookie.js
|
|
@@ -25676,8 +26309,8 @@ var zv = (target, schema) => zValidator(target, schema, (result) => {
|
|
|
25676
26309
|
});
|
|
25677
26310
|
|
|
25678
26311
|
// src/api/services/agent-service.ts
|
|
25679
|
-
var startAgentSession = async (scheduler) => ({
|
|
25680
|
-
taskId: await scheduler.startAssistantAgent()
|
|
26312
|
+
var startAgentSession = async (scheduler, override, savedCanvasId) => ({
|
|
26313
|
+
taskId: await scheduler.startAssistantAgent(override, savedCanvasId)
|
|
25681
26314
|
});
|
|
25682
26315
|
var stopAgentSession = async (scheduler) => {
|
|
25683
26316
|
scheduler?.stopAssistantAgent();
|
|
@@ -25691,21 +26324,26 @@ var getAgentSessionStatus = async (scheduler) => {
|
|
|
25691
26324
|
};
|
|
25692
26325
|
|
|
25693
26326
|
// src/api/routes/agent.ts
|
|
26327
|
+
var startSessionBodySchema = z3.object({
|
|
26328
|
+
workspaceId: z3.string(),
|
|
26329
|
+
override: agentModelChoiceSchema.optional(),
|
|
26330
|
+
savedCanvasId: z3.string().optional()
|
|
26331
|
+
});
|
|
25694
26332
|
var agentController = new Hono2().get("/session", zv("query", z3.object({ workspaceId: z3.string() })), async (c) => {
|
|
25695
26333
|
const ctx = c.var.ctx;
|
|
25696
26334
|
const { workspaceId } = c.req.valid("query");
|
|
25697
26335
|
return c.json(await getAgentSessionStatus(ctx.getScheduler(workspaceId)));
|
|
25698
|
-
}).post("/session", zv("json",
|
|
26336
|
+
}).post("/session", zv("json", startSessionBodySchema), async (c) => {
|
|
25699
26337
|
const ctx = c.var.ctx;
|
|
25700
|
-
const { workspaceId } = c.req.valid("json");
|
|
26338
|
+
const { workspaceId, override, savedCanvasId } = c.req.valid("json");
|
|
25701
26339
|
const scheduler = ctx.getScheduler(workspaceId);
|
|
25702
26340
|
if (!scheduler) {
|
|
25703
26341
|
await ctx.ensureWorkspace(workspaceId);
|
|
25704
26342
|
const retried = ctx.getScheduler(workspaceId);
|
|
25705
26343
|
if (!retried) throw NotFoundError("Workspace");
|
|
25706
|
-
return c.json(await startAgentSession(retried));
|
|
26344
|
+
return c.json(await startAgentSession(retried, override, savedCanvasId));
|
|
25707
26345
|
}
|
|
25708
|
-
return c.json(await startAgentSession(scheduler));
|
|
26346
|
+
return c.json(await startAgentSession(scheduler, override, savedCanvasId));
|
|
25709
26347
|
}).delete("/session", zv("query", z3.object({ workspaceId: z3.string() })), async (c) => {
|
|
25710
26348
|
const ctx = c.var.ctx;
|
|
25711
26349
|
const { workspaceId } = c.req.valid("query");
|
|
@@ -26251,8 +26889,8 @@ var getDiffService = async (workspaceId, cardId) => {
|
|
|
26251
26889
|
const card = board.cards[cardId];
|
|
26252
26890
|
if (!card) throw NotFoundError("Card");
|
|
26253
26891
|
const worktreePath = getWorktreePath(resolveWorktreeOwnerId(cardId, board.cards));
|
|
26254
|
-
const { existsSync:
|
|
26255
|
-
if (!
|
|
26892
|
+
const { existsSync: existsSync16 } = await import("node:fs");
|
|
26893
|
+
if (!existsSync16(worktreePath)) {
|
|
26256
26894
|
return { diff: null, error: "No worktree \u2014 agent has not started yet" };
|
|
26257
26895
|
}
|
|
26258
26896
|
const committedResult = spawnSync6("git", ["diff", `${card.baseRef}...HEAD`, "--no-color", "-U3"], {
|
|
@@ -26278,10 +26916,10 @@ var getDiffService = async (workspaceId, cardId) => {
|
|
|
26278
26916
|
encoding: "utf-8"
|
|
26279
26917
|
});
|
|
26280
26918
|
const untrackedFiles = (untrackedResult.stdout ?? "").split("\n").map((f2) => f2.trim()).filter(Boolean);
|
|
26281
|
-
const { readFileSync:
|
|
26919
|
+
const { readFileSync: readFileSync10 } = await import("node:fs");
|
|
26282
26920
|
const untrackedDiffs = untrackedFiles.map((file) => {
|
|
26283
26921
|
try {
|
|
26284
|
-
const content =
|
|
26922
|
+
const content = readFileSync10(`${worktreePath}/${file}`, "utf-8");
|
|
26285
26923
|
const lines = content.split("\n");
|
|
26286
26924
|
const addedLines = lines.map((l, _i) => `+${l}`).join("\n");
|
|
26287
26925
|
const hunkHeader = `@@ -0,0 +1,${lines.length} @@`;
|
|
@@ -26311,8 +26949,8 @@ var getCommitsService = async (workspaceId, cardId) => {
|
|
|
26311
26949
|
const card = board.cards[cardId];
|
|
26312
26950
|
if (!card) throw NotFoundError("Card");
|
|
26313
26951
|
const worktreePath = getWorktreePath(resolveWorktreeOwnerId(cardId, board.cards));
|
|
26314
|
-
const { existsSync:
|
|
26315
|
-
if (!
|
|
26952
|
+
const { existsSync: existsSync16 } = await import("node:fs");
|
|
26953
|
+
if (!existsSync16(worktreePath)) return { commits: [] };
|
|
26316
26954
|
const result = spawnSync6("git", ["log", "--pretty=format:%H%x00%h%x00%s%x00%an%x00%ai", `${card.baseRef}..HEAD`], {
|
|
26317
26955
|
cwd: worktreePath,
|
|
26318
26956
|
encoding: "utf-8"
|
|
@@ -26333,8 +26971,8 @@ var getDiffForCommitService = async (workspaceId, cardId, commitHash) => {
|
|
|
26333
26971
|
if (!card) throw NotFoundError("Card");
|
|
26334
26972
|
if (!/^[0-9a-f]{4,64}$/i.test(commitHash)) return { diff: null, error: "Invalid commit hash" };
|
|
26335
26973
|
const worktreePath = getWorktreePath(resolveWorktreeOwnerId(cardId, board.cards));
|
|
26336
|
-
const { existsSync:
|
|
26337
|
-
if (!
|
|
26974
|
+
const { existsSync: existsSync16 } = await import("node:fs");
|
|
26975
|
+
if (!existsSync16(worktreePath)) return { diff: null, error: "No worktree" };
|
|
26338
26976
|
const result = spawnSync6("git", ["show", commitHash, "--format=", "--patch", "--no-color", "-U3"], {
|
|
26339
26977
|
cwd: worktreePath,
|
|
26340
26978
|
encoding: "utf-8",
|
|
@@ -26582,6 +27220,376 @@ var cardsController = new Hono2().post("/", zv("json", runtimeCardCreateRequestS
|
|
|
26582
27220
|
}
|
|
26583
27221
|
);
|
|
26584
27222
|
|
|
27223
|
+
// src/api/routes/companion-saved-canvases.ts
|
|
27224
|
+
import { z as z6 } from "zod";
|
|
27225
|
+
|
|
27226
|
+
// src/api/services/companion-saved-canvases-service.ts
|
|
27227
|
+
async function listCompanionSavedCanvasesEntry(workspaceId) {
|
|
27228
|
+
return { canvases: listCompanionSavedCanvases(workspaceId) };
|
|
27229
|
+
}
|
|
27230
|
+
async function deleteCompanionSavedCanvasEntry(id) {
|
|
27231
|
+
deleteCompanionSavedCanvas(id);
|
|
27232
|
+
}
|
|
27233
|
+
async function clearCompanionCanvasesEntry(sessionId) {
|
|
27234
|
+
deleteCompanionCanvasesForSession(sessionId);
|
|
27235
|
+
}
|
|
27236
|
+
async function saveCompanionCanvasEntry(sessionId, workspaceId, title, blocks) {
|
|
27237
|
+
const session = getCompanionSession(sessionId);
|
|
27238
|
+
if (session) {
|
|
27239
|
+
if (session.savedCanvasId) {
|
|
27240
|
+
const updated = updateCompanionSavedCanvas(session.savedCanvasId, { title, blocks });
|
|
27241
|
+
if (updated) return updated;
|
|
27242
|
+
}
|
|
27243
|
+
const created = createCompanionSavedCanvas(workspaceId, { title, blocks, sourceSessionId: sessionId });
|
|
27244
|
+
setCompanionSessionSavedCanvasId(sessionId, created.id);
|
|
27245
|
+
return created;
|
|
27246
|
+
}
|
|
27247
|
+
const existing = findCompanionSavedCanvasBySourceSession(sessionId);
|
|
27248
|
+
if (existing) {
|
|
27249
|
+
const updated = updateCompanionSavedCanvas(existing.id, { title, blocks });
|
|
27250
|
+
if (updated) return updated;
|
|
27251
|
+
}
|
|
27252
|
+
return createCompanionSavedCanvas(workspaceId, { title, blocks, sourceSessionId: sessionId });
|
|
27253
|
+
}
|
|
27254
|
+
|
|
27255
|
+
// src/api/routes/companion-saved-canvases.ts
|
|
27256
|
+
var companionSavedCanvasesController = new Hono2().get("/", zv("query", z6.object({ workspaceId: z6.string() })), async (c) => {
|
|
27257
|
+
const { workspaceId } = c.req.valid("query");
|
|
27258
|
+
return c.json(await listCompanionSavedCanvasesEntry(workspaceId));
|
|
27259
|
+
}).delete("/:id", zv("param", z6.object({ id: z6.string() })), async (c) => {
|
|
27260
|
+
const { id } = c.req.valid("param");
|
|
27261
|
+
await deleteCompanionSavedCanvasEntry(id);
|
|
27262
|
+
return c.json({ ok: true });
|
|
27263
|
+
});
|
|
27264
|
+
|
|
27265
|
+
// src/api/routes/companion-sessions.ts
|
|
27266
|
+
init_api_contract();
|
|
27267
|
+
import { z as z7 } from "zod";
|
|
27268
|
+
|
|
27269
|
+
// src/api/services/companion-canvases-service.ts
|
|
27270
|
+
var createCompanionCanvasEntry = async (sessionId, workspaceId, blocks) => {
|
|
27271
|
+
return createCompanionCanvas(sessionId, workspaceId, blocks);
|
|
27272
|
+
};
|
|
27273
|
+
var listCompanionCanvasesEntry = async (sessionId) => {
|
|
27274
|
+
return { canvases: listCompanionCanvases(sessionId) };
|
|
27275
|
+
};
|
|
27276
|
+
|
|
27277
|
+
// src/api/services/companion-diff-service.ts
|
|
27278
|
+
import { existsSync as existsSync11, readFileSync as readFileSync7 } from "node:fs";
|
|
27279
|
+
import { spawnSync as spawnSync7 } from "node:child_process";
|
|
27280
|
+
var MAX_BUFFER = 4 * 1024 * 1024;
|
|
27281
|
+
var getCompanionDiffService = async (id) => {
|
|
27282
|
+
const session = getCompanionSession(id);
|
|
27283
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27284
|
+
if (!session.worktreePath || !existsSync11(session.worktreePath)) {
|
|
27285
|
+
return { diff: null, error: "No worktree \u2014 agent has not started yet" };
|
|
27286
|
+
}
|
|
27287
|
+
const worktreePath = session.worktreePath;
|
|
27288
|
+
const committedResult = spawnSync7("git", ["diff", `${session.baseRef}...HEAD`, "--no-color", "-U3"], {
|
|
27289
|
+
cwd: worktreePath,
|
|
27290
|
+
encoding: "utf-8",
|
|
27291
|
+
maxBuffer: MAX_BUFFER
|
|
27292
|
+
});
|
|
27293
|
+
if (committedResult.status !== 0 && committedResult.stderr) {
|
|
27294
|
+
return { diff: null, error: committedResult.stderr.trim() };
|
|
27295
|
+
}
|
|
27296
|
+
const stagedResult = spawnSync7("git", ["diff", "--cached", "--no-color", "-U3"], {
|
|
27297
|
+
cwd: worktreePath,
|
|
27298
|
+
encoding: "utf-8",
|
|
27299
|
+
maxBuffer: MAX_BUFFER
|
|
27300
|
+
});
|
|
27301
|
+
const unstagedResult = spawnSync7("git", ["diff", "--no-color", "-U3"], {
|
|
27302
|
+
cwd: worktreePath,
|
|
27303
|
+
encoding: "utf-8",
|
|
27304
|
+
maxBuffer: MAX_BUFFER
|
|
27305
|
+
});
|
|
27306
|
+
const untrackedResult = spawnSync7("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
27307
|
+
cwd: worktreePath,
|
|
27308
|
+
encoding: "utf-8"
|
|
27309
|
+
});
|
|
27310
|
+
const untrackedFiles = (untrackedResult.stdout ?? "").split("\n").map((f2) => f2.trim()).filter(Boolean);
|
|
27311
|
+
const untrackedDiffs = untrackedFiles.map((file) => {
|
|
27312
|
+
try {
|
|
27313
|
+
const content = readFileSync7(`${worktreePath}/${file}`, "utf-8");
|
|
27314
|
+
const lines = content.split("\n");
|
|
27315
|
+
const addedLines = lines.map((l) => `+${l}`).join("\n");
|
|
27316
|
+
const hunkHeader = `@@ -0,0 +1,${lines.length} @@`;
|
|
27317
|
+
return `diff --git a/${file} b/${file}
|
|
27318
|
+
new file mode 100644
|
|
27319
|
+
--- /dev/null
|
|
27320
|
+
+++ b/${file}
|
|
27321
|
+
${hunkHeader}
|
|
27322
|
+
${addedLines}`;
|
|
27323
|
+
} catch {
|
|
27324
|
+
return null;
|
|
27325
|
+
}
|
|
27326
|
+
}).filter((d) => d !== null);
|
|
27327
|
+
const diff = [committedResult.stdout, stagedResult.stdout, unstagedResult.stdout, ...untrackedDiffs].filter((s2) => s2?.trim()).join("\n");
|
|
27328
|
+
const behindResult = spawnSync7("git", ["rev-list", "--count", `HEAD..${session.baseRef}`], {
|
|
27329
|
+
cwd: worktreePath,
|
|
27330
|
+
encoding: "utf-8"
|
|
27331
|
+
});
|
|
27332
|
+
const baseBehindCount = parseInt(behindResult.stdout?.trim() ?? "0", 10) || 0;
|
|
27333
|
+
return { diff, error: null, baseBehindCount };
|
|
27334
|
+
};
|
|
27335
|
+
var getCompanionCommitsService = async (id) => {
|
|
27336
|
+
const session = getCompanionSession(id);
|
|
27337
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27338
|
+
if (!session.worktreePath || !existsSync11(session.worktreePath)) return { commits: [] };
|
|
27339
|
+
const result = spawnSync7("git", ["log", "--pretty=format:%H%x00%h%x00%s%x00%an%x00%ai", `${session.baseRef}..HEAD`], {
|
|
27340
|
+
cwd: session.worktreePath,
|
|
27341
|
+
encoding: "utf-8"
|
|
27342
|
+
});
|
|
27343
|
+
if (result.status !== 0 || !result.stdout?.trim()) return { commits: [] };
|
|
27344
|
+
const commits = result.stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
27345
|
+
const [hash = "", shortHash = "", message = "", author = "", date2 = ""] = line.split("\0");
|
|
27346
|
+
return { hash, shortHash, message, author, date: date2 };
|
|
27347
|
+
});
|
|
27348
|
+
return { commits };
|
|
27349
|
+
};
|
|
27350
|
+
var getCompanionDiffForCommitService = async (id, commitHash) => {
|
|
27351
|
+
const session = getCompanionSession(id);
|
|
27352
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27353
|
+
if (!/^[0-9a-f]{4,64}$/i.test(commitHash)) return { diff: null, error: "Invalid commit hash" };
|
|
27354
|
+
if (!session.worktreePath || !existsSync11(session.worktreePath)) return { diff: null, error: "No worktree" };
|
|
27355
|
+
const result = spawnSync7("git", ["show", commitHash, "--format=", "--patch", "--no-color", "-U3"], {
|
|
27356
|
+
cwd: session.worktreePath,
|
|
27357
|
+
encoding: "utf-8",
|
|
27358
|
+
maxBuffer: MAX_BUFFER
|
|
27359
|
+
});
|
|
27360
|
+
if (result.status !== 0) return { diff: null, error: result.stderr?.trim() || "git show failed" };
|
|
27361
|
+
return { diff: result.stdout, error: null };
|
|
27362
|
+
};
|
|
27363
|
+
|
|
27364
|
+
// src/api/services/companion-merge-service.ts
|
|
27365
|
+
init_workspace_state();
|
|
27366
|
+
async function commitAndMergeCompanionService(id, repoPath, commitMessage, scheduler) {
|
|
27367
|
+
const session = getCompanionSession(id);
|
|
27368
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27369
|
+
if (!session.useWorktree || !session.branchName) {
|
|
27370
|
+
throw BadRequestError("This session works directly in the main repo \u2014 there's nothing to merge");
|
|
27371
|
+
}
|
|
27372
|
+
if (!session.worktreePath) throw BadRequestError("Session has no worktree to merge");
|
|
27373
|
+
await scheduler?.stopCompanionAgent(id);
|
|
27374
|
+
const dirty = await isWorktreeDirty(session.worktreePath);
|
|
27375
|
+
if (dirty) {
|
|
27376
|
+
if (!commitMessage) return { status: "needs_commit" };
|
|
27377
|
+
await commitWorktree(session.worktreePath, commitMessage);
|
|
27378
|
+
}
|
|
27379
|
+
const result = attemptMerge(repoPath, id, session.branchName);
|
|
27380
|
+
setCompanionSessionWorktreePath(id, null);
|
|
27381
|
+
if (result.dirtyBase) {
|
|
27382
|
+
setCompanionSessionStatus(id, "stopped");
|
|
27383
|
+
return { status: "dirty_base" };
|
|
27384
|
+
}
|
|
27385
|
+
if (!result.ok) {
|
|
27386
|
+
abortMerge(repoPath);
|
|
27387
|
+
setCompanionSessionStatus(id, "stopped");
|
|
27388
|
+
return { status: "conflict", conflictedFiles: result.conflictedFiles };
|
|
27389
|
+
}
|
|
27390
|
+
setCompanionSessionStatus(id, "merged");
|
|
27391
|
+
return { status: "merged" };
|
|
27392
|
+
}
|
|
27393
|
+
async function commitAndPRCompanionService(id, workspaceId, commitMessage, title, description, baseRefOverride) {
|
|
27394
|
+
const session = getCompanionSession(id);
|
|
27395
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27396
|
+
if (!session.worktreePath) throw BadRequestError("Session has no worktree");
|
|
27397
|
+
const projectConfig = await loadProjectConfig(workspaceId);
|
|
27398
|
+
const token = projectConfig.secrets?.find((s2) => s2.key === "GITHUB_TOKEN")?.value;
|
|
27399
|
+
if (!token) return { status: "no_token" };
|
|
27400
|
+
const dirty = await isWorktreeDirty(session.worktreePath);
|
|
27401
|
+
if (dirty) {
|
|
27402
|
+
if (!commitMessage) return { status: "needs_commit" };
|
|
27403
|
+
await commitWorktree(session.worktreePath, commitMessage);
|
|
27404
|
+
}
|
|
27405
|
+
const branch = session.branchName ?? getCurrentBranch(session.worktreePath);
|
|
27406
|
+
if (!branch) throw BadRequestError("Could not determine the current branch to push");
|
|
27407
|
+
await pushBranch(session.worktreePath, branch);
|
|
27408
|
+
const prUrl = await createGithubPR(
|
|
27409
|
+
session.worktreePath,
|
|
27410
|
+
title,
|
|
27411
|
+
description,
|
|
27412
|
+
baseRefOverride || session.baseRef,
|
|
27413
|
+
token
|
|
27414
|
+
);
|
|
27415
|
+
return { status: "pr_created", prUrl };
|
|
27416
|
+
}
|
|
27417
|
+
|
|
27418
|
+
// src/api/services/companion-service.ts
|
|
27419
|
+
init_api_contract();
|
|
27420
|
+
init_workspace_state();
|
|
27421
|
+
async function listCompanionSessionsEntry(workspaceId) {
|
|
27422
|
+
return listCompanionSessions(workspaceId);
|
|
27423
|
+
}
|
|
27424
|
+
function getCompanionSessionEntry(id) {
|
|
27425
|
+
return getCompanionSession(id);
|
|
27426
|
+
}
|
|
27427
|
+
async function createCompanionSessionEntry(workspaceId, repoPath, req, scheduler) {
|
|
27428
|
+
const useWorktree = req.useWorktree;
|
|
27429
|
+
const branchName = req.branchName?.trim() || null;
|
|
27430
|
+
if (useWorktree && !branchName) {
|
|
27431
|
+
throw BadRequestError("Branch name is required when using an isolated worktree");
|
|
27432
|
+
}
|
|
27433
|
+
const projectConfig = await loadProjectConfig(workspaceId);
|
|
27434
|
+
const workflow = req.workflowId ? projectConfig.workflows.find((w2) => w2.id === req.workflowId) : void 0;
|
|
27435
|
+
const devSlot = workflow?.slots.find((s2) => s2.type === "dev");
|
|
27436
|
+
const seedPrompt = devSlot ? resolvePromptText(devSlot.prompt, repoPath) : "";
|
|
27437
|
+
const suggestedPair = devSlot?.pairs[0];
|
|
27438
|
+
const model = req.model ?? (suggestedPair ? { agentId: suggestedPair.binary, model: suggestedPair.model, effort: suggestedPair.effort } : DEFAULT_AGENT_MODEL_CHOICE);
|
|
27439
|
+
const savedCanvas = req.savedCanvasId ? getCompanionSavedCanvas(req.savedCanvasId) : null;
|
|
27440
|
+
const name = req.name?.trim() || savedCanvas?.title || (useWorktree ? branchName : "Main repo session");
|
|
27441
|
+
const session = createCompanionSession(workspaceId, {
|
|
27442
|
+
name,
|
|
27443
|
+
useWorktree,
|
|
27444
|
+
baseRef: req.baseRef,
|
|
27445
|
+
branchName: useWorktree ? branchName : null,
|
|
27446
|
+
workflowId: workflow?.id ?? null,
|
|
27447
|
+
seedPrompt,
|
|
27448
|
+
agentId: model.agentId ?? "claude",
|
|
27449
|
+
model: model.model ?? null,
|
|
27450
|
+
effort: model.effort ?? null,
|
|
27451
|
+
savedCanvasId: savedCanvas?.id ?? null
|
|
27452
|
+
});
|
|
27453
|
+
if (savedCanvas) createCompanionCanvas(session.id, workspaceId, savedCanvas.blocks);
|
|
27454
|
+
await scheduler.startCompanionAgent(session);
|
|
27455
|
+
return getCompanionSession(session.id) ?? session;
|
|
27456
|
+
}
|
|
27457
|
+
async function stopCompanionSessionEntry(id, scheduler) {
|
|
27458
|
+
if (!getCompanionSession(id)) throw NotFoundError("Companion session");
|
|
27459
|
+
await scheduler?.stopCompanionAgent(id);
|
|
27460
|
+
}
|
|
27461
|
+
async function discardCompanionSessionEntry(id, repoPath, scheduler) {
|
|
27462
|
+
const session = getCompanionSession(id);
|
|
27463
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27464
|
+
await scheduler?.stopCompanionAgent(id);
|
|
27465
|
+
if (session.useWorktree && session.worktreePath) {
|
|
27466
|
+
await removeWorktreeAsync(id, repoPath, session.branchName ?? void 0);
|
|
27467
|
+
}
|
|
27468
|
+
deleteCompanionSession(id);
|
|
27469
|
+
}
|
|
27470
|
+
|
|
27471
|
+
// src/api/routes/companion-sessions.ts
|
|
27472
|
+
var companionSessionsController = new Hono2().get("/", zv("query", z7.object({ workspaceId: z7.string() })), async (c) => {
|
|
27473
|
+
const { workspaceId } = c.req.valid("query");
|
|
27474
|
+
return c.json(await listCompanionSessionsEntry(workspaceId));
|
|
27475
|
+
}).get("/:id", zv("param", z7.object({ id: z7.string() })), async (c) => {
|
|
27476
|
+
const { id } = c.req.valid("param");
|
|
27477
|
+
const session = getCompanionSessionEntry(id);
|
|
27478
|
+
if (!session) throw NotFoundError("Companion session");
|
|
27479
|
+
return c.json(session);
|
|
27480
|
+
}).post("/", zv("json", companionSessionCreateRequestSchema.extend({ workspaceId: z7.string() })), async (c) => {
|
|
27481
|
+
const ctx = c.var.ctx;
|
|
27482
|
+
const { workspaceId, ...req } = c.req.valid("json");
|
|
27483
|
+
const ws = await ctx.ensureWorkspace(workspaceId);
|
|
27484
|
+
const scheduler = ctx.getScheduler(workspaceId);
|
|
27485
|
+
if (!scheduler) throw NotFoundError("Workspace");
|
|
27486
|
+
const session = await createCompanionSessionEntry(workspaceId, ws.repoPath, req, scheduler);
|
|
27487
|
+
return c.json(session);
|
|
27488
|
+
}).delete(
|
|
27489
|
+
"/:id",
|
|
27490
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27491
|
+
zv("query", z7.object({ workspaceId: z7.string() })),
|
|
27492
|
+
async (c) => {
|
|
27493
|
+
const ctx = c.var.ctx;
|
|
27494
|
+
const { id } = c.req.valid("param");
|
|
27495
|
+
const { workspaceId } = c.req.valid("query");
|
|
27496
|
+
await stopCompanionSessionEntry(id, ctx.getScheduler(workspaceId));
|
|
27497
|
+
return c.json({ ok: true });
|
|
27498
|
+
}
|
|
27499
|
+
).post(
|
|
27500
|
+
"/:id/discard",
|
|
27501
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27502
|
+
zv("json", z7.object({ workspaceId: z7.string() })),
|
|
27503
|
+
async (c) => {
|
|
27504
|
+
const ctx = c.var.ctx;
|
|
27505
|
+
const { id } = c.req.valid("param");
|
|
27506
|
+
const { workspaceId } = c.req.valid("json");
|
|
27507
|
+
const ws = await ctx.ensureWorkspace(workspaceId);
|
|
27508
|
+
await discardCompanionSessionEntry(id, ws.repoPath, ctx.getScheduler(workspaceId));
|
|
27509
|
+
return c.json({ ok: true });
|
|
27510
|
+
}
|
|
27511
|
+
).get("/:id/diff", zv("param", z7.object({ id: z7.string() })), async (c) => {
|
|
27512
|
+
const { id } = c.req.valid("param");
|
|
27513
|
+
return c.json(await getCompanionDiffService(id));
|
|
27514
|
+
}).get("/:id/commits", zv("param", z7.object({ id: z7.string() })), async (c) => {
|
|
27515
|
+
const { id } = c.req.valid("param");
|
|
27516
|
+
return c.json(await getCompanionCommitsService(id));
|
|
27517
|
+
}).get(
|
|
27518
|
+
"/:id/diff-for-commit",
|
|
27519
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27520
|
+
zv("query", z7.object({ commitHash: z7.string() })),
|
|
27521
|
+
async (c) => {
|
|
27522
|
+
const { id } = c.req.valid("param");
|
|
27523
|
+
const { commitHash } = c.req.valid("query");
|
|
27524
|
+
return c.json(await getCompanionDiffForCommitService(id, commitHash));
|
|
27525
|
+
}
|
|
27526
|
+
).post(
|
|
27527
|
+
"/:id/commit-and-merge",
|
|
27528
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27529
|
+
zv("json", z7.object({ workspaceId: z7.string(), commitMessage: z7.string().optional() })),
|
|
27530
|
+
async (c) => {
|
|
27531
|
+
const ctx = c.var.ctx;
|
|
27532
|
+
const { id } = c.req.valid("param");
|
|
27533
|
+
const { workspaceId, commitMessage } = c.req.valid("json");
|
|
27534
|
+
const ws = await ctx.ensureWorkspace(workspaceId);
|
|
27535
|
+
const result = await commitAndMergeCompanionService(
|
|
27536
|
+
id,
|
|
27537
|
+
ws.repoPath,
|
|
27538
|
+
commitMessage,
|
|
27539
|
+
ctx.getScheduler(workspaceId)
|
|
27540
|
+
);
|
|
27541
|
+
return c.json(result);
|
|
27542
|
+
}
|
|
27543
|
+
).post(
|
|
27544
|
+
"/:id/commit-and-pr",
|
|
27545
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27546
|
+
zv(
|
|
27547
|
+
"json",
|
|
27548
|
+
z7.object({
|
|
27549
|
+
workspaceId: z7.string(),
|
|
27550
|
+
commitMessage: z7.string().optional(),
|
|
27551
|
+
title: z7.string(),
|
|
27552
|
+
description: z7.string(),
|
|
27553
|
+
baseRef: z7.string().optional()
|
|
27554
|
+
})
|
|
27555
|
+
),
|
|
27556
|
+
async (c) => {
|
|
27557
|
+
const { id } = c.req.valid("param");
|
|
27558
|
+
const { workspaceId, commitMessage, title, description, baseRef } = c.req.valid("json");
|
|
27559
|
+
const result = await commitAndPRCompanionService(id, workspaceId, commitMessage, title, description, baseRef);
|
|
27560
|
+
return c.json(result);
|
|
27561
|
+
}
|
|
27562
|
+
).post(
|
|
27563
|
+
"/:id/canvas",
|
|
27564
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27565
|
+
zv("json", z7.object({ workspaceId: z7.string(), blocks: z7.array(canvasBlockSchema) })),
|
|
27566
|
+
async (c) => {
|
|
27567
|
+
const ctx = c.var.ctx;
|
|
27568
|
+
const { id } = c.req.valid("param");
|
|
27569
|
+
const { workspaceId, blocks } = c.req.valid("json");
|
|
27570
|
+
const canvas = await createCompanionCanvasEntry(id, workspaceId, blocks);
|
|
27571
|
+
ctx.stateHub.broadcastCompanionCanvasUpdate(workspaceId, id, canvas);
|
|
27572
|
+
return c.json(canvas);
|
|
27573
|
+
}
|
|
27574
|
+
).get("/:id/canvases", zv("param", z7.object({ id: z7.string() })), async (c) => {
|
|
27575
|
+
const { id } = c.req.valid("param");
|
|
27576
|
+
return c.json(await listCompanionCanvasesEntry(id));
|
|
27577
|
+
}).delete("/:id/canvases", zv("param", z7.object({ id: z7.string() })), async (c) => {
|
|
27578
|
+
const { id } = c.req.valid("param");
|
|
27579
|
+
await clearCompanionCanvasesEntry(id);
|
|
27580
|
+
return c.json({ ok: true });
|
|
27581
|
+
}).post(
|
|
27582
|
+
"/:id/save-canvas",
|
|
27583
|
+
zv("param", z7.object({ id: z7.string() })),
|
|
27584
|
+
zv("json", z7.object({ workspaceId: z7.string(), title: z7.string(), blocks: z7.array(canvasBlockSchema) })),
|
|
27585
|
+
async (c) => {
|
|
27586
|
+
const { id } = c.req.valid("param");
|
|
27587
|
+
const { workspaceId, title, blocks } = c.req.valid("json");
|
|
27588
|
+
const saved = await saveCompanionCanvasEntry(id, workspaceId, title, blocks);
|
|
27589
|
+
return c.json(saved);
|
|
27590
|
+
}
|
|
27591
|
+
);
|
|
27592
|
+
|
|
26585
27593
|
// src/api/routes/config.ts
|
|
26586
27594
|
init_api_contract();
|
|
26587
27595
|
|
|
@@ -26605,20 +27613,20 @@ var configController = new Hono2().get("/", async (c) => {
|
|
|
26605
27613
|
});
|
|
26606
27614
|
|
|
26607
27615
|
// src/api/routes/fs.ts
|
|
26608
|
-
import { z as
|
|
27616
|
+
import { z as z8 } from "zod";
|
|
26609
27617
|
|
|
26610
27618
|
// src/api/services/fs-service.ts
|
|
26611
27619
|
init_runtime_config();
|
|
26612
|
-
import { spawnSync as
|
|
26613
|
-
import { existsSync as
|
|
27620
|
+
import { spawnSync as spawnSync9 } from "node:child_process";
|
|
27621
|
+
import { existsSync as existsSync13, readdirSync as readdirSync2, statSync } from "node:fs";
|
|
26614
27622
|
import { homedir as homedir4 } from "node:os";
|
|
26615
|
-
import { dirname as dirname7, join as
|
|
27623
|
+
import { dirname as dirname7, join as join20, resolve as resolve3 } from "node:path";
|
|
26616
27624
|
|
|
26617
27625
|
// src/core/terminal-apps.ts
|
|
26618
|
-
import { spawnSync as
|
|
26619
|
-
import { existsSync as
|
|
27626
|
+
import { spawnSync as spawnSync8 } from "node:child_process";
|
|
27627
|
+
import { existsSync as existsSync12 } from "node:fs";
|
|
26620
27628
|
import { homedir as homedir3 } from "node:os";
|
|
26621
|
-
import { join as
|
|
27629
|
+
import { join as join19 } from "node:path";
|
|
26622
27630
|
var MACOS_TERMINALS = [
|
|
26623
27631
|
{ bundle: "Terminal", label: "Terminal" },
|
|
26624
27632
|
{ bundle: "iTerm", label: "iTerm" },
|
|
@@ -26650,13 +27658,13 @@ function appExists(bundle) {
|
|
|
26650
27658
|
`/Applications/${bundle}.app`,
|
|
26651
27659
|
`/System/Applications/${bundle}.app`,
|
|
26652
27660
|
`/System/Applications/Utilities/${bundle}.app`,
|
|
26653
|
-
|
|
27661
|
+
join19(homedir3(), "Applications", `${bundle}.app`)
|
|
26654
27662
|
];
|
|
26655
|
-
return paths.some((p2) =>
|
|
27663
|
+
return paths.some((p2) => existsSync12(p2));
|
|
26656
27664
|
}
|
|
26657
27665
|
function binaryExists(bin) {
|
|
26658
27666
|
const finder = process.platform === "win32" ? "where" : "which";
|
|
26659
|
-
const r =
|
|
27667
|
+
const r = spawnSync8(finder, [bin], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" });
|
|
26660
27668
|
return r.status === 0 && r.stdout.trim().length > 0;
|
|
26661
27669
|
}
|
|
26662
27670
|
function listTerminalApps() {
|
|
@@ -26671,26 +27679,26 @@ function listTerminalApps() {
|
|
|
26671
27679
|
function openTerminalAt(path2, preferredId) {
|
|
26672
27680
|
if (process.platform === "darwin") {
|
|
26673
27681
|
const bundle = preferredId && appExists(preferredId) ? preferredId : "Terminal";
|
|
26674
|
-
|
|
27682
|
+
spawnSync8("open", ["-a", bundle, path2], { stdio: "ignore" });
|
|
26675
27683
|
return;
|
|
26676
27684
|
}
|
|
26677
27685
|
if (process.platform === "win32") {
|
|
26678
27686
|
const id = preferredId && WINDOWS_TERMINALS.some((t) => t.id === preferredId && binaryExists(t.check)) ? preferredId : "cmd";
|
|
26679
27687
|
if (id === "wt") {
|
|
26680
|
-
|
|
27688
|
+
spawnSync8("wt", ["-d", path2], { stdio: "ignore" });
|
|
26681
27689
|
} else if (id === "powershell") {
|
|
26682
|
-
|
|
27690
|
+
spawnSync8("cmd", ["/c", "start", "powershell", "-NoExit", "-Command", `Set-Location -Path '${path2}'`], {
|
|
26683
27691
|
stdio: "ignore"
|
|
26684
27692
|
});
|
|
26685
27693
|
} else {
|
|
26686
|
-
|
|
27694
|
+
spawnSync8("cmd", ["/c", "start", "cmd", "/K", `cd /D "${path2}"`], { stdio: "ignore" });
|
|
26687
27695
|
}
|
|
26688
27696
|
return;
|
|
26689
27697
|
}
|
|
26690
27698
|
const bin = preferredId && binaryExists(preferredId) ? preferredId : LINUX_TERMINALS.find((t) => binaryExists(t.bin))?.bin;
|
|
26691
27699
|
if (!bin) return;
|
|
26692
27700
|
const args = linuxLaunchArgs(bin, path2);
|
|
26693
|
-
|
|
27701
|
+
spawnSync8(bin, args, { stdio: "ignore" });
|
|
26694
27702
|
}
|
|
26695
27703
|
function linuxLaunchArgs(bin, path2) {
|
|
26696
27704
|
switch (bin) {
|
|
@@ -26716,7 +27724,7 @@ function linuxLaunchArgs(bin, path2) {
|
|
|
26716
27724
|
// src/api/services/fs-service.ts
|
|
26717
27725
|
var openPath = (path2) => {
|
|
26718
27726
|
const cmd = process.platform === "win32" ? "explorer" : process.platform === "darwin" ? "open" : "xdg-open";
|
|
26719
|
-
|
|
27727
|
+
spawnSync9(cmd, [path2], { stdio: "ignore" });
|
|
26720
27728
|
return { ok: true };
|
|
26721
27729
|
};
|
|
26722
27730
|
var listTerminals = async () => listTerminalApps();
|
|
@@ -26727,15 +27735,15 @@ var openTerminal = async (path2) => {
|
|
|
26727
27735
|
};
|
|
26728
27736
|
var listDir = async (path2, includeFiles, showHidden) => {
|
|
26729
27737
|
let target = resolve3(path2 || homedir4());
|
|
26730
|
-
while (target !== dirname7(target) && !(
|
|
27738
|
+
while (target !== dirname7(target) && !(existsSync13(target) && statSync(target).isDirectory())) {
|
|
26731
27739
|
target = dirname7(target);
|
|
26732
27740
|
}
|
|
26733
27741
|
const parent = dirname7(target);
|
|
26734
27742
|
const visible = (name) => showHidden || !name.startsWith(".");
|
|
26735
27743
|
try {
|
|
26736
27744
|
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:
|
|
27745
|
+
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));
|
|
27746
|
+
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
27747
|
return { current: target, parent: parent !== target ? parent : null, dirs, files };
|
|
26740
27748
|
} catch {
|
|
26741
27749
|
return { current: target, parent: parent !== target ? parent : null, dirs: [], files: [] };
|
|
@@ -26743,22 +27751,22 @@ var listDir = async (path2, includeFiles, showHidden) => {
|
|
|
26743
27751
|
};
|
|
26744
27752
|
|
|
26745
27753
|
// src/api/routes/fs.ts
|
|
26746
|
-
var fsController = new Hono2().post("/open", zv("json",
|
|
27754
|
+
var fsController = new Hono2().post("/open", zv("json", z8.object({ path: z8.string() })), async (c) => {
|
|
26747
27755
|
const { path: path2 } = c.req.valid("json");
|
|
26748
27756
|
return c.json(openPath(path2));
|
|
26749
27757
|
}).get("/terminals", async (c) => {
|
|
26750
27758
|
return c.json(await listTerminals());
|
|
26751
|
-
}).post("/open-terminal", zv("json",
|
|
27759
|
+
}).post("/open-terminal", zv("json", z8.object({ path: z8.string() })), async (c) => {
|
|
26752
27760
|
const { path: path2 } = c.req.valid("json");
|
|
26753
27761
|
return c.json(await openTerminal(path2));
|
|
26754
27762
|
}).get(
|
|
26755
27763
|
"/list-dir",
|
|
26756
27764
|
zv(
|
|
26757
27765
|
"query",
|
|
26758
|
-
|
|
26759
|
-
path:
|
|
26760
|
-
includeFiles:
|
|
26761
|
-
showHidden:
|
|
27766
|
+
z8.object({
|
|
27767
|
+
path: z8.string().optional().default(""),
|
|
27768
|
+
includeFiles: z8.coerce.boolean().optional(),
|
|
27769
|
+
showHidden: z8.coerce.boolean().optional()
|
|
26762
27770
|
})
|
|
26763
27771
|
),
|
|
26764
27772
|
async (c) => {
|
|
@@ -26769,7 +27777,7 @@ var fsController = new Hono2().post("/open", zv("json", z6.object({ path: z6.str
|
|
|
26769
27777
|
|
|
26770
27778
|
// src/api/routes/memory.ts
|
|
26771
27779
|
init_api_contract();
|
|
26772
|
-
import { z as
|
|
27780
|
+
import { z as z9 } from "zod";
|
|
26773
27781
|
|
|
26774
27782
|
// src/api/services/memory-service.ts
|
|
26775
27783
|
var listMemoryEntries = async (filter) => listMemories(filter);
|
|
@@ -26801,9 +27809,9 @@ var memoryController = new Hono2().get(
|
|
|
26801
27809
|
"/",
|
|
26802
27810
|
zv(
|
|
26803
27811
|
"query",
|
|
26804
|
-
|
|
27812
|
+
z9.object({
|
|
26805
27813
|
scope: memoryScopeSchema,
|
|
26806
|
-
workspaceId:
|
|
27814
|
+
workspaceId: z9.string().optional(),
|
|
26807
27815
|
status: memoryStatusSchema.optional()
|
|
26808
27816
|
})
|
|
26809
27817
|
),
|
|
@@ -26817,33 +27825,33 @@ var memoryController = new Hono2().get(
|
|
|
26817
27825
|
})
|
|
26818
27826
|
);
|
|
26819
27827
|
}
|
|
26820
|
-
).get("/search", zv("query",
|
|
27828
|
+
).get("/search", zv("query", z9.object({ query: z9.string(), workspaceId: z9.string().optional() })), async (c) => {
|
|
26821
27829
|
const input = c.req.valid("query");
|
|
26822
27830
|
return c.json(await searchMemoryEntries(input.query, input.workspaceId ?? null));
|
|
26823
|
-
}).get("/for-card", zv("query",
|
|
27831
|
+
}).get("/for-card", zv("query", z9.object({ cardId: z9.string() })), async (c) => {
|
|
26824
27832
|
const input = c.req.valid("query");
|
|
26825
27833
|
return c.json(await listMemoryEntriesForCard(input.cardId));
|
|
26826
|
-
}).get("/tags", async (c) => c.json(await listTagsEntries())).get("/workspace-tags", zv("query",
|
|
27834
|
+
}).get("/tags", async (c) => c.json(await listTagsEntries())).get("/workspace-tags", zv("query", z9.object({ workspaceId: z9.string() })), async (c) => {
|
|
26827
27835
|
const { workspaceId } = c.req.valid("query");
|
|
26828
27836
|
return c.json(await getWorkspaceTagsEntry(workspaceId));
|
|
26829
|
-
}).get("/:id", zv("param",
|
|
27837
|
+
}).get("/:id", zv("param", z9.object({ id: z9.string() })), async (c) => {
|
|
26830
27838
|
const { id } = c.req.valid("param");
|
|
26831
27839
|
return c.json(await getMemoryEntry(id));
|
|
26832
27840
|
}).post(
|
|
26833
27841
|
"/propose",
|
|
26834
27842
|
zv(
|
|
26835
27843
|
"json",
|
|
26836
|
-
|
|
27844
|
+
z9.object({
|
|
26837
27845
|
scope: memoryScopeSchema,
|
|
26838
|
-
workspaceId:
|
|
26839
|
-
originWorkspaceId:
|
|
27846
|
+
workspaceId: z9.string().optional(),
|
|
27847
|
+
originWorkspaceId: z9.string().optional(),
|
|
26840
27848
|
type: memoryTypeSchema,
|
|
26841
|
-
title:
|
|
26842
|
-
content:
|
|
27849
|
+
title: z9.string().min(1),
|
|
27850
|
+
content: z9.string().min(1),
|
|
26843
27851
|
sourceType: memorySourceTypeSchema.default("task_lesson"),
|
|
26844
|
-
importance:
|
|
26845
|
-
tags:
|
|
26846
|
-
originCardId:
|
|
27852
|
+
importance: z9.number().int().min(1).max(3).optional(),
|
|
27853
|
+
tags: z9.array(z9.string()).optional(),
|
|
27854
|
+
originCardId: z9.string().optional(),
|
|
26847
27855
|
originAgent: runtimeMemoryOriginAgentSchema.optional()
|
|
26848
27856
|
})
|
|
26849
27857
|
),
|
|
@@ -26875,12 +27883,12 @@ var memoryController = new Hono2().get(
|
|
|
26875
27883
|
"/propose-update",
|
|
26876
27884
|
zv(
|
|
26877
27885
|
"json",
|
|
26878
|
-
|
|
26879
|
-
id:
|
|
27886
|
+
z9.object({
|
|
27887
|
+
id: z9.string(),
|
|
26880
27888
|
type: memoryTypeSchema.optional(),
|
|
26881
|
-
title:
|
|
26882
|
-
content:
|
|
26883
|
-
importance:
|
|
27889
|
+
title: z9.string().min(1).optional(),
|
|
27890
|
+
content: z9.string().min(1).optional(),
|
|
27891
|
+
importance: z9.number().int().min(1).max(3).optional(),
|
|
26884
27892
|
sourceType: memorySourceTypeSchema.default("task_lesson")
|
|
26885
27893
|
})
|
|
26886
27894
|
),
|
|
@@ -26894,16 +27902,16 @@ var memoryController = new Hono2().get(
|
|
|
26894
27902
|
"/",
|
|
26895
27903
|
zv(
|
|
26896
27904
|
"json",
|
|
26897
|
-
|
|
27905
|
+
z9.object({
|
|
26898
27906
|
scope: memoryScopeSchema,
|
|
26899
|
-
workspaceId:
|
|
26900
|
-
originWorkspaceId:
|
|
27907
|
+
workspaceId: z9.string().optional(),
|
|
27908
|
+
originWorkspaceId: z9.string().optional(),
|
|
26901
27909
|
type: memoryTypeSchema,
|
|
26902
|
-
title:
|
|
26903
|
-
content:
|
|
26904
|
-
importance:
|
|
26905
|
-
tags:
|
|
26906
|
-
boundWorkspaceIds:
|
|
27910
|
+
title: z9.string().min(1),
|
|
27911
|
+
content: z9.string().min(1),
|
|
27912
|
+
importance: z9.number().int().min(1).max(3).optional(),
|
|
27913
|
+
tags: z9.array(z9.string()).optional(),
|
|
27914
|
+
boundWorkspaceIds: z9.array(z9.string()).optional()
|
|
26907
27915
|
})
|
|
26908
27916
|
),
|
|
26909
27917
|
async (c) => {
|
|
@@ -26932,14 +27940,14 @@ var memoryController = new Hono2().get(
|
|
|
26932
27940
|
}
|
|
26933
27941
|
).patch(
|
|
26934
27942
|
"/:id",
|
|
26935
|
-
zv("param",
|
|
27943
|
+
zv("param", z9.object({ id: z9.string() })),
|
|
26936
27944
|
zv(
|
|
26937
27945
|
"json",
|
|
26938
|
-
|
|
27946
|
+
z9.object({
|
|
26939
27947
|
type: memoryTypeSchema.optional(),
|
|
26940
|
-
title:
|
|
26941
|
-
content:
|
|
26942
|
-
importance:
|
|
27948
|
+
title: z9.string().min(1).optional(),
|
|
27949
|
+
content: z9.string().min(1).optional(),
|
|
27950
|
+
importance: z9.number().int().min(1).max(3).optional()
|
|
26943
27951
|
})
|
|
26944
27952
|
),
|
|
26945
27953
|
async (c) => {
|
|
@@ -26948,19 +27956,19 @@ var memoryController = new Hono2().get(
|
|
|
26948
27956
|
if (!updated) throw NotFoundError("Memory");
|
|
26949
27957
|
return c.json(updated);
|
|
26950
27958
|
}
|
|
26951
|
-
).post("/:id/approve", zv("param",
|
|
27959
|
+
).post("/:id/approve", zv("param", z9.object({ id: z9.string() })), async (c) => {
|
|
26952
27960
|
const { id } = c.req.valid("param");
|
|
26953
27961
|
const approved = await approveMemoryEntry(id);
|
|
26954
27962
|
if (!approved) throw NotFoundError("Memory");
|
|
26955
27963
|
return c.json(approved);
|
|
26956
|
-
}).delete("/:id", zv("param",
|
|
27964
|
+
}).delete("/:id", zv("param", z9.object({ id: z9.string() })), async (c) => {
|
|
26957
27965
|
const { id } = c.req.valid("param");
|
|
26958
27966
|
await removeMemoryEntry(id);
|
|
26959
27967
|
return c.json({ ok: true });
|
|
26960
27968
|
}).patch(
|
|
26961
27969
|
"/:id/tags",
|
|
26962
|
-
zv("param",
|
|
26963
|
-
zv("json",
|
|
27970
|
+
zv("param", z9.object({ id: z9.string() })),
|
|
27971
|
+
zv("json", z9.object({ tags: z9.array(z9.string()) })),
|
|
26964
27972
|
async (c) => {
|
|
26965
27973
|
const { id } = c.req.valid("param");
|
|
26966
27974
|
const { tags } = c.req.valid("json");
|
|
@@ -26970,8 +27978,8 @@ var memoryController = new Hono2().get(
|
|
|
26970
27978
|
}
|
|
26971
27979
|
).patch(
|
|
26972
27980
|
"/:id/bindings",
|
|
26973
|
-
zv("param",
|
|
26974
|
-
zv("json",
|
|
27981
|
+
zv("param", z9.object({ id: z9.string() })),
|
|
27982
|
+
zv("json", z9.object({ workspaceIds: z9.array(z9.string()) })),
|
|
26975
27983
|
async (c) => {
|
|
26976
27984
|
const { id } = c.req.valid("param");
|
|
26977
27985
|
const { workspaceIds } = c.req.valid("json");
|
|
@@ -26979,7 +27987,7 @@ var memoryController = new Hono2().get(
|
|
|
26979
27987
|
if (!updated) throw NotFoundError("Memory");
|
|
26980
27988
|
return c.json(updated);
|
|
26981
27989
|
}
|
|
26982
|
-
).put("/workspace-tags", zv("json",
|
|
27990
|
+
).put("/workspace-tags", zv("json", z9.object({ workspaceId: z9.string(), tags: z9.array(z9.string()) })), async (c) => {
|
|
26983
27991
|
const { workspaceId, tags } = c.req.valid("json");
|
|
26984
27992
|
await setWorkspaceTagsEntry(workspaceId, tags);
|
|
26985
27993
|
return c.json({ ok: true });
|
|
@@ -26987,7 +27995,7 @@ var memoryController = new Hono2().get(
|
|
|
26987
27995
|
|
|
26988
27996
|
// src/api/routes/project-config.ts
|
|
26989
27997
|
init_api_contract();
|
|
26990
|
-
import { z as
|
|
27998
|
+
import { z as z10 } from "zod";
|
|
26991
27999
|
|
|
26992
28000
|
// src/api/services/project-config-service.ts
|
|
26993
28001
|
init_workspace_state();
|
|
@@ -27007,21 +28015,21 @@ var setSystemPrompt = async (workspaceId, prompt) => {
|
|
|
27007
28015
|
};
|
|
27008
28016
|
|
|
27009
28017
|
// src/api/routes/project-config.ts
|
|
27010
|
-
var projectConfigController = new Hono2().get("/", zv("query",
|
|
28018
|
+
var projectConfigController = new Hono2().get("/", zv("query", z10.object({ workspaceId: z10.string() })), async (c) => {
|
|
27011
28019
|
return c.json(await getProjectConfig(c.req.valid("query").workspaceId));
|
|
27012
|
-
}).put("/", zv("json",
|
|
28020
|
+
}).put("/", zv("json", z10.object({ workspaceId: z10.string(), config: runtimeProjectConfigSchema })), async (c) => {
|
|
27013
28021
|
const ctx = c.var.ctx;
|
|
27014
28022
|
const { workspaceId, config } = c.req.valid("json");
|
|
27015
28023
|
await saveProjectConfig2(workspaceId, config);
|
|
27016
28024
|
ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
|
|
27017
28025
|
return c.json({ ok: true });
|
|
27018
|
-
}).post("/git-instructions", zv("json",
|
|
28026
|
+
}).post("/git-instructions", zv("json", z10.object({ workspaceId: z10.string(), instructions: z10.string() })), async (c) => {
|
|
27019
28027
|
const ctx = c.var.ctx;
|
|
27020
28028
|
const { workspaceId, instructions } = c.req.valid("json");
|
|
27021
28029
|
const { cleared } = await setGitInstructions(workspaceId, instructions);
|
|
27022
28030
|
ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
|
|
27023
28031
|
return c.json({ ok: true, cleared });
|
|
27024
|
-
}).post("/system-prompt", zv("json",
|
|
28032
|
+
}).post("/system-prompt", zv("json", z10.object({ workspaceId: z10.string(), prompt: z10.string() })), async (c) => {
|
|
27025
28033
|
const ctx = c.var.ctx;
|
|
27026
28034
|
const { workspaceId, prompt } = c.req.valid("json");
|
|
27027
28035
|
const { cleared } = await setSystemPrompt(workspaceId, prompt);
|
|
@@ -27031,12 +28039,12 @@ var projectConfigController = new Hono2().get("/", zv("query", z8.object({ works
|
|
|
27031
28039
|
|
|
27032
28040
|
// src/api/routes/projects.ts
|
|
27033
28041
|
init_api_contract();
|
|
27034
|
-
import { z as
|
|
28042
|
+
import { z as z11 } from "zod";
|
|
27035
28043
|
|
|
27036
28044
|
// src/api/services/projects-service.ts
|
|
27037
28045
|
init_runtime_config();
|
|
27038
28046
|
init_api_contract();
|
|
27039
|
-
import { spawnSync as
|
|
28047
|
+
import { spawnSync as spawnSync10 } from "node:child_process";
|
|
27040
28048
|
init_logger();
|
|
27041
28049
|
|
|
27042
28050
|
// src/state/projects-layout.ts
|
|
@@ -27119,7 +28127,7 @@ var checkProjectPath = async (repoPath) => {
|
|
|
27119
28127
|
branches: []
|
|
27120
28128
|
};
|
|
27121
28129
|
}
|
|
27122
|
-
const r =
|
|
28130
|
+
const r = spawnSync10("git", ["rev-parse", "--git-dir"], {
|
|
27123
28131
|
cwd: repoPath,
|
|
27124
28132
|
encoding: "utf-8",
|
|
27125
28133
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -27135,12 +28143,12 @@ var checkProjectPath = async (repoPath) => {
|
|
|
27135
28143
|
remote: null,
|
|
27136
28144
|
branches: []
|
|
27137
28145
|
};
|
|
27138
|
-
const branchR =
|
|
28146
|
+
const branchR = spawnSync10("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
27139
28147
|
cwd: repoPath,
|
|
27140
28148
|
encoding: "utf-8",
|
|
27141
28149
|
stdio: ["ignore", "pipe", "pipe"]
|
|
27142
28150
|
});
|
|
27143
|
-
const remoteR =
|
|
28151
|
+
const remoteR = spawnSync10("git", ["remote", "get-url", "origin"], {
|
|
27144
28152
|
cwd: repoPath,
|
|
27145
28153
|
encoding: "utf-8",
|
|
27146
28154
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -27159,7 +28167,7 @@ var addProject = async (repoPath, initialConfig) => {
|
|
|
27159
28167
|
} catch {
|
|
27160
28168
|
throw BadRequestError(`Path does not exist: ${repoPath}`);
|
|
27161
28169
|
}
|
|
27162
|
-
const r =
|
|
28170
|
+
const r = spawnSync10("git", ["rev-parse", "--git-dir"], {
|
|
27163
28171
|
cwd: repoPath,
|
|
27164
28172
|
encoding: "utf-8",
|
|
27165
28173
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -27191,7 +28199,7 @@ var removeProject = async (workspaceId) => {
|
|
|
27191
28199
|
const cards = boardCards;
|
|
27192
28200
|
enqueueCleanup2(async () => {
|
|
27193
28201
|
const { rm: rm3 } = await import("node:fs/promises");
|
|
27194
|
-
const { join:
|
|
28202
|
+
const { join: join24 } = await import("node:path");
|
|
27195
28203
|
for (const [cardId, card] of Object.entries(cards)) {
|
|
27196
28204
|
if (resolveWorktreeOwnerId(cardId, cards) === cardId) {
|
|
27197
28205
|
await removeWorktreeAsync(cardId, repoPath, card.branchName).catch((err) => {
|
|
@@ -27199,11 +28207,11 @@ var removeProject = async (workspaceId) => {
|
|
|
27199
28207
|
});
|
|
27200
28208
|
}
|
|
27201
28209
|
}
|
|
27202
|
-
await rm3(
|
|
28210
|
+
await rm3(join24(WORKSPACES_DIR, workspaceId), { recursive: true, force: true }).catch((err) => {
|
|
27203
28211
|
logger.warn(`[cleanup:project:${workspaceId}] workspace dir failed: ${String(err)}`);
|
|
27204
28212
|
});
|
|
27205
28213
|
for (const cardId of Object.keys(cards)) {
|
|
27206
|
-
await rm3(
|
|
28214
|
+
await rm3(join24(ATTACHMENTS_DIR, cardId), { recursive: true, force: true }).catch(() => {
|
|
27207
28215
|
});
|
|
27208
28216
|
}
|
|
27209
28217
|
logger.info(`[cleanup:project:${workspaceId}] done`);
|
|
@@ -27217,14 +28225,14 @@ var projectsController = new Hono2().get("/", async (c) => {
|
|
|
27217
28225
|
return c.json(await listProjects());
|
|
27218
28226
|
}).get("/layout", (c) => {
|
|
27219
28227
|
return c.json(getProjectsLayout());
|
|
27220
|
-
}).get("/check-path", zv("query",
|
|
28228
|
+
}).get("/check-path", zv("query", z11.object({ repoPath: z11.string() })), async (c) => {
|
|
27221
28229
|
return c.json(await checkProjectPath(c.req.valid("query").repoPath));
|
|
27222
28230
|
}).post(
|
|
27223
28231
|
"/",
|
|
27224
28232
|
zv(
|
|
27225
28233
|
"json",
|
|
27226
|
-
|
|
27227
|
-
repoPath:
|
|
28234
|
+
z11.object({
|
|
28235
|
+
repoPath: z11.string().min(1),
|
|
27228
28236
|
initialConfig: runtimeProjectConfigSchema.partial().optional()
|
|
27229
28237
|
})
|
|
27230
28238
|
),
|
|
@@ -27251,7 +28259,7 @@ var projectsController = new Hono2().get("/", async (c) => {
|
|
|
27251
28259
|
|
|
27252
28260
|
// src/api/routes/recurring-agents.ts
|
|
27253
28261
|
init_api_contract();
|
|
27254
|
-
import { z as
|
|
28262
|
+
import { z as z12 } from "zod";
|
|
27255
28263
|
|
|
27256
28264
|
// src/api/services/recurring-agents-service.ts
|
|
27257
28265
|
function listRecurringAgentsEntry(workspaceId) {
|
|
@@ -27276,20 +28284,20 @@ function setRecurringAgentJournalEntry(id, journal) {
|
|
|
27276
28284
|
}
|
|
27277
28285
|
|
|
27278
28286
|
// src/api/routes/recurring-agents.ts
|
|
27279
|
-
var recurringAgentsController = new Hono2().get("/", zv("query",
|
|
28287
|
+
var recurringAgentsController = new Hono2().get("/", zv("query", z12.object({ workspaceId: z12.string() })), async (c) => {
|
|
27280
28288
|
const { workspaceId } = c.req.valid("query");
|
|
27281
28289
|
return c.json(listRecurringAgentsEntry(workspaceId));
|
|
27282
|
-
}).get("/:id", zv("param",
|
|
28290
|
+
}).get("/:id", zv("param", z12.object({ id: z12.string() })), async (c) => {
|
|
27283
28291
|
const { id } = c.req.valid("param");
|
|
27284
28292
|
const agent = getRecurringAgentEntry(id);
|
|
27285
28293
|
if (!agent) throw NotFoundError("Recurring agent");
|
|
27286
28294
|
return c.json(agent);
|
|
27287
|
-
}).post("/", zv("json", recurringAgentCreateRequestSchema.extend({ workspaceId:
|
|
28295
|
+
}).post("/", zv("json", recurringAgentCreateRequestSchema.extend({ workspaceId: z12.string() })), async (c) => {
|
|
27288
28296
|
const { workspaceId, ...req } = c.req.valid("json");
|
|
27289
28297
|
return c.json(createRecurringAgentEntry(workspaceId, req));
|
|
27290
28298
|
}).patch(
|
|
27291
28299
|
"/:id",
|
|
27292
|
-
zv("param",
|
|
28300
|
+
zv("param", z12.object({ id: z12.string() })),
|
|
27293
28301
|
zv("json", recurringAgentUpdateRequestSchema.omit({ id: true })),
|
|
27294
28302
|
async (c) => {
|
|
27295
28303
|
const { id } = c.req.valid("param");
|
|
@@ -27299,8 +28307,8 @@ var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ wo
|
|
|
27299
28307
|
}
|
|
27300
28308
|
).post(
|
|
27301
28309
|
"/:id/journal",
|
|
27302
|
-
zv("param",
|
|
27303
|
-
zv("json",
|
|
28310
|
+
zv("param", z12.object({ id: z12.string() })),
|
|
28311
|
+
zv("json", z12.object({ journal: z12.string() })),
|
|
27304
28312
|
async (c) => {
|
|
27305
28313
|
const { id } = c.req.valid("param");
|
|
27306
28314
|
const { journal } = c.req.valid("json");
|
|
@@ -27310,8 +28318,8 @@ var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ wo
|
|
|
27310
28318
|
}
|
|
27311
28319
|
).post(
|
|
27312
28320
|
"/:id/run",
|
|
27313
|
-
zv("param",
|
|
27314
|
-
zv("query",
|
|
28321
|
+
zv("param", z12.object({ id: z12.string() })),
|
|
28322
|
+
zv("query", z12.object({ workspaceId: z12.string() })),
|
|
27315
28323
|
async (c) => {
|
|
27316
28324
|
const { id } = c.req.valid("param");
|
|
27317
28325
|
const { workspaceId } = c.req.valid("query");
|
|
@@ -27320,14 +28328,14 @@ var recurringAgentsController = new Hono2().get("/", zv("query", z10.object({ wo
|
|
|
27320
28328
|
const started = await scheduler?.runNow(id) ?? false;
|
|
27321
28329
|
return c.json({ started });
|
|
27322
28330
|
}
|
|
27323
|
-
).delete("/:id", zv("param",
|
|
28331
|
+
).delete("/:id", zv("param", z12.object({ id: z12.string() })), async (c) => {
|
|
27324
28332
|
const { id } = c.req.valid("param");
|
|
27325
28333
|
deleteRecurringAgentEntry(id);
|
|
27326
28334
|
return c.json({ ok: true });
|
|
27327
28335
|
});
|
|
27328
28336
|
|
|
27329
28337
|
// src/api/routes/run.ts
|
|
27330
|
-
import { z as
|
|
28338
|
+
import { z as z13 } from "zod";
|
|
27331
28339
|
|
|
27332
28340
|
// src/api/services/run-service.ts
|
|
27333
28341
|
init_workspace_state();
|
|
@@ -27343,15 +28351,19 @@ var resolveCardCwd = async (workspaceId, cardId, repoPath) => {
|
|
|
27343
28351
|
if (!card) return null;
|
|
27344
28352
|
return card.worktreePath ?? repoPath;
|
|
27345
28353
|
};
|
|
28354
|
+
var resolveCompanionSessionCwd = (sessionId) => {
|
|
28355
|
+
const session = getCompanionSession(sessionId);
|
|
28356
|
+
return session?.worktreePath ?? null;
|
|
28357
|
+
};
|
|
27346
28358
|
|
|
27347
28359
|
// src/api/routes/run.ts
|
|
27348
|
-
var runController = new Hono2().get("/status", zv("query",
|
|
28360
|
+
var runController = new Hono2().get("/status", zv("query", z13.object({ workspaceId: z13.string() })), (c) => {
|
|
27349
28361
|
const ctx = c.var.ctx;
|
|
27350
28362
|
const { workspaceId } = c.req.valid("query");
|
|
27351
28363
|
const session = ctx.getRunSession(workspaceId);
|
|
27352
28364
|
if (!session) return c.json({ cardId: null, status: "stopped", errorMessage: void 0 });
|
|
27353
28365
|
return c.json({ cardId: session.cardId, status: session.status, errorMessage: session.errorMessage });
|
|
27354
|
-
}).post("/start", zv("json",
|
|
28366
|
+
}).post("/start", zv("json", z13.object({ workspaceId: z13.string(), cardId: z13.string() })), async (c) => {
|
|
27355
28367
|
const ctx = c.var.ctx;
|
|
27356
28368
|
const { workspaceId, cardId } = c.req.valid("json");
|
|
27357
28369
|
const ws = await ctx.ensureWorkspace(workspaceId);
|
|
@@ -27363,7 +28375,18 @@ var runController = new Hono2().get("/status", zv("query", z11.object({ workspac
|
|
|
27363
28375
|
if (cwd === null) throw NotFoundError("Card");
|
|
27364
28376
|
ctx.startRun(workspaceId, cardId, command, cwd);
|
|
27365
28377
|
return c.json({ ok: true });
|
|
27366
|
-
}).post("/start-
|
|
28378
|
+
}).post("/start-companion", zv("json", z13.object({ workspaceId: z13.string(), sessionId: z13.string() })), async (c) => {
|
|
28379
|
+
const ctx = c.var.ctx;
|
|
28380
|
+
const { workspaceId, sessionId } = c.req.valid("json");
|
|
28381
|
+
const command = await resolveStartCommand(workspaceId);
|
|
28382
|
+
if (!command) {
|
|
28383
|
+
throw PreconditionFailedError("No start command configured. Add one in Settings \u2192 Environment.");
|
|
28384
|
+
}
|
|
28385
|
+
const cwd = resolveCompanionSessionCwd(sessionId);
|
|
28386
|
+
if (cwd === null) throw NotFoundError("Companion session");
|
|
28387
|
+
ctx.startRun(workspaceId, sessionId, command, cwd);
|
|
28388
|
+
return c.json({ ok: true });
|
|
28389
|
+
}).post("/start-base", zv("json", z13.object({ workspaceId: z13.string() })), async (c) => {
|
|
27367
28390
|
const ctx = c.var.ctx;
|
|
27368
28391
|
const { workspaceId } = c.req.valid("json");
|
|
27369
28392
|
const ws = await ctx.ensureWorkspace(workspaceId);
|
|
@@ -27373,7 +28396,7 @@ var runController = new Hono2().get("/status", zv("query", z11.object({ workspac
|
|
|
27373
28396
|
}
|
|
27374
28397
|
ctx.startRun(workspaceId, null, command, ws.repoPath);
|
|
27375
28398
|
return c.json({ ok: true });
|
|
27376
|
-
}).post("/stop", zv("json",
|
|
28399
|
+
}).post("/stop", zv("json", z13.object({ workspaceId: z13.string() })), (c) => {
|
|
27377
28400
|
const ctx = c.var.ctx;
|
|
27378
28401
|
const { workspaceId } = c.req.valid("json");
|
|
27379
28402
|
ctx.stopRun(workspaceId);
|
|
@@ -27381,7 +28404,7 @@ var runController = new Hono2().get("/status", zv("query", z11.object({ workspac
|
|
|
27381
28404
|
});
|
|
27382
28405
|
|
|
27383
28406
|
// src/api/routes/slack.ts
|
|
27384
|
-
import { z as
|
|
28407
|
+
import { z as z14 } from "zod";
|
|
27385
28408
|
|
|
27386
28409
|
// src/api/services/slack-service.ts
|
|
27387
28410
|
init_runtime_config();
|
|
@@ -27428,20 +28451,20 @@ var createApp = async (appConfigToken, publicUrl, botName) => {
|
|
|
27428
28451
|
var slackController = new Hono2().post("/resetApp", async (c) => {
|
|
27429
28452
|
await resetApp();
|
|
27430
28453
|
return c.json({ ok: true });
|
|
27431
|
-
}).post("/updateSigningSecret", zv("json",
|
|
28454
|
+
}).post("/updateSigningSecret", zv("json", z14.object({ signingSecret: z14.string().min(1) })), async (c) => {
|
|
27432
28455
|
await updateSigningSecret(c.req.valid("json").signingSecret);
|
|
27433
28456
|
return c.json({ ok: true });
|
|
27434
28457
|
}).post(
|
|
27435
28458
|
"/importCredentials",
|
|
27436
28459
|
zv(
|
|
27437
28460
|
"json",
|
|
27438
|
-
|
|
27439
|
-
slackAppId:
|
|
27440
|
-
slackClientId:
|
|
27441
|
-
slackClientSecret:
|
|
27442
|
-
slackSigningSecret:
|
|
27443
|
-
slackOauthAuthorizeUrl:
|
|
27444
|
-
slackPublicUrl:
|
|
28461
|
+
z14.object({
|
|
28462
|
+
slackAppId: z14.string(),
|
|
28463
|
+
slackClientId: z14.string(),
|
|
28464
|
+
slackClientSecret: z14.string(),
|
|
28465
|
+
slackSigningSecret: z14.string(),
|
|
28466
|
+
slackOauthAuthorizeUrl: z14.string(),
|
|
28467
|
+
slackPublicUrl: z14.string()
|
|
27445
28468
|
})
|
|
27446
28469
|
),
|
|
27447
28470
|
async (c) => {
|
|
@@ -27450,7 +28473,7 @@ var slackController = new Hono2().post("/resetApp", async (c) => {
|
|
|
27450
28473
|
}
|
|
27451
28474
|
).post(
|
|
27452
28475
|
"/createApp",
|
|
27453
|
-
zv("json",
|
|
28476
|
+
zv("json", z14.object({ appConfigToken: z14.string(), publicUrl: z14.string(), botName: z14.string().default("Whipped") })),
|
|
27454
28477
|
async (c) => {
|
|
27455
28478
|
const { appConfigToken, publicUrl, botName } = c.req.valid("json");
|
|
27456
28479
|
return c.json(await createApp(appConfigToken, publicUrl, botName));
|
|
@@ -27458,27 +28481,27 @@ var slackController = new Hono2().post("/resetApp", async (c) => {
|
|
|
27458
28481
|
);
|
|
27459
28482
|
|
|
27460
28483
|
// src/api/routes/terminal.ts
|
|
27461
|
-
import { z as
|
|
28484
|
+
import { z as z15 } from "zod";
|
|
27462
28485
|
|
|
27463
28486
|
// src/api/services/terminal-service.ts
|
|
27464
28487
|
var toBufferResponse = (buffer) => ({ data: buffer ?? "" });
|
|
27465
28488
|
|
|
27466
28489
|
// src/api/routes/terminal.ts
|
|
27467
|
-
var terminalController = new Hono2().get("/buffer", zv("query",
|
|
28490
|
+
var terminalController = new Hono2().get("/buffer", zv("query", z15.object({ workspaceId: z15.string(), taskId: z15.string() })), (c) => {
|
|
27468
28491
|
const ctx = c.var.ctx;
|
|
27469
28492
|
const { workspaceId, taskId } = c.req.valid("query");
|
|
27470
28493
|
const buf = ctx.getScheduler(workspaceId)?.getOutputBuffer(taskId);
|
|
27471
28494
|
return c.json(toBufferResponse(buf));
|
|
27472
28495
|
}).post(
|
|
27473
28496
|
"/resize",
|
|
27474
|
-
zv("json",
|
|
28497
|
+
zv("json", z15.object({ workspaceId: z15.string(), taskId: z15.string(), cols: z15.number(), rows: z15.number() })),
|
|
27475
28498
|
(c) => {
|
|
27476
28499
|
const ctx = c.var.ctx;
|
|
27477
28500
|
const { workspaceId, taskId, cols, rows } = c.req.valid("json");
|
|
27478
28501
|
ctx.getScheduler(workspaceId)?.resizeTerminal(taskId, cols, rows);
|
|
27479
28502
|
return c.json({ ok: true });
|
|
27480
28503
|
}
|
|
27481
|
-
).post("/input", zv("json",
|
|
28504
|
+
).post("/input", zv("json", z15.object({ workspaceId: z15.string(), taskId: z15.string(), data: z15.string() })), (c) => {
|
|
27482
28505
|
const ctx = c.var.ctx;
|
|
27483
28506
|
const { workspaceId, taskId, data } = c.req.valid("json");
|
|
27484
28507
|
ctx.getScheduler(workspaceId)?.writeToTerminal(taskId, data);
|
|
@@ -27486,7 +28509,7 @@ var terminalController = new Hono2().get("/buffer", zv("query", z13.object({ wor
|
|
|
27486
28509
|
});
|
|
27487
28510
|
|
|
27488
28511
|
// src/api/routes/tunnel.ts
|
|
27489
|
-
import { z as
|
|
28512
|
+
import { z as z16 } from "zod";
|
|
27490
28513
|
|
|
27491
28514
|
// src/api/services/tunnel-service.ts
|
|
27492
28515
|
init_runtime_config();
|
|
@@ -27495,12 +28518,12 @@ init_runtime_config();
|
|
|
27495
28518
|
import { execFile as execFile9, spawn as spawn7 } from "node:child_process";
|
|
27496
28519
|
import { mkdir as mkdir4, writeFile as writeFile3, readFile as readFile3, access, unlink as unlink4 } from "node:fs/promises";
|
|
27497
28520
|
import { homedir as homedir5 } from "node:os";
|
|
27498
|
-
import { join as
|
|
28521
|
+
import { join as join21 } from "node:path";
|
|
27499
28522
|
import { promisify as promisify9 } from "node:util";
|
|
27500
28523
|
init_runtime_config();
|
|
27501
28524
|
init_logger();
|
|
27502
28525
|
var execFileAsync7 = promisify9(execFile9);
|
|
27503
|
-
var CLOUDFLARED_DIR =
|
|
28526
|
+
var CLOUDFLARED_DIR = join21(homedir5(), ".cloudflared");
|
|
27504
28527
|
async function checkCloudflaredInstalled() {
|
|
27505
28528
|
try {
|
|
27506
28529
|
const { stdout } = await execFileAsync7("cloudflared", ["--version"]);
|
|
@@ -27512,7 +28535,7 @@ async function checkCloudflaredInstalled() {
|
|
|
27512
28535
|
}
|
|
27513
28536
|
async function checkCloudflaredAuth() {
|
|
27514
28537
|
try {
|
|
27515
|
-
await access(
|
|
28538
|
+
await access(join21(CLOUDFLARED_DIR, "cert.pem"));
|
|
27516
28539
|
return true;
|
|
27517
28540
|
} catch {
|
|
27518
28541
|
return false;
|
|
@@ -27523,7 +28546,7 @@ async function openCloudflaredLogin(force = false) {
|
|
|
27523
28546
|
if (alreadyLoggedIn && !force) return { alreadyLoggedIn: true };
|
|
27524
28547
|
if (force) {
|
|
27525
28548
|
try {
|
|
27526
|
-
await unlink4(
|
|
28549
|
+
await unlink4(join21(CLOUDFLARED_DIR, "cert.pem"));
|
|
27527
28550
|
} catch {
|
|
27528
28551
|
}
|
|
27529
28552
|
}
|
|
@@ -27592,7 +28615,7 @@ async function createTunnel(tunnelName) {
|
|
|
27592
28615
|
}
|
|
27593
28616
|
}
|
|
27594
28617
|
async function writeTunnelConfig(tunnelId, tunnelName, domain) {
|
|
27595
|
-
const credentialsFile =
|
|
28618
|
+
const credentialsFile = join21(CLOUDFLARED_DIR, `${tunnelId}.json`);
|
|
27596
28619
|
const config = [
|
|
27597
28620
|
`tunnel: ${tunnelId}`,
|
|
27598
28621
|
`credentials-file: ${credentialsFile}`,
|
|
@@ -27603,12 +28626,12 @@ async function writeTunnelConfig(tunnelId, tunnelName, domain) {
|
|
|
27603
28626
|
` - service: http_status:404`
|
|
27604
28627
|
].join("\n");
|
|
27605
28628
|
await mkdir4(CLOUDFLARED_DIR, { recursive: true });
|
|
27606
|
-
await writeFile3(
|
|
28629
|
+
await writeFile3(join21(CLOUDFLARED_DIR, "config.yml"), config, "utf-8");
|
|
27607
28630
|
logger.info(`[tunnel-setup] Wrote ~/.cloudflared/config.yml for tunnel ${tunnelName}`);
|
|
27608
28631
|
}
|
|
27609
28632
|
async function readTunnelConfig() {
|
|
27610
28633
|
try {
|
|
27611
|
-
const raw2 = await readFile3(
|
|
28634
|
+
const raw2 = await readFile3(join21(CLOUDFLARED_DIR, "config.yml"), "utf-8");
|
|
27612
28635
|
const tunnelMatch = raw2.match(/^tunnel:\s*(.+)$/m);
|
|
27613
28636
|
const hostnameMatch = raw2.match(/hostname:\s*(.+)$/m);
|
|
27614
28637
|
return {
|
|
@@ -27657,9 +28680,9 @@ var resetTunnel = async () => {
|
|
|
27657
28680
|
await updateGlobalConfig({ tunnelId: void 0, tunnelDomain: void 0, autoStartTunnel: false });
|
|
27658
28681
|
const { unlink: unlink5 } = await import("node:fs/promises");
|
|
27659
28682
|
const { homedir: homedir6 } = await import("node:os");
|
|
27660
|
-
const { join:
|
|
28683
|
+
const { join: join24 } = await import("node:path");
|
|
27661
28684
|
try {
|
|
27662
|
-
await unlink5(
|
|
28685
|
+
await unlink5(join24(homedir6(), ".cloudflared", "config.yml"));
|
|
27663
28686
|
} catch {
|
|
27664
28687
|
}
|
|
27665
28688
|
};
|
|
@@ -27671,9 +28694,9 @@ var tunnelController = new Hono2().get("/checkCloudflared", async (c) => {
|
|
|
27671
28694
|
return c.json(await getTunnelConfig());
|
|
27672
28695
|
}).get("/tunnelStatus", async (c) => {
|
|
27673
28696
|
return c.json(await getTunnelStatus());
|
|
27674
|
-
}).post("/cloudflaredLogin", zv("json",
|
|
28697
|
+
}).post("/cloudflaredLogin", zv("json", z16.object({ force: z16.boolean().default(false) })), async (c) => {
|
|
27675
28698
|
return c.json(await cloudflaredLogin(c.req.valid("json").force));
|
|
27676
|
-
}).post("/createTunnel", zv("json",
|
|
28699
|
+
}).post("/createTunnel", zv("json", z16.object({ domain: z16.string() })), async (c) => {
|
|
27677
28700
|
return c.json(await createTunnel2(c.req.valid("json").domain));
|
|
27678
28701
|
}).post("/startTunnel", async (c) => {
|
|
27679
28702
|
return c.json(await startTunnel());
|
|
@@ -27686,11 +28709,11 @@ var tunnelController = new Hono2().get("/checkCloudflared", async (c) => {
|
|
|
27686
28709
|
|
|
27687
28710
|
// src/api/routes/workflows.ts
|
|
27688
28711
|
init_api_contract();
|
|
27689
|
-
import { z as
|
|
28712
|
+
import { z as z17 } from "zod";
|
|
27690
28713
|
|
|
27691
28714
|
// src/api/services/workflows-service.ts
|
|
27692
28715
|
init_workspace_state();
|
|
27693
|
-
import { existsSync as
|
|
28716
|
+
import { existsSync as existsSync14 } from "node:fs";
|
|
27694
28717
|
import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
|
|
27695
28718
|
import { dirname as dirname8, isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
|
|
27696
28719
|
var resolvePromptPath = async (workspaceId, requestedPath) => {
|
|
@@ -27735,22 +28758,22 @@ var writePromptFile = async (workspaceId, path2, content) => {
|
|
|
27735
28758
|
};
|
|
27736
28759
|
var readPromptFile = async (workspaceId, path2) => {
|
|
27737
28760
|
const targetPath = await resolvePromptPath(workspaceId, path2);
|
|
27738
|
-
if (!
|
|
28761
|
+
if (!existsSync14(targetPath)) return { content: "", exists: false };
|
|
27739
28762
|
const content = await readFile4(targetPath, "utf-8");
|
|
27740
28763
|
return { content, exists: true };
|
|
27741
28764
|
};
|
|
27742
28765
|
|
|
27743
28766
|
// src/api/routes/workflows.ts
|
|
27744
|
-
var workflowsController = new Hono2().get("/", zv("query",
|
|
28767
|
+
var workflowsController = new Hono2().get("/", zv("query", z17.object({ workspaceId: z17.string() })), async (c) => {
|
|
27745
28768
|
const { workspaceId } = c.req.valid("query");
|
|
27746
28769
|
return c.json(await listWorkflows(workspaceId));
|
|
27747
|
-
}).post("/", zv("json",
|
|
28770
|
+
}).post("/", zv("json", z17.object({ workspaceId: z17.string(), workflow: workflowSchema })), async (c) => {
|
|
27748
28771
|
const ctx = c.var.ctx;
|
|
27749
28772
|
const { workspaceId, workflow } = c.req.valid("json");
|
|
27750
28773
|
const result = await upsertWorkflow(workspaceId, workflow);
|
|
27751
28774
|
ctx.stateHub.broadcastWorkspaceUpdate(workspaceId);
|
|
27752
28775
|
return c.json(result);
|
|
27753
|
-
}).delete("/:workflowId", zv("query",
|
|
28776
|
+
}).delete("/:workflowId", zv("query", z17.object({ workspaceId: z17.string() })), async (c) => {
|
|
27754
28777
|
const ctx = c.var.ctx;
|
|
27755
28778
|
const { workspaceId } = c.req.valid("query");
|
|
27756
28779
|
const workflowId = c.req.param("workflowId");
|
|
@@ -27759,23 +28782,23 @@ var workflowsController = new Hono2().get("/", zv("query", z15.object({ workspac
|
|
|
27759
28782
|
return c.json(result);
|
|
27760
28783
|
}).post(
|
|
27761
28784
|
"/prompt-file",
|
|
27762
|
-
zv("json",
|
|
28785
|
+
zv("json", z17.object({ workspaceId: z17.string(), path: z17.string().min(1), content: z17.string() })),
|
|
27763
28786
|
async (c) => {
|
|
27764
28787
|
const { workspaceId, path: path2, content } = c.req.valid("json");
|
|
27765
28788
|
return c.json(await writePromptFile(workspaceId, path2, content));
|
|
27766
28789
|
}
|
|
27767
|
-
).get("/prompt-file", zv("query",
|
|
28790
|
+
).get("/prompt-file", zv("query", z17.object({ workspaceId: z17.string(), path: z17.string().min(1) })), async (c) => {
|
|
27768
28791
|
const { workspaceId, path: path2 } = c.req.valid("query");
|
|
27769
28792
|
return c.json(await readPromptFile(workspaceId, path2));
|
|
27770
28793
|
});
|
|
27771
28794
|
|
|
27772
28795
|
// src/api/routes/workspace.ts
|
|
27773
28796
|
init_api_contract();
|
|
27774
|
-
import { z as
|
|
28797
|
+
import { z as z18 } from "zod";
|
|
27775
28798
|
|
|
27776
28799
|
// src/api/services/workspace-service.ts
|
|
27777
28800
|
init_workspace_state();
|
|
27778
|
-
import { spawnSync as
|
|
28801
|
+
import { spawnSync as spawnSync11 } from "node:child_process";
|
|
27779
28802
|
var loadStateForWorkspace = async (workspaceId) => {
|
|
27780
28803
|
const workspaces = await listWorkspaces();
|
|
27781
28804
|
const ws = workspaces.find((w2) => w2.workspaceId === workspaceId);
|
|
@@ -27785,12 +28808,12 @@ var loadStateForWorkspace = async (workspaceId) => {
|
|
|
27785
28808
|
var loadStateForContext = async (workspaceId, repoPath) => loadWorkspaceState(workspaceId, repoPath);
|
|
27786
28809
|
var saveState = async (workspaceId, request2) => saveWorkspaceState(workspaceId, request2);
|
|
27787
28810
|
var listRootFiles = (repoPath) => {
|
|
27788
|
-
const ignored =
|
|
28811
|
+
const ignored = spawnSync11(
|
|
27789
28812
|
"git",
|
|
27790
28813
|
["ls-files", "--others", "--ignored", "--exclude-standard", "--directory", "--no-empty-directory"],
|
|
27791
28814
|
{ cwd: repoPath, encoding: "utf-8" }
|
|
27792
28815
|
);
|
|
27793
|
-
const untracked =
|
|
28816
|
+
const untracked = spawnSync11("git", ["ls-files", "--others", "--exclude-standard"], {
|
|
27794
28817
|
cwd: repoPath,
|
|
27795
28818
|
encoding: "utf-8"
|
|
27796
28819
|
});
|
|
@@ -27799,7 +28822,7 @@ var listRootFiles = (repoPath) => {
|
|
|
27799
28822
|
};
|
|
27800
28823
|
|
|
27801
28824
|
// src/api/routes/workspace.ts
|
|
27802
|
-
var workspaceController = new Hono2().get("/state", zv("query",
|
|
28825
|
+
var workspaceController = new Hono2().get("/state", zv("query", z18.object({ workspaceId: z18.string().optional() })), async (c) => {
|
|
27803
28826
|
const ctx = c.var.ctx;
|
|
27804
28827
|
const { workspaceId } = c.req.valid("query");
|
|
27805
28828
|
if (workspaceId) {
|
|
@@ -27807,10 +28830,10 @@ var workspaceController = new Hono2().get("/state", zv("query", z16.object({ wor
|
|
|
27807
28830
|
}
|
|
27808
28831
|
if (!ctx.currentWorkspaceId || !ctx.currentRepoPath) throw BadRequestError("No workspace context");
|
|
27809
28832
|
return c.json(await loadStateForContext(ctx.currentWorkspaceId, ctx.currentRepoPath));
|
|
27810
|
-
}).post("/save", zv("json", runtimeWorkspaceStateSaveRequestSchema.extend({ workspaceId:
|
|
28833
|
+
}).post("/save", zv("json", runtimeWorkspaceStateSaveRequestSchema.extend({ workspaceId: z18.string() })), async (c) => {
|
|
27811
28834
|
const { workspaceId, board, revision } = c.req.valid("json");
|
|
27812
28835
|
return c.json(await saveState(workspaceId, { board, revision }));
|
|
27813
|
-
}).get("/root-files", zv("query",
|
|
28836
|
+
}).get("/root-files", zv("query", z18.object({ workspaceId: z18.string() })), async (c) => {
|
|
27814
28837
|
const ctx = c.var.ctx;
|
|
27815
28838
|
const { workspaceId } = c.req.valid("query");
|
|
27816
28839
|
const ws = await ctx.ensureWorkspace(workspaceId);
|
|
@@ -27822,7 +28845,7 @@ function createApiApp(ctx) {
|
|
|
27822
28845
|
const app = new Hono2().basePath("/api").use("*", async (c, next) => {
|
|
27823
28846
|
c.set("ctx", ctx);
|
|
27824
28847
|
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);
|
|
28848
|
+
}).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
28849
|
app.onError(errorHandler2);
|
|
27827
28850
|
return app;
|
|
27828
28851
|
}
|
|
@@ -27932,6 +28955,9 @@ var RuntimeStateHub = class {
|
|
|
27932
28955
|
broadcastRunSessionChange(workspaceId, cardId, status, errorMessage) {
|
|
27933
28956
|
this.broadcastToWorkspace(workspaceId, { type: "run_session_changed", cardId, status, errorMessage });
|
|
27934
28957
|
}
|
|
28958
|
+
broadcastCompanionCanvasUpdate(workspaceId, sessionId, canvas) {
|
|
28959
|
+
this.broadcastToWorkspace(workspaceId, { type: "companion_canvas_updated", sessionId, canvas });
|
|
28960
|
+
}
|
|
27935
28961
|
broadcastToWorkspace(workspaceId, event) {
|
|
27936
28962
|
const clientIds = this.workspaceClients.get(workspaceId);
|
|
27937
28963
|
if (!clientIds) return;
|
|
@@ -27952,14 +28978,14 @@ var RuntimeStateHub = class {
|
|
|
27952
28978
|
|
|
27953
28979
|
// src/core/update-check.ts
|
|
27954
28980
|
init_paths();
|
|
27955
|
-
import { readFileSync as
|
|
27956
|
-
import { join as
|
|
27957
|
-
var CACHE_FILE =
|
|
28981
|
+
import { readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
|
|
28982
|
+
import { join as join22 } from "node:path";
|
|
28983
|
+
var CACHE_FILE = join22(WHIPPED_HOME_DIR, "update-check.json");
|
|
27958
28984
|
var REGISTRY_URL = "https://registry.npmjs.org/whipped/latest";
|
|
27959
28985
|
var CHECK_INTERVAL_MS = 12 * 60 * 60 * 1e3;
|
|
27960
28986
|
function readCache() {
|
|
27961
28987
|
try {
|
|
27962
|
-
return JSON.parse(
|
|
28988
|
+
return JSON.parse(readFileSync8(CACHE_FILE, "utf8"));
|
|
27963
28989
|
} catch {
|
|
27964
28990
|
return null;
|
|
27965
28991
|
}
|
|
@@ -28013,6 +29039,10 @@ async function cleanupStaleTasks(workspaceId, hub) {
|
|
|
28013
29039
|
if (staleRecurring > 0) {
|
|
28014
29040
|
logger.info(`[server] Cleared ${staleRecurring} stale recurring-agent run(s) for ${workspaceId}`);
|
|
28015
29041
|
}
|
|
29042
|
+
const staleCompanions = resetStaleCompanionSessions(workspaceId);
|
|
29043
|
+
if (staleCompanions > 0) {
|
|
29044
|
+
logger.info(`[server] Reset ${staleCompanions} stale companion session(s) for ${workspaceId}`);
|
|
29045
|
+
}
|
|
28016
29046
|
for (const card of Object.values(board.cards)) {
|
|
28017
29047
|
if (card.terminalSessions?.some((s2) => s2.endedAt === void 0)) {
|
|
28018
29048
|
await closeAllOpenTerminalSessions(workspaceId, card.id, now);
|
|
@@ -28241,9 +29271,9 @@ async function createRuntimeServer(options) {
|
|
|
28241
29271
|
};
|
|
28242
29272
|
}
|
|
28243
29273
|
const apiApp = createApiApp(createContext());
|
|
28244
|
-
const webUiDistPath =
|
|
28245
|
-
const webUiIndexPath =
|
|
28246
|
-
const hasWebUi =
|
|
29274
|
+
const webUiDistPath = join23(__dirname2, "web-ui");
|
|
29275
|
+
const webUiIndexPath = join23(webUiDistPath, "index.html");
|
|
29276
|
+
const hasWebUi = existsSync15(webUiIndexPath);
|
|
28247
29277
|
const httpServer = createServer(async (req, res) => {
|
|
28248
29278
|
const url = new URL(req.url ?? "/", `http://${host}`);
|
|
28249
29279
|
if (url.pathname === "/api/slack/oauth-callback") {
|
|
@@ -28476,7 +29506,7 @@ async function createRuntimeServer(options) {
|
|
|
28476
29506
|
res.end("Bad request");
|
|
28477
29507
|
return;
|
|
28478
29508
|
}
|
|
28479
|
-
const filePath =
|
|
29509
|
+
const filePath = join23(ATTACHMENTS_DIR, cardId, filename);
|
|
28480
29510
|
const { readFile: readFile5 } = await import("node:fs/promises");
|
|
28481
29511
|
try {
|
|
28482
29512
|
const data = await readFile5(filePath);
|
|
@@ -28551,15 +29581,15 @@ async function createRuntimeServer(options) {
|
|
|
28551
29581
|
return;
|
|
28552
29582
|
}
|
|
28553
29583
|
if (hasWebUi) {
|
|
28554
|
-
const filePath = url.pathname === "/" || !url.pathname.includes(".") ? webUiIndexPath :
|
|
28555
|
-
if (
|
|
28556
|
-
const content =
|
|
29584
|
+
const filePath = url.pathname === "/" || !url.pathname.includes(".") ? webUiIndexPath : join23(webUiDistPath, url.pathname);
|
|
29585
|
+
if (existsSync15(filePath)) {
|
|
29586
|
+
const content = readFileSync9(filePath);
|
|
28557
29587
|
res.writeHead(200, { "Content-Type": getContentType(filePath) });
|
|
28558
29588
|
res.end(content);
|
|
28559
29589
|
return;
|
|
28560
29590
|
}
|
|
28561
29591
|
res.writeHead(200, { "Content-Type": "text/html" });
|
|
28562
|
-
res.end(
|
|
29592
|
+
res.end(readFileSync9(webUiIndexPath));
|
|
28563
29593
|
return;
|
|
28564
29594
|
}
|
|
28565
29595
|
res.writeHead(200, { "Content-Type": "text/plain" });
|
|
@@ -28632,15 +29662,17 @@ async function createRuntimeServer(options) {
|
|
|
28632
29662
|
if (activeBuffer !== null) {
|
|
28633
29663
|
if (activeBuffer && ws.readyState === 1) ws.send(prepareBufferForReplay(activeBuffer));
|
|
28634
29664
|
} else {
|
|
28635
|
-
|
|
28636
|
-
|
|
28637
|
-
|
|
28638
|
-
|
|
28639
|
-
|
|
28640
|
-
|
|
28641
|
-
|
|
28642
|
-
|
|
28643
|
-
|
|
29665
|
+
loadTerminalBuffer(workspaceId, taskId).then((diskSnapshot) => {
|
|
29666
|
+
if (diskSnapshot) {
|
|
29667
|
+
if (ws.readyState === 1) ws.send(prepareBufferForReplay(diskSnapshot));
|
|
29668
|
+
return;
|
|
29669
|
+
}
|
|
29670
|
+
const hubSnapshot = stateHub.getTerminalBuffer(workspaceId, taskId);
|
|
29671
|
+
if (hubSnapshot && ws.readyState === 1) ws.send(prepareBufferForReplay(hubSnapshot));
|
|
29672
|
+
}).catch(() => {
|
|
29673
|
+
const hubSnapshot = stateHub.getTerminalBuffer(workspaceId, taskId);
|
|
29674
|
+
if (hubSnapshot && ws.readyState === 1) ws.send(prepareBufferForReplay(hubSnapshot));
|
|
29675
|
+
});
|
|
28644
29676
|
}
|
|
28645
29677
|
ws.on("message", (raw2) => {
|
|
28646
29678
|
const text = raw2.toString();
|
|
@@ -28720,6 +29752,9 @@ async function createRuntimeServer(options) {
|
|
|
28720
29752
|
await writeClaudeTaskHookSettings(port).catch((err) => {
|
|
28721
29753
|
logger.warn("[server] Failed to write claude hook settings:", err);
|
|
28722
29754
|
});
|
|
29755
|
+
await writeClaudeCompanionSettings().catch((err) => {
|
|
29756
|
+
logger.warn("[server] Failed to write claude companion settings:", err);
|
|
29757
|
+
});
|
|
28723
29758
|
if (_globalConfig.autoStartTunnel) tunnelManager.start();
|
|
28724
29759
|
scheduleUpdateChecks(VERSION10, (latestVersion) => {
|
|
28725
29760
|
stateHub.broadcastUpdateAvailable(latestVersion);
|
|
@@ -28770,7 +29805,7 @@ process.on("uncaughtException", (err) => {
|
|
|
28770
29805
|
if (err.code === "EPIPE" || err.code === "ECONNRESET") return;
|
|
28771
29806
|
throw err;
|
|
28772
29807
|
});
|
|
28773
|
-
var VERSION9 = true ? "0.
|
|
29808
|
+
var VERSION9 = true ? "0.9.0" : "0.0.0-dev";
|
|
28774
29809
|
async function isPortAvailable(port, host) {
|
|
28775
29810
|
return new Promise((resolve5) => {
|
|
28776
29811
|
const probe = createServer2();
|