@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.
Files changed (47) hide show
  1. package/.pi/skills/review-model-guidance/SKILL.md +1 -1
  2. package/.pi/skills/review-roulette/SKILL.md +1 -1
  3. package/README.md +15 -1
  4. package/package.json +2 -1
  5. package/plugin/.claude-plugin/plugin.json +1 -1
  6. package/plugin/skills/review-requests/SKILL.md +1 -1
  7. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  8. package/public/css/pr.css +296 -15
  9. package/public/index.html +121 -57
  10. package/public/js/components/AIPanel.js +2 -1
  11. package/public/js/components/AdvancedConfigTab.js +2 -2
  12. package/public/js/components/AnalysisConfigModal.js +2 -2
  13. package/public/js/components/ChatPanel.js +187 -28
  14. package/public/js/components/CouncilProgressModal.js +4 -7
  15. package/public/js/components/SplitButton.js +66 -1
  16. package/public/js/components/VoiceCentricConfigTab.js +2 -2
  17. package/public/js/index.js +274 -21
  18. package/public/js/modules/comment-manager.js +16 -12
  19. package/public/js/modules/file-comment-manager.js +8 -6
  20. package/public/js/pr.js +194 -5
  21. package/public/local.html +8 -1
  22. package/public/pr.html +17 -2
  23. package/src/ai/codex-provider.js +14 -2
  24. package/src/ai/copilot-provider.js +1 -10
  25. package/src/ai/cursor-agent-provider.js +1 -10
  26. package/src/ai/gemini-provider.js +8 -17
  27. package/src/chat/acp-bridge.js +442 -0
  28. package/src/chat/api-reference.js +539 -0
  29. package/src/chat/chat-providers.js +290 -0
  30. package/src/chat/claude-code-bridge.js +499 -0
  31. package/src/chat/codex-bridge.js +601 -0
  32. package/src/chat/pi-bridge.js +56 -3
  33. package/src/chat/prompt-builder.js +12 -11
  34. package/src/chat/session-manager.js +110 -29
  35. package/src/config.js +4 -2
  36. package/src/database.js +50 -2
  37. package/src/github/client.js +43 -0
  38. package/src/routes/chat.js +60 -27
  39. package/src/routes/config.js +24 -1
  40. package/src/routes/github-collections.js +126 -0
  41. package/src/routes/mcp.js +2 -1
  42. package/src/routes/pr.js +166 -2
  43. package/src/routes/reviews.js +2 -1
  44. package/src/routes/shared.js +70 -49
  45. package/src/server.js +27 -1
  46. package/src/utils/safe-parse-json.js +19 -0
  47. 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
+ };