@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.
Files changed (53) hide show
  1. package/dist/actions/auth.js +55 -21
  2. package/dist/actions/doctor.js +13 -6
  3. package/dist/actions/review.js +75 -10
  4. package/dist/auth.js +51 -6
  5. package/dist/config.js +34 -5
  6. package/dist/env.js +1 -0
  7. package/dist/runtime.js +1 -1
  8. package/dist/terminal.js +2 -0
  9. package/package.json +4 -4
  10. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  11. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -20
  12. package/sandbox/dist/core/check-run.js +16 -0
  13. package/sandbox/dist/core/datadog-metrics.js +6 -52
  14. package/sandbox/dist/core/logger-span-processor.js +78 -0
  15. package/sandbox/dist/core/pi.js +257 -176
  16. package/sandbox/dist/core/tracing-setup.js +15 -0
  17. package/sandbox/dist/core/tracing.js +71 -0
  18. package/sandbox/dist/main.js +4 -1
  19. package/sandbox/dist/providers/credentials.js +12 -2
  20. package/sandbox/dist/providers/gemini.js +5 -1
  21. package/sandbox/dist/providers/helpers.js +12 -17
  22. package/sandbox/dist/providers/index.js +4 -0
  23. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  24. package/sandbox/dist/providers/pi-review.js +12 -1
  25. package/sandbox/dist/tasks/chat/chat.js +161 -101
  26. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
  27. package/sandbox/dist/tasks/issue-summarize/model.js +1 -1
  28. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  29. package/sandbox/dist/tasks/issue-triage/context.js +25 -1
  30. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +31 -8
  31. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
  32. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
  33. package/sandbox/dist/tasks/issue-triage/model.js +120 -36
  34. package/sandbox/dist/tasks/issue-triage/output.js +27 -0
  35. package/sandbox/dist/tasks/issue-triage/pr-creator.js +3 -2
  36. package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
  37. package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
  38. package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
  39. package/sandbox/dist/tasks/issue-triage/triage.js +288 -418
  40. package/sandbox/dist/tasks/review/check-run.js +2 -1
  41. package/sandbox/dist/tasks/review/conversation-context.js +2 -1
  42. package/sandbox/dist/tasks/review/engines/dedupe.js +5 -4
  43. package/sandbox/dist/tasks/review/engines/multi-focus.js +40 -9
  44. package/sandbox/dist/tasks/review/engines/shared.js +8 -6
  45. package/sandbox/dist/tasks/review/github-comment-format.js +64 -10
  46. package/sandbox/dist/tasks/review/local.js +2 -4
  47. package/sandbox/dist/tasks/review/review-summary.js +45 -7
  48. package/sandbox/dist/tasks/review/review.js +38 -24
  49. package/sandbox/dist/tasks/review/runner.js +23 -2
  50. package/sandbox/dist/tasks/review/summary-model.js +164 -17
  51. package/sandbox/dist/tasks/review/thinking-level.js +4 -0
  52. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  53. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -20
@@ -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: (env) => getEnvApiKeys(env).openai
14
- ? 'OpenAI is still configured from an environment variable.'
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(formatOpenAIConfiguredMessage(status.openai.source));
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. OpenRouter API key');
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 interacting with Gemini. Please create an OpenRouter API key at https://openrouter.ai/keys and paste it here. (Use your Work Perk for it)');
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
- }
@@ -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
- ? `OpenAI (${status.openai.source === 'pi-auth'
8
- ? 'pi-auth record found; validated during review'
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
- if (configured.length > 0) {
22
+ const missingRequiredProviders = getMissingRequiredProviders({ status });
23
+ if (missingRequiredProviders.length === 0) {
20
24
  return { ok: true, message: `${configured.join(', ')} configured` };
21
25
  }
22
- return { ok: false, message: 'run `doistbot auth`' };
26
+ return {
27
+ ok: false,
28
+ message: formatMissingRequiredCliAuthProviders(missingRequiredProviders),
29
+ };
23
30
  }
24
31
  export async function runDoctor() {
25
32
  await renderDiagnosticCheck('git', checkGit);
@@ -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
- openai: 'OpenAI Codex',
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-codex') ||
21
- normalized.includes('use /login'))) {
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 via OpenRouter is rate-limited or out of quota. Check your OpenRouter credits, provider routing, or wait for quota to reset.';
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 openrouter'))) {
38
- return 'OpenRouter API key is invalid or missing. Run `doistbot auth` and paste a valid key from https://openrouter.ai/keys.';
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 runLocalReviewForCli({ source, onProgress, }) {
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: await getApiKeys(),
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('@mariozechner/pi-coding-agent');
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 resolvedStorage.login(OPENAI_CODEX_PROVIDER_ID, createOpenAICodexLoginCallbacks());
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 getAuthStatus({ env = process.env, storage, openRouterStore = createOpenRouterKeyStore(), } = {}) {
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: envApiKeys.openai !== undefined,
76
- source: envApiKeys.openai !== undefined ? 'env' : undefined,
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 createOpenAICodexLoginCallbacks() {
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.gemini = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
27
+ apiKeys.deepseek = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
28
+ apiKeys.zai = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
16
29
  }
17
- const openAIKey = openAICodexAuthConfigured ? PI_AUTH_API_KEY : envApiKeys.openai;
18
- if (openAIKey) {
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('@mariozechner/pi-coding-agent');
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.5",
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
- "@mariozechner/pi-ai": "0.73.1",
46
- "@mariozechner/pi-coding-agent": "0.73.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",
@@ -53,6 +53,6 @@
53
53
  "yocto-spinner": "1.2.0"
54
54
  },
55
55
  "devDependencies": {
56
- "vitest": "4.1.8"
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 declare const REQUIRED_REVIEW_MODELS: readonly ["gemini", "openai"];
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 parseReviewModelsEnabled(raw: string | undefined, { defaultModels, requiredModels, }?: ParseReviewModelsEnabledOptions): string[];
54
- export declare function getDefaultReviewModels(): string[];
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,