amicus 1.0.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/CHANGELOG.md +46 -0
- package/LICENSE +21 -0
- package/README.md +477 -0
- package/bin/amicus.js +382 -0
- package/electron/assets/icon.png +0 -0
- package/electron/assets/icon.svg +5 -0
- package/electron/fold.js +163 -0
- package/electron/ipc-setup.js +176 -0
- package/electron/load-failsafe.js +85 -0
- package/electron/main.js +468 -0
- package/electron/preload-setup.js +38 -0
- package/electron/preload.js +33 -0
- package/electron/setup-ui-alias-script.js +218 -0
- package/electron/setup-ui-aliases.js +85 -0
- package/electron/setup-ui-keys-script.js +115 -0
- package/electron/setup-ui-keys.js +97 -0
- package/electron/setup-ui-model.js +138 -0
- package/electron/setup-ui-styles.js +327 -0
- package/electron/setup-ui.js +465 -0
- package/electron/summary.js +118 -0
- package/electron/toolbar.js +229 -0
- package/electron/window-position.js +35 -0
- package/package.json +98 -0
- package/scripts/postinstall.js +193 -0
- package/scripts/setup-hooks.js +42 -0
- package/skill/SKILL.md +976 -0
- package/skills/second-opinion/COUNCIL-DESIGN.md +227 -0
- package/skills/second-opinion/MODEL-NOTES.md +104 -0
- package/skills/second-opinion/SKILL.md +389 -0
- package/src/cli-handlers.js +188 -0
- package/src/cli.js +400 -0
- package/src/conflict.js +144 -0
- package/src/context-compression.js +102 -0
- package/src/context.js +199 -0
- package/src/drift.js +144 -0
- package/src/environment.js +157 -0
- package/src/headless.js +742 -0
- package/src/index.js +106 -0
- package/src/jsonl-parser.js +180 -0
- package/src/mcp-server.js +625 -0
- package/src/mcp-tools.js +407 -0
- package/src/opencode-client.js +615 -0
- package/src/prompt-builder.js +355 -0
- package/src/prompts/cowork-agent-prompt.js +118 -0
- package/src/session-manager.js +414 -0
- package/src/session.js +180 -0
- package/src/sidecar/context-builder.js +297 -0
- package/src/sidecar/continue.js +212 -0
- package/src/sidecar/crash-handler.js +56 -0
- package/src/sidecar/fanout-leg.js +107 -0
- package/src/sidecar/fanout-output.js +46 -0
- package/src/sidecar/fanout.js +236 -0
- package/src/sidecar/interactive.js +217 -0
- package/src/sidecar/models.js +135 -0
- package/src/sidecar/progress.js +218 -0
- package/src/sidecar/read.js +183 -0
- package/src/sidecar/resume.js +221 -0
- package/src/sidecar/session-utils.js +288 -0
- package/src/sidecar/setup-window.js +79 -0
- package/src/sidecar/setup.js +280 -0
- package/src/sidecar/start.js +251 -0
- package/src/utils/agent-mapping.js +138 -0
- package/src/utils/alias-audit.js +98 -0
- package/src/utils/alias-resolver.js +77 -0
- package/src/utils/api-key-store.js +259 -0
- package/src/utils/api-key-validation.js +97 -0
- package/src/utils/auth-json.js +109 -0
- package/src/utils/config.js +291 -0
- package/src/utils/curated-models.js +82 -0
- package/src/utils/env-compat.js +38 -0
- package/src/utils/env-loader.js +54 -0
- package/src/utils/idle-watchdog.js +225 -0
- package/src/utils/input-validators.js +127 -0
- package/src/utils/lifecycle.js +43 -0
- package/src/utils/logger.js +84 -0
- package/src/utils/mcp-discovery.js +194 -0
- package/src/utils/mcp-validators.js +78 -0
- package/src/utils/model-catalog.js +103 -0
- package/src/utils/model-fetcher.js +179 -0
- package/src/utils/model-validator.js +207 -0
- package/src/utils/path-setup.js +41 -0
- package/src/utils/port-pid.js +39 -0
- package/src/utils/prompt-source.js +53 -0
- package/src/utils/result-schema.js +261 -0
- package/src/utils/server-setup.js +93 -0
- package/src/utils/session-abort.js +53 -0
- package/src/utils/session-lock.js +95 -0
- package/src/utils/shared-server.js +216 -0
- package/src/utils/start-helpers.js +76 -0
- package/src/utils/thinking-validators.js +92 -0
- package/src/utils/update-notifier-loader.js +18 -0
- package/src/utils/updater.js +157 -0
- package/src/utils/validators.js +300 -0
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Amicus Toolbar HTML Builder
|
|
3
|
+
*
|
|
4
|
+
* Generates the toolbar HTML for the bottom bar of the Electron window.
|
|
5
|
+
* Supports two modes: 'sidecar' (default) and 'setup'.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const TOOLBAR_H = 40;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Get the brand name based on client type
|
|
12
|
+
* @param {string} [client='code-local'] - Client type (code-local, code-web, cowork)
|
|
13
|
+
* @returns {string} Brand name to display
|
|
14
|
+
*/
|
|
15
|
+
function getBrandName(client) {
|
|
16
|
+
return client === 'cowork' ? 'Openwork Amicus' : 'Amicus';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Build toolbar HTML string
|
|
21
|
+
* @param {object} [options={}]
|
|
22
|
+
* @param {string} [options.mode='sidecar'] - 'sidecar' or 'setup'
|
|
23
|
+
* @param {string} [options.taskId='unknown'] - Task ID to display
|
|
24
|
+
* @param {string} [options.foldShortcut='Cmd+Shift+F'] - Shortcut label
|
|
25
|
+
* @param {string} [options.client='code-local'] - Client type for branding
|
|
26
|
+
* @returns {string} Complete HTML document for the toolbar
|
|
27
|
+
*/
|
|
28
|
+
function buildToolbarHTML(options = {}) {
|
|
29
|
+
const {
|
|
30
|
+
mode = 'sidecar',
|
|
31
|
+
taskId = 'unknown',
|
|
32
|
+
foldShortcut = 'Cmd+Shift+F',
|
|
33
|
+
client = 'code-local',
|
|
34
|
+
updateInfo = null
|
|
35
|
+
} = options;
|
|
36
|
+
|
|
37
|
+
const brandName = getBrandName(client);
|
|
38
|
+
|
|
39
|
+
const baseStyles = `
|
|
40
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
41
|
+
body {
|
|
42
|
+
position: fixed;
|
|
43
|
+
bottom: 0;
|
|
44
|
+
left: 0;
|
|
45
|
+
right: 0;
|
|
46
|
+
height: ${TOOLBAR_H}px;
|
|
47
|
+
background: #2D2B2A;
|
|
48
|
+
display: flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
justify-content: space-between;
|
|
51
|
+
padding: 0 14px;
|
|
52
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
53
|
+
border-top: 1px solid #3D3A38;
|
|
54
|
+
-webkit-app-region: no-drag;
|
|
55
|
+
user-select: none;
|
|
56
|
+
}
|
|
57
|
+
.info {
|
|
58
|
+
color: #A09B96;
|
|
59
|
+
font-size: 12px;
|
|
60
|
+
display: flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
gap: 12px;
|
|
63
|
+
}
|
|
64
|
+
.brand {
|
|
65
|
+
color: #D97757;
|
|
66
|
+
font-size: 11px;
|
|
67
|
+
font-weight: 600;
|
|
68
|
+
letter-spacing: 0.8px;
|
|
69
|
+
text-transform: uppercase;
|
|
70
|
+
}
|
|
71
|
+
.sep { color: #3D3A38; font-size: 14px; }
|
|
72
|
+
.detail, .timer {
|
|
73
|
+
color: #7A756F;
|
|
74
|
+
font-size: 11px;
|
|
75
|
+
font-family: 'SF Mono', Menlo, Monaco, monospace;
|
|
76
|
+
}
|
|
77
|
+
.action-btn {
|
|
78
|
+
padding: 5px 14px;
|
|
79
|
+
background: #D97757;
|
|
80
|
+
color: #FFF;
|
|
81
|
+
border: none;
|
|
82
|
+
border-radius: 4px;
|
|
83
|
+
font-size: 12px;
|
|
84
|
+
font-weight: 500;
|
|
85
|
+
cursor: pointer;
|
|
86
|
+
transition: background 0.15s;
|
|
87
|
+
}
|
|
88
|
+
.action-btn:hover { background: #C4623F; }
|
|
89
|
+
.action-btn:disabled { opacity: 0.5; cursor: default; }
|
|
90
|
+
.icon-btn {
|
|
91
|
+
background: none; border: 1px solid #3D3A38;
|
|
92
|
+
border-radius: 4px; color: #A09B96; cursor: pointer;
|
|
93
|
+
font-size: 14px; padding: 3px 8px; transition: all 0.15s;
|
|
94
|
+
display: flex; align-items: center;
|
|
95
|
+
}
|
|
96
|
+
.icon-btn:hover { border-color: #D97757; color: #D97757; }
|
|
97
|
+
.right-actions { display: flex; align-items: center; gap: 8px; }
|
|
98
|
+
.update-banner {
|
|
99
|
+
position: fixed;
|
|
100
|
+
bottom: ${TOOLBAR_H}px;
|
|
101
|
+
left: 0;
|
|
102
|
+
right: 0;
|
|
103
|
+
height: 32px;
|
|
104
|
+
background: #3D3A38;
|
|
105
|
+
border-bottom: 1px solid #4D4A48;
|
|
106
|
+
display: none;
|
|
107
|
+
align-items: center;
|
|
108
|
+
justify-content: center;
|
|
109
|
+
gap: 10px;
|
|
110
|
+
font-size: 12px;
|
|
111
|
+
color: #D4D0CC;
|
|
112
|
+
z-index: 100;
|
|
113
|
+
}
|
|
114
|
+
.update-banner .update-btn {
|
|
115
|
+
padding: 2px 10px;
|
|
116
|
+
background: #D97757;
|
|
117
|
+
color: #FFF;
|
|
118
|
+
border: none;
|
|
119
|
+
border-radius: 3px;
|
|
120
|
+
font-size: 11px;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
transition: background 0.15s;
|
|
123
|
+
}
|
|
124
|
+
.update-banner .update-btn:hover { background: #C4623F; }
|
|
125
|
+
.update-banner .update-btn:disabled { opacity: 0.5; cursor: default; }
|
|
126
|
+
.update-banner .dismiss-btn {
|
|
127
|
+
background: none;
|
|
128
|
+
border: none;
|
|
129
|
+
color: #7A756F;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
font-size: 14px;
|
|
132
|
+
padding: 0 4px;
|
|
133
|
+
}
|
|
134
|
+
.update-banner .dismiss-btn:hover { color: #D4D0CC; }`;
|
|
135
|
+
|
|
136
|
+
const logoSvg = `<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
|
|
137
|
+
<path d="M3 2v12" stroke="#D97757" stroke-width="2" stroke-linecap="round"/>
|
|
138
|
+
<path d="M10 2v5c0 2-3 3-7 5" stroke="#D97757" stroke-width="2" stroke-linecap="round" stroke-opacity="0.6"/>
|
|
139
|
+
</svg>`;
|
|
140
|
+
|
|
141
|
+
if (mode === 'setup') {
|
|
142
|
+
return `<!DOCTYPE html>
|
|
143
|
+
<html><head><style>${baseStyles}</style></head><body>
|
|
144
|
+
<div class="info">
|
|
145
|
+
${logoSvg}
|
|
146
|
+
<span class="brand">${brandName}</span>
|
|
147
|
+
</div>
|
|
148
|
+
<button class="action-btn" id="continue-btn" disabled>Continue</button>
|
|
149
|
+
<script>
|
|
150
|
+
document.getElementById('continue-btn').addEventListener('click', function() {
|
|
151
|
+
if (!this.disabled) {
|
|
152
|
+
window.sidecar && window.sidecar.setupDone();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
</script>
|
|
156
|
+
</body></html>`;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Default: sidecar mode
|
|
160
|
+
return `<!DOCTYPE html>
|
|
161
|
+
<html><head><style>${baseStyles}</style></head><body>
|
|
162
|
+
<div class="update-banner" id="update-banner">
|
|
163
|
+
<span id="update-text"></span>
|
|
164
|
+
<button class="update-btn" id="update-btn">Update</button>
|
|
165
|
+
<button class="dismiss-btn" id="dismiss-btn">×</button>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="info">
|
|
168
|
+
${logoSvg}
|
|
169
|
+
<span class="brand">${brandName}</span>
|
|
170
|
+
<span class="sep">|</span>
|
|
171
|
+
<span class="detail" title="Task ID — use with: amicus resume ${taskId}">task: ${taskId}</span>
|
|
172
|
+
<span class="sep">|</span>
|
|
173
|
+
<span class="timer" id="timer">0:00</span>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="right-actions">
|
|
176
|
+
<button class="icon-btn" id="settings-btn" title="Settings">⚙</button>
|
|
177
|
+
<button class="action-btn" id="fold-btn">Fold (${foldShortcut})</button>
|
|
178
|
+
</div>
|
|
179
|
+
<script>
|
|
180
|
+
var start = Date.now();
|
|
181
|
+
setInterval(function() {
|
|
182
|
+
var s = Math.floor((Date.now() - start) / 1000);
|
|
183
|
+
var m = Math.floor(s / 60);
|
|
184
|
+
s = s % 60;
|
|
185
|
+
document.getElementById('timer').textContent = m + ':' + (s < 10 ? '0' : '') + s;
|
|
186
|
+
}, 1000);
|
|
187
|
+
// contextBridge doesn't work with data: URLs, so use window action flags
|
|
188
|
+
// that the main process polls via executeJavaScript (same pattern as update banner).
|
|
189
|
+
window.__amicusToolbarAction = null;
|
|
190
|
+
document.getElementById('fold-btn').addEventListener('click', function() {
|
|
191
|
+
window.__amicusToolbarAction = 'fold';
|
|
192
|
+
});
|
|
193
|
+
document.getElementById('settings-btn').addEventListener('click', function() {
|
|
194
|
+
window.__amicusToolbarAction = 'open-settings';
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Update banner logic (data injected at build time, no IPC needed)
|
|
198
|
+
(function() {
|
|
199
|
+
var updateInfo = ${JSON.stringify(updateInfo)};
|
|
200
|
+
if (!updateInfo || !updateInfo.hasUpdate) { return; }
|
|
201
|
+
|
|
202
|
+
var banner = document.getElementById('update-banner');
|
|
203
|
+
var text = document.getElementById('update-text');
|
|
204
|
+
var btn = document.getElementById('update-btn');
|
|
205
|
+
var dismiss = document.getElementById('dismiss-btn');
|
|
206
|
+
|
|
207
|
+
text.textContent = 'v' + updateInfo.latest + ' available';
|
|
208
|
+
banner.style.display = 'flex';
|
|
209
|
+
|
|
210
|
+
// Notify main process to expand toolbar area
|
|
211
|
+
// Uses postMessage since preload contextBridge doesn't work with data: URLs
|
|
212
|
+
window.__amicusUpdateAction = null;
|
|
213
|
+
btn.addEventListener('click', function() {
|
|
214
|
+
btn.disabled = true;
|
|
215
|
+
btn.textContent = 'Updating...';
|
|
216
|
+
dismiss.style.display = 'none';
|
|
217
|
+
window.__amicusUpdateAction = 'perform-update';
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
dismiss.addEventListener('click', function() {
|
|
221
|
+
banner.style.display = 'none';
|
|
222
|
+
window.__amicusUpdateAction = 'dismiss';
|
|
223
|
+
});
|
|
224
|
+
})();
|
|
225
|
+
</script>
|
|
226
|
+
</body></html>`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
module.exports = { buildToolbarHTML, TOOLBAR_H, getBrandName };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Window Position Calculator
|
|
3
|
+
*
|
|
4
|
+
* Computes the (x, y) coordinates for the sidecar Electron window
|
|
5
|
+
* based on the display work area and desired position preference.
|
|
6
|
+
*
|
|
7
|
+
* Extracted as a pure function for testability (no Electron dependency).
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Compute window (x, y) position within a display work area.
|
|
12
|
+
*
|
|
13
|
+
* @param {{ x: number, y: number, width: number, height: number }} workArea
|
|
14
|
+
* @param {number} winW - Window width in pixels
|
|
15
|
+
* @param {number} winH - Window height in pixels
|
|
16
|
+
* @param {'right'|'left'|'center'|string} [position='right'] - Desired position
|
|
17
|
+
* @returns {{ x: number, y: number }}
|
|
18
|
+
*/
|
|
19
|
+
function computeWindowPosition(workArea, winW, winH, position) {
|
|
20
|
+
const { x: areaX, y: areaY, width: areaW } = workArea;
|
|
21
|
+
|
|
22
|
+
let x;
|
|
23
|
+
if (position === 'left') {
|
|
24
|
+
x = areaX;
|
|
25
|
+
} else if (position === 'center') {
|
|
26
|
+
x = Math.round(areaX + (areaW - winW) / 2);
|
|
27
|
+
} else {
|
|
28
|
+
// 'right' is the default for any unknown value
|
|
29
|
+
x = areaX + areaW - winW;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return { x, y: areaY };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
module.exports = { computeWindowPosition };
|
package/package.json
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "amicus",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Multi-model LLM Council + parallel AI window for Claude Code. Run structured council reviews across Gemini, GPT, DeepSeek and more — or fork a conversation to any model and fold the results back.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"claude",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"sidecar",
|
|
9
|
+
"multi-model",
|
|
10
|
+
"opencode",
|
|
11
|
+
"openrouter",
|
|
12
|
+
"parallel",
|
|
13
|
+
"llm",
|
|
14
|
+
"gemini",
|
|
15
|
+
"cowork",
|
|
16
|
+
"council",
|
|
17
|
+
"second-opinion",
|
|
18
|
+
"fanout"
|
|
19
|
+
],
|
|
20
|
+
"author": "BourbonDog",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/BourbonDog/amicus.git"
|
|
25
|
+
},
|
|
26
|
+
"bugs": "https://github.com/BourbonDog/amicus/issues",
|
|
27
|
+
"homepage": "https://github.com/BourbonDog/amicus#readme",
|
|
28
|
+
"bin": {
|
|
29
|
+
"amicus": "./bin/amicus.js",
|
|
30
|
+
"am": "./bin/amicus.js",
|
|
31
|
+
"sidecar": "./bin/amicus.js",
|
|
32
|
+
"claude-sidecar": "./bin/amicus.js"
|
|
33
|
+
},
|
|
34
|
+
"main": "src/index.js",
|
|
35
|
+
"exports": {
|
|
36
|
+
"./opencode-client": "./src/opencode-client.mjs"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"bin/",
|
|
40
|
+
"src/",
|
|
41
|
+
"electron/",
|
|
42
|
+
"skill/",
|
|
43
|
+
"skills/",
|
|
44
|
+
"CHANGELOG.md",
|
|
45
|
+
"scripts/postinstall.js",
|
|
46
|
+
"scripts/setup-hooks.js"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"start": "node --experimental-top-level-await --experimental-vm-modules bin/amicus.js",
|
|
50
|
+
"test": "jest",
|
|
51
|
+
"test:integration": "jest --testPathIgnorePatterns='\\.worktrees/' --testMatch='**/tests/**/*.integration.test.js'",
|
|
52
|
+
"test:all": "jest --testPathIgnorePatterns='/node_modules/' --testPathIgnorePatterns='\\.worktrees/' && node scripts/mark-test-passed.js",
|
|
53
|
+
"test:e2e:mcp": "jest tests/mcp-repomix-e2e.integration.test.js --testTimeout=180000 --forceExit",
|
|
54
|
+
"posttest": "node scripts/mark-test-passed.js",
|
|
55
|
+
"lint": "eslint src/",
|
|
56
|
+
"postinstall": "node scripts/postinstall.js",
|
|
57
|
+
"test:thinking": "node scripts/benchmark-thinking.js",
|
|
58
|
+
"test:thinking:quick": "MODELS=gemini node scripts/benchmark-thinking.js",
|
|
59
|
+
"refresh-models": "node bin/amicus.js models --refresh",
|
|
60
|
+
"models:info": "node bin/amicus.js models",
|
|
61
|
+
"models:check": "node bin/amicus.js models --check",
|
|
62
|
+
"generate-icon": "node scripts/generate-icon.js",
|
|
63
|
+
"generate-docs": "node scripts/generate-docs.js",
|
|
64
|
+
"generate-docs:check": "node scripts/generate-docs.js --check",
|
|
65
|
+
"validate-docs": "node scripts/validate-docs.js --full",
|
|
66
|
+
"prepare": "node scripts/setup-hooks.js"
|
|
67
|
+
},
|
|
68
|
+
"dependencies": {
|
|
69
|
+
"@modelcontextprotocol/sdk": "^1.27.0",
|
|
70
|
+
"@opencode-ai/sdk": "^1.1.36",
|
|
71
|
+
"dotenv": "^17.2.3",
|
|
72
|
+
"opencode-ai": "^1.2.20",
|
|
73
|
+
"tiktoken": "^1.0.0",
|
|
74
|
+
"update-notifier": "^7.3.1",
|
|
75
|
+
"zod": "^3.0.0"
|
|
76
|
+
},
|
|
77
|
+
"optionalDependencies": {
|
|
78
|
+
"electron": "^28.0.0"
|
|
79
|
+
},
|
|
80
|
+
"devDependencies": {
|
|
81
|
+
"chrome-remote-interface": "^0.33.3",
|
|
82
|
+
"eslint": "^8.0.0",
|
|
83
|
+
"husky": "^9.1.7",
|
|
84
|
+
"jest": "^29.0.0",
|
|
85
|
+
"lint-staged": "^16.3.2",
|
|
86
|
+
"puppeteer": "^24.36.0",
|
|
87
|
+
"sharp": "^0.33.5",
|
|
88
|
+
"ws": "^8.19.0"
|
|
89
|
+
},
|
|
90
|
+
"engines": {
|
|
91
|
+
"node": ">=18.0.0"
|
|
92
|
+
},
|
|
93
|
+
"lint-staged": {
|
|
94
|
+
"src/**/*.js": [
|
|
95
|
+
"eslint --fix"
|
|
96
|
+
]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Post-install script for amicus
|
|
5
|
+
*
|
|
6
|
+
* 1. Copies SKILL.md to ~/.claude/skills/sidecar/
|
|
7
|
+
* 2. Registers MCP server in Claude Code (~/.claude.json)
|
|
8
|
+
* 3. Registers MCP server in Claude Desktop/Cowork config
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const { execFileSync } = require('child_process');
|
|
15
|
+
|
|
16
|
+
const SKILL_SOURCE = path.join(__dirname, '..', 'skill', 'SKILL.md');
|
|
17
|
+
const COUNCIL_SOURCE_DIR = path.join(__dirname, '..', 'skills', 'second-opinion');
|
|
18
|
+
|
|
19
|
+
/** Council files + per-file install semantics: SKILL/COUNCIL-DESIGN are product code
|
|
20
|
+
* (overwrite on update); MODEL-NOTES is user data — its reviewer-reliability table evolves
|
|
21
|
+
* per-run, so it is seeded once and never clobbered. */
|
|
22
|
+
const COUNCIL_FILES = [
|
|
23
|
+
{ file: 'SKILL.md', mode: 'overwrite' },
|
|
24
|
+
{ file: 'COUNCIL-DESIGN.md', mode: 'overwrite' },
|
|
25
|
+
{ file: 'MODEL-NOTES.md', mode: 'if-missing' },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function skillsRoot() {
|
|
29
|
+
return path.join(os.homedir(), '.claude', 'skills');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const MCP_CONFIG = { command: 'npx', args: ['-y', 'amicus@latest', 'mcp'] };
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Add or update an MCP server in a JSON config file.
|
|
36
|
+
* Always overwrites the entry to ensure upgrades apply the latest config.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} configPath - Path to the JSON config file
|
|
39
|
+
* @param {string} name - MCP server name
|
|
40
|
+
* @param {object} config - MCP server config object
|
|
41
|
+
* @returns {string} 'added', 'updated', or 'unchanged'
|
|
42
|
+
*/
|
|
43
|
+
function addMcpToConfigFile(configPath, name, config) {
|
|
44
|
+
let existing = {};
|
|
45
|
+
try {
|
|
46
|
+
existing = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
|
|
47
|
+
} catch {
|
|
48
|
+
// File doesn't exist or invalid JSON — start fresh
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!existing.mcpServers) { existing.mcpServers = {}; }
|
|
52
|
+
|
|
53
|
+
const prev = existing.mcpServers[name];
|
|
54
|
+
const status = !prev ? 'added' : JSON.stringify(prev) !== JSON.stringify(config) ? 'updated' : 'unchanged';
|
|
55
|
+
|
|
56
|
+
existing.mcpServers[name] = config;
|
|
57
|
+
if (status !== 'unchanged') {
|
|
58
|
+
const dir = path.dirname(configPath);
|
|
59
|
+
if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); }
|
|
60
|
+
fs.writeFileSync(configPath, JSON.stringify(existing, null, 2), { mode: 0o600 });
|
|
61
|
+
}
|
|
62
|
+
return status;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Install the chat skill to ~/.claude/skills/sidecar/ */
|
|
66
|
+
function installSkill() {
|
|
67
|
+
try {
|
|
68
|
+
const destDir = path.join(skillsRoot(), 'sidecar');
|
|
69
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
70
|
+
fs.copyFileSync(SKILL_SOURCE, path.join(destDir, 'SKILL.md'));
|
|
71
|
+
console.log('[amicus] Chat skill installed to ~/.claude/skills/sidecar/');
|
|
72
|
+
} catch (err) {
|
|
73
|
+
console.error(`[amicus] Warning: Could not install chat skill: ${err.message}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Install the LLM Council skill to ~/.claude/skills/second-opinion/ */
|
|
78
|
+
function installCouncilSkill(sourceDir = COUNCIL_SOURCE_DIR) {
|
|
79
|
+
const destDir = path.join(skillsRoot(), 'second-opinion');
|
|
80
|
+
try {
|
|
81
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
82
|
+
} catch (err) {
|
|
83
|
+
console.error(`[amicus] Warning: Could not create council skill dir: ${err.message}`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
let failed = 0;
|
|
87
|
+
for (const { file, mode } of COUNCIL_FILES) {
|
|
88
|
+
try {
|
|
89
|
+
const dest = path.join(destDir, file);
|
|
90
|
+
if (mode === 'if-missing' && fs.existsSync(dest)) { continue; }
|
|
91
|
+
fs.copyFileSync(path.join(sourceDir, file), dest);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
failed++;
|
|
94
|
+
console.error(`[amicus] Warning: Could not install council file ${file}: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (failed === COUNCIL_FILES.length) {
|
|
98
|
+
console.error('[amicus] Warning: Council skill NOT installed (all files failed — see warnings above).');
|
|
99
|
+
} else if (failed > 0) {
|
|
100
|
+
console.log(`[amicus] Council skill partially installed (${failed}/${COUNCIL_FILES.length} files failed — see warnings above).`);
|
|
101
|
+
} else {
|
|
102
|
+
console.log('[amicus] Council skill installed to ~/.claude/skills/second-opinion/');
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Register MCP server in Claude Code config */
|
|
107
|
+
function registerClaudeCode() {
|
|
108
|
+
// Try the CLI first
|
|
109
|
+
try {
|
|
110
|
+
const mcpJson = JSON.stringify(MCP_CONFIG);
|
|
111
|
+
execFileSync('claude', ['mcp', 'add-json', 'amicus', mcpJson, '--scope', 'user'], {
|
|
112
|
+
stdio: 'pipe',
|
|
113
|
+
timeout: 10000,
|
|
114
|
+
});
|
|
115
|
+
console.log('[amicus] MCP registered in Claude Code (via CLI).');
|
|
116
|
+
|
|
117
|
+
// DEPRECATED(amicus-shim): also register 'sidecar' so existing clients that
|
|
118
|
+
// reference the old server name keep resolving. Remove in next major.
|
|
119
|
+
try {
|
|
120
|
+
execFileSync('claude', ['mcp', 'add-json', 'sidecar', mcpJson, '--scope', 'user'], {
|
|
121
|
+
stdio: 'pipe',
|
|
122
|
+
timeout: 10000,
|
|
123
|
+
});
|
|
124
|
+
} catch {
|
|
125
|
+
// Best-effort; ignore failures for the shim registration
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return;
|
|
129
|
+
} catch {
|
|
130
|
+
// CLI not available or failed — fall back to file edit
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Fallback: direct file edit
|
|
134
|
+
const claudeConfigPath = path.join(os.homedir(), '.claude.json');
|
|
135
|
+
const status = addMcpToConfigFile(claudeConfigPath, 'amicus', MCP_CONFIG);
|
|
136
|
+
if (status === 'added') {
|
|
137
|
+
console.log('[amicus] MCP registered in Claude Code (~/.claude.json).');
|
|
138
|
+
} else if (status === 'updated') {
|
|
139
|
+
console.log('[amicus] MCP config updated in Claude Code (~/.claude.json).');
|
|
140
|
+
} else {
|
|
141
|
+
console.log('[amicus] MCP already registered in Claude Code.');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// DEPRECATED(amicus-shim): also register 'sidecar' entry so existing clients
|
|
145
|
+
// that reference the old server name keep resolving. Remove in next major.
|
|
146
|
+
addMcpToConfigFile(claudeConfigPath, 'sidecar', MCP_CONFIG);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Register MCP server in Claude Desktop / Cowork config */
|
|
150
|
+
function registerClaudeDesktop() {
|
|
151
|
+
let configDir;
|
|
152
|
+
if (process.platform === 'darwin') {
|
|
153
|
+
configDir = path.join(os.homedir(), 'Library', 'Application Support', 'Claude');
|
|
154
|
+
} else if (process.platform === 'win32') {
|
|
155
|
+
configDir = path.join(process.env.APPDATA || '', 'Claude');
|
|
156
|
+
} else {
|
|
157
|
+
configDir = path.join(os.homedir(), '.config', 'claude');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const configPath = path.join(configDir, 'claude_desktop_config.json');
|
|
161
|
+
const status = addMcpToConfigFile(configPath, 'amicus', MCP_CONFIG);
|
|
162
|
+
if (status === 'added') {
|
|
163
|
+
console.log('[amicus] MCP registered in Claude Desktop.');
|
|
164
|
+
} else if (status === 'updated') {
|
|
165
|
+
console.log('[amicus] MCP config updated in Claude Desktop.');
|
|
166
|
+
} else {
|
|
167
|
+
console.log('[amicus] MCP already registered in Claude Desktop.');
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// DEPRECATED(amicus-shim): also register 'sidecar' entry so existing clients
|
|
171
|
+
// that reference the old server name keep resolving. Remove in next major.
|
|
172
|
+
addMcpToConfigFile(configPath, 'sidecar', MCP_CONFIG);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function main() {
|
|
176
|
+
console.log('[amicus] Installing...');
|
|
177
|
+
installSkill();
|
|
178
|
+
installCouncilSkill();
|
|
179
|
+
registerClaudeCode();
|
|
180
|
+
registerClaudeDesktop();
|
|
181
|
+
|
|
182
|
+
console.log('');
|
|
183
|
+
console.log('[amicus] Setup:');
|
|
184
|
+
console.log(' - Configure API: Run `amicus setup` or set API keys directly');
|
|
185
|
+
console.log(' - API keys: OPENROUTER_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY, OPENAI_API_KEY, etc.');
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Only run main when executed directly (not when required for testing)
|
|
189
|
+
if (require.main === module) {
|
|
190
|
+
main();
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
module.exports = { addMcpToConfigFile, installSkill, installCouncilSkill, COUNCIL_FILES };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Configure git to run the version-controlled hooks in .husky/.
|
|
5
|
+
*
|
|
6
|
+
* Replaces husky's install step, which pointed core.hooksPath at the
|
|
7
|
+
* generated, gitignored .husky/_ shim directory. That directory only
|
|
8
|
+
* exists where husky's prepare actually ran, so hooks silently never
|
|
9
|
+
* fired in linked git worktrees (relative core.hooksPath resolves against
|
|
10
|
+
* each worktree's root, and .husky/_ is never checked out there). The
|
|
11
|
+
* committed .husky/ directory exists in every checkout, so pointing
|
|
12
|
+
* core.hooksPath at it makes hooks fire in the main clone and in every
|
|
13
|
+
* worktree with no per-worktree setup.
|
|
14
|
+
*
|
|
15
|
+
* Runs automatically via npm's "prepare" lifecycle. If you install with
|
|
16
|
+
* --ignore-scripts (recommended for this repo), run it once by hand:
|
|
17
|
+
*
|
|
18
|
+
* node scripts/setup-hooks.js
|
|
19
|
+
*
|
|
20
|
+
* Safe to run anywhere: exits 0 outside a git checkout (npm tarball
|
|
21
|
+
* installs, exported archives) and never fails the install.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const { execFileSync } = require('node:child_process');
|
|
25
|
+
|
|
26
|
+
function git(...args) {
|
|
27
|
+
return execFileSync('git', args, { encoding: 'utf-8' }).trim();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
git('rev-parse', '--git-dir');
|
|
32
|
+
} catch {
|
|
33
|
+
process.exit(0); // not a git checkout — nothing to configure
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
git('config', 'core.hooksPath', '.husky');
|
|
38
|
+
console.log('setup-hooks: core.hooksPath -> .husky (applies to all worktrees of this clone)');
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.warn(`setup-hooks: could not set core.hooksPath: ${err.message}`);
|
|
41
|
+
process.exit(0); // never break npm install over hook setup
|
|
42
|
+
}
|