vibe-coding-master 0.0.6 → 0.0.7
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/README.md +168 -63
- package/dist/backend/adapters/translation-provider.js +145 -0
- package/dist/backend/api/artifact-routes.js +3 -0
- package/dist/backend/api/harness-routes.js +22 -0
- package/dist/backend/api/project-routes.js +3 -8
- package/dist/backend/api/translation-routes.js +70 -0
- package/dist/backend/runtime/node-pty-runtime.js +20 -18
- package/dist/backend/server.js +31 -1
- package/dist/backend/services/app-settings-service.js +128 -0
- package/dist/backend/services/artifact-service.js +7 -4
- package/dist/backend/services/claude-transcript-service.js +509 -0
- package/dist/backend/services/harness-service.js +178 -0
- package/dist/backend/services/project-service.js +4 -0
- package/dist/backend/services/session-service.js +7 -5
- package/dist/backend/services/status-service.js +76 -0
- package/dist/backend/services/translation-prompts.js +173 -0
- package/dist/backend/services/translation-queue.js +39 -0
- package/dist/backend/services/translation-service.js +546 -0
- package/dist/backend/templates/handoff.js +32 -0
- package/dist/backend/templates/harness/architect-agent.js +12 -0
- package/dist/backend/templates/harness/claude-root.js +14 -0
- package/dist/backend/templates/harness/coder-agent.js +11 -0
- package/dist/backend/templates/harness/project-manager-agent.js +14 -0
- package/dist/backend/templates/harness/reviewer-agent.js +13 -0
- package/dist/backend/ws/translation-ws.js +35 -0
- package/dist/shared/types/harness.js +1 -0
- package/dist/shared/types/translation.js +5 -0
- package/dist/shared/validation/artifact-check.js +15 -1
- package/dist/shared/validation/language-detect.js +46 -0
- package/dist-frontend/assets/index-BNASqKEK.css +32 -0
- package/dist-frontend/assets/index-Bp49_End.js +58 -0
- package/dist-frontend/index.html +2 -2
- package/docs/cc-best-practices.md +93 -36
- package/docs/product-design.md +313 -1408
- package/docs/v1-architecture-design.md +500 -1153
- package/docs/v1-implementation-plan.md +783 -1604
- package/package.json +1 -1
- package/scripts/verify-package.mjs +8 -0
- package/dist/backend/templates/role-messaging-context.js +0 -44
- package/dist-frontend/assets/index-Bah6k-Ix.css +0 -32
- package/dist-frontend/assets/index-EMaQuIB6.js +0 -58
- 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
|
-
|
|
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,
|
|
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
|
-
##
|
|
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
|
-
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
|
|
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
|
-
-
|
|
28
|
+
- VCM harness installer for `CLAUDE.md` and `.claude/agents/*.md`.
|
|
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
|
|
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
|
-
|
|
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
|
|
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,106 @@ 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
|
|
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
|
-
-
|
|
151
|
-
- VCM
|
|
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.
|
|
160
|
-
4.
|
|
161
|
-
5.
|
|
162
|
-
6.
|
|
163
|
-
7.
|
|
164
|
-
8.
|
|
165
|
-
9.
|
|
166
|
-
10.
|
|
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
|
+
## Sidebar UI
|
|
162
|
+
|
|
163
|
+
The left sidebar is intentionally compact and collapsible:
|
|
164
|
+
|
|
165
|
+
- `Repository Path`: path input on one row; `Recent` and `Connect` on the next row.
|
|
166
|
+
- `Repository`: connected path, branch, and working tree state. `Working tree: uncommitted changes` means `git status --porcelain` is not empty.
|
|
167
|
+
- `Workflow`: current soft gate and five workflow steps.
|
|
168
|
+
- `Settings`: `Messages`, `Events`, and the `Auto orchestration` on/off toggle.
|
|
169
|
+
- `VCM Harness`: status for `CLAUDE.md` and role agent files.
|
|
170
|
+
- `New Task`: one `task name` input.
|
|
171
|
+
- `Tasks`: task list and task status.
|
|
172
|
+
|
|
173
|
+
All sidebar sections are collapsed by default. When no task is selected, `Repository Path` opens by default.
|
|
174
|
+
|
|
175
|
+
## Translation
|
|
176
|
+
|
|
177
|
+
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.
|
|
178
|
+
|
|
179
|
+
Translation settings are local and stored in:
|
|
180
|
+
|
|
181
|
+
```text
|
|
182
|
+
~/.vcm/settings.json
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
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.
|
|
186
|
+
|
|
187
|
+
Translation behavior:
|
|
188
|
+
|
|
189
|
+
- Provider type is OpenAI-compatible chat completions.
|
|
190
|
+
- Prompt slots are `zh-to-en`, `zh-to-en-with-context`, and `en-to-zh`.
|
|
191
|
+
- The settings modal shows default prompts and allows per-slot overrides.
|
|
192
|
+
- Claude Code output translation reads semantic Claude transcript JSONL files under `~/.claude/projects`, not raw PTY output.
|
|
193
|
+
- Assistant prose is shown as English source while translating, then replaced by the translated Chinese result.
|
|
194
|
+
- Tool calls and tool results are preserved as dim one-line rows such as `● Bash({"command":"npm test"})`.
|
|
195
|
+
- User input uses one textarea. Press `Enter` to translate or send the current English draft; press `Shift+Enter` for a newline.
|
|
196
|
+
- After user input is translated, the English draft replaces the original text in the same textarea.
|
|
197
|
+
- `Send English` writes the current English draft to the active embedded terminal and submits it.
|
|
198
|
+
- The translation panel `Auto-send` toggle sends the translated draft automatically when translation succeeds without warnings.
|
|
167
199
|
|
|
168
|
-
|
|
200
|
+
## Project Harness
|
|
201
|
+
|
|
202
|
+
VCM works best when the connected repository contains VCM collaboration rules as normal project files. On first connect, VCM checks:
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
CLAUDE.md
|
|
206
|
+
.claude/agents/project-manager.md
|
|
207
|
+
.claude/agents/architect.md
|
|
208
|
+
.claude/agents/coder.md
|
|
209
|
+
.claude/agents/reviewer.md
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
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:
|
|
213
|
+
|
|
214
|
+
```md
|
|
215
|
+
<!-- VCM:BEGIN version=1 -->
|
|
216
|
+
VCM-managed rules live here.
|
|
217
|
+
<!-- VCM:END -->
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
After applying harness changes, VCM reports the exact files changed and reminds the user to review and commit them before starting long-running work.
|
|
221
|
+
|
|
222
|
+
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
223
|
|
|
170
224
|
## Message Bus
|
|
171
225
|
|
|
@@ -175,9 +229,9 @@ Role communication works like this:
|
|
|
175
229
|
|
|
176
230
|
```text
|
|
177
231
|
Claude Code role
|
|
178
|
-
-> runs vcmctl send / vcmctl reply
|
|
232
|
+
-> runs vcmctl send / vcmctl reply / vcmctl result
|
|
179
233
|
-> vcmctl calls VCM backend API
|
|
180
|
-
-> backend validates policy and persists the message
|
|
234
|
+
-> backend validates message policy and persists the message
|
|
181
235
|
-> backend writes to the target embedded terminal when allowed
|
|
182
236
|
```
|
|
183
237
|
|
|
@@ -190,7 +244,7 @@ vcmctl result --body-file /tmp/result.md --artifact .ai/handoffs/task/implementa
|
|
|
190
244
|
vcmctl inbox
|
|
191
245
|
```
|
|
192
246
|
|
|
193
|
-
|
|
247
|
+
Durable message and handoff files:
|
|
194
248
|
|
|
195
249
|
```text
|
|
196
250
|
.vcm/messages/<task>.jsonl
|
|
@@ -200,16 +254,18 @@ Files are still used for durability and auditability:
|
|
|
200
254
|
.ai/handoffs/<task>/logs/
|
|
201
255
|
```
|
|
202
256
|
|
|
257
|
+
The backend also keeps a compatibility role-command dispatch endpoint, but the primary workflow is PM-mediated `vcmctl` messaging.
|
|
258
|
+
|
|
203
259
|
## Orchestration Modes
|
|
204
260
|
|
|
205
|
-
VCM has a task-level `Auto orchestration` switch.
|
|
261
|
+
VCM has a task-level `Auto orchestration` switch in the sidebar `Settings` section.
|
|
206
262
|
|
|
207
263
|
When it is off, VCM is in manual mode:
|
|
208
264
|
|
|
209
265
|
- Roles may send messages through `vcmctl`.
|
|
210
|
-
- Messages appear in the
|
|
266
|
+
- Messages appear in the `Messages` modal.
|
|
211
267
|
- The user can inspect them.
|
|
212
|
-
- Clicking `Stage` writes
|
|
268
|
+
- Clicking `Stage` writes a prompt into the target embedded terminal input line.
|
|
213
269
|
- VCM does not press Enter for the user.
|
|
214
270
|
|
|
215
271
|
When it is on, VCM is in auto mode:
|
|
@@ -217,13 +273,61 @@ When it is on, VCM is in auto mode:
|
|
|
217
273
|
- Backend policy still applies.
|
|
218
274
|
- PM can send work to `architect`, `coder`, or `reviewer`.
|
|
219
275
|
- Non-PM roles can reply only to `project-manager`.
|
|
220
|
-
- If the target role session is running
|
|
276
|
+
- If the target role session is running, VCM writes a `[VCM MESSAGE]` envelope to the target terminal and submits it.
|
|
221
277
|
|
|
222
|
-
|
|
278
|
+
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
279
|
|
|
224
280
|
## Resume Behavior
|
|
225
281
|
|
|
226
|
-
Each role session stores
|
|
282
|
+
Each role session stores its Claude session id and transcript path under:
|
|
283
|
+
|
|
284
|
+
```text
|
|
285
|
+
.vcm/sessions/<task>.json
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
Session buttons behave as follows:
|
|
289
|
+
|
|
290
|
+
- `Start`: creates a fresh UUID, builds `claude --agent <role> --session-id <uuid>`, and stores the transcript path.
|
|
291
|
+
- `Resume`: reuses the persisted Claude session id and builds `claude --agent <role> --resume <uuid>`.
|
|
292
|
+
- `Restart`: stops the current process if needed, creates a new UUID, and starts a fresh Claude session.
|
|
293
|
+
- `Stop`: stops the embedded terminal process and leaves the persisted Claude session id resumable.
|
|
294
|
+
|
|
295
|
+
## Local Project Files
|
|
296
|
+
|
|
297
|
+
For a connected repository, VCM uses:
|
|
298
|
+
|
|
299
|
+
```text
|
|
300
|
+
.vcm/config.json
|
|
301
|
+
.vcm/tasks/<task>.json
|
|
302
|
+
.vcm/sessions/<task>.json
|
|
303
|
+
.vcm/messages/<task>.jsonl
|
|
304
|
+
.vcm/orchestration/<task>.json
|
|
305
|
+
.ai/handoffs/<task>/architecture-plan.md
|
|
306
|
+
.ai/handoffs/<task>/implementation-log.md
|
|
307
|
+
.ai/handoffs/<task>/validation-log.md
|
|
308
|
+
.ai/handoffs/<task>/review-report.md
|
|
309
|
+
.ai/handoffs/<task>/docs-sync-report.md
|
|
310
|
+
.ai/handoffs/<task>/role-commands/{architect,coder,reviewer}.md
|
|
311
|
+
.ai/handoffs/<task>/logs/{project-manager,architect,coder,reviewer}.log
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## Packaging
|
|
315
|
+
|
|
316
|
+
The npm package publishes built output, not raw TypeScript entry files. `package.json` includes:
|
|
317
|
+
|
|
318
|
+
- `bin.vcm`: `dist/main.js`
|
|
319
|
+
- `bin.vcmctl`: `dist/cli/vcmctl.js`
|
|
320
|
+
- `files`: `dist`, `dist-frontend`, `docs`, `scripts`, `README.md`
|
|
321
|
+
- `prepack`: `npm run build && npm run verify:package`
|
|
322
|
+
|
|
323
|
+
Use this before publishing:
|
|
324
|
+
|
|
325
|
+
```bash
|
|
326
|
+
npm run typecheck
|
|
327
|
+
npm test
|
|
328
|
+
npm run build
|
|
329
|
+
npm run verify:package
|
|
330
|
+
```
|
|
227
331
|
|
|
228
332
|
## Validation
|
|
229
333
|
|
|
@@ -237,8 +341,9 @@ npm run build
|
|
|
237
341
|
|
|
238
342
|
- VCM does not use tmux.
|
|
239
343
|
- VCM does not auto-confirm Claude Code permission prompts.
|
|
240
|
-
- VCM does not deeply parse Claude Code output.
|
|
241
344
|
- VCM does not isolate roles with separate worktrees in V1.
|
|
345
|
+
- VCM does not translate Claude output from raw PTY output; translation reads Claude transcript JSONL files.
|
|
346
|
+
- VCM does not write translation output into handoff artifacts unless a user or role explicitly copies it there.
|
|
242
347
|
- File writes still happen in the connected repository environment.
|
|
243
348
|
- The safest sandbox today is a container or VM boundary controlled by the user.
|
|
244
349
|
|
|
@@ -247,4 +352,4 @@ See also:
|
|
|
247
352
|
- `docs/product-design.md`
|
|
248
353
|
- `docs/v1-architecture-design.md`
|
|
249
354
|
- `docs/v1-implementation-plan.md`
|
|
250
|
-
- `docs/
|
|
355
|
+
- `docs/cc-best-practices.md`
|
|
@@ -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
|
+
}
|
|
@@ -90,6 +90,9 @@ function artifactNameToPath(paths, artifactName) {
|
|
|
90
90
|
if (artifactName === "review-report.md") {
|
|
91
91
|
return paths.reviewReportPath;
|
|
92
92
|
}
|
|
93
|
+
if (artifactName === "docs-sync-report.md") {
|
|
94
|
+
return paths.docsSyncReportPath;
|
|
95
|
+
}
|
|
93
96
|
throw new VcmError({
|
|
94
97
|
code: "ARTIFACT_UNKNOWN",
|
|
95
98
|
message: `Unknown artifact: ${artifactName}`,
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { VcmError } from "../errors.js";
|
|
2
|
+
export function registerHarnessRoutes(app, deps) {
|
|
3
|
+
app.get("/api/projects/harness", async () => {
|
|
4
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
5
|
+
return deps.harnessService.getHarnessStatus(project.repoRoot);
|
|
6
|
+
});
|
|
7
|
+
app.post("/api/projects/harness/apply", async () => {
|
|
8
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
9
|
+
return deps.harnessService.applyHarness(project.repoRoot);
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
async function requireCurrentProject(projectService) {
|
|
13
|
+
const project = await projectService.getCurrentProject();
|
|
14
|
+
if (!project) {
|
|
15
|
+
throw new VcmError({
|
|
16
|
+
code: "PROJECT_NOT_CONNECTED",
|
|
17
|
+
message: "Connect a repository first.",
|
|
18
|
+
statusCode: 409
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return project;
|
|
22
|
+
}
|
|
@@ -1,17 +1,12 @@
|
|
|
1
1
|
export function registerProjectRoutes(app, deps) {
|
|
2
2
|
app.get("/api/health", async () => ({ ok: true }));
|
|
3
|
+
app.get("/api/projects/recent", async () => {
|
|
4
|
+
return deps.projectService.getRecentRepositoryPaths();
|
|
5
|
+
});
|
|
3
6
|
app.post("/api/projects/connect", async (request) => {
|
|
4
7
|
return deps.projectService.connectProject(request.body);
|
|
5
8
|
});
|
|
6
9
|
app.get("/api/projects/current", async () => {
|
|
7
10
|
return deps.projectService.getCurrentProject();
|
|
8
11
|
});
|
|
9
|
-
app.get("/api/projects/harness", async () => ({
|
|
10
|
-
checks: [
|
|
11
|
-
{
|
|
12
|
-
name: "local-gui",
|
|
13
|
-
status: "ok"
|
|
14
|
-
}
|
|
15
|
-
]
|
|
16
|
-
}));
|
|
17
12
|
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { isRoleName } from "../../shared/constants.js";
|
|
2
|
+
import { VcmError } from "../errors.js";
|
|
3
|
+
export function registerTranslationRoutes(app, deps) {
|
|
4
|
+
app.get("/api/translation/settings", async () => {
|
|
5
|
+
return deps.translationService.getSettings();
|
|
6
|
+
});
|
|
7
|
+
app.put("/api/translation/settings", async (request) => {
|
|
8
|
+
const { apiKey, ...settings } = request.body ?? {};
|
|
9
|
+
return deps.translationService.updateSettings(settings, apiKey !== undefined ? { apiKey } : undefined);
|
|
10
|
+
});
|
|
11
|
+
app.get("/api/translation/prompts", async () => {
|
|
12
|
+
return deps.translationService.getPromptPreviews();
|
|
13
|
+
});
|
|
14
|
+
app.post("/api/translation/test", async () => {
|
|
15
|
+
return deps.translationService.testProvider();
|
|
16
|
+
});
|
|
17
|
+
app.post("/api/tasks/:taskSlug/sessions/:role/translation/input", async (request) => {
|
|
18
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
19
|
+
const role = parseRole(request.params.role);
|
|
20
|
+
await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
21
|
+
return deps.translationService.translateUserInput({
|
|
22
|
+
repoRoot: project.repoRoot,
|
|
23
|
+
taskSlug: request.params.taskSlug,
|
|
24
|
+
role,
|
|
25
|
+
...(request.body ?? { text: "" })
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
app.post("/api/tasks/:taskSlug/sessions/:role/translation/send", async (request) => {
|
|
29
|
+
const project = await requireCurrentProject(deps.projectService);
|
|
30
|
+
const role = parseRole(request.params.role);
|
|
31
|
+
await deps.taskService.loadTask(project.repoRoot, request.params.taskSlug);
|
|
32
|
+
await deps.translationService.sendTranslatedInput({
|
|
33
|
+
repoRoot: project.repoRoot,
|
|
34
|
+
taskSlug: request.params.taskSlug,
|
|
35
|
+
role,
|
|
36
|
+
englishText: request.body?.englishText ?? ""
|
|
37
|
+
});
|
|
38
|
+
return { ok: true };
|
|
39
|
+
});
|
|
40
|
+
app.post("/api/translation/sessions/:sessionId/clear", async (request) => {
|
|
41
|
+
await requireCurrentProject(deps.projectService);
|
|
42
|
+
deps.translationService.clearSession(request.params.sessionId);
|
|
43
|
+
return { ok: true };
|
|
44
|
+
});
|
|
45
|
+
app.post("/api/translation/sessions/:sessionId/retry/:translationId", async (request) => {
|
|
46
|
+
await requireCurrentProject(deps.projectService);
|
|
47
|
+
return deps.translationService.retryTranslation(request.params.sessionId, request.params.translationId);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
function parseRole(role) {
|
|
51
|
+
if (!isRoleName(role)) {
|
|
52
|
+
throw new VcmError({
|
|
53
|
+
code: "UNKNOWN_ROLE",
|
|
54
|
+
message: `Unknown role: ${role}`,
|
|
55
|
+
statusCode: 400
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return role;
|
|
59
|
+
}
|
|
60
|
+
async function requireCurrentProject(projectService) {
|
|
61
|
+
const project = await projectService.getCurrentProject();
|
|
62
|
+
if (!project) {
|
|
63
|
+
throw new VcmError({
|
|
64
|
+
code: "PROJECT_NOT_CONNECTED",
|
|
65
|
+
message: "Connect a repository first.",
|
|
66
|
+
statusCode: 409
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return project;
|
|
70
|
+
}
|
|
@@ -109,27 +109,29 @@ export function createNodePtyTerminalRuntime(deps) {
|
|
|
109
109
|
entry.process.kill();
|
|
110
110
|
return create(entry.input, sessionId);
|
|
111
111
|
},
|
|
112
|
-
subscribe(sessionId, listener) {
|
|
112
|
+
subscribe(sessionId, listener, options = {}) {
|
|
113
113
|
const entry = getEntry(entries, sessionId);
|
|
114
114
|
entry.listeners.add(listener);
|
|
115
|
-
|
|
116
|
-
.
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
115
|
+
if (options.replay !== false) {
|
|
116
|
+
void deps.fs.readText(entry.input.logPath)
|
|
117
|
+
.then((data) => {
|
|
118
|
+
if (!data || !entry.listeners.has(listener)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
listener({
|
|
122
|
+
id: `evt_${Date.now()}_${Math.random().toString(16).slice(2)}`,
|
|
123
|
+
sessionId,
|
|
124
|
+
taskSlug: entry.session.taskSlug,
|
|
125
|
+
role: entry.session.role,
|
|
126
|
+
type: "output",
|
|
127
|
+
timestamp: now(),
|
|
128
|
+
data
|
|
129
|
+
});
|
|
130
|
+
})
|
|
131
|
+
.catch(() => {
|
|
132
|
+
// The log file may not exist yet for a brand-new session.
|
|
128
133
|
});
|
|
129
|
-
}
|
|
130
|
-
.catch(() => {
|
|
131
|
-
// The log file may not exist yet for a brand-new session.
|
|
132
|
-
});
|
|
134
|
+
}
|
|
133
135
|
return () => {
|
|
134
136
|
entry.listeners.delete(listener);
|
|
135
137
|
};
|