vibe-coding-master 0.0.6 → 0.0.8

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 (50) hide show
  1. package/README.md +207 -66
  2. package/dist/backend/adapters/filesystem.js +13 -0
  3. package/dist/backend/adapters/git-adapter.js +79 -1
  4. package/dist/backend/adapters/translation-provider.js +145 -0
  5. package/dist/backend/api/artifact-routes.js +16 -7
  6. package/dist/backend/api/harness-routes.js +22 -0
  7. package/dist/backend/api/message-routes.js +2 -0
  8. package/dist/backend/api/project-routes.js +3 -8
  9. package/dist/backend/api/task-routes.js +14 -0
  10. package/dist/backend/api/translation-routes.js +70 -0
  11. package/dist/backend/runtime/node-pty-runtime.js +20 -18
  12. package/dist/backend/server.js +33 -2
  13. package/dist/backend/services/app-settings-service.js +128 -0
  14. package/dist/backend/services/artifact-service.js +7 -4
  15. package/dist/backend/services/claude-transcript-service.js +509 -0
  16. package/dist/backend/services/command-dispatcher.js +4 -2
  17. package/dist/backend/services/harness-service.js +196 -0
  18. package/dist/backend/services/message-service.js +1 -1
  19. package/dist/backend/services/project-service.js +50 -9
  20. package/dist/backend/services/session-service.js +13 -9
  21. package/dist/backend/services/status-service.js +79 -1
  22. package/dist/backend/services/task-service.js +118 -4
  23. package/dist/backend/services/translation-prompts.js +173 -0
  24. package/dist/backend/services/translation-queue.js +39 -0
  25. package/dist/backend/services/translation-service.js +546 -0
  26. package/dist/backend/templates/handoff.js +32 -0
  27. package/dist/backend/templates/harness/architect-agent.js +12 -0
  28. package/dist/backend/templates/harness/claude-root.js +14 -0
  29. package/dist/backend/templates/harness/coder-agent.js +11 -0
  30. package/dist/backend/templates/harness/gitignore.js +9 -0
  31. package/dist/backend/templates/harness/project-manager-agent.js +14 -0
  32. package/dist/backend/templates/harness/reviewer-agent.js +13 -0
  33. package/dist/backend/ws/translation-ws.js +35 -0
  34. package/dist/shared/types/harness.js +1 -0
  35. package/dist/shared/types/translation.js +5 -0
  36. package/dist/shared/validation/artifact-check.js +15 -1
  37. package/dist/shared/validation/language-detect.js +46 -0
  38. package/dist-frontend/assets/index-CuiNNOzj.css +32 -0
  39. package/dist-frontend/assets/index-D59GuHCR.js +58 -0
  40. package/dist-frontend/index.html +2 -2
  41. package/docs/cc-best-practices.md +109 -40
  42. package/docs/product-design.md +370 -1374
  43. package/docs/v1-architecture-design.md +595 -1114
  44. package/docs/v1-implementation-plan.md +898 -1603
  45. package/package.json +1 -1
  46. package/scripts/verify-package.mjs +8 -0
  47. package/dist/backend/templates/role-messaging-context.js +0 -44
  48. package/dist-frontend/assets/index-Bah6k-Ix.css +0 -32
  49. package/dist-frontend/assets/index-EMaQuIB6.js +0 -58
  50. package/docs/v1-message-bus-orchestration-design.md +0 -534
package/README.md CHANGED
@@ -2,41 +2,32 @@
2
2
 
3
3
  VibeCodingMaster is a local GUI workspace for managing multiple Claude Code role sessions around one engineering task.
4
4
 
5
- It is designed for long-running coding work where one Claude Code conversation is not enough. VCM gives the user a task workspace with four role sessions:
5
+ VCM is designed for long-running coding work where one Claude Code conversation is not enough. It gives the user a task workspace with four embedded Claude Code sessions:
6
6
 
7
7
  - `project-manager`
8
8
  - `architect`
9
9
  - `coder`
10
10
  - `reviewer`
11
11
 
12
- Each role runs as a real Claude Code process inside an embedded terminal. The GUI lets the user start, stop, resume, switch, inspect, and manually intervene in those sessions without juggling several terminal windows.
12
+ Each role runs as a real Claude Code process inside an embedded terminal. The GUI lets the user start, stop, resume, restart, switch, observe, and manually intervene in those sessions without juggling separate terminal windows.
13
13
 
14
- ## What It Does
15
-
16
- ```text
17
- Open local GUI
18
- -> connect a Git repository
19
- -> create a task
20
- -> start Claude Code role sessions
21
- -> talk to Claude Code through embedded terminals
22
- -> let project-manager coordinate architect / coder / reviewer
23
- -> approve or automate role-to-role messages
24
- -> resume interrupted role sessions later
25
- ```
26
-
27
- Current V1 capabilities:
14
+ ## Current V1 Capabilities
28
15
 
29
16
  - GUI-first task workspace.
30
- - Embedded Claude Code terminals powered by `node-pty`.
31
- - One Claude Code session per role.
32
- - Role session recovery with persisted Claude session ids and `claude --resume`.
33
- - Permission mode selection before start/restart:
34
- - default
17
+ - Collapsible sidebar with repository connection, workflow, settings, harness status, task creation, and task list.
18
+ - Recent repository path dropdown, stored locally with the five most recent paths.
19
+ - Embedded Claude Code terminals powered by `node-pty` and `xterm.js`.
20
+ - One Claude Code session per role, with role tabs in the task header.
21
+ - Role session recovery through persisted Claude session ids and `claude --resume`.
22
+ - Permission mode selection before start, resume, or restart:
23
+ - `default`
35
24
  - `bypassPermissions`
36
25
  - `--dangerously-skip-permissions`
37
26
  - PM-mediated role messaging through `vcmctl`.
38
27
  - Manual and automatic orchestration modes.
39
- - Durable task state, session state, raw logs, handoff artifacts, and message history.
28
+ - VCM harness installer for `CLAUDE.md`, `.claude/agents/*.md`, and the VCM-managed `.gitignore` block.
29
+ - Translation panel powered by an OpenAI-compatible low-cost model.
30
+ - Durable task state, session state, raw terminal logs, handoff artifacts, and message history.
40
31
 
41
32
  ## Requirements
42
33
 
@@ -56,20 +47,16 @@ From npm:
56
47
 
57
48
  ```bash
58
49
  npm install -g vibe-coding-master
59
- ```
60
-
61
- Then start the packaged app:
62
-
63
- ```bash
64
50
  vcm
65
51
  ```
66
52
 
67
- The published package also installs `vcmctl`, which Claude Code role sessions use internally for VCM message bus commands.
53
+ The package also installs `vcmctl`, which Claude Code role sessions use internally to send VCM messages.
68
54
 
69
55
  From source:
70
56
 
71
57
  ```bash
72
58
  npm install
59
+ npm run dev
73
60
  ```
74
61
 
75
62
  ## Run Locally
@@ -107,7 +94,7 @@ Then open:
107
94
  http://127.0.0.1:4173/
108
95
  ```
109
96
 
110
- If installed globally from npm, `vcm` runs the production-style app and serves the GUI from the backend port:
97
+ The global `vcm` command runs the production-style app and serves the GUI from the backend port:
111
98
 
112
99
  ```text
113
100
  http://127.0.0.1:4173/
@@ -115,7 +102,7 @@ http://127.0.0.1:4173/
115
102
 
116
103
  ## Run In VS Code Dev Containers
117
104
 
118
- VCM works well inside a VS Code `devContainer` as long as VCM, Claude Code, and the target repository are all inside the same container filesystem.
105
+ VCM works inside a VS Code `devContainer` when VCM, Claude Code, and the target repository all run inside the same container filesystem.
119
106
 
120
107
  Add port forwarding to `.devcontainer/devcontainer.json`:
121
108
 
@@ -133,39 +120,141 @@ Add port forwarding to `.devcontainer/devcontainer.json`:
133
120
  }
134
121
  ```
135
122
 
136
- Use:
137
-
138
- ```bash
139
- npm install
140
- npm run dev
141
- ```
142
-
143
- Open the forwarded `5173` port for development mode. If you run `npm run build && npm start`, only `4173` is required.
123
+ Use the path as seen from inside the container, for example `/workspace`.
144
124
 
145
125
  Important container notes:
146
126
 
147
- - Install Claude Code inside the container, or make the `claude` command available in the container PATH.
127
+ - Install Claude Code inside the container, or make `claude` available in the container `PATH`.
148
128
  - Make sure Claude Code authentication works inside the container.
149
- - Make sure the container has network access to Claude services.
150
- - Use the path as seen from inside the container, for example `/workspace`.
151
- - VCM accepts normal repositories by checking `/workspace/.git` directly; it does not require global Git `safe.directory` config to connect.
152
- - Keep the user project, `.vcm`, and `.ai/handoffs` on the same mounted workspace so paths are consistent.
129
+ - Make sure the container has network access to Claude services and to the translation provider if translation is enabled.
130
+ - VCM accepts normal Git repositories by checking `.git` directly. It also supports `.git` files that point to worktree gitdirs.
131
+ - VCM uses per-command `git -c safe.directory=...` for Git metadata reads and does not require global `git config --global --add safe.directory`.
153
132
  - Treat the container as the sandbox boundary, especially when using relaxed Claude Code permission modes.
154
133
 
155
134
  ## Basic Usage
156
135
 
157
136
  1. Start VCM.
158
137
  2. Open the GUI.
159
- 3. Connect a Git repository.
160
- 4. Create a task.
161
- 5. Select a role tab.
162
- 6. Choose the Claude Code permission mode for that role.
163
- 7. Click `Start`.
164
- 8. Talk to `project-manager` first.
165
- 9. Let PM delegate to `architect`, `coder`, or `reviewer`.
166
- 10. Inspect and approve role messages in the `Messages` panel.
138
+ 3. In the sidebar, open `Repository Path`, enter a repository path or choose one from `Recent`, then click `Connect`.
139
+ 4. Review `VCM Harness`; if files need install/update, click `Install / Update`.
140
+ 5. Review any changed harness files and commit them if they look right.
141
+ 6. Create a task from `New Task` with a single task name.
142
+ 7. Select the task from `Tasks`.
143
+ 8. Use the role tabs in the task header to switch between `Project Manager`, `Architect`, `Coder`, and `Reviewer`.
144
+ 9. Choose the permission mode for the active role.
145
+ 10. Click `Start`, `Resume`, `Restart`, or `Stop` as needed.
146
+ 11. Talk mostly to `project-manager`; let PM coordinate the other roles through VCM messaging.
147
+
148
+ The recommended flow is:
149
+
150
+ ```text
151
+ project-manager
152
+ -> architect architecture plan
153
+ -> coder implementation and validation
154
+ -> reviewer independent review
155
+ -> architect docs sync / architecture drift check
156
+ -> project-manager final acceptance, commit, and PR
157
+ ```
158
+
159
+ The workflow status is shown in the sidebar `Workflow` section. It is a soft guide in V1: VCM highlights missing or incomplete handoff artifacts and suggests the next step, but it does not hard-block the user from manually starting or switching roles.
160
+
161
+ ## Task Worktree Management
167
162
 
168
- The recommended flow is to mostly talk to `project-manager`. The PM role should coordinate the other roles through VCM messaging instead of asking the user to copy prompts between terminals.
163
+ VCM uses task-level worktree management:
164
+
165
+ ```text
166
+ one task = one branch + one git worktree + one handoff directory + one role-session set
167
+ ```
168
+
169
+ The default when creating a task:
170
+
171
+ - task name: `<task>`
172
+ - branch: `feature/<task>`
173
+ - worktree path: `.ai/vcm/worktrees/<task>` inside the connected base repository
174
+ - role session cwd: that task worktree
175
+
176
+ VCM will not create worktrees per role. `project-manager`, `architect`, `coder`, and `reviewer` for the same task share the same task worktree.
177
+
178
+ VCM will not offer a separate `Create task worktree` button after a task exists, and a task should not be switched to another branch/worktree after creation.
179
+
180
+ Because worktrees live under `.ai/vcm/worktrees/`, the connected repository must ignore `.ai/vcm/`. Apply the VCM Harness before creating tasks so `.gitignore` contains the managed ignore block. The base repository must also be clean because the task branch/worktree is created from the connected repo's current `HEAD`.
181
+
182
+ When a task is complete, VCM provides a guarded cleanup action that removes the task worktree and VCM task metadata. Cleanup refuses to remove a dirty task worktree unless a force cleanup is requested through the API. Branch deletion stays separate and requires explicit confirmation.
183
+
184
+ ## Sidebar UI
185
+
186
+ The left sidebar is intentionally compact and collapsible:
187
+
188
+ - `Repository Path`: path input on one row; `Recent` and `Connect` on the next row.
189
+ - `Repository`: connected path, branch, and working tree state. `Working tree: uncommitted changes` means `git status --porcelain` is not empty.
190
+ - `Workflow`: current soft gate and five workflow steps.
191
+ - `Settings`: `Messages`, `Events`, and the `Auto orchestration` on/off toggle.
192
+ - `VCM Harness`: status for `CLAUDE.md`, role agent files, and `.gitignore`.
193
+ - `New Task`: one `task name` input.
194
+ - `Tasks`: task list and task status.
195
+
196
+ All sidebar sections are collapsed by default. When no task is selected, `Repository Path` opens by default.
197
+
198
+ ## Translation
199
+
200
+ The `Translate` button in the role toolbar opens a translation panel beside the embedded terminal. The terminal and translation panel split the available width evenly.
201
+
202
+ Translation settings are local and stored in:
203
+
204
+ ```text
205
+ ~/.vcm/settings.json
206
+ ```
207
+
208
+ The same file stores recent repository paths. The translation API key is stored locally under `translation.secrets.apiKey`; it is not written to the connected repository, `.ai/handoffs`, raw terminal logs, or git diffs.
209
+
210
+ Translation behavior:
211
+
212
+ - Provider type is OpenAI-compatible chat completions.
213
+ - Prompt slots are `zh-to-en`, `zh-to-en-with-context`, and `en-to-zh`.
214
+ - The settings modal shows default prompts and allows per-slot overrides.
215
+ - Claude Code output translation reads semantic Claude transcript JSONL files under `~/.claude/projects`, not raw PTY output.
216
+ - Assistant prose is shown as English source while translating, then replaced by the translated Chinese result.
217
+ - Tool calls and tool results are preserved as dim one-line rows such as `● Bash({"command":"npm test"})`.
218
+ - User input uses one textarea. Press `Enter` to translate or send the current English draft; press `Shift+Enter` for a newline.
219
+ - After user input is translated, the English draft replaces the original text in the same textarea.
220
+ - `Send English` writes the current English draft to the active embedded terminal and submits it.
221
+ - The translation panel `Auto-send` toggle sends the translated draft automatically when translation succeeds without warnings.
222
+
223
+ ## Project Harness
224
+
225
+ VCM works best when the connected repository contains VCM collaboration rules as normal project files. On first connect, VCM checks:
226
+
227
+ ```text
228
+ CLAUDE.md
229
+ .gitignore
230
+ .claude/agents/project-manager.md
231
+ .claude/agents/architect.md
232
+ .claude/agents/coder.md
233
+ .claude/agents/reviewer.md
234
+ ```
235
+
236
+ If a file is missing, VCM can create a recommended default. If a file already exists, VCM preserves user-authored content and only inserts or replaces a managed block:
237
+
238
+ ```md
239
+ <!-- VCM:BEGIN version=1 -->
240
+ VCM-managed rules live here.
241
+ <!-- VCM:END -->
242
+ ```
243
+
244
+ For `.gitignore`, VCM uses a gitignore-native managed block:
245
+
246
+ ```gitignore
247
+ # VCM:BEGIN version=1
248
+ .ai/vcm/
249
+ .vcm/
250
+ # VCM:END
251
+ ```
252
+
253
+ `.ai/vcm/` is the active VCM local control area. `.vcm/` is ignored only as a legacy safety rule so older local state cannot be accidentally committed during migration.
254
+
255
+ After applying harness changes, VCM reports the exact files changed and reminds the user to review and commit them before starting long-running work.
256
+
257
+ Role sessions learn VCM rules from `CLAUDE.md` and `.claude/agents/*.md`. VCM does not paste a long context block into the terminal at session start.
169
258
 
170
259
  ## Message Bus
171
260
 
@@ -175,9 +264,9 @@ Role communication works like this:
175
264
 
176
265
  ```text
177
266
  Claude Code role
178
- -> runs vcmctl send / vcmctl reply
267
+ -> runs vcmctl send / vcmctl reply / vcmctl result
179
268
  -> vcmctl calls VCM backend API
180
- -> backend validates policy and persists the message
269
+ -> backend validates message policy and persists the message
181
270
  -> backend writes to the target embedded terminal when allowed
182
271
  ```
183
272
 
@@ -190,26 +279,28 @@ vcmctl result --body-file /tmp/result.md --artifact .ai/handoffs/task/implementa
190
279
  vcmctl inbox
191
280
  ```
192
281
 
193
- Files are still used for durability and auditability:
282
+ Durable message and handoff files:
194
283
 
195
284
  ```text
196
- .vcm/messages/<task>.jsonl
197
- .vcm/orchestration/<task>.json
285
+ .ai/vcm/messages/<task>.jsonl
286
+ .ai/vcm/orchestration/<task>.json
198
287
  .ai/handoffs/<task>/messages/<message-id>.md
199
288
  .ai/handoffs/<task>/role-commands/
200
289
  .ai/handoffs/<task>/logs/
201
290
  ```
202
291
 
292
+ The backend also keeps a compatibility role-command dispatch endpoint, but the primary workflow is PM-mediated `vcmctl` messaging.
293
+
203
294
  ## Orchestration Modes
204
295
 
205
- VCM has a task-level `Auto orchestration` switch.
296
+ VCM has a task-level `Auto orchestration` switch in the sidebar `Settings` section.
206
297
 
207
298
  When it is off, VCM is in manual mode:
208
299
 
209
300
  - Roles may send messages through `vcmctl`.
210
- - Messages appear in the GUI.
301
+ - Messages appear in the `Messages` modal.
211
302
  - The user can inspect them.
212
- - Clicking `Stage` writes the message prompt into the target embedded terminal input line.
303
+ - Clicking `Stage` writes a prompt into the target embedded terminal input line.
213
304
  - VCM does not press Enter for the user.
214
305
 
215
306
  When it is on, VCM is in auto mode:
@@ -217,13 +308,62 @@ When it is on, VCM is in auto mode:
217
308
  - Backend policy still applies.
218
309
  - PM can send work to `architect`, `coder`, or `reviewer`.
219
310
  - Non-PM roles can reply only to `project-manager`.
220
- - If the target role session is running and orchestration is not paused, VCM can deliver the message directly to the target terminal.
311
+ - If the target role session is running, VCM writes a `[VCM MESSAGE]` envelope to the target terminal and submits it.
221
312
 
222
- High-risk work should still stop for human review.
313
+ The backend state model still contains a `paused` field for compatibility with existing API routes, but the current GUI exposes only a single on/off orchestration toggle.
223
314
 
224
315
  ## Resume Behavior
225
316
 
226
- Each role session stores a Claude session id under `.vcm/sessions`. If VCM exits or a task is interrupted, reopen the task and use `Resume` for the role. VCM starts Claude Code with `claude --resume <session-id>` where supported by Claude Code.
317
+ Each role session stores its Claude session id and transcript path under:
318
+
319
+ ```text
320
+ .ai/vcm/sessions/<task>.json
321
+ ```
322
+
323
+ Session buttons behave as follows:
324
+
325
+ - `Start`: creates a fresh UUID, builds `claude --agent <role> --session-id <uuid>`, and stores the transcript path.
326
+ - `Resume`: reuses the persisted Claude session id and builds `claude --agent <role> --resume <uuid>`.
327
+ - `Restart`: stops the current process if needed, creates a new UUID, and starts a fresh Claude session.
328
+ - `Stop`: stops the embedded terminal process and leaves the persisted Claude session id resumable.
329
+
330
+ ## Local Project Files
331
+
332
+ For a connected repository, VCM uses:
333
+
334
+ ```text
335
+ .ai/vcm/config.json
336
+ .ai/vcm/tasks/<task>.json
337
+ .ai/vcm/sessions/<task>.json
338
+ .ai/vcm/messages/<task>.jsonl
339
+ .ai/vcm/orchestration/<task>.json
340
+ .ai/vcm/worktrees/<task>/
341
+ .ai/handoffs/<task>/architecture-plan.md
342
+ .ai/handoffs/<task>/implementation-log.md
343
+ .ai/handoffs/<task>/validation-log.md
344
+ .ai/handoffs/<task>/review-report.md
345
+ .ai/handoffs/<task>/docs-sync-report.md
346
+ .ai/handoffs/<task>/role-commands/{architect,coder,reviewer}.md
347
+ .ai/handoffs/<task>/logs/{project-manager,architect,coder,reviewer}.log
348
+ ```
349
+
350
+ ## Packaging
351
+
352
+ The npm package publishes built output, not raw TypeScript entry files. `package.json` includes:
353
+
354
+ - `bin.vcm`: `dist/main.js`
355
+ - `bin.vcmctl`: `dist/cli/vcmctl.js`
356
+ - `files`: `dist`, `dist-frontend`, `docs`, `scripts`, `README.md`
357
+ - `prepack`: `npm run build && npm run verify:package`
358
+
359
+ Use this before publishing:
360
+
361
+ ```bash
362
+ npm run typecheck
363
+ npm test
364
+ npm run build
365
+ npm run verify:package
366
+ ```
227
367
 
228
368
  ## Validation
229
369
 
@@ -237,9 +377,10 @@ npm run build
237
377
 
238
378
  - VCM does not use tmux.
239
379
  - VCM does not auto-confirm Claude Code permission prompts.
240
- - VCM does not deeply parse Claude Code output.
241
380
  - VCM does not isolate roles with separate worktrees in V1.
242
- - File writes still happen in the connected repository environment.
381
+ - VCM does not translate Claude output from raw PTY output; translation reads Claude transcript JSONL files.
382
+ - VCM does not write translation output into handoff artifacts unless a user or role explicitly copies it there.
383
+ - Role file writes happen in the task worktree when a task has a worktree.
243
384
  - The safest sandbox today is a container or VM boundary controlled by the user.
244
385
 
245
386
  See also:
@@ -247,4 +388,4 @@ See also:
247
388
  - `docs/product-design.md`
248
389
  - `docs/v1-architecture-design.md`
249
390
  - `docs/v1-implementation-plan.md`
250
- - `docs/v1-message-bus-orchestration-design.md`
391
+ - `docs/cc-best-practices.md`
@@ -46,6 +46,19 @@ export function createNodeFileSystemAdapter() {
46
46
  }
47
47
  await this.writeText(targetPath, content);
48
48
  return true;
49
+ },
50
+ async copyDir(sourcePath, targetPath) {
51
+ await fs.cp(sourcePath, targetPath, {
52
+ recursive: true,
53
+ force: false,
54
+ errorOnExist: false
55
+ });
56
+ },
57
+ async removePath(targetPath, options = {}) {
58
+ await fs.rm(targetPath, {
59
+ recursive: options.recursive ?? false,
60
+ force: options.force ?? false
61
+ });
49
62
  }
50
63
  };
51
64
  }
@@ -22,6 +22,9 @@ export function createGitAdapter(runner) {
22
22
  return result.stdout.trim() || "detached";
23
23
  },
24
24
  async isDirty(repoRoot) {
25
+ return (await this.getStatusPorcelain(repoRoot)).trim().length > 0;
26
+ },
27
+ async getStatusPorcelain(repoRoot) {
25
28
  const result = await runGit(runner, repoRoot, ["status", "--porcelain"]);
26
29
  if (result.exitCode !== 0) {
27
30
  throw new VcmError({
@@ -31,7 +34,82 @@ export function createGitAdapter(runner) {
31
34
  hint: result.stderr
32
35
  });
33
36
  }
34
- return result.stdout.trim().length > 0;
37
+ return result.stdout;
38
+ },
39
+ async isIgnored(repoRoot, repoRelativePath) {
40
+ const result = await runGit(runner, repoRoot, ["check-ignore", "-q", "--", repoRelativePath]);
41
+ if (result.exitCode === 0) {
42
+ return true;
43
+ }
44
+ if (result.exitCode === 1) {
45
+ return false;
46
+ }
47
+ throw new VcmError({
48
+ code: "GIT_ERROR",
49
+ message: `Unable to check whether Git ignores ${repoRelativePath}.`,
50
+ statusCode: 400,
51
+ hint: result.stderr
52
+ });
53
+ },
54
+ async branchExists(repoRoot, branch) {
55
+ const result = await runGit(runner, repoRoot, ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`]);
56
+ if (result.exitCode === 0) {
57
+ return true;
58
+ }
59
+ if (result.exitCode === 1) {
60
+ return false;
61
+ }
62
+ throw new VcmError({
63
+ code: "GIT_ERROR",
64
+ message: `Unable to check Git branch: ${branch}`,
65
+ statusCode: 400,
66
+ hint: result.stderr
67
+ });
68
+ },
69
+ async createWorktree(input) {
70
+ const result = await runGit(runner, input.repoRoot, [
71
+ "worktree",
72
+ "add",
73
+ "-b",
74
+ input.branch,
75
+ input.worktreePath,
76
+ input.baseRef ?? "HEAD"
77
+ ]);
78
+ if (result.exitCode !== 0) {
79
+ throw new VcmError({
80
+ code: "GIT_WORKTREE_CREATE_FAILED",
81
+ message: `Unable to create task worktree: ${input.worktreePath}`,
82
+ statusCode: 400,
83
+ hint: result.stderr
84
+ });
85
+ }
86
+ },
87
+ async removeWorktree(repoRoot, worktreePath, options = {}) {
88
+ const args = ["worktree", "remove"];
89
+ if (options.force) {
90
+ args.push("--force");
91
+ }
92
+ args.push(worktreePath);
93
+ const result = await runGit(runner, repoRoot, args);
94
+ if (result.exitCode !== 0) {
95
+ throw new VcmError({
96
+ code: "GIT_WORKTREE_REMOVE_FAILED",
97
+ message: `Unable to remove task worktree: ${worktreePath}`,
98
+ statusCode: 400,
99
+ hint: result.stderr
100
+ });
101
+ }
102
+ },
103
+ async deleteBranch(repoRoot, branch, options = {}) {
104
+ const result = await runGit(runner, repoRoot, ["branch", options.force ? "-D" : "-d", branch]);
105
+ if (result.exitCode !== 0) {
106
+ throw new VcmError({
107
+ code: "GIT_BRANCH_DELETE_FAILED",
108
+ message: `Unable to delete Git branch: ${branch}`,
109
+ statusCode: 400,
110
+ hint: result.stderr
111
+ });
112
+ }
35
113
  }
36
114
  };
37
115
  }
@@ -0,0 +1,145 @@
1
+ export class TranslationProviderError extends Error {
2
+ code;
3
+ elapsedMs;
4
+ constructor(message, code, elapsedMs = 0) {
5
+ super(message);
6
+ this.name = "TranslationProviderError";
7
+ this.code = code;
8
+ this.elapsedMs = elapsedMs;
9
+ }
10
+ }
11
+ export function createOpenAiCompatibleTranslationProvider(fetchImpl = fetch) {
12
+ return {
13
+ async testConnection(settings, secrets) {
14
+ const startedAt = performance.now();
15
+ try {
16
+ await this.translate({
17
+ settings,
18
+ secrets,
19
+ systemPrompt: "Reply with exactly: ok",
20
+ userPrompt: "ok"
21
+ });
22
+ return {
23
+ ok: true,
24
+ model: settings.model,
25
+ elapsedMs: Math.round(performance.now() - startedAt)
26
+ };
27
+ }
28
+ catch (error) {
29
+ return {
30
+ ok: false,
31
+ model: settings.model,
32
+ elapsedMs: Math.round(performance.now() - startedAt),
33
+ error: error instanceof Error ? error.message : "Translation provider failed."
34
+ };
35
+ }
36
+ },
37
+ async translate(input) {
38
+ const apiKey = input.secrets.apiKey?.trim();
39
+ if (!apiKey) {
40
+ throw new TranslationProviderError("Translation API key is not configured.", "config");
41
+ }
42
+ const startedAt = performance.now();
43
+ const elapsed = () => Math.round(performance.now() - startedAt);
44
+ const controller = new AbortController();
45
+ const timeout = setTimeout(() => controller.abort(), input.settings.requestTimeoutMs);
46
+ const externalAbort = () => controller.abort();
47
+ if (input.signal) {
48
+ if (input.signal.aborted) {
49
+ controller.abort();
50
+ }
51
+ else {
52
+ input.signal.addEventListener("abort", externalAbort, { once: true });
53
+ }
54
+ }
55
+ try {
56
+ const response = await fetchImpl(buildChatCompletionsUrl(input.settings.baseUrl), {
57
+ method: "POST",
58
+ headers: {
59
+ "content-type": "application/json",
60
+ authorization: `Bearer ${apiKey}`
61
+ },
62
+ body: JSON.stringify({
63
+ model: input.settings.model,
64
+ messages: [
65
+ { role: "system", content: input.systemPrompt },
66
+ { role: "user", content: input.userPrompt }
67
+ ],
68
+ temperature: input.settings.temperature,
69
+ stream: false
70
+ }),
71
+ signal: controller.signal
72
+ });
73
+ const rawText = await response.text();
74
+ let payload = null;
75
+ try {
76
+ payload = rawText ? JSON.parse(rawText) : null;
77
+ }
78
+ catch {
79
+ if (!response.ok) {
80
+ throw new TranslationProviderError(rawText || response.statusText, `HTTP ${response.status}`, elapsed());
81
+ }
82
+ throw new TranslationProviderError("Translation provider returned invalid JSON.", "parse", elapsed());
83
+ }
84
+ if (!response.ok) {
85
+ throw new TranslationProviderError(extractErrorMessage(payload) ?? response.statusText, `HTTP ${response.status}`, elapsed());
86
+ }
87
+ const content = extractContent(payload);
88
+ if (!content) {
89
+ throw new TranslationProviderError("Translation provider returned empty content.", "parse", elapsed());
90
+ }
91
+ return {
92
+ text: content,
93
+ elapsedMs: elapsed(),
94
+ tokenUsage: parseOpenAiUsage(payload?.usage)
95
+ };
96
+ }
97
+ catch (error) {
98
+ if (error instanceof TranslationProviderError) {
99
+ throw error;
100
+ }
101
+ const message = error instanceof Error ? error.message : String(error);
102
+ const code = message.toLowerCase().includes("abort") ? "timeout" : "network";
103
+ throw new TranslationProviderError(message, code, elapsed());
104
+ }
105
+ finally {
106
+ clearTimeout(timeout);
107
+ input.signal?.removeEventListener("abort", externalAbort);
108
+ }
109
+ }
110
+ };
111
+ }
112
+ export function buildChatCompletionsUrl(baseUrl) {
113
+ return `${baseUrl.replace(/\/$/, "")}/chat/completions`;
114
+ }
115
+ export function parseOpenAiUsage(raw) {
116
+ if (!raw || typeof raw !== "object") {
117
+ return undefined;
118
+ }
119
+ const usage = raw;
120
+ const input = typeof usage.prompt_tokens === "number" ? usage.prompt_tokens : 0;
121
+ const output = typeof usage.completion_tokens === "number" ? usage.completion_tokens : 0;
122
+ const total = typeof usage.total_tokens === "number" ? usage.total_tokens : input + output;
123
+ if (input === 0 && output === 0 && total === 0) {
124
+ return undefined;
125
+ }
126
+ return { input, output, total };
127
+ }
128
+ function extractContent(payload) {
129
+ const choices = payload?.choices;
130
+ if (!Array.isArray(choices) || choices.length === 0) {
131
+ return null;
132
+ }
133
+ const content = choices[0].message?.content;
134
+ return typeof content === "string" ? content.trim() : null;
135
+ }
136
+ function extractErrorMessage(payload) {
137
+ const error = payload?.error;
138
+ if (!error) {
139
+ return null;
140
+ }
141
+ if (typeof error === "string") {
142
+ return error;
143
+ }
144
+ return error.message ?? JSON.stringify(error);
145
+ }