@doist/doistbot-cli 1.0.4 → 1.0.5

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 (62) hide show
  1. package/dist/actions/auth.js +23 -23
  2. package/dist/actions/doctor.js +3 -5
  3. package/dist/actions/review.js +5 -4
  4. package/dist/auth.js +17 -17
  5. package/dist/config.js +14 -9
  6. package/dist/env.js +1 -2
  7. package/dist/file-store.js +1 -1
  8. package/dist/terminal.js +0 -7
  9. package/package.json +5 -5
  10. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
  11. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -72
  12. package/sandbox/dist/core/bot-logins.js +0 -13
  13. package/sandbox/dist/core/datadog-metrics.js +0 -2
  14. package/sandbox/dist/core/pi.js +16 -13
  15. package/sandbox/dist/providers/config.js +1 -1
  16. package/sandbox/dist/providers/credentials.js +30 -0
  17. package/sandbox/dist/providers/gemini.js +4 -10
  18. package/sandbox/dist/providers/helpers.js +6 -21
  19. package/sandbox/dist/providers/index.js +0 -3
  20. package/sandbox/dist/providers/openai.js +1 -8
  21. package/sandbox/dist/providers/pi-review.js +16 -54
  22. package/sandbox/dist/providers/schemas.js +0 -39
  23. package/sandbox/dist/tasks/chat/chat.js +2 -1
  24. package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
  25. package/sandbox/dist/tasks/issue-summarize/model.js +3 -1
  26. package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
  27. package/sandbox/dist/tasks/issue-triage/context.js +1 -2
  28. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +3 -3
  29. package/sandbox/dist/tasks/persist-logs/dd-proxy.js +2 -2
  30. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
  31. package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
  32. package/sandbox/dist/tasks/review/conversation-context.js +13 -7
  33. package/sandbox/dist/tasks/review/engines/dedupe.js +18 -9
  34. package/sandbox/dist/tasks/review/engines/multi-focus.js +26 -31
  35. package/sandbox/dist/tasks/review/engines/shared.js +11 -59
  36. package/sandbox/dist/tasks/review/local.js +4 -29
  37. package/sandbox/dist/tasks/review/prompt.js +2 -49
  38. package/sandbox/dist/tasks/review/review-summary.js +82 -0
  39. package/sandbox/dist/tasks/review/review.js +23 -57
  40. package/sandbox/dist/tasks/review/runner.js +35 -51
  41. package/sandbox/dist/tasks/review/summary-model.js +11 -2
  42. package/sandbox/dist/tasks/review/thinking-level.js +0 -2
  43. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
  44. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -72
  45. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  46. package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  47. package/sandbox/dist/providers/anthropic.js +0 -30
  48. package/sandbox/dist/tasks/review/author-format.js +0 -14
  49. package/sandbox/dist/tasks/review/author-profile.js +0 -67
  50. package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
  51. package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
  52. package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
  53. package/sandbox/dist/tasks/review/council/summary.js +0 -192
  54. package/sandbox/dist/tasks/review/council/types.js +0 -4
  55. package/sandbox/dist/tasks/review/council/voter.js +0 -384
  56. package/sandbox/dist/tasks/review/engines/council.js +0 -126
  57. package/sandbox/dist/tasks/review/engines/index.js +0 -12
  58. package/sandbox/dist/tasks/review/review-engine.js +0 -5
  59. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
  60. package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
  61. package/sandbox/review-prompt.md +0 -209
  62. package/sandbox/vote-prompt.md +0 -69
@@ -1,4 +1,4 @@
1
- import { deleteGeminiApiKey, getAuthStatus, loginOpenAICodex, logoutOpenAICodex, saveGeminiApiKey, } from '../auth.js';
1
+ import { deleteOpenRouterApiKey, getAuthStatus, loginOpenAICodex, logoutOpenAICodex, 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';
@@ -17,17 +17,17 @@ const AUTH_LOGOUT_PROVIDER_CONFIGS = [
17
17
  value: 'openai',
18
18
  },
19
19
  {
20
- clear: async ({ deleteGeminiApiKey }) => {
21
- const deleted = await deleteGeminiApiKey();
20
+ clear: async ({ deleteOpenRouterApiKey }) => {
21
+ const deleted = await deleteOpenRouterApiKey();
22
22
  return deleted
23
- ? '✓ Gemini stored API key cleared.'
24
- : 'Gemini had no stored API key to clear.';
23
+ ? '✓ OpenRouter stored API key cleared.'
24
+ : 'OpenRouter had no stored API key to clear.';
25
25
  },
26
- envWarning: (env) => getEnvApiKeys(env).gemini
27
- ? 'Gemini is still configured from GEMINI_API_KEY.'
26
+ envWarning: (env) => getEnvApiKeys(env).openrouter
27
+ ? 'OpenRouter is still configured from OPENROUTER_API_KEY.'
28
28
  : undefined,
29
- name: 'Gemini',
30
- value: 'gemini',
29
+ name: 'OpenRouter',
30
+ value: 'openrouter',
31
31
  },
32
32
  ];
33
33
  const AUTH_LOGOUT_PROVIDERS = AUTH_LOGOUT_PROVIDER_CONFIGS.map(({ value }) => value);
@@ -38,7 +38,7 @@ export async function runAuthSetup(dependencies = {}) {
38
38
  isInteractive: process.stdin.isTTY === true,
39
39
  loginOpenAICodex,
40
40
  promptHiddenInput,
41
- saveGeminiApiKey,
41
+ saveOpenRouterApiKey,
42
42
  writeLine,
43
43
  ...dependencies,
44
44
  };
@@ -57,39 +57,39 @@ export async function runAuthSetup(dependencies = {}) {
57
57
  deps.writeLine('✓ OpenAI configured.');
58
58
  }
59
59
  deps.writeLine();
60
- deps.writeLine('2. Gemini API key');
60
+ deps.writeLine('2. OpenRouter API key');
61
61
  deps.writeLine();
62
- if (status.gemini.source === 'env') {
63
- deps.writeLine('✓ Gemini already configured from GEMINI_API_KEY.');
62
+ if (status.openrouter.source === 'env') {
63
+ deps.writeLine('✓ OpenRouter already configured from OPENROUTER_API_KEY.');
64
64
  return;
65
65
  }
66
- if (status.gemini.source === 'keychain') {
67
- deps.writeLine(`✓ Gemini already configured in the ${getSecureStoreLabel()}.`);
66
+ if (status.openrouter.source === 'keychain') {
67
+ deps.writeLine(`✓ OpenRouter already configured in the ${getSecureStoreLabel()}.`);
68
68
  return;
69
69
  }
70
- if (status.gemini.source === 'file') {
71
- deps.writeLine(`✓ Gemini already configured (${FILE_STORE_PATH}).`);
70
+ if (status.openrouter.source === 'file') {
71
+ deps.writeLine(`✓ OpenRouter already configured (${FILE_STORE_PATH}).`);
72
72
  return;
73
73
  }
74
74
  if (!deps.isInteractive) {
75
- throw new Error('Cannot prompt for Gemini API key in non-interactive mode. Set GEMINI_API_KEY instead.');
75
+ throw new Error('Cannot prompt for OpenRouter API key in non-interactive mode. Set OPENROUTER_API_KEY instead.');
76
76
  }
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: '));
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: '));
79
79
  deps.writeLine();
80
80
  if (backend === 'file') {
81
81
  deps.writeLine(`Note: ${getSecureStoreLabel()} unavailable; key stored in ${FILE_STORE_PATH} with 0600 permissions.`);
82
82
  }
83
- deps.writeLine('✓ Gemini configured.');
83
+ deps.writeLine('✓ OpenRouter configured.');
84
84
  }
85
85
  export async function runAuthStatus() {
86
86
  const status = await getAuthStatus();
87
87
  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'}`);
88
+ writeLine(`OpenRouter: ${status.openrouter.configured ? `configured (${status.openrouter.source ?? 'unknown'})` : 'not configured'}`);
89
89
  }
90
90
  export async function runAuthLogout(target, dependencies = {}) {
91
91
  const deps = {
92
- deleteGeminiApiKey,
92
+ deleteOpenRouterApiKey,
93
93
  env: process.env,
94
94
  isInteractive: process.stdin.isTTY === true,
95
95
  logoutOpenAICodex,
@@ -1,5 +1,4 @@
1
1
  import { getAuthStatus } from '../auth.js';
2
- import { getEnvApiKeys } from '../env.js';
3
2
  import { checkGit, checkPiDependency, checkRepository, checkSandboxBuild } from '../runtime.js';
4
3
  import { renderDiagnosticCheck } from '../terminal.js';
5
4
  export function getConfiguredProviders({ status, }) {
@@ -9,7 +8,9 @@ export function getConfiguredProviders({ status, }) {
9
8
  ? 'pi-auth record found; validated during review'
10
9
  : (status.openai.source ?? 'configured')})`
11
10
  : undefined,
12
- status.gemini.configured ? `Gemini (${status.gemini.source ?? 'configured'})` : undefined,
11
+ status.openrouter.configured
12
+ ? `OpenRouter (${status.openrouter.source ?? 'configured'})`
13
+ : undefined,
13
14
  ].filter((provider) => provider !== undefined);
14
15
  }
15
16
  export async function checkAuth({ env = process.env, getStatus = getAuthStatus, } = {}) {
@@ -18,9 +19,6 @@ export async function checkAuth({ env = process.env, getStatus = getAuthStatus,
18
19
  if (configured.length > 0) {
19
20
  return { ok: true, message: `${configured.join(', ')} configured` };
20
21
  }
21
- if (getEnvApiKeys(env).anthropic) {
22
- return { ok: true, message: 'provider auth configured' };
23
- }
24
22
  return { ok: false, message: 'run `doistbot auth`' };
25
23
  }
26
24
  export async function runDoctor() {
@@ -6,7 +6,6 @@ const JSON_SCHEMA_VERSION = 1;
6
6
  const PROVIDER_DISPLAY_NAMES = {
7
7
  gemini: 'Gemini',
8
8
  openai: 'OpenAI Codex',
9
- anthropic: 'Anthropic',
10
9
  };
11
10
  function stripProviderFailurePrefix(error) {
12
11
  return error
@@ -28,13 +27,15 @@ function getReadableProviderFailure(failure) {
28
27
  normalized.includes('quota') ||
29
28
  normalized.includes('rate-limit') ||
30
29
  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.';
30
+ return 'Gemini via OpenRouter is rate-limited or out of quota. Check your OpenRouter credits, provider routing, or wait for quota to reset.';
32
31
  }
33
32
  if (failure.model === 'gemini' &&
34
33
  (normalized.includes('api_key_invalid') ||
35
34
  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.';
35
+ normalized.includes('invalid_argument') ||
36
+ 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.';
38
39
  }
39
40
  const firstLine = error
40
41
  .split(/\r?\n/)
package/dist/auth.js CHANGED
@@ -5,9 +5,9 @@ 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';
9
- export function createGeminiKeyStore() {
10
- return createCompositeStore(createKeyringStore(GEMINI_ACCOUNT_NAME), createFileStore(GEMINI_ACCOUNT_NAME));
8
+ const OPENROUTER_ACCOUNT_NAME = 'openrouter-api-key';
9
+ export function createOpenRouterKeyStore() {
10
+ return createCompositeStore(createKeyringStore(OPENROUTER_ACCOUNT_NAME), createFileStore(OPENROUTER_ACCOUNT_NAME));
11
11
  }
12
12
  export const OPENAI_CODEX_PROVIDER_ID = 'openai-codex';
13
13
  export const PI_AUTH_API_KEY = `pi-auth://${OPENAI_CODEX_PROVIDER_ID}`;
@@ -36,37 +36,37 @@ export async function logoutOpenAICodex(storage) {
36
36
  const resolvedStorage = storage ?? (await createPiAuthStorage());
37
37
  resolvedStorage.logout(OPENAI_CODEX_PROVIDER_ID);
38
38
  }
39
- export async function readStoredGeminiApiKey(store = createGeminiKeyStore()) {
39
+ export async function readStoredOpenRouterApiKey(store = createOpenRouterKeyStore()) {
40
40
  const result = await store.getSecret();
41
41
  const trimmed = result.value?.trim();
42
42
  return trimmed ? { value: trimmed, backend: result.backend } : { value: undefined };
43
43
  }
44
- export async function getStoredGeminiApiKey(store = createGeminiKeyStore()) {
45
- return (await readStoredGeminiApiKey(store)).value;
44
+ export async function getStoredOpenRouterApiKey(store = createOpenRouterKeyStore()) {
45
+ return (await readStoredOpenRouterApiKey(store)).value;
46
46
  }
47
- export async function saveGeminiApiKey(apiKey, store = createGeminiKeyStore()) {
47
+ export async function saveOpenRouterApiKey(apiKey, store = createOpenRouterKeyStore()) {
48
48
  const normalized = apiKey.trim();
49
49
  if (!normalized) {
50
- throw new Error('Gemini API key cannot be empty');
50
+ throw new Error('OpenRouter API key cannot be empty');
51
51
  }
52
52
  try {
53
53
  return await store.setSecret(normalized);
54
54
  }
55
55
  catch (error) {
56
56
  if (error instanceof SecureStoreUnavailableError) {
57
- throw new SecureStoreUnavailableError(`${error.message} Set GEMINI_API_KEY in your environment instead.`);
57
+ throw new SecureStoreUnavailableError(`${error.message} Set OPENROUTER_API_KEY in your environment instead.`);
58
58
  }
59
59
  throw error;
60
60
  }
61
61
  }
62
- export async function deleteGeminiApiKey(store = createGeminiKeyStore()) {
62
+ export async function deleteOpenRouterApiKey(store = createOpenRouterKeyStore()) {
63
63
  return await store.deleteSecret();
64
64
  }
65
- export async function getAuthStatus({ env = process.env, storage, geminiStore = createGeminiKeyStore(), } = {}) {
65
+ export async function getAuthStatus({ env = process.env, storage, openRouterStore = createOpenRouterKeyStore(), } = {}) {
66
66
  const envApiKeys = getEnvApiKeys(env);
67
- const geminiStored = envApiKeys.gemini
67
+ const openRouterStored = envApiKeys.openrouter
68
68
  ? { value: undefined }
69
- : await readStoredGeminiApiKey(geminiStore).catch(() => ({ value: undefined }));
69
+ : await readStoredOpenRouterApiKey(openRouterStore).catch(() => ({ value: undefined }));
70
70
  const openAICodexAuthConfigured = await hasOpenAICodexAuth(storage);
71
71
  return {
72
72
  openai: openAICodexAuthConfigured
@@ -75,12 +75,12 @@ export async function getAuthStatus({ env = process.env, storage, geminiStore =
75
75
  configured: envApiKeys.openai !== undefined,
76
76
  source: envApiKeys.openai !== undefined ? 'env' : undefined,
77
77
  },
78
- gemini: envApiKeys.gemini
78
+ openrouter: envApiKeys.openrouter
79
79
  ? { configured: true, source: 'env' }
80
80
  : {
81
- configured: geminiStored.value !== undefined,
82
- source: geminiStored.value !== undefined
83
- ? geminiStored.backend === 'file'
81
+ configured: openRouterStored.value !== undefined,
82
+ source: openRouterStored.value !== undefined
83
+ ? openRouterStored.backend === 'file'
84
84
  ? 'file'
85
85
  : 'keychain'
86
86
  : undefined,
package/dist/config.js CHANGED
@@ -1,17 +1,22 @@
1
- import { getStoredGeminiApiKey, hasOpenAICodexAuth, PI_AUTH_API_KEY } from './auth.js';
1
+ import { getStoredOpenRouterApiKey, hasOpenAICodexAuth, PI_AUTH_API_KEY } from './auth.js';
2
2
  import { getEnvApiKeys } from './env.js';
3
3
  export async function getApiKeys(env = process.env, auth = {
4
- getStoredGeminiApiKey,
4
+ getStoredOpenRouterApiKey,
5
5
  hasOpenAICodexAuth,
6
6
  }) {
7
7
  const envApiKeys = getEnvApiKeys(env);
8
- const storedGeminiApiKey = envApiKeys.gemini
8
+ const storedOpenRouterApiKey = envApiKeys.openrouter
9
9
  ? undefined
10
- : await auth.getStoredGeminiApiKey().catch(() => undefined);
10
+ : await auth.getStoredOpenRouterApiKey().catch(() => undefined);
11
11
  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
- };
12
+ const openRouterApiKey = envApiKeys.openrouter || storedOpenRouterApiKey;
13
+ const apiKeys = {};
14
+ if (openRouterApiKey) {
15
+ apiKeys.gemini = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
16
+ }
17
+ const openAIKey = openAICodexAuthConfigured ? PI_AUTH_API_KEY : envApiKeys.openai;
18
+ if (openAIKey) {
19
+ apiKeys.openai = openAIKey;
20
+ }
21
+ return apiKeys;
17
22
  }
package/dist/env.js CHANGED
@@ -3,8 +3,7 @@ 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
- gemini: getEnvValue(env.GEMINI_API_KEY),
8
6
  openai: getEnvValue(env.OPENAI_KEY) || getEnvValue(env.OPENAI_API_KEY),
7
+ openrouter: getEnvValue(env.OPENROUTER_API_KEY),
9
8
  };
10
9
  }
@@ -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/terminal.js CHANGED
@@ -116,7 +116,6 @@ export async function withSpinner(message, task, terminalConfig = getDefaultTerm
116
116
  const PROVIDER_DISPLAY_NAMES = {
117
117
  gemini: 'Gemini',
118
118
  openai: 'OpenAI',
119
- anthropic: 'Anthropic',
120
119
  };
121
120
  function pluralizeChangedFiles(fileCount) {
122
121
  return `${fileCount} changed file${fileCount === 1 ? '' : 's'}`;
@@ -131,12 +130,6 @@ export function formatReviewProgress(event) {
131
130
  return `Prepared ${pluralizeChangedFiles(event.fileCount)} for review`;
132
131
  case 'formatting_results':
133
132
  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
133
  case 'agent_start':
141
134
  return `${PROVIDER_DISPLAY_NAMES[event.provider]} started reviewing the diff`;
142
135
  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.5",
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",
43
+ "@inquirer/checkbox": "5.2.1",
44
+ "@inquirer/password": "5.1.1",
45
45
  "@mariozechner/pi-ai": "0.73.1",
46
46
  "@mariozechner/pi-coding-agent": "0.73.1",
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.8"
57
57
  }
58
58
  }
@@ -1,9 +1,11 @@
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 = {
4
+ export declare const DEFAULT_REVIEW_MODELS: readonly ["gemini", "openai"];
5
+ export declare const REQUIRED_REVIEW_MODELS: readonly ["gemini", "openai"];
6
+ type ParseReviewModelsEnabledOptions = {
6
7
  defaultModels?: readonly string[];
8
+ requiredModels?: readonly string[];
7
9
  };
8
10
  export type RepoConfig = {
9
11
  review?: {
@@ -11,11 +13,6 @@ export type RepoConfig = {
11
13
  skip_drafts?: boolean;
12
14
  skip_title_pattern?: string;
13
15
  skip_branch_pattern?: string;
14
- prompt_file?: string;
15
- engine?: 'council' | 'multi_focus';
16
- council?: {
17
- enabled: boolean;
18
- };
19
16
  /**
20
17
  * @deprecated Ignored as of #274. Doistbot now posts a GitHub Check
21
18
  * Run for every `/review` (always-on; no opt-in) when `HEAD_SHA` is
@@ -53,17 +50,6 @@ export declare function getLocalRepoConfig({ workspaceDir, logger, }: {
53
50
  workspaceDir: string;
54
51
  logger: Logger;
55
52
  }): 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';
53
+ export declare function parseReviewModelsEnabled(raw: string | undefined, { defaultModels, requiredModels, }?: ParseReviewModelsEnabledOptions): string[];
54
+ export declare function getDefaultReviewModels(): string[];
55
+ export {};
@@ -2,19 +2,11 @@ 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_COUNCIL_MODELS = ['gemini', 'openai'];
5
+ export const DEFAULT_REVIEW_MODELS = ['gemini', 'openai'];
6
+ export const REQUIRED_REVIEW_MODELS = DEFAULT_REVIEW_MODELS;
6
7
  function parseRepoConfigContent(content) {
7
8
  return yamlLoad(content);
8
9
  }
9
- function resolveWorkspacePath(workspaceDir, repoPath) {
10
- const root = path.resolve(workspaceDir);
11
- const resolved = path.resolve(root, repoPath);
12
- const relative = path.relative(root, resolved);
13
- if (relative.startsWith('..') || path.isAbsolute(relative)) {
14
- return null;
15
- }
16
- return resolved;
17
- }
18
10
  async function fetchFileContent({ octokit, owner, repo, path, ref, logger, }) {
19
11
  const params = {
20
12
  owner,
@@ -99,67 +91,11 @@ export function getLocalRepoConfig({ workspaceDir, logger, }) {
99
91
  return null;
100
92
  }
101
93
  }
102
- /**
103
- * Fetches a custom prompt file from the repository.
104
- * Returns null if the file doesn't exist.
105
- */
106
- export async function getCustomPrompt({ octokit, owner, repo, path, ref, logger, }) {
107
- let content;
108
- try {
109
- content = await fetchFileContent({ octokit, owner, repo, path, ref, logger });
110
- }
111
- catch (error) {
112
- logger.warn('Error fetching custom prompt', {
113
- component: 'repo-config',
114
- repository: `${owner}/${repo}`,
115
- path,
116
- error: error instanceof Error ? error.message : String(error),
117
- });
118
- return null;
119
- }
120
- if (content !== null) {
121
- logger.info('Loaded custom prompt', {
122
- component: 'repo-config',
123
- repository: `${owner}/${repo}`,
124
- path,
125
- });
126
- }
127
- return content;
128
- }
129
- export function getLocalCustomPrompt({ workspaceDir, path: promptPath, logger, }) {
130
- const resolvedPath = resolveWorkspacePath(workspaceDir, promptPath);
131
- if (resolvedPath === null) {
132
- logger.warn('Custom prompt path escapes workspace', {
133
- component: 'repo-config',
134
- path: promptPath,
135
- });
136
- return null;
137
- }
138
- if (!fs.existsSync(resolvedPath)) {
139
- return null;
140
- }
141
- try {
142
- const content = fs.readFileSync(resolvedPath, 'utf8');
143
- logger.info('Loaded local custom prompt', {
144
- component: 'repo-config',
145
- path: promptPath,
146
- });
147
- return content;
148
- }
149
- catch (error) {
150
- logger.warn('Failed to load local custom prompt', {
151
- component: 'repo-config',
152
- path: promptPath,
153
- error: error instanceof Error ? error.message : String(error),
154
- });
155
- return null;
156
- }
157
- }
158
- export function parseCouncilModelsEnabled(raw, { defaultModels = DEFAULT_COUNCIL_MODELS } = {}) {
94
+ export function parseReviewModelsEnabled(raw, { defaultModels = DEFAULT_REVIEW_MODELS, requiredModels = REQUIRED_REVIEW_MODELS, } = {}) {
159
95
  if (!raw?.trim()) {
160
96
  return [...defaultModels];
161
97
  }
162
- const allowedModels = new Set(DEFAULT_COUNCIL_MODELS);
98
+ const allowedModels = new Set(DEFAULT_REVIEW_MODELS);
163
99
  const models = [];
164
100
  for (const token of raw.split(',')) {
165
101
  const model = token.trim().toLowerCase();
@@ -170,9 +106,9 @@ export function parseCouncilModelsEnabled(raw, { defaultModels = DEFAULT_COUNCIL
170
106
  models.push(model);
171
107
  }
172
108
  }
173
- return models.length > 0 ? models : [...defaultModels];
109
+ const hasRequiredModels = requiredModels.every((model) => models.includes(model));
110
+ return models.length > 0 && hasRequiredModels ? models : [...defaultModels];
174
111
  }
175
- export function getDefaultCouncilModels() {
176
- return [...DEFAULT_COUNCIL_MODELS];
112
+ export function getDefaultReviewModels() {
113
+ return [...DEFAULT_REVIEW_MODELS];
177
114
  }
178
- export { DEFAULT_REVIEW_ENGINE, parseReviewEngine, resolveReviewEngine } from './review-engine.js';
@@ -6,22 +6,9 @@ export const KNOWN_DOISTBOT_LOGINS = new Set([
6
6
  'doistbot-app',
7
7
  'doistbot-app[bot]',
8
8
  ]);
9
- export const KNOWN_AUTOMATION_LOGINS = new Set([
10
- ...KNOWN_DOISTBOT_LOGINS,
11
- 'dependabot',
12
- 'dependabot[bot]',
13
- 'renovate',
14
- 'renovate[bot]',
15
- ]);
16
9
  export function isKnownDoistbotLogin(login) {
17
10
  if (!login) {
18
11
  return false;
19
12
  }
20
13
  return KNOWN_DOISTBOT_LOGINS.has(login.toLowerCase());
21
14
  }
22
- export function isKnownAutomationLogin(login) {
23
- if (!login) {
24
- return false;
25
- }
26
- return KNOWN_AUTOMATION_LOGINS.has(login.toLowerCase());
27
- }
@@ -10,8 +10,6 @@ function toTaskMetricBaseModel(model) {
10
10
  return 'google';
11
11
  case 'openai':
12
12
  return 'openai';
13
- case 'anthropic':
14
- return 'anthropic';
15
13
  case 'none':
16
14
  return 'none';
17
15
  }
@@ -4,26 +4,26 @@ import { tmpdir } from 'os';
4
4
  import path from 'path';
5
5
  import { fileURLToPath } from 'url';
6
6
  const PI_PROVIDER_BY_DOISTBOT_PROVIDER = {
7
- gemini: 'google',
7
+ gemini: 'openrouter',
8
8
  openai: 'openai',
9
- anthropic: 'anthropic',
10
- };
11
- const PI_API_KEY_ENV_VAR_BY_PROVIDER = {
12
- gemini: 'GEMINI_API_KEY',
13
- openai: 'OPENAI_API_KEY',
14
- anthropic: 'ANTHROPIC_API_KEY',
15
9
  };
16
10
  export const DEFAULT_PI_MODELS = {
17
11
  gemini: 'gemini-3.1-pro-preview',
18
12
  openai: 'gpt-5.4',
19
- anthropic: 'claude-opus-4-6',
13
+ };
14
+ export const DEFAULT_OPENROUTER_MODELS = {
15
+ gemini: 'google/gemini-3.1-pro-preview',
20
16
  };
21
17
  export const MAX_PI_THINKING_BY_PROVIDER = {
22
18
  gemini: 'high',
23
19
  openai: 'xhigh',
24
- anthropic: 'xhigh',
25
20
  };
26
- const PI_PROVIDER_IDS = new Set(['google', 'openai', 'openai-codex', 'anthropic']);
21
+ const PI_API_KEY_ENV_VAR_BY_PI_PROVIDER = {
22
+ openai: 'OPENAI_API_KEY',
23
+ 'openai-codex': 'OPENAI_API_KEY',
24
+ openrouter: 'OPENROUTER_API_KEY',
25
+ };
26
+ const PI_PROVIDER_IDS = new Set(['openai', 'openai-codex', 'openrouter']);
27
27
  export const PI_AUTH_API_KEY_PREFIX = 'pi-auth://';
28
28
  const DOISTBOT_PI_SYSTEM_PROMPT = 'You are running inside doistbot in a non-interactive environment. Inspect the repository, configuration, documentation, and relevant history when they can improve accuracy. Use available tools proactively to gather evidence, avoid guessing when the workspace can answer the question, and follow the task prompt exactly.';
29
29
  const PI_PROGRESS_HEARTBEAT_MS = 30_000;
@@ -62,7 +62,9 @@ function canExecuteNode(candidate) {
62
62
  }
63
63
  function getPiModelSpec({ provider, piProviderId, modelName, thinkingLevel, }) {
64
64
  const providerId = piProviderId ?? PI_PROVIDER_BY_DOISTBOT_PROVIDER[provider];
65
- const resolvedModelName = modelName ?? DEFAULT_PI_MODELS[provider];
65
+ const resolvedModelName = modelName ??
66
+ (providerId === 'openrouter' ? DEFAULT_OPENROUTER_MODELS[provider] : undefined) ??
67
+ DEFAULT_PI_MODELS[provider];
66
68
  return `${providerId}/${resolvedModelName}:${thinkingLevel}`;
67
69
  }
68
70
  function parsePiAuthProviderId(apiKey) {
@@ -222,9 +224,10 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
222
224
  async invoke({ prompt, modelName, timeoutMs = 5 * 60 * 1000, images, }) {
223
225
  const startedAt = Date.now();
224
226
  const piAuthProviderId = parsePiAuthProviderId(apiKey);
227
+ const resolvedPiProviderId = piAuthProviderId ?? piProviderId ?? PI_PROVIDER_BY_DOISTBOT_PROVIDER[provider];
225
228
  const modelSpec = getPiModelSpec({
226
229
  provider,
227
- piProviderId: piAuthProviderId ?? piProviderId,
230
+ piProviderId: resolvedPiProviderId,
228
231
  modelName,
229
232
  thinkingLevel: thinkingLevel ?? MAX_PI_THINKING_BY_PROVIDER[provider],
230
233
  });
@@ -253,7 +256,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
253
256
  GIT_TERMINAL_PROMPT: '0',
254
257
  ...extraEnv,
255
258
  };
256
- const apiKeyEnvVar = PI_API_KEY_ENV_VAR_BY_PROVIDER[provider];
259
+ const apiKeyEnvVar = PI_API_KEY_ENV_VAR_BY_PI_PROVIDER[resolvedPiProviderId];
257
260
  if (piAuthProviderId) {
258
261
  delete childEnv[apiKeyEnvVar];
259
262
  }
@@ -15,7 +15,7 @@ export function resolveProviderTimeoutMs(explicitTimeoutMs) {
15
15
  explicitTimeoutMs > 0) {
16
16
  return explicitTimeoutMs;
17
17
  }
18
- const sharedTimeout = parseTimeoutMs(process.env.COUNCIL_MODEL_TIMEOUT_MS);
18
+ const sharedTimeout = parseTimeoutMs(process.env.REVIEW_MODEL_TIMEOUT_MS);
19
19
  if (sharedTimeout != null) {
20
20
  return sharedTimeout;
21
21
  }
@@ -0,0 +1,30 @@
1
+ export function resolveReviewProviderCredential(credential) {
2
+ if (typeof credential === 'string') {
3
+ const apiKey = credential.trim();
4
+ return apiKey ? { apiKey } : undefined;
5
+ }
6
+ if (!credential) {
7
+ return undefined;
8
+ }
9
+ const apiKey = credential.apiKey.trim();
10
+ if (!apiKey) {
11
+ return undefined;
12
+ }
13
+ return {
14
+ apiKey,
15
+ ...(credential.piProviderId ? { piProviderId: credential.piProviderId } : {}),
16
+ ...(credential.modelName ? { modelName: credential.modelName } : {}),
17
+ };
18
+ }
19
+ export function hasReviewProviderCredential(credential) {
20
+ return resolveReviewProviderCredential(credential) !== undefined;
21
+ }
22
+ export function getReviewProviderCredential(apiKeys, provider) {
23
+ return resolveReviewProviderCredential(apiKeys[provider]);
24
+ }
25
+ export function createOpenRouterCredential(apiKey) {
26
+ return {
27
+ apiKey,
28
+ piProviderId: 'openrouter',
29
+ };
30
+ }