@in-the-loop-labs/pair-review 2.3.2 → 2.4.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/.pi/skills/review-model-guidance/SKILL.md +1 -1
- package/.pi/skills/review-roulette/SKILL.md +1 -1
- package/README.md +15 -1
- package/package.json +2 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/review-requests/SKILL.md +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/css/pr.css +296 -15
- package/public/index.html +121 -57
- package/public/js/components/AIPanel.js +2 -1
- package/public/js/components/AdvancedConfigTab.js +2 -2
- package/public/js/components/AnalysisConfigModal.js +2 -2
- package/public/js/components/ChatPanel.js +187 -28
- package/public/js/components/CouncilProgressModal.js +4 -7
- package/public/js/components/SplitButton.js +66 -1
- package/public/js/components/VoiceCentricConfigTab.js +2 -2
- package/public/js/index.js +274 -21
- package/public/js/modules/comment-manager.js +16 -12
- package/public/js/modules/file-comment-manager.js +8 -6
- package/public/js/pr.js +194 -5
- package/public/local.html +8 -1
- package/public/pr.html +17 -2
- package/src/ai/codex-provider.js +14 -2
- package/src/ai/copilot-provider.js +1 -10
- package/src/ai/cursor-agent-provider.js +1 -10
- package/src/ai/gemini-provider.js +8 -17
- package/src/chat/acp-bridge.js +442 -0
- package/src/chat/api-reference.js +539 -0
- package/src/chat/chat-providers.js +290 -0
- package/src/chat/claude-code-bridge.js +499 -0
- package/src/chat/codex-bridge.js +601 -0
- package/src/chat/pi-bridge.js +56 -3
- package/src/chat/prompt-builder.js +12 -11
- package/src/chat/session-manager.js +110 -29
- package/src/config.js +4 -2
- package/src/database.js +50 -2
- package/src/github/client.js +43 -0
- package/src/routes/chat.js +60 -27
- package/src/routes/config.js +24 -1
- package/src/routes/github-collections.js +126 -0
- package/src/routes/mcp.js +2 -1
- package/src/routes/pr.js +166 -2
- package/src/routes/reviews.js +2 -1
- package/src/routes/shared.js +70 -49
- package/src/server.js +27 -1
- package/src/utils/safe-parse-json.js +19 -0
- package/.pi/skills/pair-review-api/SKILL.md +0 -448
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
2
|
+
/**
|
|
3
|
+
* Chat Provider Registry
|
|
4
|
+
*
|
|
5
|
+
* Defines named chat providers (Pi, Copilot, Gemini, OpenCode, Claude, Codex, Cursor) with their
|
|
6
|
+
* default commands/args, config overrides, and availability checks.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { spawn } = require('child_process');
|
|
10
|
+
const { getCachedAvailability } = require('../ai');
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
|
|
13
|
+
// Default dependencies (overridable for testing)
|
|
14
|
+
const defaults = { spawn };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Built-in chat provider definitions.
|
|
18
|
+
* ACP providers communicate over stdin/stdout using the Agent Client Protocol.
|
|
19
|
+
*/
|
|
20
|
+
const CHAT_PROVIDERS = {
|
|
21
|
+
pi: {
|
|
22
|
+
id: 'pi',
|
|
23
|
+
name: 'Pi (RPC)',
|
|
24
|
+
type: 'pi',
|
|
25
|
+
},
|
|
26
|
+
'copilot-acp': {
|
|
27
|
+
id: 'copilot-acp',
|
|
28
|
+
name: 'Copilot (ACP)',
|
|
29
|
+
type: 'acp',
|
|
30
|
+
command: 'copilot',
|
|
31
|
+
args: ['--acp', '--stdio'],
|
|
32
|
+
env: {},
|
|
33
|
+
},
|
|
34
|
+
'gemini-acp': {
|
|
35
|
+
id: 'gemini-acp',
|
|
36
|
+
name: 'Gemini (ACP)',
|
|
37
|
+
type: 'acp',
|
|
38
|
+
command: 'gemini',
|
|
39
|
+
args: ['--experimental-acp'],
|
|
40
|
+
env: {},
|
|
41
|
+
},
|
|
42
|
+
'opencode-acp': {
|
|
43
|
+
id: 'opencode-acp',
|
|
44
|
+
name: 'OpenCode (ACP)',
|
|
45
|
+
type: 'acp',
|
|
46
|
+
command: 'opencode',
|
|
47
|
+
args: ['acp'],
|
|
48
|
+
env: {},
|
|
49
|
+
},
|
|
50
|
+
'cursor-acp': {
|
|
51
|
+
id: 'cursor-acp',
|
|
52
|
+
name: 'Cursor (ACP)',
|
|
53
|
+
type: 'acp',
|
|
54
|
+
command: 'agent',
|
|
55
|
+
args: ['acp'],
|
|
56
|
+
env: {},
|
|
57
|
+
},
|
|
58
|
+
claude: {
|
|
59
|
+
id: 'claude',
|
|
60
|
+
name: 'Claude (NDJSON)',
|
|
61
|
+
type: 'claude',
|
|
62
|
+
command: 'claude',
|
|
63
|
+
args: [],
|
|
64
|
+
env: {},
|
|
65
|
+
},
|
|
66
|
+
codex: {
|
|
67
|
+
id: 'codex',
|
|
68
|
+
name: 'Codex (JSON-RPC)',
|
|
69
|
+
type: 'codex',
|
|
70
|
+
command: 'codex',
|
|
71
|
+
// Shell environment config prevents zsh -l from reconstructing PATH,
|
|
72
|
+
// ensuring git-diff-lines and other bin/ scripts remain findable.
|
|
73
|
+
args: [
|
|
74
|
+
'app-server',
|
|
75
|
+
'-c', 'allow_login_shell=false',
|
|
76
|
+
'-c', 'shell_environment_policy.include_only=["PATH","HOME","USER"]',
|
|
77
|
+
],
|
|
78
|
+
env: {},
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
/** Stored config overrides from `config.chat_providers` */
|
|
83
|
+
let _configOverrides = {};
|
|
84
|
+
|
|
85
|
+
/** Availability cache: { [providerId]: { available: boolean, error?: string } } */
|
|
86
|
+
const _availabilityCache = {};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Store config overrides that will be merged into provider definitions.
|
|
90
|
+
* Call once at startup with `config.chat_providers || {}`.
|
|
91
|
+
* @param {Object} providersConfig - e.g. { 'copilot-acp': { command: '/usr/local/bin/copilot' } }
|
|
92
|
+
*/
|
|
93
|
+
function applyConfigOverrides(providersConfig) {
|
|
94
|
+
_configOverrides = providersConfig || {};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Get a chat provider definition with config overrides merged.
|
|
99
|
+
* @param {string} id - Provider ID (e.g. 'copilot-acp')
|
|
100
|
+
* @returns {Object|null} Provider definition or null if unknown
|
|
101
|
+
*/
|
|
102
|
+
function getChatProvider(id) {
|
|
103
|
+
const base = CHAT_PROVIDERS[id];
|
|
104
|
+
if (!base) return null;
|
|
105
|
+
|
|
106
|
+
const overrides = _configOverrides[id];
|
|
107
|
+
if (!overrides) return { ...base };
|
|
108
|
+
|
|
109
|
+
const merged = { ...base };
|
|
110
|
+
if (overrides.command) merged.command = overrides.command;
|
|
111
|
+
if (overrides.model) merged.model = overrides.model;
|
|
112
|
+
if (overrides.env) merged.env = { ...merged.env, ...overrides.env };
|
|
113
|
+
if (overrides.args) {
|
|
114
|
+
merged.args = overrides.args;
|
|
115
|
+
}
|
|
116
|
+
// extra_args appends to the default/overridden args
|
|
117
|
+
if (overrides.extra_args && Array.isArray(overrides.extra_args)) {
|
|
118
|
+
merged.args = [...(merged.args || []), ...overrides.extra_args];
|
|
119
|
+
}
|
|
120
|
+
// For multi-word commands (e.g. "devx claude"), use shell mode
|
|
121
|
+
if (merged.command && merged.command.includes(' ')) {
|
|
122
|
+
merged.useShell = true;
|
|
123
|
+
}
|
|
124
|
+
return merged;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Get all chat provider definitions (with overrides applied).
|
|
129
|
+
* @returns {Array<Object>}
|
|
130
|
+
*/
|
|
131
|
+
function getAllChatProviders() {
|
|
132
|
+
return Object.keys(CHAT_PROVIDERS).map(id => getChatProvider(id));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if a provider ID corresponds to an ACP provider.
|
|
137
|
+
* @param {string} id
|
|
138
|
+
* @returns {boolean}
|
|
139
|
+
*/
|
|
140
|
+
function isAcpProvider(id) {
|
|
141
|
+
const provider = CHAT_PROVIDERS[id];
|
|
142
|
+
return provider?.type === 'acp';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if a provider ID corresponds to a Claude Code provider.
|
|
147
|
+
* @param {string} id
|
|
148
|
+
* @returns {boolean}
|
|
149
|
+
*/
|
|
150
|
+
function isClaudeCodeProvider(id) {
|
|
151
|
+
const provider = CHAT_PROVIDERS[id];
|
|
152
|
+
return provider?.type === 'claude';
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if a provider ID corresponds to a Codex provider.
|
|
157
|
+
* @param {string} id
|
|
158
|
+
* @returns {boolean}
|
|
159
|
+
*/
|
|
160
|
+
function isCodexProvider(id) {
|
|
161
|
+
const provider = CHAT_PROVIDERS[id];
|
|
162
|
+
return provider?.type === 'codex';
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check availability of a single chat provider.
|
|
167
|
+
* For Pi, delegates to the existing AI provider availability cache.
|
|
168
|
+
* For ACP providers, spawns `<command> --version` to verify the binary exists.
|
|
169
|
+
* @param {string} id - Provider ID
|
|
170
|
+
* @param {Object} [_deps] - Dependency overrides for testing
|
|
171
|
+
* @returns {Promise<{available: boolean, error?: string}>}
|
|
172
|
+
*/
|
|
173
|
+
async function checkChatProviderAvailability(id, _deps) {
|
|
174
|
+
const provider = getChatProvider(id);
|
|
175
|
+
if (!provider) {
|
|
176
|
+
return { available: false, error: `Unknown provider: ${id}` };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Pi delegates to existing AI provider availability
|
|
180
|
+
if (provider.type === 'pi') {
|
|
181
|
+
const cached = getCachedAvailability('pi');
|
|
182
|
+
return { available: cached?.available || false, error: cached?.error };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Codex uses the same binary-check pattern as ACP providers
|
|
186
|
+
// (falls through to the spawn check below)
|
|
187
|
+
|
|
188
|
+
const deps = { ...defaults, ..._deps };
|
|
189
|
+
const command = provider.command;
|
|
190
|
+
const useShell = provider.useShell || false;
|
|
191
|
+
|
|
192
|
+
return new Promise((resolve) => {
|
|
193
|
+
try {
|
|
194
|
+
// For multi-word commands, use shell mode
|
|
195
|
+
const spawnCmd = useShell ? `${command} --version` : command;
|
|
196
|
+
const spawnArgs = useShell ? [] : ['--version'];
|
|
197
|
+
const proc = deps.spawn(spawnCmd, spawnArgs, {
|
|
198
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
199
|
+
timeout: 10000,
|
|
200
|
+
shell: useShell,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
proc.on('error', (err) => {
|
|
204
|
+
resolve({ available: false, error: err.message });
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
proc.on('close', (code) => {
|
|
208
|
+
if (code === 0) {
|
|
209
|
+
resolve({ available: true });
|
|
210
|
+
} else {
|
|
211
|
+
resolve({ available: false, error: `${command} --version exited with code ${code}` });
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
} catch (err) {
|
|
215
|
+
resolve({ available: false, error: err.message });
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Check availability of all chat providers in parallel and populate cache.
|
|
222
|
+
* @param {Object} [_deps] - Dependency overrides for testing
|
|
223
|
+
* @returns {Promise<void>}
|
|
224
|
+
*/
|
|
225
|
+
async function checkAllChatProviders(_deps) {
|
|
226
|
+
const ids = Object.keys(CHAT_PROVIDERS);
|
|
227
|
+
const results = await Promise.all(
|
|
228
|
+
ids.map(async (id) => {
|
|
229
|
+
const result = await checkChatProviderAvailability(id, _deps);
|
|
230
|
+
return { id, result };
|
|
231
|
+
})
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
for (const { id, result } of results) {
|
|
235
|
+
_availabilityCache[id] = result;
|
|
236
|
+
if (result.available) {
|
|
237
|
+
logger.info(`[ChatProviders] ${id}: available`);
|
|
238
|
+
} else {
|
|
239
|
+
logger.debug(`[ChatProviders] ${id}: not available${result.error ? ` (${result.error})` : ''}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Get cached availability for a single chat provider.
|
|
246
|
+
* @param {string} id
|
|
247
|
+
* @returns {{available: boolean, error?: string}|null}
|
|
248
|
+
*/
|
|
249
|
+
function getCachedChatAvailability(id) {
|
|
250
|
+
return _availabilityCache[id] || null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get all cached chat provider availability.
|
|
255
|
+
* @returns {Object} Map of provider ID to availability result
|
|
256
|
+
*/
|
|
257
|
+
function getAllCachedChatAvailability() {
|
|
258
|
+
return { ..._availabilityCache };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Clear the availability cache (for testing).
|
|
263
|
+
*/
|
|
264
|
+
function clearChatAvailabilityCache() {
|
|
265
|
+
for (const key of Object.keys(_availabilityCache)) {
|
|
266
|
+
delete _availabilityCache[key];
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Reset config overrides (for testing).
|
|
272
|
+
*/
|
|
273
|
+
function clearConfigOverrides() {
|
|
274
|
+
_configOverrides = {};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
module.exports = {
|
|
278
|
+
getChatProvider,
|
|
279
|
+
getAllChatProviders,
|
|
280
|
+
isAcpProvider,
|
|
281
|
+
isClaudeCodeProvider,
|
|
282
|
+
isCodexProvider,
|
|
283
|
+
checkChatProviderAvailability,
|
|
284
|
+
checkAllChatProviders,
|
|
285
|
+
getCachedChatAvailability,
|
|
286
|
+
getAllCachedChatAvailability,
|
|
287
|
+
applyConfigOverrides,
|
|
288
|
+
clearChatAvailabilityCache,
|
|
289
|
+
clearConfigOverrides,
|
|
290
|
+
};
|