@doist/doistbot-cli 1.0.4 → 1.0.6
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/dist/actions/auth.js +62 -28
- package/dist/actions/doctor.js +16 -11
- package/dist/actions/review.js +77 -11
- package/dist/auth.js +53 -8
- package/dist/config.js +40 -6
- package/dist/env.js +1 -1
- package/dist/file-store.js +1 -1
- package/dist/runtime.js +1 -1
- package/dist/terminal.js +2 -7
- package/package.json +7 -7
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +9 -85
- package/sandbox/dist/core/bot-logins.js +0 -13
- package/sandbox/dist/core/check-run.js +16 -0
- package/sandbox/dist/core/datadog-metrics.js +6 -54
- package/sandbox/dist/core/logger-span-processor.js +78 -0
- package/sandbox/dist/core/pi.js +270 -186
- package/sandbox/dist/core/tracing-setup.js +15 -0
- package/sandbox/dist/core/tracing.js +71 -0
- package/sandbox/dist/main.js +4 -1
- package/sandbox/dist/providers/config.js +1 -1
- package/sandbox/dist/providers/credentials.js +40 -0
- package/sandbox/dist/providers/gemini.js +8 -10
- package/sandbox/dist/providers/helpers.js +18 -38
- package/sandbox/dist/providers/index.js +4 -3
- package/sandbox/dist/providers/openai.js +1 -8
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +28 -55
- package/sandbox/dist/providers/schemas.js +0 -39
- package/sandbox/dist/tasks/chat/chat.js +162 -101
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
- package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
- package/sandbox/dist/tasks/issue-summarize/model.js +4 -2
- package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +25 -2
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +31 -8
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
- package/sandbox/dist/tasks/issue-triage/model.js +120 -36
- package/sandbox/dist/tasks/issue-triage/output.js +27 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +3 -2
- package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
- package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
- package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +288 -418
- package/sandbox/dist/tasks/persist-logs/dd-proxy.js +2 -2
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
- package/sandbox/dist/tasks/review/check-run.js +2 -1
- package/sandbox/dist/tasks/review/conversation-context.js +15 -8
- package/sandbox/dist/tasks/review/engines/dedupe.js +19 -9
- package/sandbox/dist/tasks/review/engines/multi-focus.js +62 -36
- package/sandbox/dist/tasks/review/engines/shared.js +15 -61
- package/sandbox/dist/tasks/review/github-comment-format.js +64 -10
- package/sandbox/dist/tasks/review/local.js +2 -29
- package/sandbox/dist/tasks/review/prompt.js +2 -49
- package/sandbox/dist/tasks/review/review-summary.js +120 -0
- package/sandbox/dist/tasks/review/review.js +49 -69
- package/sandbox/dist/tasks/review/runner.js +57 -52
- package/sandbox/dist/tasks/review/summary-model.js +172 -16
- package/sandbox/dist/tasks/review/thinking-level.js +4 -2
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +5 -23
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +9 -85
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/dist/providers/anthropic.js +0 -30
- package/sandbox/dist/tasks/review/author-format.js +0 -14
- package/sandbox/dist/tasks/review/author-profile.js +0 -67
- package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
- package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
- package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
- package/sandbox/dist/tasks/review/council/summary.js +0 -192
- package/sandbox/dist/tasks/review/council/types.js +0 -4
- package/sandbox/dist/tasks/review/council/voter.js +0 -384
- package/sandbox/dist/tasks/review/engines/council.js +0 -126
- package/sandbox/dist/tasks/review/engines/index.js +0 -12
- package/sandbox/dist/tasks/review/review-engine.js +0 -5
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/review-prompt.md +0 -209
- package/sandbox/vote-prompt.md +0 -69
package/dist/actions/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { deleteGeminiApiKey, getAuthStatus, loginOpenAICodex, logoutOpenAICodex, saveGeminiApiKey, } from '../auth.js';
|
|
1
|
+
import { deleteGeminiApiKey, deleteOpenRouterApiKey, getAuthStatus, loginOpenAICodex, logoutOpenAICodex, saveGeminiApiKey, saveOpenRouterApiKey, } from '../auth.js';
|
|
2
2
|
import { getEnvApiKeys } from '../env.js';
|
|
3
3
|
import { FILE_STORE_PATH } from '../file-store.js';
|
|
4
4
|
import { promptHiddenInput, promptMultiSelect } from '../input.js';
|
|
@@ -10,10 +10,8 @@ const AUTH_LOGOUT_PROVIDER_CONFIGS = [
|
|
|
10
10
|
await logoutOpenAICodex();
|
|
11
11
|
return '✓ OpenAI stored auth cleared.';
|
|
12
12
|
},
|
|
13
|
-
envWarning: (
|
|
14
|
-
|
|
15
|
-
: undefined,
|
|
16
|
-
name: 'OpenAI Codex',
|
|
13
|
+
envWarning: () => undefined,
|
|
14
|
+
name: 'OpenAI',
|
|
17
15
|
value: 'openai',
|
|
18
16
|
},
|
|
19
17
|
{
|
|
@@ -29,6 +27,19 @@ const AUTH_LOGOUT_PROVIDER_CONFIGS = [
|
|
|
29
27
|
name: 'Gemini',
|
|
30
28
|
value: 'gemini',
|
|
31
29
|
},
|
|
30
|
+
{
|
|
31
|
+
clear: async ({ deleteOpenRouterApiKey }) => {
|
|
32
|
+
const deleted = await deleteOpenRouterApiKey();
|
|
33
|
+
return deleted
|
|
34
|
+
? '✓ OpenRouter stored API key cleared.'
|
|
35
|
+
: 'OpenRouter had no stored API key to clear.';
|
|
36
|
+
},
|
|
37
|
+
envWarning: (env) => getEnvApiKeys(env).openrouter
|
|
38
|
+
? 'OpenRouter is still configured from OPENROUTER_API_KEY.'
|
|
39
|
+
: undefined,
|
|
40
|
+
name: 'OpenRouter',
|
|
41
|
+
value: 'openrouter',
|
|
42
|
+
},
|
|
32
43
|
];
|
|
33
44
|
const AUTH_LOGOUT_PROVIDERS = AUTH_LOGOUT_PROVIDER_CONFIGS.map(({ value }) => value);
|
|
34
45
|
export const AUTH_LOGOUT_PROVIDER_USAGE = `${AUTH_LOGOUT_PROVIDERS.join('|')}|all`;
|
|
@@ -39,6 +50,7 @@ export async function runAuthSetup(dependencies = {}) {
|
|
|
39
50
|
loginOpenAICodex,
|
|
40
51
|
promptHiddenInput,
|
|
41
52
|
saveGeminiApiKey,
|
|
53
|
+
saveOpenRouterApiKey,
|
|
42
54
|
writeLine,
|
|
43
55
|
...dependencies,
|
|
44
56
|
};
|
|
@@ -46,7 +58,7 @@ export async function runAuthSetup(dependencies = {}) {
|
|
|
46
58
|
deps.writeLine('1. OpenAI Codex');
|
|
47
59
|
deps.writeLine();
|
|
48
60
|
if (status.openai.configured) {
|
|
49
|
-
deps.writeLine(
|
|
61
|
+
deps.writeLine('✓ OpenAI already configured (pi-auth).');
|
|
50
62
|
}
|
|
51
63
|
else {
|
|
52
64
|
if (!deps.isInteractive) {
|
|
@@ -61,35 +73,70 @@ export async function runAuthSetup(dependencies = {}) {
|
|
|
61
73
|
deps.writeLine();
|
|
62
74
|
if (status.gemini.source === 'env') {
|
|
63
75
|
deps.writeLine('✓ Gemini already configured from GEMINI_API_KEY.');
|
|
64
|
-
return;
|
|
65
76
|
}
|
|
66
|
-
if (status.gemini.source === 'keychain') {
|
|
77
|
+
else if (status.gemini.source === 'keychain') {
|
|
67
78
|
deps.writeLine(`✓ Gemini already configured in the ${getSecureStoreLabel()}.`);
|
|
68
|
-
return;
|
|
69
79
|
}
|
|
70
|
-
if (status.gemini.source === 'file') {
|
|
80
|
+
else if (status.gemini.source === 'file') {
|
|
71
81
|
deps.writeLine(`✓ Gemini already configured (${FILE_STORE_PATH}).`);
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
if (!deps.isInteractive) {
|
|
85
|
+
deps.writeLine('Gemini not configured. Skipping optional Gemini provider.');
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
deps.writeLine('Doistbot can use Gemini via Google Vertex. Paste a Gemini API key, or press Enter to skip.');
|
|
89
|
+
const geminiApiKey = (await deps.promptHiddenInput('Gemini API key (optional): ')).trim();
|
|
90
|
+
if (!geminiApiKey) {
|
|
91
|
+
deps.writeLine();
|
|
92
|
+
deps.writeLine('Gemini skipped. You can add it later with `doistbot auth`.');
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
const backend = await deps.saveGeminiApiKey(geminiApiKey);
|
|
96
|
+
deps.writeLine();
|
|
97
|
+
if (backend === 'file') {
|
|
98
|
+
deps.writeLine(`Note: ${getSecureStoreLabel()} unavailable; key stored in ${FILE_STORE_PATH} with 0600 permissions.`);
|
|
99
|
+
}
|
|
100
|
+
deps.writeLine('✓ Gemini configured.');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
deps.writeLine();
|
|
105
|
+
deps.writeLine('3. OpenRouter API key');
|
|
106
|
+
deps.writeLine();
|
|
107
|
+
if (status.openrouter.source === 'env') {
|
|
108
|
+
deps.writeLine('✓ OpenRouter already configured from OPENROUTER_API_KEY.');
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (status.openrouter.source === 'keychain') {
|
|
112
|
+
deps.writeLine(`✓ OpenRouter already configured in the ${getSecureStoreLabel()}.`);
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
if (status.openrouter.source === 'file') {
|
|
116
|
+
deps.writeLine(`✓ OpenRouter already configured (${FILE_STORE_PATH}).`);
|
|
72
117
|
return;
|
|
73
118
|
}
|
|
74
119
|
if (!deps.isInteractive) {
|
|
75
|
-
throw new Error('Cannot prompt for
|
|
120
|
+
throw new Error('Cannot prompt for OpenRouter API key in non-interactive mode. Set OPENROUTER_API_KEY instead.');
|
|
76
121
|
}
|
|
77
|
-
deps.writeLine('Doistbot
|
|
78
|
-
const backend = await deps.
|
|
122
|
+
deps.writeLine('Doistbot uses OpenRouter for DeepSeek, Z.ai, and other non-GPT review models. Please create an OpenRouter API key at https://openrouter.ai/keys and paste it here. (Use your Work Perk for it)');
|
|
123
|
+
const backend = await deps.saveOpenRouterApiKey(await deps.promptHiddenInput('OpenRouter API key: '));
|
|
79
124
|
deps.writeLine();
|
|
80
125
|
if (backend === 'file') {
|
|
81
126
|
deps.writeLine(`Note: ${getSecureStoreLabel()} unavailable; key stored in ${FILE_STORE_PATH} with 0600 permissions.`);
|
|
82
127
|
}
|
|
83
|
-
deps.writeLine('✓
|
|
128
|
+
deps.writeLine('✓ OpenRouter configured.');
|
|
84
129
|
}
|
|
85
130
|
export async function runAuthStatus() {
|
|
86
131
|
const status = await getAuthStatus();
|
|
87
132
|
writeLine(`OpenAI: ${status.openai.configured ? `configured (${status.openai.source ?? 'unknown'})` : 'not configured'}`);
|
|
88
|
-
writeLine(`Gemini: ${status.gemini.configured ? `configured (${status.gemini.source ?? 'unknown'})` : 'not configured'}`);
|
|
133
|
+
writeLine(`Gemini (optional): ${status.gemini.configured ? `configured (${status.gemini.source ?? 'unknown'})` : 'not configured'}`);
|
|
134
|
+
writeLine(`OpenRouter: ${status.openrouter.configured ? `configured (${status.openrouter.source ?? 'unknown'})` : 'not configured'}`);
|
|
89
135
|
}
|
|
90
136
|
export async function runAuthLogout(target, dependencies = {}) {
|
|
91
137
|
const deps = {
|
|
92
138
|
deleteGeminiApiKey,
|
|
139
|
+
deleteOpenRouterApiKey,
|
|
93
140
|
env: process.env,
|
|
94
141
|
isInteractive: process.stdin.isTTY === true,
|
|
95
142
|
logoutOpenAICodex,
|
|
@@ -142,16 +189,3 @@ async function promptLogoutProviderSelection() {
|
|
|
142
189
|
choices: AUTH_LOGOUT_PROVIDER_CONFIGS.map(({ name, value }) => ({ name, value })),
|
|
143
190
|
});
|
|
144
191
|
}
|
|
145
|
-
function formatOpenAIConfiguredMessage(source) {
|
|
146
|
-
switch (source) {
|
|
147
|
-
case 'pi-auth':
|
|
148
|
-
return '✓ OpenAI already configured (pi-auth).';
|
|
149
|
-
case 'env':
|
|
150
|
-
return '✓ OpenAI already configured from the environment.';
|
|
151
|
-
case 'keychain':
|
|
152
|
-
return '✓ OpenAI already configured (keychain).';
|
|
153
|
-
case 'file':
|
|
154
|
-
case undefined:
|
|
155
|
-
return '✓ OpenAI already configured.';
|
|
156
|
-
}
|
|
157
|
-
}
|
package/dist/actions/doctor.js
CHANGED
|
@@ -1,27 +1,32 @@
|
|
|
1
1
|
import { getAuthStatus } from '../auth.js';
|
|
2
|
-
import {
|
|
2
|
+
import { formatMissingRequiredCliAuthProviders, getMissingRequiredCliAuthProviders, } from '../config.js';
|
|
3
3
|
import { checkGit, checkPiDependency, checkRepository, checkSandboxBuild } from '../runtime.js';
|
|
4
4
|
import { renderDiagnosticCheck } from '../terminal.js';
|
|
5
5
|
export function getConfiguredProviders({ status, }) {
|
|
6
6
|
return [
|
|
7
|
-
status.openai.configured
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
status.openai.configured ? `OpenAI (${status.openai.source ?? 'configured'})` : undefined,
|
|
8
|
+
status.gemini.configured
|
|
9
|
+
? `Gemini optional (${status.gemini.source ?? 'configured'})`
|
|
10
|
+
: undefined,
|
|
11
|
+
status.openrouter.configured
|
|
12
|
+
? `OpenRouter (${status.openrouter.source ?? 'configured'})`
|
|
11
13
|
: undefined,
|
|
12
|
-
status.gemini.configured ? `Gemini (${status.gemini.source ?? 'configured'})` : undefined,
|
|
13
14
|
].filter((provider) => provider !== undefined);
|
|
14
15
|
}
|
|
16
|
+
export function getMissingRequiredProviders({ status, }) {
|
|
17
|
+
return getMissingRequiredCliAuthProviders(status);
|
|
18
|
+
}
|
|
15
19
|
export async function checkAuth({ env = process.env, getStatus = getAuthStatus, } = {}) {
|
|
16
20
|
const status = await getStatus({ env });
|
|
17
21
|
const configured = getConfiguredProviders({ status });
|
|
18
|
-
|
|
22
|
+
const missingRequiredProviders = getMissingRequiredProviders({ status });
|
|
23
|
+
if (missingRequiredProviders.length === 0) {
|
|
19
24
|
return { ok: true, message: `${configured.join(', ')} configured` };
|
|
20
25
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
message: formatMissingRequiredCliAuthProviders(missingRequiredProviders),
|
|
29
|
+
};
|
|
25
30
|
}
|
|
26
31
|
export async function runDoctor() {
|
|
27
32
|
await renderDiagnosticCheck('git', checkGit);
|
package/dist/actions/review.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { getApiKeys } from '../config.js';
|
|
1
|
+
import { formatMissingRequiredCliAuthProviders, getApiKeys, getMissingRequiredCliReviewAuthProviders, } from '../config.js';
|
|
2
2
|
import { getRepositoryRoot, loadSandboxFormatModule, loadSandboxReviewModule } from '../runtime.js';
|
|
3
3
|
import { formatReviewProgress, renderAlert, renderMarkdown, withSpinner, writeLine, } from '../terminal.js';
|
|
4
4
|
const LOCAL_REVIEW_SOURCE_STAGED = 'staged';
|
|
5
5
|
const JSON_SCHEMA_VERSION = 1;
|
|
6
6
|
const PROVIDER_DISPLAY_NAMES = {
|
|
7
|
+
deepseek: 'DeepSeek',
|
|
7
8
|
gemini: 'Gemini',
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
zai: 'Z.ai',
|
|
10
|
+
openai: 'OpenAI',
|
|
10
11
|
};
|
|
11
12
|
function stripProviderFailurePrefix(error) {
|
|
12
13
|
return error
|
|
@@ -18,8 +19,11 @@ function getReadableProviderFailure(failure) {
|
|
|
18
19
|
const error = stripProviderFailurePrefix(failure.error);
|
|
19
20
|
const normalized = error.toLowerCase();
|
|
20
21
|
if (failure.model === 'openai' &&
|
|
21
|
-
(normalized.includes('no api key found for openai
|
|
22
|
-
normalized.includes('
|
|
22
|
+
(normalized.includes('no api key found for openai') ||
|
|
23
|
+
normalized.includes('no api key found for openai-codex') ||
|
|
24
|
+
normalized.includes('use /login') ||
|
|
25
|
+
normalized.includes('invalid api key') ||
|
|
26
|
+
normalized.includes('incorrect api key'))) {
|
|
23
27
|
return 'OpenAI Codex is not authenticated. Run `doistbot auth` to reconnect OpenAI.';
|
|
24
28
|
}
|
|
25
29
|
if (failure.model === 'gemini' &&
|
|
@@ -28,13 +32,16 @@ function getReadableProviderFailure(failure) {
|
|
|
28
32
|
normalized.includes('quota') ||
|
|
29
33
|
normalized.includes('rate-limit') ||
|
|
30
34
|
normalized.includes('rate limit'))) {
|
|
31
|
-
return 'Gemini is rate-limited or out of quota.
|
|
35
|
+
return 'Gemini is rate-limited or out of quota. Check your Google Gemini quota or wait for quota to reset.';
|
|
32
36
|
}
|
|
33
37
|
if (failure.model === 'gemini' &&
|
|
34
38
|
(normalized.includes('api_key_invalid') ||
|
|
35
39
|
normalized.includes('api key not valid') ||
|
|
36
|
-
normalized.includes('invalid_argument')
|
|
37
|
-
|
|
40
|
+
normalized.includes('invalid_argument') ||
|
|
41
|
+
normalized.includes('invalid api key') ||
|
|
42
|
+
normalized.includes('no api key found for google') ||
|
|
43
|
+
normalized.includes('no api key found for gemini'))) {
|
|
44
|
+
return 'Gemini API key is invalid or missing. Run `doistbot auth` and paste a valid Gemini key.';
|
|
38
45
|
}
|
|
39
46
|
const firstLine = error
|
|
40
47
|
.split(/\r?\n/)
|
|
@@ -186,19 +193,77 @@ function formatReviewForJson(result) {
|
|
|
186
193
|
providerFailures: getReadableProviderFailureEntries(result.reviewResult.providerFailures),
|
|
187
194
|
};
|
|
188
195
|
}
|
|
189
|
-
async function
|
|
196
|
+
async function getReviewAuthCheck() {
|
|
197
|
+
const apiKeys = await getApiKeys();
|
|
198
|
+
return {
|
|
199
|
+
apiKeys,
|
|
200
|
+
missingProviders: getMissingRequiredCliReviewAuthProviders(apiKeys),
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
function getMissingAuthProviderFailures(missingProviders) {
|
|
204
|
+
const failures = [];
|
|
205
|
+
if (missingProviders.includes('OpenAI')) {
|
|
206
|
+
failures.push({
|
|
207
|
+
model: 'openai',
|
|
208
|
+
message: 'OpenAI not configured; run `doistbot auth`',
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
if (missingProviders.includes('OpenRouter')) {
|
|
212
|
+
failures.push({
|
|
213
|
+
model: 'deepseek',
|
|
214
|
+
message: 'OpenRouter not configured; run `doistbot auth`',
|
|
215
|
+
}, {
|
|
216
|
+
model: 'zai',
|
|
217
|
+
message: 'OpenRouter not configured; run `doistbot auth`',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return failures;
|
|
221
|
+
}
|
|
222
|
+
function formatMissingAuthForJson({ source, missingProviders, }) {
|
|
223
|
+
return {
|
|
224
|
+
schemaVersion: JSON_SCHEMA_VERSION,
|
|
225
|
+
status: 'unverified',
|
|
226
|
+
verified: false,
|
|
227
|
+
unverifiedReason: 'missing_auth',
|
|
228
|
+
reviewSource: source,
|
|
229
|
+
repository: {
|
|
230
|
+
owner: 'unknown',
|
|
231
|
+
repo: 'unknown',
|
|
232
|
+
branch: 'unknown',
|
|
233
|
+
baseBranch: 'unknown',
|
|
234
|
+
},
|
|
235
|
+
review: {
|
|
236
|
+
count: 0,
|
|
237
|
+
findings: [],
|
|
238
|
+
},
|
|
239
|
+
providerFailures: getMissingAuthProviderFailures(missingProviders),
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
async function runLocalReviewForCli({ source, apiKeys, onProgress, }) {
|
|
190
243
|
const reviewModule = await loadSandboxReviewModule();
|
|
191
244
|
return await reviewModule.runLocalReview({
|
|
192
245
|
workspaceDir: getRepositoryRoot(),
|
|
193
246
|
source,
|
|
194
|
-
apiKeys
|
|
247
|
+
apiKeys,
|
|
195
248
|
suppressLogs: true,
|
|
196
249
|
onProgress,
|
|
197
250
|
});
|
|
198
251
|
}
|
|
199
252
|
export async function runReview({ source, json = false } = { source: 'base' }) {
|
|
253
|
+
const authCheck = await getReviewAuthCheck();
|
|
254
|
+
if (authCheck.missingProviders.length > 0) {
|
|
255
|
+
if (json) {
|
|
256
|
+
writeLine(JSON.stringify(formatMissingAuthForJson({
|
|
257
|
+
source,
|
|
258
|
+
missingProviders: authCheck.missingProviders,
|
|
259
|
+
}), null, 2));
|
|
260
|
+
process.exitCode = 1;
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
throw new Error(formatMissingRequiredCliAuthProviders(authCheck.missingProviders));
|
|
264
|
+
}
|
|
200
265
|
if (json) {
|
|
201
|
-
const result = await runLocalReviewForCli({ source });
|
|
266
|
+
const result = await runLocalReviewForCli({ source, apiKeys: authCheck.apiKeys });
|
|
202
267
|
const output = formatReviewForJson(result);
|
|
203
268
|
writeLine(JSON.stringify(output, null, 2));
|
|
204
269
|
if (output.status === 'unverified') {
|
|
@@ -210,6 +275,7 @@ export async function runReview({ source, json = false } = { source: 'base' }) {
|
|
|
210
275
|
? 'Reviewing staged changes...'
|
|
211
276
|
: 'Reviewing local changes...', async (status) => await runLocalReviewForCli({
|
|
212
277
|
source,
|
|
278
|
+
apiKeys: authCheck.apiKeys,
|
|
213
279
|
onProgress: status.enabled
|
|
214
280
|
? (event) => status.update(formatReviewProgress(event))
|
|
215
281
|
: undefined,
|
package/dist/auth.js
CHANGED
|
@@ -6,13 +6,17 @@ import { createFileStore } from './file-store.js';
|
|
|
6
6
|
import { promptInput } from './input.js';
|
|
7
7
|
import { createCompositeStore, createKeyringStore, SecureStoreUnavailableError, } from './secure-store.js';
|
|
8
8
|
const GEMINI_ACCOUNT_NAME = 'gemini-api-key';
|
|
9
|
+
const OPENROUTER_ACCOUNT_NAME = 'openrouter-api-key';
|
|
9
10
|
export function createGeminiKeyStore() {
|
|
10
11
|
return createCompositeStore(createKeyringStore(GEMINI_ACCOUNT_NAME), createFileStore(GEMINI_ACCOUNT_NAME));
|
|
11
12
|
}
|
|
13
|
+
export function createOpenRouterKeyStore() {
|
|
14
|
+
return createCompositeStore(createKeyringStore(OPENROUTER_ACCOUNT_NAME), createFileStore(OPENROUTER_ACCOUNT_NAME));
|
|
15
|
+
}
|
|
12
16
|
export const OPENAI_CODEX_PROVIDER_ID = 'openai-codex';
|
|
13
17
|
export const PI_AUTH_API_KEY = `pi-auth://${OPENAI_CODEX_PROVIDER_ID}`;
|
|
14
18
|
export async function createPiAuthStorage() {
|
|
15
|
-
const { AuthStorage } = await import('@
|
|
19
|
+
const { AuthStorage } = await import('@earendil-works/pi-coding-agent');
|
|
16
20
|
return AuthStorage.create(path.join(homedir(), '.pi', 'agent', 'auth.json'));
|
|
17
21
|
}
|
|
18
22
|
export function getBrowserOpenCommand(platform, url) {
|
|
@@ -30,7 +34,12 @@ export async function hasOpenAICodexAuth(storage) {
|
|
|
30
34
|
}
|
|
31
35
|
export async function loginOpenAICodex(storage) {
|
|
32
36
|
const resolvedStorage = storage ?? (await createPiAuthStorage());
|
|
33
|
-
await
|
|
37
|
+
const { loginOpenAICodex: loginInBrowser } = await import('@earendil-works/pi-ai/oauth');
|
|
38
|
+
const credentials = await loginInBrowser(createOpenAICodexBrowserLoginOptions());
|
|
39
|
+
saveOpenAICodexCredentials(resolvedStorage, credentials);
|
|
40
|
+
}
|
|
41
|
+
export function saveOpenAICodexCredentials(storage, credentials) {
|
|
42
|
+
storage.set(OPENAI_CODEX_PROVIDER_ID, { type: 'oauth', ...credentials });
|
|
34
43
|
}
|
|
35
44
|
export async function logoutOpenAICodex(storage) {
|
|
36
45
|
const resolvedStorage = storage ?? (await createPiAuthStorage());
|
|
@@ -44,6 +53,14 @@ export async function readStoredGeminiApiKey(store = createGeminiKeyStore()) {
|
|
|
44
53
|
export async function getStoredGeminiApiKey(store = createGeminiKeyStore()) {
|
|
45
54
|
return (await readStoredGeminiApiKey(store)).value;
|
|
46
55
|
}
|
|
56
|
+
export async function readStoredOpenRouterApiKey(store = createOpenRouterKeyStore()) {
|
|
57
|
+
const result = await store.getSecret();
|
|
58
|
+
const trimmed = result.value?.trim();
|
|
59
|
+
return trimmed ? { value: trimmed, backend: result.backend } : { value: undefined };
|
|
60
|
+
}
|
|
61
|
+
export async function getStoredOpenRouterApiKey(store = createOpenRouterKeyStore()) {
|
|
62
|
+
return (await readStoredOpenRouterApiKey(store)).value;
|
|
63
|
+
}
|
|
47
64
|
export async function saveGeminiApiKey(apiKey, store = createGeminiKeyStore()) {
|
|
48
65
|
const normalized = apiKey.trim();
|
|
49
66
|
if (!normalized) {
|
|
@@ -59,22 +76,40 @@ export async function saveGeminiApiKey(apiKey, store = createGeminiKeyStore()) {
|
|
|
59
76
|
throw error;
|
|
60
77
|
}
|
|
61
78
|
}
|
|
79
|
+
export async function saveOpenRouterApiKey(apiKey, store = createOpenRouterKeyStore()) {
|
|
80
|
+
const normalized = apiKey.trim();
|
|
81
|
+
if (!normalized) {
|
|
82
|
+
throw new Error('OpenRouter API key cannot be empty');
|
|
83
|
+
}
|
|
84
|
+
try {
|
|
85
|
+
return await store.setSecret(normalized);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error instanceof SecureStoreUnavailableError) {
|
|
89
|
+
throw new SecureStoreUnavailableError(`${error.message} Set OPENROUTER_API_KEY in your environment instead.`);
|
|
90
|
+
}
|
|
91
|
+
throw error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export async function deleteOpenRouterApiKey(store = createOpenRouterKeyStore()) {
|
|
95
|
+
return await store.deleteSecret();
|
|
96
|
+
}
|
|
62
97
|
export async function deleteGeminiApiKey(store = createGeminiKeyStore()) {
|
|
63
98
|
return await store.deleteSecret();
|
|
64
99
|
}
|
|
65
|
-
export async function getAuthStatus({ env = process.env, storage, geminiStore = createGeminiKeyStore(), } = {}) {
|
|
100
|
+
export async function getAuthStatus({ env = process.env, storage, geminiStore = createGeminiKeyStore(), openRouterStore = createOpenRouterKeyStore(), } = {}) {
|
|
66
101
|
const envApiKeys = getEnvApiKeys(env);
|
|
67
102
|
const geminiStored = envApiKeys.gemini
|
|
68
103
|
? { value: undefined }
|
|
69
104
|
: await readStoredGeminiApiKey(geminiStore).catch(() => ({ value: undefined }));
|
|
105
|
+
const openRouterStored = envApiKeys.openrouter
|
|
106
|
+
? { value: undefined }
|
|
107
|
+
: await readStoredOpenRouterApiKey(openRouterStore).catch(() => ({ value: undefined }));
|
|
70
108
|
const openAICodexAuthConfigured = await hasOpenAICodexAuth(storage);
|
|
71
109
|
return {
|
|
72
110
|
openai: openAICodexAuthConfigured
|
|
73
111
|
? { configured: true, source: 'pi-auth' }
|
|
74
|
-
: {
|
|
75
|
-
configured: envApiKeys.openai !== undefined,
|
|
76
|
-
source: envApiKeys.openai !== undefined ? 'env' : undefined,
|
|
77
|
-
},
|
|
112
|
+
: { configured: false, source: undefined },
|
|
78
113
|
gemini: envApiKeys.gemini
|
|
79
114
|
? { configured: true, source: 'env' }
|
|
80
115
|
: {
|
|
@@ -85,9 +120,19 @@ export async function getAuthStatus({ env = process.env, storage, geminiStore =
|
|
|
85
120
|
: 'keychain'
|
|
86
121
|
: undefined,
|
|
87
122
|
},
|
|
123
|
+
openrouter: envApiKeys.openrouter
|
|
124
|
+
? { configured: true, source: 'env' }
|
|
125
|
+
: {
|
|
126
|
+
configured: openRouterStored.value !== undefined,
|
|
127
|
+
source: openRouterStored.value !== undefined
|
|
128
|
+
? openRouterStored.backend === 'file'
|
|
129
|
+
? 'file'
|
|
130
|
+
: 'keychain'
|
|
131
|
+
: undefined,
|
|
132
|
+
},
|
|
88
133
|
};
|
|
89
134
|
}
|
|
90
|
-
function
|
|
135
|
+
function createOpenAICodexBrowserLoginOptions() {
|
|
91
136
|
return {
|
|
92
137
|
onAuth: (info) => {
|
|
93
138
|
openUrlInBrowser(info.url);
|
package/dist/config.js
CHANGED
|
@@ -1,17 +1,51 @@
|
|
|
1
|
-
import { getStoredGeminiApiKey, hasOpenAICodexAuth, PI_AUTH_API_KEY } from './auth.js';
|
|
1
|
+
import { getStoredGeminiApiKey, getStoredOpenRouterApiKey, hasOpenAICodexAuth, PI_AUTH_API_KEY, } from './auth.js';
|
|
2
2
|
import { getEnvApiKeys } from './env.js';
|
|
3
|
+
const REQUIRED_CLI_AUTH_PROVIDERS = [
|
|
4
|
+
{ provider: 'openai', label: 'OpenAI' },
|
|
5
|
+
{ provider: 'openrouter', label: 'OpenRouter' },
|
|
6
|
+
];
|
|
3
7
|
export async function getApiKeys(env = process.env, auth = {
|
|
4
8
|
getStoredGeminiApiKey,
|
|
9
|
+
getStoredOpenRouterApiKey,
|
|
5
10
|
hasOpenAICodexAuth,
|
|
6
11
|
}) {
|
|
7
12
|
const envApiKeys = getEnvApiKeys(env);
|
|
8
13
|
const storedGeminiApiKey = envApiKeys.gemini
|
|
9
14
|
? undefined
|
|
10
15
|
: await auth.getStoredGeminiApiKey().catch(() => undefined);
|
|
16
|
+
const storedOpenRouterApiKey = envApiKeys.openrouter
|
|
17
|
+
? undefined
|
|
18
|
+
: await auth.getStoredOpenRouterApiKey().catch(() => undefined);
|
|
11
19
|
const openAICodexAuthConfigured = await auth.hasOpenAICodexAuth();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
const openRouterApiKey = envApiKeys.openrouter || storedOpenRouterApiKey;
|
|
21
|
+
const apiKeys = {};
|
|
22
|
+
const geminiApiKey = envApiKeys.gemini || storedGeminiApiKey;
|
|
23
|
+
if (geminiApiKey) {
|
|
24
|
+
apiKeys.gemini = geminiApiKey;
|
|
25
|
+
}
|
|
26
|
+
if (openRouterApiKey) {
|
|
27
|
+
apiKeys.deepseek = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
|
|
28
|
+
apiKeys.zai = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
|
|
29
|
+
}
|
|
30
|
+
if (openAICodexAuthConfigured) {
|
|
31
|
+
apiKeys.openai = PI_AUTH_API_KEY;
|
|
32
|
+
}
|
|
33
|
+
return apiKeys;
|
|
34
|
+
}
|
|
35
|
+
export function getMissingRequiredCliAuthProviders(status) {
|
|
36
|
+
return REQUIRED_CLI_AUTH_PROVIDERS.filter(({ provider }) => !status[provider].configured).map(({ label }) => label);
|
|
37
|
+
}
|
|
38
|
+
export function getMissingRequiredCliReviewAuthProviders(apiKeys) {
|
|
39
|
+
return REQUIRED_CLI_AUTH_PROVIDERS.filter(({ provider }) => {
|
|
40
|
+
switch (provider) {
|
|
41
|
+
case 'openai':
|
|
42
|
+
return !apiKeys.openai;
|
|
43
|
+
case 'openrouter':
|
|
44
|
+
return !(apiKeys.deepseek && apiKeys.zai);
|
|
45
|
+
}
|
|
46
|
+
return false;
|
|
47
|
+
}).map(({ label }) => label);
|
|
48
|
+
}
|
|
49
|
+
export function formatMissingRequiredCliAuthProviders(missingProviders) {
|
|
50
|
+
return `${missingProviders.join(' and ')} not configured; run \`doistbot auth\``;
|
|
17
51
|
}
|
package/dist/env.js
CHANGED
|
@@ -3,8 +3,8 @@ function getEnvValue(value) {
|
|
|
3
3
|
}
|
|
4
4
|
export function getEnvApiKeys(env = process.env) {
|
|
5
5
|
return {
|
|
6
|
-
anthropic: getEnvValue(env.ANTHROPIC_KEY) || getEnvValue(env.ANTHROPIC_API_KEY),
|
|
7
6
|
gemini: getEnvValue(env.GEMINI_API_KEY),
|
|
8
7
|
openai: getEnvValue(env.OPENAI_KEY) || getEnvValue(env.OPENAI_API_KEY),
|
|
8
|
+
openrouter: getEnvValue(env.OPENROUTER_API_KEY),
|
|
9
9
|
};
|
|
10
10
|
}
|
package/dist/file-store.js
CHANGED
|
@@ -41,7 +41,7 @@ export function createFileStore(account, filePath = FILE_STORE_PATH) {
|
|
|
41
41
|
}
|
|
42
42
|
function assertWritableOnThisPlatform() {
|
|
43
43
|
if (process.platform === 'win32') {
|
|
44
|
-
throw new SecureStoreUnavailableError('File-based credential fallback is not supported on Windows because POSIX 0600 permissions cannot be enforced. Use Windows Credential Manager or set
|
|
44
|
+
throw new SecureStoreUnavailableError('File-based credential fallback is not supported on Windows because POSIX 0600 permissions cannot be enforced. Use Windows Credential Manager or set an API key environment variable.');
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
47
|
async function readCredentialsFile(filePath) {
|
package/dist/runtime.js
CHANGED
|
@@ -93,7 +93,7 @@ export function checkSandboxBuild() {
|
|
|
93
93
|
}
|
|
94
94
|
export function checkPiDependency() {
|
|
95
95
|
try {
|
|
96
|
-
import.meta.resolve('@
|
|
96
|
+
import.meta.resolve('@earendil-works/pi-coding-agent');
|
|
97
97
|
return { ok: true, message: 'Pi coding agent is installed' };
|
|
98
98
|
}
|
|
99
99
|
catch {
|
package/dist/terminal.js
CHANGED
|
@@ -114,9 +114,10 @@ export async function withSpinner(message, task, terminalConfig = getDefaultTerm
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
const PROVIDER_DISPLAY_NAMES = {
|
|
117
|
+
deepseek: 'DeepSeek',
|
|
117
118
|
gemini: 'Gemini',
|
|
119
|
+
zai: 'Z.ai',
|
|
118
120
|
openai: 'OpenAI',
|
|
119
|
-
anthropic: 'Anthropic',
|
|
120
121
|
};
|
|
121
122
|
function pluralizeChangedFiles(fileCount) {
|
|
122
123
|
return `${fileCount} changed file${fileCount === 1 ? '' : 's'}`;
|
|
@@ -131,12 +132,6 @@ export function formatReviewProgress(event) {
|
|
|
131
132
|
return `Prepared ${pluralizeChangedFiles(event.fileCount)} for review`;
|
|
132
133
|
case 'formatting_results':
|
|
133
134
|
return 'Review complete. Formatting results';
|
|
134
|
-
case 'council_findings_collected':
|
|
135
|
-
return `Collected ${event.findingCount} model finding${event.findingCount === 1 ? '' : 's'}`;
|
|
136
|
-
case 'council_consensus_check':
|
|
137
|
-
return event.findingGroupCount > 0
|
|
138
|
-
? `Checking consensus across ${event.findingGroupCount} finding group${event.findingGroupCount === 1 ? '' : 's'}`
|
|
139
|
-
: 'No model findings to vote on. Finalizing the review';
|
|
140
135
|
case 'agent_start':
|
|
141
136
|
return `${PROVIDER_DISPLAY_NAMES[event.provider]} started reviewing the diff`;
|
|
142
137
|
case 'agent_end':
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doist/doistbot-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"description": "Local self-review CLI for Doistbot",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -40,19 +40,19 @@
|
|
|
40
40
|
"test": "vitest run src scripts --config ../../config/vitest.base.ts"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@inquirer/checkbox": "5.1
|
|
44
|
-
"@inquirer/password": "5.
|
|
45
|
-
"@
|
|
46
|
-
"@
|
|
43
|
+
"@inquirer/checkbox": "5.2.1",
|
|
44
|
+
"@inquirer/password": "5.1.1",
|
|
45
|
+
"@earendil-works/pi-ai": "0.79.6",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "0.79.6",
|
|
47
47
|
"@napi-rs/keyring": "1.3.0",
|
|
48
48
|
"ajv": "8.20.0",
|
|
49
49
|
"chalk": "4.1.2",
|
|
50
50
|
"commander": "14.0.3",
|
|
51
|
-
"js-yaml": "4.
|
|
51
|
+
"js-yaml": "4.2.0",
|
|
52
52
|
"octokit": "5.0.5",
|
|
53
53
|
"yocto-spinner": "1.2.0"
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
|
-
"vitest": "4.1.
|
|
56
|
+
"vitest": "4.1.9"
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -1,21 +1,14 @@
|
|
|
1
1
|
import type { Logger } from 'doistbot-logging';
|
|
2
2
|
import type { Octokit } from 'octokit';
|
|
3
3
|
export declare const CONFIG_FILE_PATH = ".github/doistbot.yml";
|
|
4
|
-
export declare const
|
|
5
|
-
type
|
|
6
|
-
defaultModels?: readonly string[];
|
|
7
|
-
};
|
|
4
|
+
export declare const DEFAULT_REVIEW_MODELS: readonly ["gemini", "openai", "deepseek", "zai"];
|
|
5
|
+
export type DefaultReviewModel = (typeof DEFAULT_REVIEW_MODELS)[number];
|
|
8
6
|
export type RepoConfig = {
|
|
9
7
|
review?: {
|
|
10
8
|
skip_forks?: boolean;
|
|
11
9
|
skip_drafts?: boolean;
|
|
12
10
|
skip_title_pattern?: string;
|
|
13
11
|
skip_branch_pattern?: string;
|
|
14
|
-
prompt_file?: string;
|
|
15
|
-
engine?: 'council' | 'multi_focus';
|
|
16
|
-
council?: {
|
|
17
|
-
enabled: boolean;
|
|
18
|
-
};
|
|
19
12
|
/**
|
|
20
13
|
* @deprecated Ignored as of #274. Doistbot now posts a GitHub Check
|
|
21
14
|
* Run for every `/review` (always-on; no opt-in) when `HEAD_SHA` is
|
|
@@ -53,17 +46,6 @@ export declare function getLocalRepoConfig({ workspaceDir, logger, }: {
|
|
|
53
46
|
workspaceDir: string;
|
|
54
47
|
logger: Logger;
|
|
55
48
|
}): RepoConfig | null;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
*/
|
|
60
|
-
export declare function getCustomPrompt({ octokit, owner, repo, path, ref, logger, }: FetchFileParams): Promise<string | null>;
|
|
61
|
-
export declare function getLocalCustomPrompt({ workspaceDir, path: promptPath, logger, }: {
|
|
62
|
-
workspaceDir: string;
|
|
63
|
-
path: string;
|
|
64
|
-
logger: Logger;
|
|
65
|
-
}): string | null;
|
|
66
|
-
export declare function parseCouncilModelsEnabled(raw: string | undefined, { defaultModels }?: ParseCouncilModelsEnabledOptions): string[];
|
|
67
|
-
export declare function getDefaultCouncilModels(): string[];
|
|
68
|
-
export type { ReviewEngine } from './review-engine.js';
|
|
69
|
-
export { DEFAULT_REVIEW_ENGINE, parseReviewEngine, resolveReviewEngine } from './review-engine.js';
|
|
49
|
+
export declare function getDefaultReviewModels(): DefaultReviewModel[];
|
|
50
|
+
export declare function parseReviewModelsEnabled(value: string | undefined): string[];
|
|
51
|
+
export {};
|