@aiworker/web 1.22.2 → 1.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/bin/cli.js +7 -0
  2. package/dist/assets/{CodeMirrorCommentWidgets-CSICpWQD.js → CodeMirrorCommentWidgets-teB3HmBT.js} +1 -1
  3. package/dist/assets/DiffView-DC9RX3MF.js +4 -0
  4. package/dist/assets/{DirtyBranchSwitchDialog-C22HdWj4.js → DirtyBranchSwitchDialog-DwPkzXsS.js} +1 -1
  5. package/dist/assets/FilesView-BFZkuu4O.js +13 -0
  6. package/dist/assets/{GitView-VQQkYuUw.js → GitView-B_x52MHY.js} +4 -4
  7. package/dist/assets/{JsonTreeView-6lrhwD3q.js → JsonTreeView-cmAzUTiw.js} +1 -1
  8. package/dist/assets/{LinearIssuesView-Ba6Q9569.js → LinearIssuesView-Dehh5afm.js} +1 -1
  9. package/dist/assets/MarkdownImageGallery-DgNOqqhE.js +1 -0
  10. package/dist/assets/MarkdownRendererImpl-DusmMZ_p.js +12 -0
  11. package/dist/assets/OnboardingScreen-B8fm1H4m.js +1 -0
  12. package/dist/assets/{PatchDiff-BzcZPlOq.js → PatchDiff-BVr7gJU0.js} +1 -1
  13. package/dist/assets/PierreDiffViewer-DzLt-nel.js +161 -0
  14. package/dist/assets/PlanView-BmQUYT_z.js +5 -0
  15. package/dist/assets/SettingsView-ChWmAcZT.js +28 -0
  16. package/dist/assets/{SettingsWindow-DDQYaKWQ.js → SettingsWindow-Dy3M6ZB9.js} +1 -1
  17. package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
  18. package/dist/assets/{ToolOutputDialog-iYF8Y2bz.js → ToolOutputDialog-cTl_eLIH.js} +5 -5
  19. package/dist/assets/{ToolPartDiffPreview-CdjbOVMM.js → ToolPartDiffPreview-C_3iYNJQ.js} +1 -1
  20. package/dist/assets/{VirtualizedFileDiff-fFFm2QgH.js → VirtualizedFileDiff-CAWthkoy.js} +4 -4
  21. package/dist/assets/WalkthroughView-C7AHOpCA.js +4 -0
  22. package/dist/assets/{computeVirtualFileMetrics-CzgLum_K.js → computeVirtualFileMetrics-BkiDSlTW.js} +2 -2
  23. package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
  24. package/dist/assets/de-DJsQr-p7.js +10 -0
  25. package/dist/assets/es-BuYMlA9g.js +16 -0
  26. package/dist/assets/fr-ePDay33C.js +14 -0
  27. package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
  28. package/dist/assets/index-BD8ILbxF.js +40 -0
  29. package/dist/assets/{index-Bl3LSbjn.js → index-n6O3hklM.js} +1 -1
  30. package/dist/assets/index-zhgg-SDz.css +1 -0
  31. package/dist/assets/{isDiffPlainText-Dx-dB_8l.js → isDiffPlainText-GMdPz-71.js} +1 -1
  32. package/dist/assets/{isFilePlainText-CWFPwmnU.js → isFilePlainText-BVuH1zH9.js} +1 -1
  33. package/dist/assets/ja-BzR9xOY_.js +16 -0
  34. package/dist/assets/ko-BEjHAm14.js +16 -0
  35. package/dist/assets/main-BydQ1tr_.js +930 -0
  36. package/dist/assets/{main-DnBAoktx.js → main-D3ZDQglc.js} +2 -2
  37. package/dist/assets/markdownCore-Cb2MdZnV.js +5 -0
  38. package/dist/assets/{miniChat-BKyvLseN.js → miniChat-d01k801U.js} +2 -2
  39. package/dist/assets/{mobile-DKjeFgMN.js → mobile-D7VV-Ufz.js} +2 -2
  40. package/dist/assets/patchFileDiff-CdztVMz0.js +13 -0
  41. package/dist/assets/pl-rQmpxNIF.js +16 -0
  42. package/dist/assets/pt-BR-CorbsSW-.js +16 -0
  43. package/dist/assets/renderElectronMiniChatApp-BGG9UIlh.js +2 -0
  44. package/dist/assets/renderMobileApp-BRP3J1iv.js +25 -0
  45. package/dist/assets/runtimeEndpointReset-BhT_-c52.js +41 -0
  46. package/dist/assets/scheduledTasksApi-BEuUF_cj.js +1 -0
  47. package/dist/assets/{shikiHighlight-TEQCmzW8.js → shikiHighlight-BzXUjlby.js} +1 -1
  48. package/dist/assets/surface-COKb77Ua.js +1 -0
  49. package/dist/assets/tr-CoAMXPtI.js +7 -0
  50. package/dist/assets/uk-YlAX0rRU.js +16 -0
  51. package/dist/assets/{useAppFontEffects-D7sNpuL2.js → useAppFontEffects-Cwyhjfl5.js} +141 -140
  52. package/dist/assets/useGitComparison-BCZVefdE.js +1 -0
  53. package/dist/assets/useInlineCommentController-DDkpBEU8.js +2 -0
  54. package/dist/assets/useRootScrollLock-CwjoWuC4.js +1 -0
  55. package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-BY_5qla_.js} +1 -1
  56. package/dist/assets/{vendor-codemirror-language-data-Dp3KMRQG.js → vendor-codemirror-language-data-Cb1YnICj.js} +2 -2
  57. package/dist/assets/vendor-legendapp-list-BuBPBC3R.js +1 -0
  58. package/dist/assets/vendor-zod-B6551dwG.js +39 -0
  59. package/dist/assets/{web-components-_mNEMjFY.js → web-components-LV3vjTxy.js} +1 -1
  60. package/dist/assets/zh-CN-DDYg0Jl7.js +16 -0
  61. package/dist/assets/zh-TW-CQKaCayr.js +16 -0
  62. package/dist/index.html +5 -43
  63. package/dist/mini-chat.html +4 -4
  64. package/dist/mobile.html +4 -4
  65. package/package.json +2 -3
  66. package/server/index.d.ts +15 -0
  67. package/server/index.js +48 -22
  68. package/server/lib/agent-memory/runtime.js +7 -2
  69. package/server/lib/agent-memory/runtime.test.js +22 -0
  70. package/server/lib/data-dir-migration.js +40 -0
  71. package/server/lib/data-dir-migration.test.js +59 -0
  72. package/server/lib/event-stream/DOCUMENTATION.md +3 -0
  73. package/server/lib/event-stream/global-hub.js +29 -4
  74. package/server/lib/event-stream/global-hub.test.js +22 -0
  75. package/server/lib/event-stream/global-ws-bridge.js +15 -18
  76. package/server/lib/event-stream/protocol.js +21 -7
  77. package/server/lib/event-stream/runtime.js +9 -6
  78. package/server/lib/event-stream/runtime.test.js +72 -12
  79. package/server/lib/fs/DOCUMENTATION.md +3 -0
  80. package/server/lib/fs/routes.js +31 -0
  81. package/server/lib/fs/routes.test.js +110 -1
  82. package/server/lib/git/DOCUMENTATION.md +39 -6
  83. package/server/lib/git/routes.js +45 -12
  84. package/server/lib/git/routes.test.js +78 -0
  85. package/server/lib/git/service.js +343 -202
  86. package/server/lib/git/service.test.js +496 -25
  87. package/server/lib/github/DOCUMENTATION.md +5 -0
  88. package/server/lib/github/routes.js +2 -2
  89. package/server/lib/inherited-env.js +16 -9
  90. package/server/lib/inherited-env.test.js +8 -9
  91. package/server/lib/message-queue/DOCUMENTATION.md +11 -1
  92. package/server/lib/message-queue/runtime.js +13 -0
  93. package/server/lib/message-queue/runtime.test.js +31 -1
  94. package/server/lib/network-defaults.js +23 -0
  95. package/server/lib/network-defaults.test.js +52 -0
  96. package/server/lib/notifications/DOCUMENTATION.md +1 -1
  97. package/server/lib/notifications/emitter-runtime.js +4 -3
  98. package/server/lib/opencode/DOCUMENTATION.md +35 -5
  99. package/server/lib/opencode/bootstrap-runtime.js +2 -0
  100. package/server/lib/opencode/env-runtime.js +5 -1
  101. package/server/lib/opencode/env-runtime.test.js +72 -0
  102. package/server/lib/opencode/feature-routes-runtime.js +17 -1
  103. package/server/lib/opencode/lifecycle.js +4 -3
  104. package/server/lib/opencode/lifecycle.test.js +29 -0
  105. package/server/lib/opencode/managed-plugin-config.js +1 -1
  106. package/server/lib/opencode/openchamber-routes.js +64 -4
  107. package/server/lib/opencode/openchamber-routes.test.js +128 -1
  108. package/server/lib/opencode/proxy.js +33 -0
  109. package/server/lib/opencode/routes.js +5 -3
  110. package/server/lib/opencode/settings-files.js +214 -0
  111. package/server/lib/opencode/settings-helpers.js +88 -41
  112. package/server/lib/opencode/settings-helpers.test.js +176 -1
  113. package/server/lib/opencode/settings-registry.json +750 -0
  114. package/server/lib/opencode/settings-runtime.js +175 -27
  115. package/server/lib/opencode/settings-runtime.test.js +321 -7
  116. package/server/lib/project-context/DOCUMENTATION.md +38 -4
  117. package/server/lib/project-context/routes.http.test.js +18 -0
  118. package/server/lib/project-context/routes.js +15 -0
  119. package/server/lib/project-context/runtime.js +256 -25
  120. package/server/lib/project-context/runtime.test.js +160 -3
  121. package/server/lib/projects/DOCUMENTATION.md +77 -0
  122. package/server/lib/projects/project-config.js +179 -17
  123. package/server/lib/projects/project-config.test.js +97 -0
  124. package/server/lib/projects/project-id.js +43 -0
  125. package/server/lib/projects/project-setup.js +409 -0
  126. package/server/lib/projects/project-setup.test.js +557 -0
  127. package/server/lib/projects/routes.js +59 -0
  128. package/server/lib/quota/DOCUMENTATION.md +31 -1
  129. package/server/lib/quota/providers/cline-pass.js +125 -0
  130. package/server/lib/quota/providers/cline-pass.test.js +103 -0
  131. package/server/lib/quota/providers/index.js +7 -0
  132. package/server/lib/quota/providers/neuralwatt.js +3 -6
  133. package/server/lib/quota/providers/neuralwatt.test.js +7 -7
  134. package/server/lib/quota/providers/ollama-cloud.js +31 -0
  135. package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
  136. package/server/lib/quota/providers/opencode-go.js +1 -1
  137. package/server/lib/quota/providers/opencode-go.test.js +26 -1
  138. package/server/lib/quota/providers/openrouter.js +119 -24
  139. package/server/lib/quota/providers/openrouter.test.js +330 -0
  140. package/server/lib/relay/DOCUMENTATION.md +75 -0
  141. package/server/lib/relay/cross-compat.test.js +26 -0
  142. package/server/lib/relay/downstream-scheduler.js +154 -0
  143. package/server/lib/relay/downstream-scheduler.test.js +162 -0
  144. package/server/lib/relay/e2ee.js +10 -8
  145. package/server/lib/relay/flow-control.test.js +177 -0
  146. package/server/lib/relay/host-client.js +44 -8
  147. package/server/lib/relay/host-client.test.js +14 -0
  148. package/server/lib/relay/tunnel-codec.js +9 -0
  149. package/server/lib/relay/tunnel-host.js +66 -17
  150. package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
  151. package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
  152. package/server/lib/scheduled-tasks/runtime.js +12 -2
  153. package/server/lib/scheduled-tasks/runtime.test.js +53 -0
  154. package/server/lib/session-assist/DOCUMENTATION.md +131 -66
  155. package/server/lib/session-assist/context.js +127 -0
  156. package/server/lib/session-assist/context.test.js +132 -0
  157. package/server/lib/session-assist/prompt.js +54 -0
  158. package/server/lib/session-assist/runtime.js +117 -267
  159. package/server/lib/session-assist/runtime.test.js +203 -0
  160. package/server/lib/session-goal/runtime.js +4 -9
  161. package/server/lib/small-model/DOCUMENTATION.md +3 -2
  162. package/server/lib/small-model/call.js +9 -4
  163. package/server/lib/small-model/call.test.js +29 -0
  164. package/server/lib/small-model/index.js +5 -10
  165. package/server/lib/small-model/runtime-providers.js +15 -7
  166. package/server/lib/small-model/runtime-providers.test.js +7 -1
  167. package/server/lib/terminal/DOCUMENTATION.md +4 -4
  168. package/server/lib/terminal/runtime.js +11 -6
  169. package/server/lib/terminal/runtime.test.js +20 -13
  170. package/server/lib/walkthrough/DOCUMENTATION.md +49 -20
  171. package/server/lib/walkthrough/model-settings.js +8 -12
  172. package/server/lib/walkthrough/pr-source.test.js +21 -0
  173. package/server/lib/walkthrough/prompt.js +3 -1
  174. package/server/lib/walkthrough/pull-request.js +7 -4
  175. package/server/lib/walkthrough/pull-request.test.js +16 -0
  176. package/server/lib/walkthrough/routes.js +19 -0
  177. package/server/lib/walkthrough/routes.test.js +26 -0
  178. package/server/lib/walkthrough/sources.js +36 -4
  179. package/server/opencode-worktree-readiness.test.js +130 -0
  180. package/dist/assets/DiffView-hDBKesA_.js +0 -4
  181. package/dist/assets/FilesView-CdWzeOp9.js +0 -17
  182. package/dist/assets/MarkdownImageGallery-Czv_mhvS.js +0 -1
  183. package/dist/assets/MarkdownRendererImpl-BrMcswiy.js +0 -12
  184. package/dist/assets/OnboardingScreen-ChxQoAkx.js +0 -1
  185. package/dist/assets/PierreDiffViewer-BchBtZlp.js +0 -157
  186. package/dist/assets/PlanView-DyDt_nZ_.js +0 -5
  187. package/dist/assets/SettingsView-BKf5xBsP.js +0 -28
  188. package/dist/assets/WalkthroughView-DOeCsMju.js +0 -4
  189. package/dist/assets/contextPanelEmbeddedChat-C0ZLAepL.js +0 -1
  190. package/dist/assets/de-B5JaA3SA.js +0 -10
  191. package/dist/assets/es-EPIH88ii.js +0 -16
  192. package/dist/assets/fr-CAMfxUdZ.js +0 -14
  193. package/dist/assets/index-DF5kMP-i.js +0 -39
  194. package/dist/assets/index-Dl3xbAB_.css +0 -1
  195. package/dist/assets/ja-CifOE1VN.js +0 -16
  196. package/dist/assets/ko-BBDyAyYQ.js +0 -16
  197. package/dist/assets/main-MhSaOZoB.js +0 -930
  198. package/dist/assets/markdownCore-BrBwOOMn.js +0 -3
  199. package/dist/assets/patchFileDiff-o8ms1me8.js +0 -10
  200. package/dist/assets/pl-DgEHYfFM.js +0 -16
  201. package/dist/assets/pt-BR-DuWHojUw.js +0 -16
  202. package/dist/assets/renderElectronMiniChatApp-CDgBwwCS.js +0 -2
  203. package/dist/assets/renderMobileApp-DJIKIK1r.js +0 -25
  204. package/dist/assets/runtimeEndpointReset-CYJ9O_xX.js +0 -17
  205. package/dist/assets/scheduledTasksApi-D8yamh1b.js +0 -1
  206. package/dist/assets/tr-D5m2yIWL.js +0 -7
  207. package/dist/assets/uk-D_q60oog.js +0 -16
  208. package/dist/assets/useInlineCommentController-Brz_cLcQ.js +0 -2
  209. package/dist/assets/useRootScrollLock-lXEnWlaU.js +0 -1
  210. package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
  211. package/dist/assets/vendor-legendapp-list-gg7YLRW7.js +0 -1
  212. package/dist/assets/vendor-zod-CK21ihK-.js +0 -39
  213. package/dist/assets/zh-CN-D8QvsVeg.js +0 -16
  214. package/dist/assets/zh-TW-B9t7rQQO.js +0 -16
  215. package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
  216. package/server/lib/mcp-reconnect/runtime.js +0 -122
  217. package/server/lib/mcp-reconnect/runtime.test.js +0 -140
@@ -0,0 +1,409 @@
1
+ // A project's setup — worktree setup commands, project actions, pinned draft
2
+ // starters — comes from two files:
3
+ //
4
+ // - the personal file `~/.config/openchamber/projects/<projectId>.json`
5
+ // (client-owned keys; server-owned `version` / `scheduledTasks` live beside
6
+ // them and are never touched here), and
7
+ // - the shared file `<repo>/.aiworker/project.json`, committed by a team so
8
+ // a teammate who pulls the code gets the setup without configuring anything.
9
+ //
10
+ // This module knows both shapes and the one merge rule per field. The route
11
+ // and the VS Code bridge (`packages/vscode/src/project-setup.ts`, a mirror of
12
+ // this file) use the same code paths so a value reads back the same on every
13
+ // surface.
14
+
15
+ import crypto from 'node:crypto';
16
+
17
+ const ACTION_NAME_MAX_LENGTH = 80;
18
+ const ACTION_COMMAND_MAX_LENGTH = 4000;
19
+ const ACTION_OPEN_URL_MAX_LENGTH = 2000;
20
+ const ACTION_DESKTOP_FORWARD_MAX_LENGTH = 300;
21
+ const SETUP_COMMAND_MAX_LENGTH = 4000;
22
+ const SETUP_COMMANDS_MAX = 50;
23
+
24
+ const ACTION_PLATFORMS = new Set(['macos', 'linux', 'windows']);
25
+ const SETUP_WORKTREE_MODES = new Set(['append', 'replace']);
26
+
27
+ const isObjectRecord = (value) => Boolean(value) && typeof value === 'object' && !Array.isArray(value);
28
+
29
+ const clamp = (value, maxLength) => (value.length > maxLength ? value.slice(0, maxLength) : value);
30
+
31
+ const trimmedString = (value) => (typeof value === 'string' ? value.trim() : '');
32
+
33
+ /** Setup commands: non-empty trimmed strings, capped in count and length. */
34
+ export const sanitizeSetupCommands = (value) => {
35
+ if (!Array.isArray(value)) return [];
36
+ const commands = [];
37
+ for (const entry of value) {
38
+ const command = clamp(trimmedString(entry), SETUP_COMMAND_MAX_LENGTH);
39
+ if (!command) continue;
40
+ commands.push(command);
41
+ if (commands.length >= SETUP_COMMANDS_MAX) break;
42
+ }
43
+ return commands;
44
+ };
45
+
46
+ const sanitizeActionPlatforms = (value) => {
47
+ if (!Array.isArray(value)) return [];
48
+ const platforms = [];
49
+ for (const entry of value) {
50
+ const platform = trimmedString(entry).toLowerCase();
51
+ if (ACTION_PLATFORMS.has(platform) && !platforms.includes(platform)) platforms.push(platform);
52
+ }
53
+ return platforms;
54
+ };
55
+
56
+ /**
57
+ * Project actions: `id`, `name`, and `command` are required and ids are
58
+ * unique; every optional field is dropped when empty so the stored record
59
+ * carries only what the user set. `runIn` keeps only the one value the UI
60
+ * understands (`parent`); anything else means "run in the worktree".
61
+ */
62
+ export const sanitizeProjectActions = (value) => {
63
+ if (!Array.isArray(value)) return [];
64
+ const actions = [];
65
+ const seenIds = new Set();
66
+ for (const entry of value) {
67
+ if (!isObjectRecord(entry)) continue;
68
+ const id = trimmedString(entry.id);
69
+ const name = clamp(trimmedString(entry.name), ACTION_NAME_MAX_LENGTH);
70
+ const command = clamp(trimmedString(entry.command), ACTION_COMMAND_MAX_LENGTH);
71
+ if (!id || !name || !command || seenIds.has(id)) continue;
72
+ seenIds.add(id);
73
+
74
+ const icon = trimmedString(entry.icon);
75
+ const platforms = sanitizeActionPlatforms(entry.platforms);
76
+ const openUrl = clamp(trimmedString(entry.openUrl), ACTION_OPEN_URL_MAX_LENGTH);
77
+ const desktopOpenSshForward = clamp(trimmedString(entry.desktopOpenSshForward), ACTION_DESKTOP_FORWARD_MAX_LENGTH);
78
+
79
+ const action = { id, name, command, icon: icon || null };
80
+ if (entry.autoOpenUrl === true) action.autoOpenUrl = true;
81
+ if (openUrl) action.openUrl = openUrl;
82
+ if (desktopOpenSshForward) action.desktopOpenSshForward = desktopOpenSshForward;
83
+ if (platforms.length > 0) action.platforms = platforms;
84
+ if (entry.runIn === 'parent') action.runIn = 'parent';
85
+ actions.push(action);
86
+ }
87
+ return actions;
88
+ };
89
+
90
+ /** Draft starters: `{ type: 'command' | 'skill', name }`, unique by `type:name`. */
91
+ export const sanitizeDraftStarters = (value) => {
92
+ if (!Array.isArray(value)) return [];
93
+ const starters = [];
94
+ const seen = new Set();
95
+ for (const entry of value) {
96
+ if (!isObjectRecord(entry)) continue;
97
+ const type = entry.type === 'command' || entry.type === 'skill' ? entry.type : null;
98
+ const name = trimmedString(entry.name);
99
+ if (!type || !name) continue;
100
+ const key = `${type}:${name}`;
101
+ if (seen.has(key)) continue;
102
+ seen.add(key);
103
+ starters.push({ type, name });
104
+ }
105
+ return starters;
106
+ };
107
+
108
+ /**
109
+ * The client-facing view of a raw config document. A primary action id that
110
+ * names no action is reported as `null`.
111
+ */
112
+ const sanitizeIdList = (value) => {
113
+ if (!Array.isArray(value)) return [];
114
+ const ids = [];
115
+ for (const entry of value) {
116
+ const id = trimmedString(entry);
117
+ if (id && !ids.includes(id)) ids.push(id);
118
+ }
119
+ return ids;
120
+ };
121
+
122
+ /**
123
+ * The personal part of the view, straight from the personal file. The wait
124
+ * flag is `null` when the file does not set it, so the merge can let a
125
+ * shared value through; `hiddenSharedActionIds` and `setupWorktreeMode` only
126
+ * matter when a shared file exists.
127
+ */
128
+ export const projectSetupViewOf = (raw) => {
129
+ const document = isObjectRecord(raw) ? raw : {};
130
+ const projectActions = sanitizeProjectActions(document.projectActions);
131
+ const primaryRaw = trimmedString(document.projectActionsPrimaryId);
132
+ return {
133
+ setupWorktree: sanitizeSetupCommands(document['setup-worktree']),
134
+ setupWorktreeWait: typeof document['setup-worktree-wait'] === 'boolean' ? document['setup-worktree-wait'] : null,
135
+ setupWorktreeMode: SETUP_WORKTREE_MODES.has(document.setupWorktreeMode) ? document.setupWorktreeMode : 'append',
136
+ projectActions,
137
+ projectActionsPrimaryId: primaryRaw && projectActions.some((action) => action.id === primaryRaw) ? primaryRaw : null,
138
+ draftStarters: sanitizeDraftStarters(document.draftStarters),
139
+ hiddenSharedActionIds: sanitizeIdList(document.hiddenSharedActionIds),
140
+ sharedTrust: sharedTrustOf(document.sharedTrust),
141
+ };
142
+ };
143
+
144
+ /** The recorded answer to the trust prompt: which shared commands were trusted, and when. */
145
+ const sharedTrustOf = (value) => {
146
+ if (!isObjectRecord(value)) return null;
147
+ const hash = trimmedString(value.hash);
148
+ if (!hash) return null;
149
+ return { hash, trustedAt: Number.isFinite(value.trustedAt) ? value.trustedAt : 0 };
150
+ };
151
+
152
+ /**
153
+ * Turn a client patch (view keys) into the on-disk keys it changes. Only the
154
+ * keys present in the patch are returned, so a caller can merge the result
155
+ * over the raw document without clearing what the patch did not mention.
156
+ * A key with the wrong shape is a validation error, never silently dropped.
157
+ */
158
+ export const projectSetupPatchToStored = (patch) => {
159
+ if (!isObjectRecord(patch)) {
160
+ throw new Error('patch must be an object');
161
+ }
162
+ const stored = {};
163
+ if ('setupWorktree' in patch) {
164
+ if (!Array.isArray(patch.setupWorktree)) throw new Error('setupWorktree must be an array of commands');
165
+ stored['setup-worktree'] = sanitizeSetupCommands(patch.setupWorktree);
166
+ }
167
+ if ('setupWorktreeWait' in patch) {
168
+ if (typeof patch.setupWorktreeWait !== 'boolean') throw new Error('setupWorktreeWait must be a boolean');
169
+ stored['setup-worktree-wait'] = patch.setupWorktreeWait;
170
+ }
171
+ if ('projectActions' in patch) {
172
+ if (!Array.isArray(patch.projectActions)) throw new Error('projectActions must be an array');
173
+ stored.projectActions = sanitizeProjectActions(patch.projectActions);
174
+ }
175
+ if ('projectActionsPrimaryId' in patch) {
176
+ const primary = patch.projectActionsPrimaryId;
177
+ if (primary !== null && typeof primary !== 'string') throw new Error('projectActionsPrimaryId must be a string or null');
178
+ stored.projectActionsPrimaryId = trimmedString(primary) || undefined;
179
+ }
180
+ if ('draftStarters' in patch) {
181
+ if (!Array.isArray(patch.draftStarters)) throw new Error('draftStarters must be an array');
182
+ stored.draftStarters = sanitizeDraftStarters(patch.draftStarters);
183
+ }
184
+ if ('hiddenSharedActionIds' in patch) {
185
+ if (!Array.isArray(patch.hiddenSharedActionIds)) throw new Error('hiddenSharedActionIds must be an array');
186
+ stored.hiddenSharedActionIds = sanitizeIdList(patch.hiddenSharedActionIds);
187
+ }
188
+ if ('setupWorktreeMode' in patch) {
189
+ if (!SETUP_WORKTREE_MODES.has(patch.setupWorktreeMode)) throw new Error('setupWorktreeMode must be "append" or "replace"');
190
+ stored.setupWorktreeMode = patch.setupWorktreeMode;
191
+ }
192
+ if ('sharedTrustHash' in patch) {
193
+ const hash = patch.sharedTrustHash;
194
+ if (hash !== null && (typeof hash !== 'string' || !hash.trim())) throw new Error('sharedTrustHash must be a non-empty string or null');
195
+ stored.sharedTrust = hash === null ? undefined : { hash: hash.trim(), trustedAt: Date.now() };
196
+ }
197
+ if ('projectPath' in patch) {
198
+ if (typeof patch.projectPath !== 'string') throw new Error('projectPath must be a string');
199
+ const projectPath = patch.projectPath.trim();
200
+ if (projectPath) stored.projectPath = projectPath;
201
+ }
202
+ return stored;
203
+ };
204
+
205
+ // ── Shared file ──
206
+
207
+ export const SHARED_CONFIG_RELATIVE_PATH = '.aiworker/project.json';
208
+ /** Where repository plans live unless the shared file's `plansDir` says otherwise. */
209
+ export const DEFAULT_PLANS_DIR = '.aiworker/plans';
210
+ const SHARED_CONFIG_VERSION = 1;
211
+
212
+ /**
213
+ * A `plansDir` is a relative path inside the repo: no absolute paths, no
214
+ * drive letters, no `..` segments, forward slashes. Returns the normalized
215
+ * value or `null` when the value is not acceptable.
216
+ */
217
+ export const normalizePlansDir = (value) => {
218
+ const raw = trimmedString(value).replace(/\\/g, '/');
219
+ if (!raw) return null;
220
+ if (raw.startsWith('/') || /^[A-Za-z]:/.test(raw)) return null;
221
+ const segments = raw.split('/').filter((segment) => segment.length > 0 && segment !== '.');
222
+ if (segments.length === 0 || segments.some((segment) => segment === '..')) return null;
223
+ return segments.join('/');
224
+ };
225
+
226
+ const EMPTY_SHARED = Object.freeze({
227
+ setupWorktree: [],
228
+ setupWorktreeWait: null,
229
+ projectActions: [],
230
+ draftStarters: [],
231
+ plansDir: null,
232
+ });
233
+
234
+ /**
235
+ * Parse the text of a shared file. Anything that is not a version-1 object
236
+ * is `invalid` with a reason (never an empty config: a teammate must see that
237
+ * the file is broken, not that the project has no shared setup). A `plansDir`
238
+ * that points outside the repo is invalid for the same reason.
239
+ */
240
+ export const parseSharedProjectConfig = (raw) => {
241
+ let parsed;
242
+ try {
243
+ parsed = JSON.parse(raw);
244
+ } catch (error) {
245
+ return { status: 'invalid', reason: `invalid JSON: ${error instanceof Error ? error.message : String(error)}` };
246
+ }
247
+ if (!isObjectRecord(parsed)) return { status: 'invalid', reason: 'not an object' };
248
+ if (parsed.version !== SHARED_CONFIG_VERSION) return { status: 'invalid', reason: `unsupported version ${JSON.stringify(parsed.version)}` };
249
+ if ('setupWorktree' in parsed && !Array.isArray(parsed.setupWorktree)) return { status: 'invalid', reason: 'setupWorktree must be an array' };
250
+ if ('setupWorktreeWait' in parsed && typeof parsed.setupWorktreeWait !== 'boolean') return { status: 'invalid', reason: 'setupWorktreeWait must be a boolean' };
251
+ if ('projectActions' in parsed && !Array.isArray(parsed.projectActions)) return { status: 'invalid', reason: 'projectActions must be an array' };
252
+ if ('draftStarters' in parsed && !Array.isArray(parsed.draftStarters)) return { status: 'invalid', reason: 'draftStarters must be an array' };
253
+ let plansDir = null;
254
+ if ('plansDir' in parsed && parsed.plansDir !== null) {
255
+ plansDir = normalizePlansDir(parsed.plansDir);
256
+ if (!plansDir) return { status: 'invalid', reason: 'plansDir must be a relative path inside the repository' };
257
+ }
258
+ return {
259
+ status: 'ok',
260
+ config: {
261
+ setupWorktree: sanitizeSetupCommands(parsed.setupWorktree),
262
+ setupWorktreeWait: typeof parsed.setupWorktreeWait === 'boolean' ? parsed.setupWorktreeWait : null,
263
+ projectActions: sanitizeProjectActions(parsed.projectActions),
264
+ draftStarters: sanitizeDraftStarters(parsed.draftStarters),
265
+ plansDir,
266
+ },
267
+ };
268
+ };
269
+
270
+ const withSource = (entries, source) => entries.map((entry) => ({ ...entry, source }));
271
+
272
+ /**
273
+ * What a trust answer covers: the shared setup commands and the shared
274
+ * actions' commands, in a canonical order, hashed. A pull that changes any
275
+ * of them changes the hash, so the prompt returns for the new commands.
276
+ * `null` when the shared config has nothing that executes.
277
+ */
278
+ export const sharedTrustHashOf = (shared) => {
279
+ const commands = shared.setupWorktree;
280
+ const actions = shared.projectActions
281
+ .map((action) => {
282
+ const executable = { id: action.id, command: action.command };
283
+ if (action.runIn) executable.runIn = action.runIn;
284
+ return executable;
285
+ })
286
+ .sort((left, right) => (left.id < right.id ? -1 : left.id > right.id ? 1 : 0));
287
+ if (commands.length === 0 && actions.length === 0) return null;
288
+ const digest = crypto.createHash('sha256').update(JSON.stringify({ setupWorktree: commands, projectActions: actions })).digest('hex');
289
+ return `sha256:${digest}`;
290
+ };
291
+
292
+ /**
293
+ * One merged view from the personal view and the shared read. Rules:
294
+ * - setup commands: shared first, then personal; personal `setupWorktreeMode`
295
+ * `replace` uses only the personal list;
296
+ * - wait flag: personal when the personal file sets it, else shared, else off;
297
+ * - actions: union by id, a personal action replaces the shared one with the
298
+ * same id, hidden shared ids are dropped, primary is personal only;
299
+ * - draft starters: union by `type:name`, shared first.
300
+ * Every merged action and starter carries `source`. The `shared` and
301
+ * `personal` blocks are returned too so a page can edit one without guessing
302
+ * which entries came from where.
303
+ */
304
+ export const mergeProjectSetup = (personal, sharedRead) => {
305
+ const shared = sharedRead.status === 'ok' ? sharedRead.config : EMPTY_SHARED;
306
+ const hidden = new Set(personal.hiddenSharedActionIds);
307
+ const personalIds = new Set(personal.projectActions.map((action) => action.id));
308
+ const sharedActions = shared.projectActions.filter((action) => !hidden.has(action.id) && !personalIds.has(action.id));
309
+ const starterKeys = new Set(shared.draftStarters.map((starter) => `${starter.type}:${starter.name}`));
310
+ const personalStarters = personal.draftStarters.filter((starter) => !starterKeys.has(`${starter.type}:${starter.name}`));
311
+ const trustHash = sharedTrustHashOf(shared);
312
+ return {
313
+ // Nothing executable in the shared file means nothing to trust; otherwise
314
+ // the recorded answer must match the current commands exactly.
315
+ trust: { hash: trustHash, trusted: trustHash === null || personal.sharedTrust?.hash === trustHash },
316
+ setupWorktree: personal.setupWorktreeMode === 'replace'
317
+ ? personal.setupWorktree
318
+ : [...shared.setupWorktree, ...personal.setupWorktree],
319
+ setupWorktreeWait: personal.setupWorktreeWait !== null
320
+ ? personal.setupWorktreeWait
321
+ : shared.setupWorktreeWait === true,
322
+ projectActions: [...withSource(sharedActions, 'shared'), ...withSource(personal.projectActions, 'personal')],
323
+ projectActionsPrimaryId: personal.projectActionsPrimaryId,
324
+ draftStarters: [...withSource(shared.draftStarters, 'shared'), ...withSource(personalStarters, 'personal')],
325
+ shared: sharedBlockOf(sharedRead, shared),
326
+ personal,
327
+ };
328
+ };
329
+
330
+ const sharedBlockOf = (sharedRead, shared) => {
331
+ const block = { status: sharedRead.status, path: SHARED_CONFIG_RELATIVE_PATH, ...shared };
332
+ if (sharedRead.status === 'invalid') block.reason = sharedRead.reason;
333
+ return block;
334
+ };
335
+
336
+ /** An action without an icon is written without the key; readers fall back to the play icon. */
337
+ const withoutEmptyIcon = (action) => {
338
+ if (action.icon !== null) return action;
339
+ const { icon: _emptyIcon, ...rest } = action;
340
+ return rest;
341
+ };
342
+
343
+ /** True when the shared config carries nothing: the file should not exist. */
344
+ export const isSharedProjectConfigEmpty = (config) => (
345
+ config.setupWorktree.length === 0
346
+ && config.setupWorktreeWait === null
347
+ && config.projectActions.length === 0
348
+ && config.draftStarters.length === 0
349
+ && config.plansDir === null
350
+ );
351
+
352
+ /**
353
+ * The bytes of a shared file: version first, then only the keys that carry
354
+ * something, in a fixed order, pretty-printed — the file is committed and
355
+ * reviewed, so its diffs must stay readable. Actions lose their `source`
356
+ * mark and keep only the fields the user set.
357
+ */
358
+ export const serializeSharedProjectConfig = (config) => {
359
+ const document = { version: SHARED_CONFIG_VERSION };
360
+ if (config.setupWorktree.length > 0) document.setupWorktree = config.setupWorktree;
361
+ if (config.setupWorktreeWait !== null) document.setupWorktreeWait = config.setupWorktreeWait;
362
+ if (config.projectActions.length > 0) document.projectActions = sanitizeProjectActions(config.projectActions).map(withoutEmptyIcon);
363
+ if (config.draftStarters.length > 0) document.draftStarters = config.draftStarters;
364
+ if (config.plansDir !== null) document.plansDir = config.plansDir;
365
+ return `${JSON.stringify(document, null, 2)}\n`;
366
+ };
367
+
368
+ /**
369
+ * The next shared config after a client patch over the current one. Every
370
+ * named key replaces the current value; a wrongly shaped key is a validation
371
+ * error, and a `plansDir` outside the repo is refused rather than stored.
372
+ */
373
+ export const applySharedProjectSetupPatch = (current, patch) => {
374
+ if (!isObjectRecord(patch)) throw new Error('patch must be an object');
375
+ const next = { ...current };
376
+ if ('setupWorktree' in patch) {
377
+ if (!Array.isArray(patch.setupWorktree)) throw new Error('setupWorktree must be an array of commands');
378
+ next.setupWorktree = sanitizeSetupCommands(patch.setupWorktree);
379
+ }
380
+ if ('setupWorktreeWait' in patch) {
381
+ if (patch.setupWorktreeWait !== null && typeof patch.setupWorktreeWait !== 'boolean') throw new Error('setupWorktreeWait must be a boolean or null');
382
+ next.setupWorktreeWait = patch.setupWorktreeWait;
383
+ }
384
+ if ('projectActions' in patch) {
385
+ if (!Array.isArray(patch.projectActions)) throw new Error('projectActions must be an array');
386
+ next.projectActions = sanitizeProjectActions(patch.projectActions);
387
+ }
388
+ if ('draftStarters' in patch) {
389
+ if (!Array.isArray(patch.draftStarters)) throw new Error('draftStarters must be an array');
390
+ next.draftStarters = sanitizeDraftStarters(patch.draftStarters);
391
+ }
392
+ if ('plansDir' in patch) {
393
+ if (patch.plansDir === null || (typeof patch.plansDir === 'string' && !patch.plansDir.trim())) {
394
+ next.plansDir = null;
395
+ } else {
396
+ const plansDir = normalizePlansDir(patch.plansDir);
397
+ if (!plansDir) throw new Error('plansDir must be a relative path inside the repository');
398
+ next.plansDir = plansDir;
399
+ }
400
+ }
401
+ return next;
402
+ };
403
+
404
+ export const EMPTY_SHARED_PROJECT_CONFIG = EMPTY_SHARED;
405
+
406
+ export const isProjectSetupValidationError = (error) => {
407
+ const message = error instanceof Error ? error.message : '';
408
+ return message.includes('must be') || message.includes('is required') || message.includes('unsupported characters') || message.includes('not found');
409
+ };