amicus 4.4.0 → 4.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +162 -0
- package/README.md +17 -2
- package/bin/amicus.js +10 -0
- package/docs/DISTRIBUTION.md +234 -0
- package/docs/ROADMAP.md +226 -0
- package/docs/SHIMS.md +62 -0
- package/docs/architecture.md +104 -0
- package/docs/configuration.md +395 -0
- package/docs/council.md +970 -0
- package/docs/doc-system.md +92 -0
- package/docs/electron-testing.md +471 -0
- package/docs/jsdoc-setup.md +75 -0
- package/docs/opencode-integration.md +114 -0
- package/docs/publishing.md +60 -0
- package/docs/schemas.md +56 -0
- package/docs/testing.md +589 -0
- package/docs/troubleshooting.md +298 -0
- package/docs/usage.md +849 -0
- package/electron/fold.js +1 -1
- package/electron/main.js +4 -1
- package/electron/setup-ui-aliases.js +6 -6
- package/electron/workspace-ui/live-model.js +12 -1
- package/electron/workspace-ui/md-lite.js +52 -8
- package/electron/workspace-ui/workspace-app.js +39 -17
- package/electron/workspace-ui/workspace-matrix.js +46 -9
- package/electron/workspace-ui/workspace-panels.js +88 -19
- package/electron/workspace-ui/workspace-render.js +17 -1
- package/electron/workspace-ui/workspace-verbs.js +48 -2
- package/package.json +8 -3
- package/schemas/council-run-live.schema.json +1 -1
- package/schemas/council-run.schema.json +34 -0
- package/schemas/error.schema.json +1 -1
- package/schemas/event.schema.json +1 -1
- package/schemas/pack.schema.json +30 -0
- package/schemas/progress.schema.json +13 -1
- package/schemas/run-live.schema.json +1 -1
- package/schemas/run.schema.json +2 -1
- package/schemas/spend.schema.json +52 -4
- package/schemas/wave-live.schema.json +1 -1
- package/schemas/wave.schema.json +2 -1
- package/skills/second-opinion/SKILL.md +5 -0
- package/src/cli-handlers-council-run.js +51 -8
- package/src/cli-handlers-pack.js +238 -0
- package/src/cli-handlers-run.js +36 -8
- package/src/cli-handlers-spend.js +20 -2
- package/src/cli-handlers-template.js +53 -0
- package/src/cli-handlers-watch.js +11 -0
- package/src/cli.js +68 -5
- package/src/council/briefings-debate.js +27 -7
- package/src/council/briefings-stage2.js +155 -25
- package/src/council/briefings.js +24 -1
- package/src/council/findings.js +199 -9
- package/src/council/parse-stage2.js +10 -2
- package/src/council/presets-cli.js +23 -11
- package/src/council/report.js +19 -8
- package/src/council/run-assemble.js +42 -1
- package/src/council/run-budget.js +64 -11
- package/src/council/run-chair.js +4 -1
- package/src/council/run-debate.js +4 -2
- package/src/council/run-finalize.js +102 -0
- package/src/council/run-launch.js +29 -1
- package/src/council/run-server.js +248 -0
- package/src/council/run-stage2.js +118 -0
- package/src/council/run-stages.js +134 -110
- package/src/council/run-state.js +40 -1
- package/src/council/run.js +45 -47
- package/src/council/tally.js +10 -0
- package/src/headless.js +180 -7
- package/src/mcp-council-run.js +108 -4
- package/src/mcp-server.js +203 -7
- package/src/mcp-tools.js +15 -5
- package/src/observe/council-legs.js +60 -3
- package/src/observe/live-doc.js +18 -1
- package/src/observe/watch-render.js +4 -1
- package/src/pack/pack-cli.js +38 -0
- package/src/pack/pack-forward.js +96 -0
- package/src/pack/pack-resolve.js +297 -0
- package/src/pack/pack-store.js +130 -0
- package/src/pack/pack-validate.js +113 -0
- package/src/sidecar/child-sessions.js +1 -2
- package/src/sidecar/fanout-leg-fallback.js +69 -21
- package/src/sidecar/fanout-leg.js +6 -0
- package/src/sidecar/fanout-signals.js +61 -0
- package/src/sidecar/fanout-wave-io.js +75 -0
- package/src/sidecar/fanout.js +82 -74
- package/src/sidecar/progress-fields.js +26 -4
- package/src/sidecar/progress.js +42 -1
- package/src/sidecar/session-utils.js +23 -14
- package/src/sidecar/start.js +5 -4
- package/src/sidecar/workspace-auto-open.js +69 -0
- package/src/sidecar/workspace-window.js +46 -1
- package/src/spend-query.js +17 -5
- package/src/template/apply.js +88 -0
- package/src/template/render.js +86 -0
- package/src/template/store.js +106 -0
- package/src/utils/config.js +65 -25
- package/src/utils/error-doc.js +5 -0
- package/src/utils/lifecycle.js +37 -1
- package/src/utils/path-fence.js +39 -1
- package/src/utils/pricing.js +26 -10
- package/src/utils/result-schema-rebuild.js +1 -0
- package/src/utils/result-schema.js +8 -2
- package/src/utils/server-setup.js +79 -1
- package/src/utils/spend-ledger.js +24 -3
- package/src/workspace/artifact-guard.js +66 -7
- package/src/workspace/fold-format.js +33 -4
- package/src/workspace/live-normalize.js +28 -15
- package/src/workspace/run-detail.js +13 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Documentation System
|
|
2
|
+
|
|
3
|
+
Canonical reference for the auto-documentation system that keeps CLAUDE.md in sync with the codebase.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
CLAUDE.md uses **progressive disclosure**: a slim main file (~400 lines) with auto-generated sections and pointers to deeper topic docs. This replaces a monolithic 875-line file that was too large for effective agent context.
|
|
8
|
+
|
|
9
|
+
Inspired by [OpenAI's Harness Engineering](https://openai.com/index/harness-engineering/) approach: "give Codex a map, not a 1,000-page instruction manual."
|
|
10
|
+
|
|
11
|
+
## Auto-Generated Sections
|
|
12
|
+
|
|
13
|
+
Sections between `<!-- AUTO:name -->` markers in CLAUDE.md are maintained by `scripts/generate-docs.js`. Do NOT edit these by hand.
|
|
14
|
+
|
|
15
|
+
### Marker Format
|
|
16
|
+
|
|
17
|
+
```markdown
|
|
18
|
+
<!-- AUTO:tree -->
|
|
19
|
+
(generated content here)
|
|
20
|
+
<!-- /AUTO:tree -->
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Current Markers
|
|
24
|
+
|
|
25
|
+
| Marker | Content | Source |
|
|
26
|
+
|--------|---------|--------|
|
|
27
|
+
| `tree` | ASCII directory tree with JSDoc annotations | Filesystem scan of `bin/`, `src/`, `electron/`, `scripts/`, `evals/` (note: `tests/` is NOT included) |
|
|
28
|
+
| `modules` | Markdown table of all `src/**/*.js` modules | JSDoc description + `module.exports` extraction |
|
|
29
|
+
|
|
30
|
+
### How It Works
|
|
31
|
+
|
|
32
|
+
1. `scripts/generate-docs.js` scans the codebase
|
|
33
|
+
2. For each marker, it generates new content from the source of truth (filesystem, JSDoc)
|
|
34
|
+
3. It replaces the content between the open/close marker tags
|
|
35
|
+
4. If CLAUDE.md changed, it auto-stages the file with `git add`
|
|
36
|
+
|
|
37
|
+
### Adding a New Auto-Generated Section
|
|
38
|
+
|
|
39
|
+
1. Add a new marker pair to CLAUDE.md (short lowercase name, no hyphens required but keep it terse):
|
|
40
|
+
```markdown
|
|
41
|
+
<!-- AUTO:my-section -->
|
|
42
|
+
<!-- /AUTO:my-section -->
|
|
43
|
+
```
|
|
44
|
+
2. Add a generator function in `scripts/generate-docs.js` (or `scripts/generate-docs-helpers.js`)
|
|
45
|
+
3. Add it to the `generated` map in `main()`
|
|
46
|
+
4. Add tests in `tests/scripts/generate-docs.test.js`
|
|
47
|
+
|
|
48
|
+
## Cross-Link Validation
|
|
49
|
+
|
|
50
|
+
When running `--check` mode, the script validates every markdown link in CLAUDE.md:
|
|
51
|
+
|
|
52
|
+
- `[text](path)` links are resolved relative to the project root
|
|
53
|
+
- External URLs (`https://...`) are skipped
|
|
54
|
+
- Anchor-only links (`#section`) are skipped
|
|
55
|
+
- Broken links cause `--check` to exit 1
|
|
56
|
+
|
|
57
|
+
## Plans Index
|
|
58
|
+
|
|
59
|
+
`docs/plans/index.md` is auto-generated by the same script. It lists all `.md` files in:
|
|
60
|
+
- `docs/plans/` (active plans)
|
|
61
|
+
- `docs/archive/plans/` (archived plans)
|
|
62
|
+
|
|
63
|
+
Each entry shows the filename, first heading, and date extracted from the filename.
|
|
64
|
+
|
|
65
|
+
## Commands
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
node scripts/generate-docs.js # Regenerate all auto sections + plans index
|
|
69
|
+
node scripts/generate-docs.js --check # Verify everything is current (CI mode)
|
|
70
|
+
npm run generate-docs # Alias for write mode
|
|
71
|
+
npm run generate-docs:check # Alias for check mode
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Pre-Commit Integration
|
|
75
|
+
|
|
76
|
+
The pre-commit hook runs `generate-docs.js` in write mode automatically. If CLAUDE.md changes, it stages the update. Developers never need to run the script manually.
|
|
77
|
+
|
|
78
|
+
Hook order:
|
|
79
|
+
1. lint-staged
|
|
80
|
+
2. check-secrets
|
|
81
|
+
3. check-file-sizes
|
|
82
|
+
4. **generate-docs.js** (auto-stages CLAUDE.md if changed)
|
|
83
|
+
5. validate-docs.js (warns about manual drift)
|
|
84
|
+
|
|
85
|
+
## Troubleshooting
|
|
86
|
+
|
|
87
|
+
| Problem | Solution |
|
|
88
|
+
|---------|---------|
|
|
89
|
+
| "Marker not found" error | Ensure `<!-- AUTO:name -->` and `<!-- /AUTO:name -->` exist in CLAUDE.md |
|
|
90
|
+
| Stale markers after code change | Run `node scripts/generate-docs.js` manually |
|
|
91
|
+
| Cross-link validation failure | Fix the broken link in CLAUDE.md or create the missing file |
|
|
92
|
+
| Plans index missing | Run the script; it creates `docs/plans/index.md` |
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
# Electron UI Testing (Chrome DevTools Protocol)
|
|
2
|
+
|
|
3
|
+
The Electron Amicus window runs with remote debugging enabled via the Chrome DevTools Protocol. This allows programmatic inspection and testing of the UI state.
|
|
4
|
+
|
|
5
|
+
## Prerequisites
|
|
6
|
+
|
|
7
|
+
### Debug Port Configuration
|
|
8
|
+
|
|
9
|
+
The default debug port is 9222, but **Chrome browser also uses port 9222**. If Chrome is running, Electron will silently fail to bind. Use `AMICUS_DEBUG_PORT` to set a different port:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
# Use port 9223 to avoid conflicts with Chrome
|
|
13
|
+
AMICUS_DEBUG_PORT=9223 amicus start --model gemini --prompt "test"
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Verify it's accessible:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# Use the same port you configured (default: 9222, recommended: 9223)
|
|
20
|
+
curl -s http://127.0.0.1:9223/json | python3 -m json.tool
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Known Limitations
|
|
24
|
+
|
|
25
|
+
- **`contextBridge` does not work with `data:` URLs** — The toolbar is loaded via a `data:` URL in the main window. Electron's `contextBridge.exposeInMainWorld()` silently fails for `data:` origins, so `window.sidecar` is `undefined` in the toolbar. Any toolbar↔main-process communication must use `executeJavaScript()` polling instead of IPC.
|
|
26
|
+
- **Debug targets by URL scheme** — sidecar mode creates two pages (OpenCode content at `http://localhost:<port>`, toolbar at `data:text/html`); the Council Workspace mode (`AMICUS_MODE=council-workspace`, v4.4) creates ONE page at a `file://` URL (`electron/workspace-ui/index.html`, loaded via `loadFile`). Filter by URL prefix: `CdpClient.toolbar()` → `data:`, `CdpClient.content()` → `http://localhost`, `CdpClient.workspace()` → `file://`. The workspace e2e suite runs on port 9225 (9223 is the manual/docs port used throughout this page; 9224 belongs to the toolbar suite). Unlike the `data:`-URL toolbar, the workspace page has a **working** `contextBridge` (`window.amicusWorkspace`) because `loadFile` is a real `file://` origin — see [docs/council.md's Council Workspace section](./council.md#council-workspace-gui) for what that bridge exposes.
|
|
27
|
+
|
|
28
|
+
## Testing UI State with Node.js
|
|
29
|
+
|
|
30
|
+
Use the WebSocket API to execute JavaScript in the Electron renderer and inspect UI state:
|
|
31
|
+
|
|
32
|
+
```javascript
|
|
33
|
+
// test-electron-ui.js
|
|
34
|
+
const WebSocket = require('ws');
|
|
35
|
+
|
|
36
|
+
const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/<PAGE_ID>');
|
|
37
|
+
|
|
38
|
+
ws.on('open', () => {
|
|
39
|
+
ws.send(JSON.stringify({
|
|
40
|
+
id: 1,
|
|
41
|
+
method: 'Runtime.evaluate',
|
|
42
|
+
params: {
|
|
43
|
+
expression: `
|
|
44
|
+
(function() {
|
|
45
|
+
const messages = document.querySelectorAll('.message');
|
|
46
|
+
const toolCalls = document.querySelectorAll('.tool-call');
|
|
47
|
+
return {
|
|
48
|
+
sessionId: window.sessionId,
|
|
49
|
+
messagesCount: messages.length,
|
|
50
|
+
toolCallsCount: toolCalls.length,
|
|
51
|
+
messages: Array.from(messages).map(m => ({
|
|
52
|
+
class: m.className,
|
|
53
|
+
text: m.textContent.slice(0, 200)
|
|
54
|
+
}))
|
|
55
|
+
};
|
|
56
|
+
})()
|
|
57
|
+
`,
|
|
58
|
+
returnByValue: true
|
|
59
|
+
}
|
|
60
|
+
}));
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
ws.on('message', (data) => {
|
|
64
|
+
const response = JSON.parse(data);
|
|
65
|
+
if (response.id === 1) {
|
|
66
|
+
console.log(JSON.stringify(response.result?.result?.value, null, 2));
|
|
67
|
+
ws.close();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Common UI Test Queries
|
|
73
|
+
|
|
74
|
+
**Get page ID first:**
|
|
75
|
+
```bash
|
|
76
|
+
curl -s http://127.0.0.1:9223/json | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Check UI state (inline):**
|
|
80
|
+
```bash
|
|
81
|
+
node << 'EOF'
|
|
82
|
+
const WebSocket = require('ws');
|
|
83
|
+
const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/<PAGE_ID>');
|
|
84
|
+
|
|
85
|
+
ws.on('open', () => {
|
|
86
|
+
ws.send(JSON.stringify({
|
|
87
|
+
id: 1,
|
|
88
|
+
method: 'Runtime.evaluate',
|
|
89
|
+
params: {
|
|
90
|
+
expression: `({
|
|
91
|
+
hasConfig: !!window.sidecarConfig,
|
|
92
|
+
model: window.sidecarConfig?.model,
|
|
93
|
+
messagesCount: document.querySelectorAll('.message').length,
|
|
94
|
+
toolCallsCount: document.querySelectorAll('.tool-call').length,
|
|
95
|
+
errorMessages: Array.from(document.querySelectorAll('.error-message')).map(e => e.textContent)
|
|
96
|
+
})`,
|
|
97
|
+
returnByValue: true
|
|
98
|
+
}
|
|
99
|
+
}));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
ws.on('message', (data) => {
|
|
103
|
+
const r = JSON.parse(data);
|
|
104
|
+
if (r.id === 1) { console.log(JSON.stringify(r.result?.result?.value, null, 2)); ws.close(); }
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
setTimeout(() => { ws.close(); process.exit(0); }, 5000);
|
|
108
|
+
EOF
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**Get tool call details:**
|
|
112
|
+
```bash
|
|
113
|
+
node << 'EOF'
|
|
114
|
+
const WebSocket = require('ws');
|
|
115
|
+
const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/<PAGE_ID>');
|
|
116
|
+
|
|
117
|
+
ws.on('open', () => {
|
|
118
|
+
ws.send(JSON.stringify({
|
|
119
|
+
id: 1,
|
|
120
|
+
method: 'Runtime.evaluate',
|
|
121
|
+
params: {
|
|
122
|
+
expression: `
|
|
123
|
+
Array.from(document.querySelectorAll('.tool-call')).map(t => ({
|
|
124
|
+
class: t.className,
|
|
125
|
+
html: t.innerHTML.slice(0, 500)
|
|
126
|
+
}))
|
|
127
|
+
`,
|
|
128
|
+
returnByValue: true
|
|
129
|
+
}
|
|
130
|
+
}));
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
ws.on('message', (data) => {
|
|
134
|
+
const r = JSON.parse(data);
|
|
135
|
+
if (r.id === 1) { console.log(JSON.stringify(r.result?.result?.value, null, 2)); ws.close(); }
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
setTimeout(() => { ws.close(); process.exit(0); }, 5000);
|
|
139
|
+
EOF
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Expected UI Elements
|
|
143
|
+
|
|
144
|
+
When testing the Amicus UI, verify these elements:
|
|
145
|
+
|
|
146
|
+
| Selector | Description | Expected Content |
|
|
147
|
+
|----------|-------------|------------------|
|
|
148
|
+
| `.message.system` | Task briefing | "Task: {briefing}" |
|
|
149
|
+
| `.message.assistant` | Model response | Response text |
|
|
150
|
+
| `.message.user` | User input | User's message |
|
|
151
|
+
| `.tool-call` | Tool execution | Tool name, input, output |
|
|
152
|
+
| `.tool-call.completed` | Completed tool | Has ✓ status |
|
|
153
|
+
| `.tool-call.running` | Running tool | Has ... status |
|
|
154
|
+
| `.tool-status-panel` | Tool summary | "Tools: X/Y completed" |
|
|
155
|
+
| `.reasoning` | Model reasoning | Collapsible thinking |
|
|
156
|
+
| `.error-message` | Error display | Error text |
|
|
157
|
+
|
|
158
|
+
## Debugging Tips
|
|
159
|
+
|
|
160
|
+
1. **Get WebSocket URL**: `curl -s http://127.0.0.1:9223/json | jq '.[0].webSocketDebuggerUrl'`
|
|
161
|
+
2. **Enable console capture**: Send `{"method": "Console.enable"}` first
|
|
162
|
+
3. **Screenshot**: Use `Page.captureScreenshot` method
|
|
163
|
+
4. **Timeout**: Always add a timeout to prevent hanging scripts
|
|
164
|
+
|
|
165
|
+
## Quick WebSocket Testing Patterns
|
|
166
|
+
|
|
167
|
+
The WebSocket approach via Chrome DevTools Protocol is the most efficient way to test the Amicus UI programmatically. Here are streamlined patterns for common testing scenarios:
|
|
168
|
+
|
|
169
|
+
**1. Get Page ID and Check UI State (one-liner):**
|
|
170
|
+
```bash
|
|
171
|
+
PAGE_ID=$(curl -s http://127.0.0.1:9223/json | node -e "const d=require('fs').readFileSync(0,'utf8');const p=JSON.parse(d);console.log(p[0]?.id || 'NO_ID')")
|
|
172
|
+
echo "Page ID: $PAGE_ID"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**2. Inspect UI State:**
|
|
176
|
+
```bash
|
|
177
|
+
node -e "
|
|
178
|
+
const WebSocket = require('ws');
|
|
179
|
+
const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/$PAGE_ID');
|
|
180
|
+
|
|
181
|
+
ws.on('open', () => {
|
|
182
|
+
ws.send(JSON.stringify({
|
|
183
|
+
id: 1,
|
|
184
|
+
method: 'Runtime.evaluate',
|
|
185
|
+
params: {
|
|
186
|
+
expression: \`
|
|
187
|
+
(function() {
|
|
188
|
+
const messages = document.querySelectorAll('.message');
|
|
189
|
+
return {
|
|
190
|
+
sseSubscribed: typeof sseSubscribed !== 'undefined' ? sseSubscribed : false,
|
|
191
|
+
messagesCount: messages.length,
|
|
192
|
+
messages: Array.from(messages).map(m => ({
|
|
193
|
+
class: m.className,
|
|
194
|
+
text: (m.textContent || '').slice(0, 200)
|
|
195
|
+
}))
|
|
196
|
+
};
|
|
197
|
+
})()
|
|
198
|
+
\`,
|
|
199
|
+
returnByValue: true
|
|
200
|
+
}
|
|
201
|
+
}));
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
ws.on('message', (data) => {
|
|
205
|
+
const msg = JSON.parse(data.toString());
|
|
206
|
+
if (msg.id === 1) {
|
|
207
|
+
console.log(JSON.stringify(msg.result?.result?.value, null, 2));
|
|
208
|
+
ws.close();
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
setTimeout(() => { ws.close(); process.exit(0); }, 3000);
|
|
214
|
+
"
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
**3. Send a Message via UI:**
|
|
218
|
+
```bash
|
|
219
|
+
node -e "
|
|
220
|
+
const WebSocket = require('ws');
|
|
221
|
+
const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/$PAGE_ID');
|
|
222
|
+
|
|
223
|
+
ws.on('open', () => {
|
|
224
|
+
ws.send(JSON.stringify({
|
|
225
|
+
id: 1,
|
|
226
|
+
method: 'Runtime.evaluate',
|
|
227
|
+
params: {
|
|
228
|
+
expression: \`
|
|
229
|
+
(function() {
|
|
230
|
+
const input = document.getElementById('message-input');
|
|
231
|
+
input.value = 'What is 2+2? Just give me the number.';
|
|
232
|
+
input.dispatchEvent(new Event('input'));
|
|
233
|
+
document.getElementById('send-btn').click();
|
|
234
|
+
return 'Message sent';
|
|
235
|
+
})()
|
|
236
|
+
\`,
|
|
237
|
+
returnByValue: true
|
|
238
|
+
}
|
|
239
|
+
}));
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
ws.on('message', (data) => {
|
|
243
|
+
const msg = JSON.parse(data.toString());
|
|
244
|
+
if (msg.id === 1) {
|
|
245
|
+
console.log(msg.result?.result?.value);
|
|
246
|
+
ws.close();
|
|
247
|
+
process.exit(0);
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
setTimeout(() => { ws.close(); process.exit(0); }, 3000);
|
|
252
|
+
"
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**4. Check for Errors:**
|
|
256
|
+
```bash
|
|
257
|
+
node -e "
|
|
258
|
+
const WebSocket = require('ws');
|
|
259
|
+
const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/$PAGE_ID');
|
|
260
|
+
|
|
261
|
+
ws.on('open', () => {
|
|
262
|
+
ws.send(JSON.stringify({
|
|
263
|
+
id: 1,
|
|
264
|
+
method: 'Runtime.evaluate',
|
|
265
|
+
params: {
|
|
266
|
+
expression: \`({
|
|
267
|
+
lastError: document.querySelector('.error-message')?.textContent,
|
|
268
|
+
sessionId: typeof sessionId !== 'undefined' ? sessionId : 'undefined',
|
|
269
|
+
isWaiting: typeof isWaitingForResponse !== 'undefined' ? isWaitingForResponse : 'undefined'
|
|
270
|
+
})\`,
|
|
271
|
+
returnByValue: true
|
|
272
|
+
}
|
|
273
|
+
}));
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
ws.on('message', (data) => {
|
|
277
|
+
const msg = JSON.parse(data.toString());
|
|
278
|
+
if (msg.id === 1) {
|
|
279
|
+
console.log(JSON.stringify(msg.result?.result?.value, null, 2));
|
|
280
|
+
ws.close();
|
|
281
|
+
process.exit(0);
|
|
282
|
+
}
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
setTimeout(() => { ws.close(); process.exit(0); }, 3000);
|
|
286
|
+
"
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
**Why WebSocket Testing is Efficient:**
|
|
290
|
+
- **No file creation**: Tests run inline without creating temporary files
|
|
291
|
+
- **Direct DOM access**: Query and manipulate any UI element
|
|
292
|
+
- **Real-time state**: Access JavaScript variables like `sessionId`, `sseSubscribed`, `isWaitingForResponse`
|
|
293
|
+
- **Click simulation**: Trigger button clicks and input events programmatically
|
|
294
|
+
- **Fast iteration**: Quickly test changes without restarting the app
|
|
295
|
+
|
|
296
|
+
**Important Notes:**
|
|
297
|
+
- Run commands from the amicus directory to access the `ws` module
|
|
298
|
+
- Page ID changes on each Electron launch - always fetch dynamically
|
|
299
|
+
- Add timeouts to prevent hanging on WebSocket errors
|
|
300
|
+
- Use `data.toString()` when parsing WebSocket messages in newer Node.js versions
|
|
301
|
+
|
|
302
|
+
## Integration with CI
|
|
303
|
+
|
|
304
|
+
For automated testing, launch Amicus with a known task and verify UI state:
|
|
305
|
+
|
|
306
|
+
```bash
|
|
307
|
+
# Launch Amicus in background
|
|
308
|
+
node bin/amicus.js start --model "openrouter/google/gemini-2.5-pro" \
|
|
309
|
+
--briefing "Echo hello" &
|
|
310
|
+
AMICUS_PID=$!
|
|
311
|
+
|
|
312
|
+
# Wait for window to open
|
|
313
|
+
sleep 5
|
|
314
|
+
|
|
315
|
+
# Get page ID and test UI
|
|
316
|
+
PAGE_ID=$(curl -s http://127.0.0.1:9223/json | python3 -c "import sys,json; print(json.load(sys.stdin)[0]['id'])")
|
|
317
|
+
|
|
318
|
+
# Run UI verification script
|
|
319
|
+
node scripts/verify-ui-state.js "$PAGE_ID"
|
|
320
|
+
|
|
321
|
+
# Cleanup
|
|
322
|
+
kill $AMICUS_PID
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
## Visual UI Testing with Screenshots
|
|
326
|
+
|
|
327
|
+
### macOS (native tools)
|
|
328
|
+
|
|
329
|
+
**Launch and position Electron window:**
|
|
330
|
+
```bash
|
|
331
|
+
# Start Amicus in background
|
|
332
|
+
node bin/amicus.js start --model "openrouter/google/gemini-3-flash-preview" --briefing "Test task" &
|
|
333
|
+
sleep 8
|
|
334
|
+
|
|
335
|
+
# Bring window to front and position it (window may open off-screen)
|
|
336
|
+
# Note: osascript / AppleScript is macOS-only
|
|
337
|
+
osascript << 'EOF'
|
|
338
|
+
tell application "System Events"
|
|
339
|
+
tell process "Electron"
|
|
340
|
+
set frontmost to true
|
|
341
|
+
set position of window 1 to {100, 100}
|
|
342
|
+
end tell
|
|
343
|
+
end tell
|
|
344
|
+
EOF
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
**Take screenshot (macOS — `screencapture` is macOS-only):**
|
|
348
|
+
```bash
|
|
349
|
+
screencapture -x /tmp/amicus-screenshot.png
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
### Windows (CDP-based, cross-platform)
|
|
353
|
+
|
|
354
|
+
`screencapture` and AppleScript are not available on Windows. Use CDP `Page.captureScreenshot` instead (works on all platforms):
|
|
355
|
+
|
|
356
|
+
```javascript
|
|
357
|
+
const { CdpClient } = require('./tests/helpers/cdp-client');
|
|
358
|
+
const cdp = await CdpClient.toolbar(9223);
|
|
359
|
+
await cdp.screenshot('C:\\tmp\\amicus-screenshot.png');
|
|
360
|
+
cdp.close();
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
To verify window visibility on Windows without a screenshot:
|
|
364
|
+
```powershell
|
|
365
|
+
Get-Process electron | Select-Object MainWindowTitle, MainWindowHandle
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
**Dynamic page ID retrieval (required - ID changes each session):**
|
|
369
|
+
```bash
|
|
370
|
+
PAGE_ID=$(curl -s http://127.0.0.1:9223/json | node -e "const d=require('fs').readFileSync(0,'utf8');console.log(JSON.parse(d)[0].id)")
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
**Click UI elements and inspect state (run from amicus directory for `ws` module):**
|
|
374
|
+
```bash
|
|
375
|
+
cd /path/to/amicus
|
|
376
|
+
cat << EOF > test-ui.js
|
|
377
|
+
const WebSocket = require('ws');
|
|
378
|
+
const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/${PAGE_ID}');
|
|
379
|
+
|
|
380
|
+
ws.on('open', () => {
|
|
381
|
+
ws.send(JSON.stringify({
|
|
382
|
+
id: 1,
|
|
383
|
+
method: 'Runtime.evaluate',
|
|
384
|
+
params: {
|
|
385
|
+
expression: \`
|
|
386
|
+
(function() {
|
|
387
|
+
// Click model selector
|
|
388
|
+
document.getElementById('model-selector-display')?.click();
|
|
389
|
+
|
|
390
|
+
// Or force dropdown visible
|
|
391
|
+
document.getElementById('model-selector-dropdown')?.classList.add('visible');
|
|
392
|
+
|
|
393
|
+
// Return state
|
|
394
|
+
return Array.from(document.querySelectorAll('.model-option'))
|
|
395
|
+
.map(opt => ({
|
|
396
|
+
name: opt.querySelector('.model-name-display')?.textContent,
|
|
397
|
+
selected: opt.classList.contains('selected')
|
|
398
|
+
}));
|
|
399
|
+
})()
|
|
400
|
+
\`,
|
|
401
|
+
returnByValue: true
|
|
402
|
+
}
|
|
403
|
+
}));
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
ws.on('message', (data) => {
|
|
407
|
+
const msg = JSON.parse(data);
|
|
408
|
+
if (msg.id === 1) {
|
|
409
|
+
console.log(JSON.stringify(msg.result?.result?.value, null, 2));
|
|
410
|
+
ws.close();
|
|
411
|
+
process.exit(0);
|
|
412
|
+
}
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
setTimeout(() => { ws.close(); process.exit(0); }, 3000);
|
|
416
|
+
EOF
|
|
417
|
+
node test-ui.js
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Common gotchas:**
|
|
421
|
+
- Window may open off-screen (negative Y coordinate) - use AppleScript to reposition
|
|
422
|
+
- Page ID changes on each Electron launch - always fetch dynamically
|
|
423
|
+
- Run Node.js scripts from amicus directory to access `ws` module
|
|
424
|
+
- Add `setTimeout` to prevent hanging on WebSocket errors
|
|
425
|
+
- **Always use `AMICUS_DEBUG_PORT=9223`** when Chrome is running (Chrome claims 9222)
|
|
426
|
+
|
|
427
|
+
## Toolbar-Specific Testing
|
|
428
|
+
|
|
429
|
+
The toolbar is a `data:text/html` page — a separate debug target from the OpenCode content view.
|
|
430
|
+
|
|
431
|
+
**Find the toolbar page ID:**
|
|
432
|
+
```bash
|
|
433
|
+
TOOLBAR_ID=$(curl -s http://127.0.0.1:9223/json | node -e "
|
|
434
|
+
const d=require('fs').readFileSync(0,'utf8');
|
|
435
|
+
const pages=JSON.parse(d);
|
|
436
|
+
const toolbar = pages.find(p => p.url && p.url.startsWith('data:'));
|
|
437
|
+
console.log(toolbar ? toolbar.id : 'NOT_FOUND');
|
|
438
|
+
")
|
|
439
|
+
echo "Toolbar ID: $TOOLBAR_ID"
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Inspect toolbar state (update banner, buttons, timer):**
|
|
443
|
+
```bash
|
|
444
|
+
cd /path/to/amicus
|
|
445
|
+
node -e "
|
|
446
|
+
const WebSocket = require('ws');
|
|
447
|
+
const ws = new WebSocket('ws://127.0.0.1:9223/devtools/page/$TOOLBAR_ID');
|
|
448
|
+
ws.on('open', () => {
|
|
449
|
+
ws.send(JSON.stringify({
|
|
450
|
+
id: 1,
|
|
451
|
+
method: 'Runtime.evaluate',
|
|
452
|
+
params: {
|
|
453
|
+
expression: \`({
|
|
454
|
+
bannerVisible: document.getElementById('update-banner')?.style?.display === 'flex',
|
|
455
|
+
bannerText: document.getElementById('update-text')?.textContent,
|
|
456
|
+
timerText: document.getElementById('timer')?.textContent,
|
|
457
|
+
foldBtnText: document.getElementById('fold-btn')?.textContent
|
|
458
|
+
})\`,
|
|
459
|
+
returnByValue: true
|
|
460
|
+
}
|
|
461
|
+
}));
|
|
462
|
+
});
|
|
463
|
+
ws.on('message', (data) => {
|
|
464
|
+
const msg = JSON.parse(data.toString());
|
|
465
|
+
if (msg.id === 1) { console.log(JSON.stringify(msg.result?.result?.value, null, 2)); ws.close(); process.exit(0); }
|
|
466
|
+
});
|
|
467
|
+
setTimeout(() => { ws.close(); process.exit(0); }, 3000);
|
|
468
|
+
"
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
**Note:** `window.sidecar` is `undefined` in the toolbar (see Known Limitations above). The toolbar communicates with the main process via `window.__amicusUpdateAction` polling, not IPC.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# JSDoc + TypeScript Declarations
|
|
2
|
+
|
|
3
|
+
This project uses **JSDoc comments** to provide TypeScript type information without converting to TypeScript. This gives npm consumers autocomplete and type checking.
|
|
4
|
+
|
|
5
|
+
## JSDoc Pattern for Public APIs
|
|
6
|
+
|
|
7
|
+
```javascript
|
|
8
|
+
/**
|
|
9
|
+
* Start a new Amicus session
|
|
10
|
+
* @param {Object} options - Amicus configuration
|
|
11
|
+
* @param {string} options.model - LLM model identifier (e.g., 'google/gemini-2.5-flash')
|
|
12
|
+
* @param {string} options.briefing - Task description for the Amicus session
|
|
13
|
+
* @param {string} [options.sessionId] - Optional Claude Code session ID
|
|
14
|
+
* @param {boolean} [options.headless=false] - Run without GUI
|
|
15
|
+
* @param {number} [options.timeout=15] - Headless timeout in minutes
|
|
16
|
+
* @returns {Promise<AmicusResult>} Session result with summary
|
|
17
|
+
*/
|
|
18
|
+
async function startAmicus(options) {
|
|
19
|
+
// ...
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* @typedef {Object} AmicusResult
|
|
24
|
+
* @property {string} taskId - Unique session identifier
|
|
25
|
+
* @property {string} summary - Fold summary from Amicus
|
|
26
|
+
* @property {string} status - Session status (completed|timeout|error)
|
|
27
|
+
* @property {string[]} [conflicts] - Files with potential conflicts
|
|
28
|
+
*/
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Generating .d.ts Files
|
|
32
|
+
|
|
33
|
+
Add to `package.json`:
|
|
34
|
+
|
|
35
|
+
```json
|
|
36
|
+
{
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build:types": "tsc --declaration --emitDeclarationOnly --allowJs --outDir types"
|
|
39
|
+
},
|
|
40
|
+
"types": "types/index.d.ts",
|
|
41
|
+
"files": ["bin/", "src/", "electron/", "types/"]
|
|
42
|
+
}
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Create `jsconfig.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"compilerOptions": {
|
|
50
|
+
"checkJs": true,
|
|
51
|
+
"declaration": true,
|
|
52
|
+
"emitDeclarationOnly": true,
|
|
53
|
+
"allowJs": true,
|
|
54
|
+
"outDir": "types",
|
|
55
|
+
"lib": ["ES2022"],
|
|
56
|
+
"module": "CommonJS",
|
|
57
|
+
"target": "ES2022"
|
|
58
|
+
},
|
|
59
|
+
"include": ["src/**/*.js", "bin/**/*.js"],
|
|
60
|
+
"exclude": ["node_modules", "tests"]
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Pre-publish Workflow
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# Generate types before publishing
|
|
68
|
+
npm run build:types
|
|
69
|
+
|
|
70
|
+
# Verify types are generated
|
|
71
|
+
ls types/
|
|
72
|
+
|
|
73
|
+
# Publish with types
|
|
74
|
+
npm publish
|
|
75
|
+
```
|