whipped 0.6.0 → 0.7.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 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, 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, 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;
3533
3533
  var init_api_contract = __esm({
3534
3534
  "src/core/api-contract.ts"() {
3535
3535
  "use strict";
@@ -3890,8 +3890,13 @@ Do NOT include:
3890
3890
  runtimeGithubConfigSchema = z.object({
3891
3891
  token: z.string()
3892
3892
  });
3893
+ runtimeWorktreeCopyEntrySchema = z.object({
3894
+ path: z.string().min(1),
3895
+ symlink: z.boolean().default(false)
3896
+ });
3893
3897
  runtimeWorktreeSetupSchema = z.object({
3894
- filesToCopy: z.array(z.string()).default([]),
3898
+ // Legacy configs stored bare strings; accept and normalize them to copy entries.
3899
+ filesToCopy: z.array(z.union([z.string().transform((path2) => ({ path: path2, symlink: false })), runtimeWorktreeCopyEntrySchema])).default([]),
3895
3900
  installCommand: z.string().default("")
3896
3901
  });
3897
3902
  runtimeProjectSecretSchema = z.object({
@@ -13557,11 +13562,12 @@ async function runLogs(options) {
13557
13562
  process.exit(1);
13558
13563
  }
13559
13564
  if (options.follow) {
13560
- const child = spawn2("tail", ["-f", "-n", String(options.lines), path2], {
13565
+ const [cmd, args] = process.platform === "win32" ? ["powershell", ["-NoProfile", "-Command", `Get-Content -Path '${path2}' -Tail ${options.lines} -Wait`]] : ["tail", ["-f", "-n", String(options.lines), path2]];
13566
+ const child = spawn2(cmd, args, {
13561
13567
  stdio: "inherit"
13562
13568
  });
13563
13569
  child.on("error", (err) => {
13564
- console.error(`Failed to spawn tail: ${err.message}`);
13570
+ console.error(`Failed to follow log file: ${err.message}`);
13565
13571
  process.exit(1);
13566
13572
  });
13567
13573
  child.on("exit", (code) => process.exit(code ?? 0));
@@ -14397,6 +14403,16 @@ var slackNotifier = new SlackNotifier();
14397
14403
  // src/server/runtime-server.ts
14398
14404
  init_runtime_config();
14399
14405
  init_logger();
14406
+
14407
+ // src/core/shell.ts
14408
+ function getShellInvocation(command) {
14409
+ if (process.platform === "win32") {
14410
+ return [process.env.ComSpec ?? "cmd.exe", ["/c", command]];
14411
+ }
14412
+ return [process.env.SHELL ?? "/bin/sh", ["-c", command]];
14413
+ }
14414
+
14415
+ // src/server/runtime-server.ts
14400
14416
  init_task_id();
14401
14417
 
14402
14418
  // src/notifications/sound-player.ts
@@ -14424,7 +14440,11 @@ async function playNotificationSound(event) {
14424
14440
  }
14425
14441
  }
14426
14442
  function playOnHost(file) {
14427
- const [command, args] = process.platform === "darwin" ? ["afplay", [file]] : process.platform === "linux" ? ["paplay", [file]] : [null, null];
14443
+ const [command, args] = process.platform === "darwin" ? ["afplay", [file]] : process.platform === "win32" ? (
14444
+ // PowerShell's SoundPlayer plays the WAV chimes synchronously in the
14445
+ // detached child (it exits once done); no extra binary needed.
14446
+ ["powershell", ["-NoProfile", "-Command", `(New-Object Media.SoundPlayer '${file}').PlaySync()`]]
14447
+ ) : process.platform === "linux" ? ["paplay", [file]] : [null, null];
14428
14448
  if (!command) return;
14429
14449
  const child = spawn4(command, [...args], { stdio: "ignore", detached: true });
14430
14450
  child.on("error", () => {
@@ -18047,7 +18067,7 @@ async function commitIfDirty(worktreePath, message) {
18047
18067
  function attemptMerge(repoPath, effectiveWorktreeId, taskBranch) {
18048
18068
  const worktreePath = join12(WORKTREES_DIR, effectiveWorktreeId);
18049
18069
  if (existsSync5(worktreePath)) {
18050
- rmSync2(worktreePath, { recursive: true, force: true });
18070
+ rmSync2(worktreePath, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 });
18051
18071
  git(["worktree", "prune"], repoPath);
18052
18072
  }
18053
18073
  const statusResult = git(["status", "--porcelain"], repoPath);
@@ -18415,7 +18435,7 @@ async function removeWorktreeAsync(taskId, repoPath, branchName) {
18415
18435
  const t0 = Date.now();
18416
18436
  logger.info(`[cleanup:${taskId}] starting worktree removal`);
18417
18437
  try {
18418
- await rm2(worktreePath, { recursive: true, force: true });
18438
+ await rm2(worktreePath, { recursive: true, force: true, maxRetries: 10, retryDelay: 200 });
18419
18439
  logger.info(`[cleanup:${taskId}] rm worktree dir done (${Date.now() - t0}ms)`);
18420
18440
  } catch (err) {
18421
18441
  logger.error({ err }, `[cleanup:${taskId}] rm worktree dir failed:`);
@@ -19149,12 +19169,34 @@ function getAvailableAgents() {
19149
19169
  }
19150
19170
  });
19151
19171
  }
19172
+ var resolvedCommandCache = /* @__PURE__ */ new Map();
19173
+ function resolveCommandPath(command) {
19174
+ if (process.platform !== "win32") return command;
19175
+ const cached2 = resolvedCommandCache.get(command);
19176
+ if (cached2) return cached2;
19177
+ try {
19178
+ const result = spawnSync4("where.exe", [command], {
19179
+ stdio: ["ignore", "pipe", "ignore"],
19180
+ timeout: 5e3,
19181
+ encoding: "utf-8"
19182
+ });
19183
+ if (result.status === 0 && result.stdout) {
19184
+ const first = result.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean)[0];
19185
+ if (first) {
19186
+ resolvedCommandCache.set(command, first);
19187
+ return first;
19188
+ }
19189
+ }
19190
+ } catch {
19191
+ }
19192
+ return command;
19193
+ }
19152
19194
  function getAgentCommand(agentId) {
19153
19195
  const agent = AGENT_DEFINITIONS.find((a) => a.id === agentId);
19154
19196
  if (!agent) {
19155
19197
  throw new Error(`Unknown agent: ${agentId}`);
19156
19198
  }
19157
- return agent.command;
19199
+ return resolveCommandPath(agent.command);
19158
19200
  }
19159
19201
  var READONLY_DISALLOWED_TOOLS = ["Edit", "Write", "NotebookEdit", "Bash"];
19160
19202
  function buildAgentArgs(agentId, prompt, ctx = {}) {
@@ -20437,8 +20479,8 @@ init_workspace_state();
20437
20479
 
20438
20480
  // src/daemon/scheduler.ts
20439
20481
  import { spawn as spawn6 } from "node:child_process";
20440
- import { cpSync, existsSync as existsSync10, mkdirSync as mkdirSync9 } from "node:fs";
20441
- import { unlink as unlink2 } from "node:fs/promises";
20482
+ import { existsSync as existsSync10 } from "node:fs";
20483
+ import { cp, link, mkdir as mkdir3, stat as stat2, symlink, unlink as unlink2 } from "node:fs/promises";
20442
20484
  import { dirname as dirname6, join as join17, resolve as resolve2 } from "node:path";
20443
20485
  import { fileURLToPath as fileURLToPath4 } from "node:url";
20444
20486
  init_api_contract();
@@ -20626,7 +20668,7 @@ async function runReviewSlot(slot, card, streamId, options, customPrompt) {
20626
20668
  useIncremental,
20627
20669
  diffRef: priorReviewedSha ?? card.baseRef
20628
20670
  };
20629
- const stat2 = getGitStat(worktreePath, card.baseRef);
20671
+ const stat3 = getGitStat(worktreePath, card.baseRef);
20630
20672
  const fullDiff = getGitFullDiff(worktreePath, useIncremental ? priorReviewedSha : card.baseRef);
20631
20673
  const context = formatPriorComments(card);
20632
20674
  let subtaskCards = [];
@@ -20636,7 +20678,7 @@ async function runReviewSlot(slot, card, streamId, options, customPrompt) {
20636
20678
  const rawSystemPrompt = buildReviewSlotSystemPrompt(
20637
20679
  slot,
20638
20680
  card,
20639
- stat2,
20681
+ stat3,
20640
20682
  fullDiff,
20641
20683
  customPrompt,
20642
20684
  context.text,
@@ -21218,16 +21260,16 @@ ${fullDiff}`;
21218
21260
  return `Large changeset (${fullDiff.length.toLocaleString()} chars). Use \`git diff ${baseRef}...HEAD\` and read individual files to explore.`;
21219
21261
  }
21220
21262
  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.`;
21221
- function renderReviewDiff(stat2, fullDiff, baseRef, scope) {
21263
+ function renderReviewDiff(stat3, fullDiff, baseRef, scope) {
21222
21264
  if (!scope.useIncremental) {
21223
21265
  return `## Changed files
21224
- ${stat2}
21266
+ ${stat3}
21225
21267
 
21226
21268
  ## Diff
21227
21269
  ${formatDiffBlock(fullDiff, baseRef)}`;
21228
21270
  }
21229
21271
  return `## Changed files (full changeset vs \`${baseRef}\`, for context)
21230
- ${stat2}
21272
+ ${stat3}
21231
21273
 
21232
21274
  ## Changes since your last review (review THIS)
21233
21275
  _The diff below is only what changed since you last reviewed this card. The full changeset is summarised above; run \`git diff ${baseRef}...HEAD\` only if you need it to chase a regression._
@@ -21267,11 +21309,11 @@ Revise these values rather than rewriting from scratch, unless they no longer re
21267
21309
  let statSection = "";
21268
21310
  if (worktreePath) {
21269
21311
  if (fullDiff) {
21270
- const stat2 = getGitStat(worktreePath, statBase);
21312
+ const stat3 = getGitStat(worktreePath, statBase);
21271
21313
  statSection = `
21272
21314
 
21273
21315
  ## Current worktree state (vs ${statBase})
21274
- ${stat2}
21316
+ ${stat3}
21275
21317
 
21276
21318
  ## Diff (vs ${statBase})
21277
21319
  ${formatDiffBlock(fullDiff, statBase, "Git diff")}`;
@@ -21282,11 +21324,11 @@ ${formatDiffBlock(fullDiff, statBase, "Git diff")}`;
21282
21324
 
21283
21325
  This is the initial dev run on this card. The worktree is clean and branched from \`${statBase}\` \u2014 there is no prior diff to inspect. Skip \`git diff\` and start implementing.`;
21284
21326
  } else {
21285
- const stat2 = getGitStat(worktreePath, statBase);
21327
+ const stat3 = getGitStat(worktreePath, statBase);
21286
21328
  statSection = `
21287
21329
 
21288
21330
  ## Current worktree state (vs ${statBase})
21289
- ${stat2}`;
21331
+ ${stat3}`;
21290
21332
  }
21291
21333
  }
21292
21334
  const descAttachNote = (card.descriptionAttachments?.length ?? 0) > 0 ? `
@@ -21395,12 +21437,12 @@ ${systemPrompt.trim()}`);
21395
21437
  ${effectiveGitInstructions}`);
21396
21438
  return { text: parts.join("\n\n"), files: context.files };
21397
21439
  }
21398
- function buildReviewSlotSystemPrompt(slot, card, stat2, fullDiff, customPrompt, priorContext, scope, secrets = [], systemPrompt, autoCommit = true, subtaskCards = [], browserEnabled = false) {
21440
+ function buildReviewSlotSystemPrompt(slot, card, stat3, fullDiff, customPrompt, priorContext, scope, secrets = [], systemPrompt, autoCommit = true, subtaskCards = [], browserEnabled = false) {
21399
21441
  if (slot.type === "orch") {
21400
21442
  return buildOrchSystemPrompt(
21401
21443
  slot,
21402
21444
  card,
21403
- stat2,
21445
+ stat3,
21404
21446
  fullDiff,
21405
21447
  customPrompt,
21406
21448
  priorContext,
@@ -21414,7 +21456,7 @@ function buildReviewSlotSystemPrompt(slot, card, stat2, fullDiff, customPrompt,
21414
21456
  return buildMergedReviewSystemPrompt(
21415
21457
  slot,
21416
21458
  card,
21417
- stat2,
21459
+ stat3,
21418
21460
  fullDiff,
21419
21461
  customPrompt,
21420
21462
  priorContext,
@@ -21424,7 +21466,7 @@ function buildReviewSlotSystemPrompt(slot, card, stat2, fullDiff, customPrompt,
21424
21466
  browserEnabled
21425
21467
  );
21426
21468
  }
21427
- function buildMergedReviewSystemPrompt(slot, card, stat2, fullDiff, customPrompt, priorContext, scope, secrets, systemPrompt, browserEnabled = false) {
21469
+ function buildMergedReviewSystemPrompt(slot, card, stat3, fullDiff, customPrompt, priorContext, scope, secrets, systemPrompt, browserEnabled = false) {
21428
21470
  const custom = customPrompt.trim() ? `
21429
21471
 
21430
21472
  ## Project-specific instructions
@@ -21460,7 +21502,7 @@ When you set status "fail" (reopening for rework), you can right-size the **tier
21460
21502
  ## Task to review
21461
21503
  ${card.description ?? ""}${descAttachSection}${priorContext}
21462
21504
 
21463
- ${renderReviewDiff(stat2, fullDiff, card.baseRef, scope)}
21505
+ ${renderReviewDiff(stat3, fullDiff, card.baseRef, scope)}
21464
21506
 
21465
21507
  ${ITERATION_SCOPING_NOTE}
21466
21508
 
@@ -21488,7 +21530,7 @@ ${secretsSection}` : ""}${projectContext}${runningAppSection}${levelAdjustSectio
21488
21530
 
21489
21531
  This MCP call is required \u2014 without it the pipeline has no record of your verdict.`;
21490
21532
  }
21491
- function buildOrchSystemPrompt(slot, card, stat2, fullDiff, customPrompt, priorContext, scope, secrets, systemPrompt, autoCommit = true, subtaskCards = []) {
21533
+ function buildOrchSystemPrompt(slot, card, stat3, fullDiff, customPrompt, priorContext, scope, secrets, systemPrompt, autoCommit = true, subtaskCards = []) {
21492
21534
  const commentType = slotCommentType(slot);
21493
21535
  const custom = customPrompt.trim() ? `
21494
21536
 
@@ -21534,7 +21576,7 @@ ${card.description}
21534
21576
 
21535
21577
  ${subtasksSection}${priorContext}
21536
21578
 
21537
- ${renderReviewDiff(stat2, fullDiff, card.baseRef, scope)}
21579
+ ${renderReviewDiff(stat3, fullDiff, card.baseRef, scope)}
21538
21580
 
21539
21581
  ${ITERATION_SCOPING_NOTE}
21540
21582
 
@@ -22066,19 +22108,33 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
22066
22108
  const { filesToCopy, installCommand } = projectConfig.worktreeSetup;
22067
22109
  if (filesToCopy.length > 0) {
22068
22110
  const copied = [];
22069
- for (const relPath of filesToCopy) {
22070
- const src = join17(repoPath, relPath);
22111
+ const linked = [];
22112
+ for (const entry of filesToCopy) {
22113
+ const src = join17(repoPath, entry.path);
22071
22114
  if (!existsSync10(src)) continue;
22072
- const dst = join17(worktree.path, relPath);
22073
- mkdirSync9(dirname6(dst), { recursive: true });
22115
+ const dst = join17(worktree.path, entry.path);
22116
+ await mkdir3(dirname6(dst), { recursive: true });
22074
22117
  try {
22075
- cpSync(src, dst, { recursive: true });
22076
- copied.push(relPath);
22077
- } catch {
22118
+ if (entry.symlink) {
22119
+ await shareIntoWorktree(src, dst);
22120
+ linked.push(entry.path);
22121
+ } else {
22122
+ await cp(src, dst, { recursive: true, dereference: true });
22123
+ copied.push(entry.path);
22124
+ }
22125
+ } catch (err) {
22126
+ logger.warn(
22127
+ { err },
22128
+ `[scheduler] worktree setup: failed to ${entry.symlink ? "link" : "copy"} ${entry.path}`
22129
+ );
22078
22130
  }
22079
22131
  }
22080
- if (copied.length > 0) {
22081
- await appendActivityLog(workspaceId, taskId, `Copied to worktree: ${copied.join(", ")}`);
22132
+ const summary = [
22133
+ copied.length ? `Copied: ${copied.join(", ")}` : "",
22134
+ linked.length ? `Linked: ${linked.join(", ")}` : ""
22135
+ ].filter(Boolean).join(" \xB7 ");
22136
+ if (summary) {
22137
+ await appendActivityLog(workspaceId, taskId, `Worktree setup \u2014 ${summary}`);
22082
22138
  stateHub.broadcastWorkspaceUpdate(workspaceId);
22083
22139
  }
22084
22140
  }
@@ -22086,7 +22142,8 @@ ${assistantSystemPrompt}` : assistantSystemPrompt;
22086
22142
  await appendActivityLog(workspaceId, taskId, `Running: ${installCommand.trim()}`);
22087
22143
  stateHub.broadcastWorkspaceUpdate(workspaceId);
22088
22144
  await new Promise((resolve5) => {
22089
- const proc = spawn6("sh", ["-c", installCommand.trim()], {
22145
+ const [shell, shellArgs] = getShellInvocation(installCommand.trim());
22146
+ const proc = spawn6(shell, shellArgs, {
22090
22147
  cwd: worktree.path,
22091
22148
  stdio: "ignore",
22092
22149
  env: { ...process.env, REPO_PATH: repoPath }
@@ -22618,6 +22675,22 @@ ${devSystemPromptResult.text}`;
22618
22675
  this.isShuttingDown = true;
22619
22676
  }
22620
22677
  };
22678
+ async function shareIntoWorktree(src, dst) {
22679
+ const isDir = (await stat2(src)).isDirectory();
22680
+ if (process.platform !== "win32") {
22681
+ await symlink(src, dst, isDir ? "dir" : "file");
22682
+ return;
22683
+ }
22684
+ if (isDir) {
22685
+ await symlink(src, dst, "junction");
22686
+ return;
22687
+ }
22688
+ try {
22689
+ await link(src, dst);
22690
+ } catch {
22691
+ await cp(src, dst, { recursive: true, dereference: true });
22692
+ }
22693
+ }
22621
22694
  function getMcpServerPath() {
22622
22695
  const thisFile = fileURLToPath4(import.meta.url);
22623
22696
  const thisDir = dirname6(thisFile);
@@ -26500,7 +26573,8 @@ function appExists(bundle) {
26500
26573
  return paths.some((p2) => existsSync11(p2));
26501
26574
  }
26502
26575
  function binaryExists(bin) {
26503
- const r = spawnSync7("which", [bin], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" });
26576
+ const finder = process.platform === "win32" ? "where" : "which";
26577
+ const r = spawnSync7(finder, [bin], { stdio: ["ignore", "pipe", "ignore"], encoding: "utf-8" });
26504
26578
  return r.status === 0 && r.stdout.trim().length > 0;
26505
26579
  }
26506
26580
  function listTerminalApps() {
@@ -26941,8 +27015,8 @@ var checkProjectPath = async (repoPath) => {
26941
27015
  return { valid: false, isGitRepo: false, error: null, name: null, branch: null, remote: null, branches: [] };
26942
27016
  const { statSync: statSync2 } = await import("node:fs");
26943
27017
  try {
26944
- const stat2 = statSync2(repoPath);
26945
- if (!stat2.isDirectory())
27018
+ const stat3 = statSync2(repoPath);
27019
+ if (!stat3.isDirectory())
26946
27020
  return {
26947
27021
  valid: false,
26948
27022
  isGitRepo: false,
@@ -26998,8 +27072,8 @@ var checkProjectPath = async (repoPath) => {
26998
27072
  var addProject = async (repoPath, initialConfig) => {
26999
27073
  const { statSync: statSync2 } = await import("node:fs");
27000
27074
  try {
27001
- const stat2 = statSync2(repoPath);
27002
- if (!stat2.isDirectory()) throw new Error("Not a directory");
27075
+ const stat3 = statSync2(repoPath);
27076
+ if (!stat3.isDirectory()) throw new Error("Not a directory");
27003
27077
  } catch {
27004
27078
  throw BadRequestError(`Path does not exist: ${repoPath}`);
27005
27079
  }
@@ -27336,13 +27410,13 @@ import { z as z14 } from "zod";
27336
27410
  init_runtime_config();
27337
27411
 
27338
27412
  // src/tunnel/cloudflare-setup.ts
27339
- init_runtime_config();
27340
- init_logger();
27341
27413
  import { execFile as execFile9, spawn as spawn7 } from "node:child_process";
27342
- import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile3, access, unlink as unlink4 } from "node:fs/promises";
27414
+ import { mkdir as mkdir4, writeFile as writeFile3, readFile as readFile3, access, unlink as unlink4 } from "node:fs/promises";
27343
27415
  import { homedir as homedir5 } from "node:os";
27344
27416
  import { join as join20 } from "node:path";
27345
27417
  import { promisify as promisify9 } from "node:util";
27418
+ init_runtime_config();
27419
+ init_logger();
27346
27420
  var execFileAsync7 = promisify9(execFile9);
27347
27421
  var CLOUDFLARED_DIR = join20(homedir5(), ".cloudflared");
27348
27422
  async function checkCloudflaredInstalled() {
@@ -27385,7 +27459,8 @@ async function openCloudflaredLogin(force = false) {
27385
27459
  resolved = true;
27386
27460
  const loginUrl = match2[1].trim();
27387
27461
  logger.info(`[cloudflared-login] Auth URL: ${loginUrl}`);
27388
- spawn7("open", [loginUrl], { stdio: "ignore" });
27462
+ open_default(loginUrl).catch(() => {
27463
+ });
27389
27464
  proc.unref();
27390
27465
  resolve5({ alreadyLoggedIn: false, loginUrl });
27391
27466
  }
@@ -27445,7 +27520,7 @@ async function writeTunnelConfig(tunnelId, tunnelName, domain) {
27445
27520
  ` service: http://127.0.0.1:${DEFAULT_PORT}`,
27446
27521
  ` - service: http_status:404`
27447
27522
  ].join("\n");
27448
- await mkdir3(CLOUDFLARED_DIR, { recursive: true });
27523
+ await mkdir4(CLOUDFLARED_DIR, { recursive: true });
27449
27524
  await writeFile3(join20(CLOUDFLARED_DIR, "config.yml"), config, "utf-8");
27450
27525
  logger.info(`[tunnel-setup] Wrote ~/.cloudflared/config.yml for tunnel ${tunnelName}`);
27451
27526
  }
@@ -27534,7 +27609,7 @@ import { z as z15 } from "zod";
27534
27609
  // src/api/services/workflows-service.ts
27535
27610
  init_workspace_state();
27536
27611
  import { existsSync as existsSync13 } from "node:fs";
27537
- import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
27612
+ import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile4 } from "node:fs/promises";
27538
27613
  import { dirname as dirname8, isAbsolute as isAbsolute2, resolve as resolve4 } from "node:path";
27539
27614
  var resolvePromptPath = async (workspaceId, requestedPath) => {
27540
27615
  const workspaces = await listWorkspaces();
@@ -27572,7 +27647,7 @@ var deleteWorkflow = async (workspaceId, workflowId) => {
27572
27647
  };
27573
27648
  var writePromptFile = async (workspaceId, path2, content) => {
27574
27649
  const targetPath = await resolvePromptPath(workspaceId, path2);
27575
- await mkdir4(dirname8(targetPath), { recursive: true });
27650
+ await mkdir5(dirname8(targetPath), { recursive: true });
27576
27651
  await writeFile4(targetPath, content, "utf-8");
27577
27652
  return { path: path2 };
27578
27653
  };
@@ -27902,8 +27977,8 @@ async function createRuntimeServer(options) {
27902
27977
  const runTerminalListeners = /* @__PURE__ */ new Map();
27903
27978
  function startRun(workspaceId, cardId, command, cwd) {
27904
27979
  stopRun(workspaceId);
27905
- const shell = process.env.SHELL ?? "/bin/bash";
27906
- const pty = nodePty2.spawn(shell, ["-c", command], {
27980
+ const [shell, shellArgs] = getShellInvocation(command);
27981
+ const pty = nodePty2.spawn(shell, shellArgs, {
27907
27982
  name: "xterm-256color",
27908
27983
  cols: 120,
27909
27984
  rows: 40,
@@ -28613,7 +28688,7 @@ process.on("uncaughtException", (err) => {
28613
28688
  if (err.code === "EPIPE" || err.code === "ECONNRESET") return;
28614
28689
  throw err;
28615
28690
  });
28616
- var VERSION9 = true ? "0.6.0" : "0.0.0-dev";
28691
+ var VERSION9 = true ? "0.7.0" : "0.0.0-dev";
28617
28692
  async function isPortAvailable(port, host) {
28618
28693
  return new Promise((resolve5) => {
28619
28694
  const probe = createServer2();
@@ -33,7 +33,7 @@ function highestWorkflowLevel(workflow) {
33
33
  }
34
34
  return LEVEL_ORDER[bestIdx] ?? "medium";
35
35
  }
36
- 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, 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;
36
+ 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;
37
37
  var init_api_contract = __esm({
38
38
  "src/core/api-contract.ts"() {
39
39
  "use strict";
@@ -394,8 +394,13 @@ Do NOT include:
394
394
  runtimeGithubConfigSchema = z.object({
395
395
  token: z.string()
396
396
  });
397
+ runtimeWorktreeCopyEntrySchema = z.object({
398
+ path: z.string().min(1),
399
+ symlink: z.boolean().default(false)
400
+ });
397
401
  runtimeWorktreeSetupSchema = z.object({
398
- filesToCopy: z.array(z.string()).default([]),
402
+ // Legacy configs stored bare strings; accept and normalize them to copy entries.
403
+ filesToCopy: z.array(z.union([z.string().transform((path) => ({ path, symlink: false })), runtimeWorktreeCopyEntrySchema])).default([]),
399
404
  installCommand: z.string().default("")
400
405
  });
401
406
  runtimeProjectSecretSchema = z.object({
@@ -27603,8 +27603,13 @@ const runtimeGlobalConfigSchema = object({
27603
27603
  const runtimeGithubConfigSchema = object({
27604
27604
  token: string$2()
27605
27605
  });
27606
+ const runtimeWorktreeCopyEntrySchema = object({
27607
+ path: string$2().min(1),
27608
+ symlink: boolean$1().default(false)
27609
+ });
27606
27610
  const runtimeWorktreeSetupSchema = object({
27607
- filesToCopy: array(string$2()).default([]),
27611
+ // Legacy configs stored bare strings; accept and normalize them to copy entries.
27612
+ filesToCopy: array(union([string$2().transform((path2) => ({ path: path2, symlink: false })), runtimeWorktreeCopyEntrySchema])).default([]),
27608
27613
  installCommand: string$2().default("")
27609
27614
  });
27610
27615
  const runtimeProjectSecretSchema = object({
@@ -78188,6 +78193,9 @@ const globalConfigFormSchema = runtimeGlobalConfigSchema.extend({
78188
78193
  });
78189
78194
  const notificationSoundsFormSchema = notificationSoundsConfigSchema;
78190
78195
  const environmentFormSchema = runtimeWorktreeSetupSchema.extend({
78196
+ // The form always works with normalized copy entries (legacy-string tolerance
78197
+ // lives in the persistence schema), so the RHF value type stays clean.
78198
+ filesToCopy: array(runtimeWorktreeCopyEntrySchema).default([]),
78191
78199
  startCommand: string$2().default("")
78192
78200
  });
78193
78201
  object({
@@ -79867,39 +79875,70 @@ function FilesBox({
79867
79875
  }, [filesError]);
79868
79876
  const rootFiles = (data == null ? void 0 : data.files) ?? null;
79869
79877
  const discoveredSet = new Set(rootFiles ?? []);
79870
- const allFiles = [.../* @__PURE__ */ new Set([...rootFiles ?? [], ...filesToCopy])].sort();
79878
+ const byPath = new Map(filesToCopy.map((e) => [e.path, e]));
79879
+ const allFiles = [.../* @__PURE__ */ new Set([...rootFiles ?? [], ...filesToCopy.map((e) => e.path)])].sort();
79871
79880
  const toggle = (file, checked) => {
79872
- onChange(checked ? [.../* @__PURE__ */ new Set([...filesToCopy, file])] : filesToCopy.filter((f) => f !== file));
79881
+ if (checked) {
79882
+ if (!byPath.has(file)) onChange([...filesToCopy, { path: file, symlink: false }]);
79883
+ } else {
79884
+ onChange(filesToCopy.filter((e) => e.path !== file));
79885
+ }
79886
+ };
79887
+ const setSymlink = (file, symlink) => {
79888
+ onChange(filesToCopy.map((e) => e.path === file ? { ...e, symlink } : e));
79873
79889
  };
79874
79890
  const addManual = () => {
79875
79891
  const val = addInput.trim();
79876
79892
  if (!val) return;
79877
- onChange([.../* @__PURE__ */ new Set([...filesToCopy, val])]);
79893
+ if (!byPath.has(val)) onChange([...filesToCopy, { path: val, symlink: false }]);
79878
79894
  setAddInput("");
79879
79895
  };
79880
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-1.5 bg-[#0c0c0f] border border-[#2a2a35] rounded-md px-3 py-2 flex-1", children: [
79896
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex flex-col gap-0.5 bg-[#0c0c0f] border border-[#2a2a35] rounded-md px-3 py-2 flex-1", children: [
79881
79897
  rootFiles === null && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] py-1 text-[#4a4a5a]", children: "Scanning..." }),
79882
79898
  rootFiles !== null && allFiles.length === 0 && /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "text-[11px] py-1 text-[#4a4a5a]", children: "No gitignored files found in repo root" }),
79883
79899
  allFiles.map((file) => {
79884
- const checked = filesToCopy.includes(file);
79900
+ const entry = byPath.get(file);
79901
+ const checked = !!entry;
79885
79902
  const isManual = !discoveredSet.has(file);
79886
- return /* @__PURE__ */ jsxRuntimeExports.jsxs("label", { className: "flex items-center gap-2 cursor-pointer group", children: [
79887
- /* @__PURE__ */ jsxRuntimeExports.jsx(CustomCheckbox, { checked, onChange: (v2) => toggle(file, v2) }),
79888
- /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 text-[12px] font-mono text-[#c0c0d0]", children: file }),
79889
- isManual && /* @__PURE__ */ jsxRuntimeExports.jsx(
79890
- "button",
79891
- {
79892
- onClick: (e) => {
79893
- e.preventDefault();
79894
- onChange(filesToCopy.filter((f) => f !== file));
79895
- },
79896
- className: "opacity-0 group-hover:opacity-100 transition-opacity text-[#60607a]",
79897
- children: /* @__PURE__ */ jsxRuntimeExports.jsx(X$1, { size: 11 })
79898
- }
79899
- )
79900
- ] }, file);
79903
+ return /* @__PURE__ */ jsxRuntimeExports.jsxs(
79904
+ "label",
79905
+ {
79906
+ className: "flex items-center gap-2 cursor-pointer group -mx-2 px-2 py-1 rounded transition-colors hover:bg-[#17171f]",
79907
+ children: [
79908
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "shrink-0 w-4 flex justify-center", children: entry && /* @__PURE__ */ jsxRuntimeExports.jsx(
79909
+ "button",
79910
+ {
79911
+ type: "button",
79912
+ title: entry.symlink ? "Symlinked (shared from repo). Click to copy instead." : "Copied into worktree. Click to symlink (share from repo, e.g. node_modules).",
79913
+ onClick: (e) => {
79914
+ e.preventDefault();
79915
+ setSymlink(file, !entry.symlink);
79916
+ },
79917
+ className: `transition-colors ${entry.symlink ? "text-[#7aa2f7]" : "text-[#45455a] hover:text-[#9a9ab0]"}`,
79918
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(Link2, { size: 12 })
79919
+ }
79920
+ ) }),
79921
+ /* @__PURE__ */ jsxRuntimeExports.jsx(CustomCheckbox, { checked, onChange: (v2) => toggle(file, v2) }),
79922
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "flex-1 text-[12px] font-mono text-[#c0c0d0]", children: file }),
79923
+ isManual && /* @__PURE__ */ jsxRuntimeExports.jsx(
79924
+ "button",
79925
+ {
79926
+ type: "button",
79927
+ onClick: (e) => {
79928
+ e.preventDefault();
79929
+ onChange(filesToCopy.filter((f) => f.path !== file));
79930
+ },
79931
+ className: "opacity-0 group-hover:opacity-100 transition-opacity text-[#60607a]",
79932
+ children: /* @__PURE__ */ jsxRuntimeExports.jsx(X$1, { size: 11 })
79933
+ }
79934
+ )
79935
+ ]
79936
+ },
79937
+ file
79938
+ );
79901
79939
  }),
79902
- /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 pt-1", children: [
79940
+ /* @__PURE__ */ jsxRuntimeExports.jsxs("div", { className: "flex items-center gap-2 pt-1.5", children: [
79941
+ /* @__PURE__ */ jsxRuntimeExports.jsx("span", { className: "shrink-0 w-4" }),
79903
79942
  /* @__PURE__ */ jsxRuntimeExports.jsx("div", { className: "shrink-0 w-4 h-4 border border-[#2a2a35] rounded-[3px]" }),
79904
79943
  /* @__PURE__ */ jsxRuntimeExports.jsx(
79905
79944
  "input",
@@ -6460,6 +6460,10 @@
6460
6460
  color: #7a7a40;
6461
6461
  }
6462
6462
 
6463
+ .text-\[\#7aa2f7\] {
6464
+ color: #7aa2f7;
6465
+ }
6466
+
6463
6467
  .text-\[\#7c6aff\] {
6464
6468
  color: #7c6aff;
6465
6469
  }
@@ -6472,6 +6476,10 @@
6472
6476
  color: #8888a0;
6473
6477
  }
6474
6478
 
6479
+ .text-\[\#45455a\] {
6480
+ color: #45455a;
6481
+ }
6482
+
6475
6483
  .text-\[\#60607a\] {
6476
6484
  color: #60607a;
6477
6485
  }
@@ -7054,6 +7062,10 @@
7054
7062
  background-color: #15151b;
7055
7063
  }
7056
7064
 
7065
+ .hover\:bg-\[\#17171f\]:hover {
7066
+ background-color: #17171f;
7067
+ }
7068
+
7057
7069
  .hover\:bg-\[\#252510\]:hover {
7058
7070
  background-color: #252510;
7059
7071
  }
@@ -7140,6 +7152,10 @@
7140
7152
  color: #7c6aff;
7141
7153
  }
7142
7154
 
7155
+ .hover\:text-\[\#9a9ab0\]:hover {
7156
+ color: #9a9ab0;
7157
+ }
7158
+
7143
7159
  .hover\:text-\[\#22c55e\]:hover {
7144
7160
  color: #22c55e;
7145
7161
  }
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" type="image/png" href="/favicon.png" />
7
7
  <title>whipped</title>
8
- <script type="module" crossorigin src="/assets/index-peNzhkq8.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-Do7b5IJu.css">
8
+ <script type="module" crossorigin src="/assets/index-BDiIsuhZ.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-CRXPsGTP.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "whipped",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Autonomous AI agent kanban board for Claude, Codex, Opencode, and Cursor.",
5
5
  "type": "module",
6
6
  "homepage": "https://github.com/nxnom/whipped#readme",
@@ -22,20 +22,6 @@
22
22
  "engines": {
23
23
  "node": ">=22"
24
24
  },
25
- "scripts": {
26
- "clean": "rm -rf dist",
27
- "build": "pnpm clean && pnpm web:build && node scripts/build.mjs",
28
- "prepublishOnly": "pnpm build",
29
- "postinstall": "node scripts/postinstall.mjs",
30
- "dev": "NODE_ENV=development tsx src/cli.ts",
31
- "dev:full": "node scripts/dev-full.mjs",
32
- "web:dev": "pnpm --filter @whipped/web dev",
33
- "web:build": "pnpm --filter @whipped/web build",
34
- "typecheck": "tsc -p tsconfig.json --noEmit",
35
- "web:typecheck": "pnpm --filter @whipped/web typecheck",
36
- "lint": "biome lint src web-ui/src",
37
- "format": "biome format --write ."
38
- },
39
25
  "dependencies": {
40
26
  "@hono/zod-validator": "^0.8.0",
41
27
  "@modelcontextprotocol/sdk": "^1.29.0",
@@ -68,14 +54,17 @@
68
54
  "tsx": "^4.20.3",
69
55
  "typescript": "^5.9.3"
70
56
  },
71
- "pnpm": {
72
- "onlyBuiltDependencies": [
73
- "better-sqlite3",
74
- "esbuild",
75
- "node-pty"
76
- ],
77
- "overrides": {
78
- "hono": "4.12.23"
79
- }
57
+ "scripts": {
58
+ "clean": "rm -rf dist",
59
+ "build": "pnpm clean && pnpm web:build && node scripts/build.mjs",
60
+ "postinstall": "node scripts/postinstall.mjs",
61
+ "dev": "NODE_ENV=development tsx src/cli.ts",
62
+ "dev:full": "node scripts/dev-full.mjs",
63
+ "web:dev": "pnpm --filter @whipped/web dev",
64
+ "web:build": "pnpm --filter @whipped/web build",
65
+ "typecheck": "tsc -p tsconfig.json --noEmit",
66
+ "web:typecheck": "pnpm --filter @whipped/web typecheck",
67
+ "lint": "biome lint src web-ui/src",
68
+ "format": "biome format --write ."
80
69
  }
81
- }
70
+ }