@doist/doistbot-cli 1.0.5 → 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 +55 -21
- package/dist/actions/doctor.js +13 -6
- package/dist/actions/review.js +75 -10
- package/dist/auth.js +51 -6
- package/dist/config.js +34 -5
- package/dist/env.js +1 -0
- package/dist/runtime.js +1 -1
- package/dist/terminal.js +2 -0
- package/package.json +4 -4
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -20
- package/sandbox/dist/core/check-run.js +16 -0
- package/sandbox/dist/core/datadog-metrics.js +6 -52
- package/sandbox/dist/core/logger-span-processor.js +78 -0
- package/sandbox/dist/core/pi.js +257 -176
- 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/credentials.js +12 -2
- package/sandbox/dist/providers/gemini.js +5 -1
- package/sandbox/dist/providers/helpers.js +12 -17
- package/sandbox/dist/providers/index.js +4 -0
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +12 -1
- package/sandbox/dist/tasks/chat/chat.js +161 -101
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
- package/sandbox/dist/tasks/issue-summarize/model.js +1 -1
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +25 -1
- 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/review/check-run.js +2 -1
- package/sandbox/dist/tasks/review/conversation-context.js +2 -1
- package/sandbox/dist/tasks/review/engines/dedupe.js +5 -4
- package/sandbox/dist/tasks/review/engines/multi-focus.js +40 -9
- package/sandbox/dist/tasks/review/engines/shared.js +8 -6
- package/sandbox/dist/tasks/review/github-comment-format.js +64 -10
- package/sandbox/dist/tasks/review/local.js +2 -4
- package/sandbox/dist/tasks/review/review-summary.js +45 -7
- package/sandbox/dist/tasks/review/review.js +38 -24
- package/sandbox/dist/tasks/review/runner.js +23 -2
- package/sandbox/dist/tasks/review/summary-model.js +164 -17
- package/sandbox/dist/tasks/review/thinking-level.js +4 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -20
package/dist/actions/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { deleteOpenRouterApiKey, getAuthStatus, loginOpenAICodex, logoutOpenAICodex, saveOpenRouterApiKey, } 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,12 +10,23 @@ 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
|
},
|
|
17
|
+
{
|
|
18
|
+
clear: async ({ deleteGeminiApiKey }) => {
|
|
19
|
+
const deleted = await deleteGeminiApiKey();
|
|
20
|
+
return deleted
|
|
21
|
+
? '✓ Gemini stored API key cleared.'
|
|
22
|
+
: 'Gemini had no stored API key to clear.';
|
|
23
|
+
},
|
|
24
|
+
envWarning: (env) => getEnvApiKeys(env).gemini
|
|
25
|
+
? 'Gemini is still configured from GEMINI_API_KEY.'
|
|
26
|
+
: undefined,
|
|
27
|
+
name: 'Gemini',
|
|
28
|
+
value: 'gemini',
|
|
29
|
+
},
|
|
19
30
|
{
|
|
20
31
|
clear: async ({ deleteOpenRouterApiKey }) => {
|
|
21
32
|
const deleted = await deleteOpenRouterApiKey();
|
|
@@ -38,6 +49,7 @@ export async function runAuthSetup(dependencies = {}) {
|
|
|
38
49
|
isInteractive: process.stdin.isTTY === true,
|
|
39
50
|
loginOpenAICodex,
|
|
40
51
|
promptHiddenInput,
|
|
52
|
+
saveGeminiApiKey,
|
|
41
53
|
saveOpenRouterApiKey,
|
|
42
54
|
writeLine,
|
|
43
55
|
...dependencies,
|
|
@@ -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) {
|
|
@@ -57,7 +69,40 @@ export async function runAuthSetup(dependencies = {}) {
|
|
|
57
69
|
deps.writeLine('✓ OpenAI configured.');
|
|
58
70
|
}
|
|
59
71
|
deps.writeLine();
|
|
60
|
-
deps.writeLine('2.
|
|
72
|
+
deps.writeLine('2. Gemini API key');
|
|
73
|
+
deps.writeLine();
|
|
74
|
+
if (status.gemini.source === 'env') {
|
|
75
|
+
deps.writeLine('✓ Gemini already configured from GEMINI_API_KEY.');
|
|
76
|
+
}
|
|
77
|
+
else if (status.gemini.source === 'keychain') {
|
|
78
|
+
deps.writeLine(`✓ Gemini already configured in the ${getSecureStoreLabel()}.`);
|
|
79
|
+
}
|
|
80
|
+
else if (status.gemini.source === 'file') {
|
|
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');
|
|
61
106
|
deps.writeLine();
|
|
62
107
|
if (status.openrouter.source === 'env') {
|
|
63
108
|
deps.writeLine('✓ OpenRouter already configured from OPENROUTER_API_KEY.');
|
|
@@ -74,7 +119,7 @@ export async function runAuthSetup(dependencies = {}) {
|
|
|
74
119
|
if (!deps.isInteractive) {
|
|
75
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 uses OpenRouter for
|
|
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)');
|
|
78
123
|
const backend = await deps.saveOpenRouterApiKey(await deps.promptHiddenInput('OpenRouter API key: '));
|
|
79
124
|
deps.writeLine();
|
|
80
125
|
if (backend === 'file') {
|
|
@@ -85,10 +130,12 @@ export async function runAuthSetup(dependencies = {}) {
|
|
|
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'}`);
|
|
133
|
+
writeLine(`Gemini (optional): ${status.gemini.configured ? `configured (${status.gemini.source ?? 'unknown'})` : 'not configured'}`);
|
|
88
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 = {
|
|
138
|
+
deleteGeminiApiKey,
|
|
92
139
|
deleteOpenRouterApiKey,
|
|
93
140
|
env: process.env,
|
|
94
141
|
isInteractive: process.stdin.isTTY === true,
|
|
@@ -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,25 +1,32 @@
|
|
|
1
1
|
import { getAuthStatus } from '../auth.js';
|
|
2
|
+
import { formatMissingRequiredCliAuthProviders, getMissingRequiredCliAuthProviders, } from '../config.js';
|
|
2
3
|
import { checkGit, checkPiDependency, checkRepository, checkSandboxBuild } from '../runtime.js';
|
|
3
4
|
import { renderDiagnosticCheck } from '../terminal.js';
|
|
4
5
|
export function getConfiguredProviders({ status, }) {
|
|
5
6
|
return [
|
|
6
|
-
status.openai.configured
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
: (status.openai.source ?? 'configured')})`
|
|
7
|
+
status.openai.configured ? `OpenAI (${status.openai.source ?? 'configured'})` : undefined,
|
|
8
|
+
status.gemini.configured
|
|
9
|
+
? `Gemini optional (${status.gemini.source ?? 'configured'})`
|
|
10
10
|
: undefined,
|
|
11
11
|
status.openrouter.configured
|
|
12
12
|
? `OpenRouter (${status.openrouter.source ?? 'configured'})`
|
|
13
13
|
: undefined,
|
|
14
14
|
].filter((provider) => provider !== undefined);
|
|
15
15
|
}
|
|
16
|
+
export function getMissingRequiredProviders({ status, }) {
|
|
17
|
+
return getMissingRequiredCliAuthProviders(status);
|
|
18
|
+
}
|
|
16
19
|
export async function checkAuth({ env = process.env, getStatus = getAuthStatus, } = {}) {
|
|
17
20
|
const status = await getStatus({ env });
|
|
18
21
|
const configured = getConfiguredProviders({ status });
|
|
19
|
-
|
|
22
|
+
const missingRequiredProviders = getMissingRequiredProviders({ status });
|
|
23
|
+
if (missingRequiredProviders.length === 0) {
|
|
20
24
|
return { ok: true, message: `${configured.join(', ')} configured` };
|
|
21
25
|
}
|
|
22
|
-
return {
|
|
26
|
+
return {
|
|
27
|
+
ok: false,
|
|
28
|
+
message: formatMissingRequiredCliAuthProviders(missingRequiredProviders),
|
|
29
|
+
};
|
|
23
30
|
}
|
|
24
31
|
export async function runDoctor() {
|
|
25
32
|
await renderDiagnosticCheck('git', checkGit);
|
package/dist/actions/review.js
CHANGED
|
@@ -1,11 +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
|
+
zai: 'Z.ai',
|
|
10
|
+
openai: 'OpenAI',
|
|
9
11
|
};
|
|
10
12
|
function stripProviderFailurePrefix(error) {
|
|
11
13
|
return error
|
|
@@ -17,8 +19,11 @@ function getReadableProviderFailure(failure) {
|
|
|
17
19
|
const error = stripProviderFailurePrefix(failure.error);
|
|
18
20
|
const normalized = error.toLowerCase();
|
|
19
21
|
if (failure.model === 'openai' &&
|
|
20
|
-
(normalized.includes('no api key found for openai
|
|
21
|
-
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'))) {
|
|
22
27
|
return 'OpenAI Codex is not authenticated. Run `doistbot auth` to reconnect OpenAI.';
|
|
23
28
|
}
|
|
24
29
|
if (failure.model === 'gemini' &&
|
|
@@ -27,15 +32,16 @@ function getReadableProviderFailure(failure) {
|
|
|
27
32
|
normalized.includes('quota') ||
|
|
28
33
|
normalized.includes('rate-limit') ||
|
|
29
34
|
normalized.includes('rate limit'))) {
|
|
30
|
-
return 'Gemini
|
|
35
|
+
return 'Gemini is rate-limited or out of quota. Check your Google Gemini quota or wait for quota to reset.';
|
|
31
36
|
}
|
|
32
37
|
if (failure.model === 'gemini' &&
|
|
33
38
|
(normalized.includes('api_key_invalid') ||
|
|
34
39
|
normalized.includes('api key not valid') ||
|
|
35
40
|
normalized.includes('invalid_argument') ||
|
|
36
41
|
normalized.includes('invalid api key') ||
|
|
37
|
-
normalized.includes('no api key found for
|
|
38
|
-
|
|
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.';
|
|
39
45
|
}
|
|
40
46
|
const firstLine = error
|
|
41
47
|
.split(/\r?\n/)
|
|
@@ -187,19 +193,77 @@ function formatReviewForJson(result) {
|
|
|
187
193
|
providerFailures: getReadableProviderFailureEntries(result.reviewResult.providerFailures),
|
|
188
194
|
};
|
|
189
195
|
}
|
|
190
|
-
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, }) {
|
|
191
243
|
const reviewModule = await loadSandboxReviewModule();
|
|
192
244
|
return await reviewModule.runLocalReview({
|
|
193
245
|
workspaceDir: getRepositoryRoot(),
|
|
194
246
|
source,
|
|
195
|
-
apiKeys
|
|
247
|
+
apiKeys,
|
|
196
248
|
suppressLogs: true,
|
|
197
249
|
onProgress,
|
|
198
250
|
});
|
|
199
251
|
}
|
|
200
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
|
+
}
|
|
201
265
|
if (json) {
|
|
202
|
-
const result = await runLocalReviewForCli({ source });
|
|
266
|
+
const result = await runLocalReviewForCli({ source, apiKeys: authCheck.apiKeys });
|
|
203
267
|
const output = formatReviewForJson(result);
|
|
204
268
|
writeLine(JSON.stringify(output, null, 2));
|
|
205
269
|
if (output.status === 'unverified') {
|
|
@@ -211,6 +275,7 @@ export async function runReview({ source, json = false } = { source: 'base' }) {
|
|
|
211
275
|
? 'Reviewing staged changes...'
|
|
212
276
|
: 'Reviewing local changes...', async (status) => await runLocalReviewForCli({
|
|
213
277
|
source,
|
|
278
|
+
apiKeys: authCheck.apiKeys,
|
|
214
279
|
onProgress: status.enabled
|
|
215
280
|
? (event) => status.update(formatReviewProgress(event))
|
|
216
281
|
: undefined,
|
package/dist/auth.js
CHANGED
|
@@ -5,14 +5,18 @@ import { getEnvApiKeys } from './env.js';
|
|
|
5
5
|
import { createFileStore } from './file-store.js';
|
|
6
6
|
import { promptInput } from './input.js';
|
|
7
7
|
import { createCompositeStore, createKeyringStore, SecureStoreUnavailableError, } from './secure-store.js';
|
|
8
|
+
const GEMINI_ACCOUNT_NAME = 'gemini-api-key';
|
|
8
9
|
const OPENROUTER_ACCOUNT_NAME = 'openrouter-api-key';
|
|
10
|
+
export function createGeminiKeyStore() {
|
|
11
|
+
return createCompositeStore(createKeyringStore(GEMINI_ACCOUNT_NAME), createFileStore(GEMINI_ACCOUNT_NAME));
|
|
12
|
+
}
|
|
9
13
|
export function createOpenRouterKeyStore() {
|
|
10
14
|
return createCompositeStore(createKeyringStore(OPENROUTER_ACCOUNT_NAME), createFileStore(OPENROUTER_ACCOUNT_NAME));
|
|
11
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,12 +34,25 @@ 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());
|
|
37
46
|
resolvedStorage.logout(OPENAI_CODEX_PROVIDER_ID);
|
|
38
47
|
}
|
|
48
|
+
export async function readStoredGeminiApiKey(store = createGeminiKeyStore()) {
|
|
49
|
+
const result = await store.getSecret();
|
|
50
|
+
const trimmed = result.value?.trim();
|
|
51
|
+
return trimmed ? { value: trimmed, backend: result.backend } : { value: undefined };
|
|
52
|
+
}
|
|
53
|
+
export async function getStoredGeminiApiKey(store = createGeminiKeyStore()) {
|
|
54
|
+
return (await readStoredGeminiApiKey(store)).value;
|
|
55
|
+
}
|
|
39
56
|
export async function readStoredOpenRouterApiKey(store = createOpenRouterKeyStore()) {
|
|
40
57
|
const result = await store.getSecret();
|
|
41
58
|
const trimmed = result.value?.trim();
|
|
@@ -44,6 +61,21 @@ export async function readStoredOpenRouterApiKey(store = createOpenRouterKeyStor
|
|
|
44
61
|
export async function getStoredOpenRouterApiKey(store = createOpenRouterKeyStore()) {
|
|
45
62
|
return (await readStoredOpenRouterApiKey(store)).value;
|
|
46
63
|
}
|
|
64
|
+
export async function saveGeminiApiKey(apiKey, store = createGeminiKeyStore()) {
|
|
65
|
+
const normalized = apiKey.trim();
|
|
66
|
+
if (!normalized) {
|
|
67
|
+
throw new Error('Gemini API key cannot be empty');
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return await store.setSecret(normalized);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
if (error instanceof SecureStoreUnavailableError) {
|
|
74
|
+
throw new SecureStoreUnavailableError(`${error.message} Set GEMINI_API_KEY in your environment instead.`);
|
|
75
|
+
}
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
47
79
|
export async function saveOpenRouterApiKey(apiKey, store = createOpenRouterKeyStore()) {
|
|
48
80
|
const normalized = apiKey.trim();
|
|
49
81
|
if (!normalized) {
|
|
@@ -62,8 +94,14 @@ export async function saveOpenRouterApiKey(apiKey, store = createOpenRouterKeySt
|
|
|
62
94
|
export async function deleteOpenRouterApiKey(store = createOpenRouterKeyStore()) {
|
|
63
95
|
return await store.deleteSecret();
|
|
64
96
|
}
|
|
65
|
-
export async function
|
|
97
|
+
export async function deleteGeminiApiKey(store = createGeminiKeyStore()) {
|
|
98
|
+
return await store.deleteSecret();
|
|
99
|
+
}
|
|
100
|
+
export async function getAuthStatus({ env = process.env, storage, geminiStore = createGeminiKeyStore(), openRouterStore = createOpenRouterKeyStore(), } = {}) {
|
|
66
101
|
const envApiKeys = getEnvApiKeys(env);
|
|
102
|
+
const geminiStored = envApiKeys.gemini
|
|
103
|
+
? { value: undefined }
|
|
104
|
+
: await readStoredGeminiApiKey(geminiStore).catch(() => ({ value: undefined }));
|
|
67
105
|
const openRouterStored = envApiKeys.openrouter
|
|
68
106
|
? { value: undefined }
|
|
69
107
|
: await readStoredOpenRouterApiKey(openRouterStore).catch(() => ({ value: undefined }));
|
|
@@ -71,9 +109,16 @@ export async function getAuthStatus({ env = process.env, storage, openRouterStor
|
|
|
71
109
|
return {
|
|
72
110
|
openai: openAICodexAuthConfigured
|
|
73
111
|
? { configured: true, source: 'pi-auth' }
|
|
112
|
+
: { configured: false, source: undefined },
|
|
113
|
+
gemini: envApiKeys.gemini
|
|
114
|
+
? { configured: true, source: 'env' }
|
|
74
115
|
: {
|
|
75
|
-
configured:
|
|
76
|
-
source:
|
|
116
|
+
configured: geminiStored.value !== undefined,
|
|
117
|
+
source: geminiStored.value !== undefined
|
|
118
|
+
? geminiStored.backend === 'file'
|
|
119
|
+
? 'file'
|
|
120
|
+
: 'keychain'
|
|
121
|
+
: undefined,
|
|
77
122
|
},
|
|
78
123
|
openrouter: envApiKeys.openrouter
|
|
79
124
|
? { configured: true, source: 'env' }
|
|
@@ -87,7 +132,7 @@ export async function getAuthStatus({ env = process.env, storage, openRouterStor
|
|
|
87
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,22 +1,51 @@
|
|
|
1
|
-
import { getStoredOpenRouterApiKey, 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 = {
|
|
8
|
+
getStoredGeminiApiKey,
|
|
4
9
|
getStoredOpenRouterApiKey,
|
|
5
10
|
hasOpenAICodexAuth,
|
|
6
11
|
}) {
|
|
7
12
|
const envApiKeys = getEnvApiKeys(env);
|
|
13
|
+
const storedGeminiApiKey = envApiKeys.gemini
|
|
14
|
+
? undefined
|
|
15
|
+
: await auth.getStoredGeminiApiKey().catch(() => undefined);
|
|
8
16
|
const storedOpenRouterApiKey = envApiKeys.openrouter
|
|
9
17
|
? undefined
|
|
10
18
|
: await auth.getStoredOpenRouterApiKey().catch(() => undefined);
|
|
11
19
|
const openAICodexAuthConfigured = await auth.hasOpenAICodexAuth();
|
|
12
20
|
const openRouterApiKey = envApiKeys.openrouter || storedOpenRouterApiKey;
|
|
13
21
|
const apiKeys = {};
|
|
22
|
+
const geminiApiKey = envApiKeys.gemini || storedGeminiApiKey;
|
|
23
|
+
if (geminiApiKey) {
|
|
24
|
+
apiKeys.gemini = geminiApiKey;
|
|
25
|
+
}
|
|
14
26
|
if (openRouterApiKey) {
|
|
15
|
-
apiKeys.
|
|
27
|
+
apiKeys.deepseek = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
|
|
28
|
+
apiKeys.zai = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
|
|
16
29
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
apiKeys.openai = openAIKey;
|
|
30
|
+
if (openAICodexAuthConfigured) {
|
|
31
|
+
apiKeys.openai = PI_AUTH_API_KEY;
|
|
20
32
|
}
|
|
21
33
|
return apiKeys;
|
|
22
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\``;
|
|
51
|
+
}
|
package/dist/env.js
CHANGED
|
@@ -3,6 +3,7 @@ function getEnvValue(value) {
|
|
|
3
3
|
}
|
|
4
4
|
export function getEnvApiKeys(env = process.env) {
|
|
5
5
|
return {
|
|
6
|
+
gemini: getEnvValue(env.GEMINI_API_KEY),
|
|
6
7
|
openai: getEnvValue(env.OPENAI_KEY) || getEnvValue(env.OPENAI_API_KEY),
|
|
7
8
|
openrouter: getEnvValue(env.OPENROUTER_API_KEY),
|
|
8
9
|
};
|
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,7 +114,9 @@ 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
121
|
};
|
|
120
122
|
function pluralizeChangedFiles(fileCount) {
|
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",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@inquirer/checkbox": "5.2.1",
|
|
44
44
|
"@inquirer/password": "5.1.1",
|
|
45
|
-
"@
|
|
46
|
-
"@
|
|
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",
|
|
@@ -53,6 +53,6 @@
|
|
|
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,12 +1,8 @@
|
|
|
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 DEFAULT_REVIEW_MODELS: readonly ["gemini", "openai"];
|
|
5
|
-
export
|
|
6
|
-
type ParseReviewModelsEnabledOptions = {
|
|
7
|
-
defaultModels?: readonly string[];
|
|
8
|
-
requiredModels?: readonly string[];
|
|
9
|
-
};
|
|
4
|
+
export declare const DEFAULT_REVIEW_MODELS: readonly ["gemini", "openai", "deepseek", "zai"];
|
|
5
|
+
export type DefaultReviewModel = (typeof DEFAULT_REVIEW_MODELS)[number];
|
|
10
6
|
export type RepoConfig = {
|
|
11
7
|
review?: {
|
|
12
8
|
skip_forks?: boolean;
|
|
@@ -50,6 +46,6 @@ export declare function getLocalRepoConfig({ workspaceDir, logger, }: {
|
|
|
50
46
|
workspaceDir: string;
|
|
51
47
|
logger: Logger;
|
|
52
48
|
}): RepoConfig | null;
|
|
53
|
-
export declare function
|
|
54
|
-
export declare function
|
|
49
|
+
export declare function getDefaultReviewModels(): DefaultReviewModel[];
|
|
50
|
+
export declare function parseReviewModelsEnabled(value: string | undefined): string[];
|
|
55
51
|
export {};
|
|
@@ -2,8 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { load as yamlLoad } from 'js-yaml';
|
|
4
4
|
export const CONFIG_FILE_PATH = '.github/doistbot.yml';
|
|
5
|
-
export const DEFAULT_REVIEW_MODELS = ['gemini', 'openai'];
|
|
6
|
-
export const REQUIRED_REVIEW_MODELS = DEFAULT_REVIEW_MODELS;
|
|
5
|
+
export const DEFAULT_REVIEW_MODELS = ['gemini', 'openai', 'deepseek', 'zai'];
|
|
7
6
|
function parseRepoConfigContent(content) {
|
|
8
7
|
return yamlLoad(content);
|
|
9
8
|
}
|
|
@@ -91,24 +90,13 @@ export function getLocalRepoConfig({ workspaceDir, logger, }) {
|
|
|
91
90
|
return null;
|
|
92
91
|
}
|
|
93
92
|
}
|
|
94
|
-
export function parseReviewModelsEnabled(raw, { defaultModels = DEFAULT_REVIEW_MODELS, requiredModels = REQUIRED_REVIEW_MODELS, } = {}) {
|
|
95
|
-
if (!raw?.trim()) {
|
|
96
|
-
return [...defaultModels];
|
|
97
|
-
}
|
|
98
|
-
const allowedModels = new Set(DEFAULT_REVIEW_MODELS);
|
|
99
|
-
const models = [];
|
|
100
|
-
for (const token of raw.split(',')) {
|
|
101
|
-
const model = token.trim().toLowerCase();
|
|
102
|
-
if (!model || !allowedModels.has(model)) {
|
|
103
|
-
continue;
|
|
104
|
-
}
|
|
105
|
-
if (!models.includes(model)) {
|
|
106
|
-
models.push(model);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
const hasRequiredModels = requiredModels.every((model) => models.includes(model));
|
|
110
|
-
return models.length > 0 && hasRequiredModels ? models : [...defaultModels];
|
|
111
|
-
}
|
|
112
93
|
export function getDefaultReviewModels() {
|
|
113
94
|
return [...DEFAULT_REVIEW_MODELS];
|
|
114
95
|
}
|
|
96
|
+
export function parseReviewModelsEnabled(value) {
|
|
97
|
+
const models = value
|
|
98
|
+
?.split(',')
|
|
99
|
+
.map((model) => model.trim())
|
|
100
|
+
.filter(Boolean) ?? [];
|
|
101
|
+
return models.length > 0 ? [...new Set(models)] : getDefaultReviewModels();
|
|
102
|
+
}
|
|
@@ -24,6 +24,7 @@ function truncateTitle(value) {
|
|
|
24
24
|
*/
|
|
25
25
|
export async function postCheckRun(params) {
|
|
26
26
|
const { octokit, owner, repo, sha, title, summary, detailsUrl, name, externalId } = params;
|
|
27
|
+
const checkRunId = params.checkRunId;
|
|
27
28
|
const status = params.status ?? 'completed';
|
|
28
29
|
const conclusion = status === 'completed' ? params.conclusion : undefined;
|
|
29
30
|
const truncatedTitle = truncateTitle(title);
|
|
@@ -39,6 +40,21 @@ export async function postCheckRun(params) {
|
|
|
39
40
|
output: { title: truncatedTitle, summary },
|
|
40
41
|
};
|
|
41
42
|
try {
|
|
43
|
+
if (checkRunId !== undefined) {
|
|
44
|
+
await octokit.rest.checks.update({
|
|
45
|
+
...payload,
|
|
46
|
+
check_run_id: checkRunId,
|
|
47
|
+
});
|
|
48
|
+
logger.info('Updated known check run', {
|
|
49
|
+
component: 'check-run',
|
|
50
|
+
name,
|
|
51
|
+
sha,
|
|
52
|
+
status,
|
|
53
|
+
conclusion,
|
|
54
|
+
checkRunId,
|
|
55
|
+
});
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
42
58
|
const existing = await octokit.rest.checks.listForRef({
|
|
43
59
|
owner,
|
|
44
60
|
repo,
|