@doist/doistbot-cli 1.0.5 → 1.0.7

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 (61) hide show
  1. package/dist/actions/auth.js +78 -24
  2. package/dist/actions/doctor.js +14 -7
  3. package/dist/actions/review.js +67 -10
  4. package/dist/auth.js +51 -6
  5. package/dist/config.js +77 -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 +285 -174
  16. package/sandbox/dist/core/prompt-template.js +45 -3
  17. package/sandbox/dist/core/repository-map.js +24 -0
  18. package/sandbox/dist/core/tracing-setup.js +15 -0
  19. package/sandbox/dist/core/tracing.js +80 -0
  20. package/sandbox/dist/main.js +4 -1
  21. package/sandbox/dist/providers/credentials.js +12 -2
  22. package/sandbox/dist/providers/gemini.js +5 -1
  23. package/sandbox/dist/providers/helpers.js +63 -17
  24. package/sandbox/dist/providers/index.js +4 -0
  25. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  26. package/sandbox/dist/providers/pi-review.js +19 -2
  27. package/sandbox/dist/tasks/chat/chat.js +173 -102
  28. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +210 -185
  29. package/sandbox/dist/tasks/issue-summarize/model.js +120 -49
  30. package/sandbox/dist/tasks/issue-summarize/prompt.js +2 -2
  31. package/sandbox/dist/tasks/issue-summarize/summarize.js +104 -88
  32. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  33. package/sandbox/dist/tasks/issue-triage/context.js +25 -1
  34. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +33 -10
  35. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +154 -101
  36. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +15 -6
  37. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +2 -0
  38. package/sandbox/dist/tasks/issue-triage/model.js +120 -36
  39. package/sandbox/dist/tasks/issue-triage/output.js +27 -0
  40. package/sandbox/dist/tasks/issue-triage/pr-creator.js +5 -4
  41. package/sandbox/dist/tasks/issue-triage/prompt.js +2 -2
  42. package/sandbox/dist/tasks/issue-triage/resolution.js +26 -0
  43. package/sandbox/dist/tasks/issue-triage/routing.js +9 -9
  44. package/sandbox/dist/tasks/issue-triage/side-effects.js +9 -0
  45. package/sandbox/dist/tasks/issue-triage/triage.js +290 -420
  46. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +3 -3
  47. package/sandbox/dist/tasks/review/check-run.js +2 -1
  48. package/sandbox/dist/tasks/review/conversation-context.js +2 -1
  49. package/sandbox/dist/tasks/review/engines/dedupe.js +110 -44
  50. package/sandbox/dist/tasks/review/engines/multi-focus.js +254 -113
  51. package/sandbox/dist/tasks/review/engines/shared.js +43 -6
  52. package/sandbox/dist/tasks/review/github-comment-format.js +72 -10
  53. package/sandbox/dist/tasks/review/local.js +7 -4
  54. package/sandbox/dist/tasks/review/review-summary.js +48 -7
  55. package/sandbox/dist/tasks/review/review.js +267 -186
  56. package/sandbox/dist/tasks/review/runner.js +33 -7
  57. package/sandbox/dist/tasks/review/summary-model.js +189 -20
  58. package/sandbox/dist/tasks/review/thinking-level.js +4 -0
  59. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +4 -8
  60. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -20
  61. package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +1 -0
@@ -1,4 +1,5 @@
1
- import { deleteOpenRouterApiKey, getAuthStatus, loginOpenAICodex, logoutOpenAICodex, saveOpenRouterApiKey, } from '../auth.js';
1
+ import chalk from 'chalk';
2
+ import { deleteGeminiApiKey, deleteOpenRouterApiKey, getAuthStatus, loginOpenAICodex, logoutOpenAICodex, saveGeminiApiKey, saveOpenRouterApiKey, } from '../auth.js';
2
3
  import { getEnvApiKeys } from '../env.js';
3
4
  import { FILE_STORE_PATH } from '../file-store.js';
4
5
  import { promptHiddenInput, promptMultiSelect } from '../input.js';
@@ -10,12 +11,23 @@ const AUTH_LOGOUT_PROVIDER_CONFIGS = [
10
11
  await logoutOpenAICodex();
11
12
  return '✓ OpenAI stored auth cleared.';
12
13
  },
13
- envWarning: (env) => getEnvApiKeys(env).openai
14
- ? 'OpenAI is still configured from an environment variable.'
15
- : undefined,
16
- name: 'OpenAI Codex',
14
+ envWarning: () => undefined,
15
+ name: 'OpenAI',
17
16
  value: 'openai',
18
17
  },
18
+ {
19
+ clear: async ({ deleteGeminiApiKey }) => {
20
+ const deleted = await deleteGeminiApiKey();
21
+ return deleted
22
+ ? '✓ Gemini stored API key cleared.'
23
+ : 'Gemini had no stored API key to clear.';
24
+ },
25
+ envWarning: (env) => getEnvApiKeys(env).gemini
26
+ ? 'Gemini is still configured from GEMINI_API_KEY.'
27
+ : undefined,
28
+ name: 'Gemini',
29
+ value: 'gemini',
30
+ },
19
31
  {
20
32
  clear: async ({ deleteOpenRouterApiKey }) => {
21
33
  const deleted = await deleteOpenRouterApiKey();
@@ -32,12 +44,25 @@ const AUTH_LOGOUT_PROVIDER_CONFIGS = [
32
44
  ];
33
45
  const AUTH_LOGOUT_PROVIDERS = AUTH_LOGOUT_PROVIDER_CONFIGS.map(({ value }) => value);
34
46
  export const AUTH_LOGOUT_PROVIDER_USAGE = `${AUTH_LOGOUT_PROVIDERS.join('|')}|all`;
47
+ const OPTIONAL_LABEL = '(optional)';
48
+ const SKIP_INSTRUCTION = 'press Enter to skip';
49
+ const API_KEY_PROMPT = 'Paste your API Key: ';
50
+ function highlightOptionalText(value) {
51
+ return chalk.bold.yellow(value);
52
+ }
53
+ function formatOptionalLabel() {
54
+ return highlightOptionalText(OPTIONAL_LABEL);
55
+ }
56
+ function formatSkipInstruction() {
57
+ return highlightOptionalText(SKIP_INSTRUCTION);
58
+ }
35
59
  export async function runAuthSetup(dependencies = {}) {
36
60
  const deps = {
37
61
  getAuthStatus,
38
62
  isInteractive: process.stdin.isTTY === true,
39
63
  loginOpenAICodex,
40
64
  promptHiddenInput,
65
+ saveGeminiApiKey,
41
66
  saveOpenRouterApiKey,
42
67
  writeLine,
43
68
  ...dependencies,
@@ -46,7 +71,7 @@ export async function runAuthSetup(dependencies = {}) {
46
71
  deps.writeLine('1. OpenAI Codex');
47
72
  deps.writeLine();
48
73
  if (status.openai.configured) {
49
- deps.writeLine(formatOpenAIConfiguredMessage(status.openai.source));
74
+ deps.writeLine('✓ OpenAI already configured (pi-auth).');
50
75
  }
51
76
  else {
52
77
  if (!deps.isInteractive) {
@@ -57,7 +82,40 @@ export async function runAuthSetup(dependencies = {}) {
57
82
  deps.writeLine('✓ OpenAI configured.');
58
83
  }
59
84
  deps.writeLine();
60
- deps.writeLine('2. OpenRouter API key');
85
+ deps.writeLine(`2. Gemini API key ${formatOptionalLabel()}`);
86
+ deps.writeLine();
87
+ if (status.gemini.source === 'env') {
88
+ deps.writeLine('✓ Gemini already configured from GEMINI_API_KEY.');
89
+ }
90
+ else if (status.gemini.source === 'keychain') {
91
+ deps.writeLine(`✓ Gemini already configured in the ${getSecureStoreLabel()}.`);
92
+ }
93
+ else if (status.gemini.source === 'file') {
94
+ deps.writeLine(`✓ Gemini already configured (${FILE_STORE_PATH}).`);
95
+ }
96
+ else {
97
+ if (!deps.isInteractive) {
98
+ deps.writeLine('Gemini not configured. Skipping optional Gemini provider.');
99
+ }
100
+ else {
101
+ deps.writeLine(`Doistbot can use Gemini via Google Vertex. Paste a Gemini API key, or ${formatSkipInstruction()}.`);
102
+ const geminiApiKey = (await deps.promptHiddenInput(API_KEY_PROMPT)).trim();
103
+ if (!geminiApiKey) {
104
+ deps.writeLine();
105
+ deps.writeLine('Gemini skipped. You can add it later with `doistbot auth`.');
106
+ }
107
+ else {
108
+ const backend = await deps.saveGeminiApiKey(geminiApiKey);
109
+ deps.writeLine();
110
+ if (backend === 'file') {
111
+ deps.writeLine(`Note: ${getSecureStoreLabel()} unavailable; key stored in ${FILE_STORE_PATH} with 0600 permissions.`);
112
+ }
113
+ deps.writeLine('✓ Gemini configured.');
114
+ }
115
+ }
116
+ }
117
+ deps.writeLine();
118
+ deps.writeLine(`3. OpenRouter API key ${formatOptionalLabel()}`);
61
119
  deps.writeLine();
62
120
  if (status.openrouter.source === 'env') {
63
121
  deps.writeLine('✓ OpenRouter already configured from OPENROUTER_API_KEY.');
@@ -72,10 +130,17 @@ export async function runAuthSetup(dependencies = {}) {
72
130
  return;
73
131
  }
74
132
  if (!deps.isInteractive) {
75
- throw new Error('Cannot prompt for OpenRouter API key in non-interactive mode. Set OPENROUTER_API_KEY instead.');
133
+ deps.writeLine('OpenRouter not configured. Skipping optional OpenRouter provider.');
134
+ return;
135
+ }
136
+ deps.writeLine(`Doistbot can use OpenRouter for DeepSeek, Z.ai, and other open models. Paste an OpenRouter API key, or ${formatSkipInstruction()}.`);
137
+ const openRouterApiKey = (await deps.promptHiddenInput(API_KEY_PROMPT)).trim();
138
+ if (!openRouterApiKey) {
139
+ deps.writeLine();
140
+ deps.writeLine('OpenRouter skipped. You can add it later with `doistbot auth`.');
141
+ return;
76
142
  }
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)');
78
- const backend = await deps.saveOpenRouterApiKey(await deps.promptHiddenInput('OpenRouter API key: '));
143
+ const backend = await deps.saveOpenRouterApiKey(openRouterApiKey);
79
144
  deps.writeLine();
80
145
  if (backend === 'file') {
81
146
  deps.writeLine(`Note: ${getSecureStoreLabel()} unavailable; key stored in ${FILE_STORE_PATH} with 0600 permissions.`);
@@ -85,10 +150,12 @@ export async function runAuthSetup(dependencies = {}) {
85
150
  export async function runAuthStatus() {
86
151
  const status = await getAuthStatus();
87
152
  writeLine(`OpenAI: ${status.openai.configured ? `configured (${status.openai.source ?? 'unknown'})` : 'not configured'}`);
88
- writeLine(`OpenRouter: ${status.openrouter.configured ? `configured (${status.openrouter.source ?? 'unknown'})` : 'not configured'}`);
153
+ writeLine(`Gemini ${formatOptionalLabel()}: ${status.gemini.configured ? `configured (${status.gemini.source ?? 'unknown'})` : 'not configured'}`);
154
+ writeLine(`OpenRouter ${formatOptionalLabel()}: ${status.openrouter.configured ? `configured (${status.openrouter.source ?? 'unknown'})` : 'not configured'}`);
89
155
  }
90
156
  export async function runAuthLogout(target, dependencies = {}) {
91
157
  const deps = {
158
+ deleteGeminiApiKey,
92
159
  deleteOpenRouterApiKey,
93
160
  env: process.env,
94
161
  isInteractive: process.stdin.isTTY === true,
@@ -142,16 +209,3 @@ async function promptLogoutProviderSelection() {
142
209
  choices: AUTH_LOGOUT_PROVIDER_CONFIGS.map(({ name, value }) => ({ name, value })),
143
210
  });
144
211
  }
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
- ? `OpenRouter (${status.openrouter.source ?? 'configured'})`
12
+ ? `OpenRouter optional (${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, getCliReviewRouting, 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,69 @@ 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
+ return failures;
212
+ }
213
+ function formatMissingAuthForJson({ source, missingProviders, }) {
214
+ return {
215
+ schemaVersion: JSON_SCHEMA_VERSION,
216
+ status: 'unverified',
217
+ verified: false,
218
+ unverifiedReason: 'missing_auth',
219
+ reviewSource: source,
220
+ repository: {
221
+ owner: 'unknown',
222
+ repo: 'unknown',
223
+ branch: 'unknown',
224
+ baseBranch: 'unknown',
225
+ },
226
+ review: {
227
+ count: 0,
228
+ findings: [],
229
+ },
230
+ providerFailures: getMissingAuthProviderFailures(missingProviders),
231
+ };
232
+ }
233
+ async function runLocalReviewForCli({ source, apiKeys, onProgress, }) {
191
234
  const reviewModule = await loadSandboxReviewModule();
192
235
  return await reviewModule.runLocalReview({
193
236
  workspaceDir: getRepositoryRoot(),
194
237
  source,
195
- apiKeys: await getApiKeys(),
238
+ apiKeys,
239
+ ...getCliReviewRouting(apiKeys),
196
240
  suppressLogs: true,
197
241
  onProgress,
198
242
  });
199
243
  }
200
244
  export async function runReview({ source, json = false } = { source: 'base' }) {
245
+ const authCheck = await getReviewAuthCheck();
246
+ if (authCheck.missingProviders.length > 0) {
247
+ if (json) {
248
+ writeLine(JSON.stringify(formatMissingAuthForJson({
249
+ source,
250
+ missingProviders: authCheck.missingProviders,
251
+ }), null, 2));
252
+ process.exitCode = 1;
253
+ return;
254
+ }
255
+ throw new Error(formatMissingRequiredCliAuthProviders(authCheck.missingProviders));
256
+ }
201
257
  if (json) {
202
- const result = await runLocalReviewForCli({ source });
258
+ const result = await runLocalReviewForCli({ source, apiKeys: authCheck.apiKeys });
203
259
  const output = formatReviewForJson(result);
204
260
  writeLine(JSON.stringify(output, null, 2));
205
261
  if (output.status === 'unverified') {
@@ -211,6 +267,7 @@ export async function runReview({ source, json = false } = { source: 'base' }) {
211
267
  ? 'Reviewing staged changes...'
212
268
  : 'Reviewing local changes...', async (status) => await runLocalReviewForCli({
213
269
  source,
270
+ apiKeys: authCheck.apiKeys,
214
271
  onProgress: status.enabled
215
272
  ? (event) => status.update(formatReviewProgress(event))
216
273
  : 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,94 @@
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 = [{ provider: 'openai', label: 'OpenAI' }];
4
+ const ALL_REVIEW_FOCUSES = [
5
+ 'general',
6
+ 'quality',
7
+ 'efficiency',
8
+ 'tests',
9
+ 'standards',
10
+ ];
11
+ const OPENROUTER_REVIEW_MODELS = ['deepseek', 'zai'];
12
+ const CLI_GEMINI_REVIEW_MODEL = 'gemini-3.5-flash';
13
+ function createCliGeminiCredential(apiKey) {
14
+ return {
15
+ apiKey,
16
+ modelName: CLI_GEMINI_REVIEW_MODEL,
17
+ };
18
+ }
19
+ function createCliGeminiSummaryModel(credential) {
20
+ if (!credential) {
21
+ return undefined;
22
+ }
23
+ return {
24
+ provider: 'gemini',
25
+ credential,
26
+ modelName: CLI_GEMINI_REVIEW_MODEL,
27
+ };
28
+ }
3
29
  export async function getApiKeys(env = process.env, auth = {
30
+ getStoredGeminiApiKey,
4
31
  getStoredOpenRouterApiKey,
5
32
  hasOpenAICodexAuth,
6
33
  }) {
7
34
  const envApiKeys = getEnvApiKeys(env);
35
+ const storedGeminiApiKey = envApiKeys.gemini
36
+ ? undefined
37
+ : await auth.getStoredGeminiApiKey().catch(() => undefined);
8
38
  const storedOpenRouterApiKey = envApiKeys.openrouter
9
39
  ? undefined
10
40
  : await auth.getStoredOpenRouterApiKey().catch(() => undefined);
11
41
  const openAICodexAuthConfigured = await auth.hasOpenAICodexAuth();
12
42
  const openRouterApiKey = envApiKeys.openrouter || storedOpenRouterApiKey;
13
43
  const apiKeys = {};
44
+ const geminiApiKey = envApiKeys.gemini || storedGeminiApiKey;
45
+ if (geminiApiKey) {
46
+ apiKeys.gemini = createCliGeminiCredential(geminiApiKey);
47
+ }
14
48
  if (openRouterApiKey) {
15
- apiKeys.gemini = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
49
+ apiKeys.deepseek = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
50
+ apiKeys.zai = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
16
51
  }
17
- const openAIKey = openAICodexAuthConfigured ? PI_AUTH_API_KEY : envApiKeys.openai;
18
- if (openAIKey) {
19
- apiKeys.openai = openAIKey;
52
+ if (openAICodexAuthConfigured) {
53
+ apiKeys.openai = PI_AUTH_API_KEY;
20
54
  }
21
55
  return apiKeys;
22
56
  }
57
+ export function getMissingRequiredCliAuthProviders(status) {
58
+ return REQUIRED_CLI_AUTH_PROVIDERS.filter(({ provider }) => !status[provider].configured).map(({ label }) => label);
59
+ }
60
+ export function getMissingRequiredCliReviewAuthProviders(apiKeys) {
61
+ return REQUIRED_CLI_AUTH_PROVIDERS.filter(({ provider }) => {
62
+ switch (provider) {
63
+ case 'openai':
64
+ return !apiKeys.openai;
65
+ }
66
+ return false;
67
+ }).map(({ label }) => label);
68
+ }
69
+ export function formatMissingRequiredCliAuthProviders(missingProviders) {
70
+ return `${missingProviders.join(' and ')} not configured; run \`doistbot auth\``;
71
+ }
72
+ export function getCliReviewRouting(apiKeys) {
73
+ const hasGemini = Boolean(apiKeys.gemini);
74
+ const hasOpenRouter = Boolean(apiKeys.deepseek && apiKeys.zai);
75
+ if (hasGemini && hasOpenRouter) {
76
+ return {
77
+ reviewModelsEnabled: ['openai', ...OPENROUTER_REVIEW_MODELS],
78
+ geminiFallbackFocuses: ALL_REVIEW_FOCUSES,
79
+ };
80
+ }
81
+ const focusModels = [
82
+ 'openai',
83
+ ...(hasGemini ? ['gemini'] : []),
84
+ ...(hasOpenRouter ? OPENROUTER_REVIEW_MODELS : []),
85
+ ];
86
+ return {
87
+ reviewModelsEnabled: focusModels,
88
+ reviewModelsByFocus: Object.fromEntries(ALL_REVIEW_FOCUSES.map((focus) => [focus, focusModels])),
89
+ geminiFallbackFocuses: [],
90
+ ...(hasGemini && !hasOpenRouter
91
+ ? { reviewSummaryModel: createCliGeminiSummaryModel(apiKeys.gemini) }
92
+ : {}),
93
+ };
94
+ }
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.7",
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 ["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 = ['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
+ }