@doist/doistbot-cli 1.0.4 → 1.0.6

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