@todoist/doistbot-cli 0.0.0-stage → 1.0.16

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 (185) hide show
  1. package/dist/actions/auth.js +225 -0
  2. package/dist/actions/doctor.js +39 -0
  3. package/dist/actions/review-usage.js +25 -0
  4. package/dist/actions/review.js +311 -0
  5. package/dist/actions/setup.js +14 -0
  6. package/dist/auth.js +202 -0
  7. package/dist/config.js +145 -0
  8. package/dist/env.js +10 -0
  9. package/dist/file-store.js +112 -0
  10. package/dist/index.js +164 -0
  11. package/dist/input.js +40 -0
  12. package/dist/runtime.js +105 -0
  13. package/dist/secure-store.js +148 -0
  14. package/dist/terminal.js +185 -0
  15. package/dist/test-utils/secure-store-mocks.js +29 -0
  16. package/dist/update-cache-entrypoint.js +16 -0
  17. package/dist/update-check.js +235 -0
  18. package/package.json +65 -5
  19. package/sandbox/_node_modules/doistbot-logging/dist/index.d.ts +23 -0
  20. package/sandbox/_node_modules/doistbot-logging/dist/index.js +51 -0
  21. package/sandbox/_node_modules/doistbot-logging/package.json +12 -0
  22. package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
  23. package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +197 -0
  24. package/sandbox/_node_modules/doistbot-repo-config/package.json +12 -0
  25. package/sandbox/dist/core/autofix-resources.js +58 -0
  26. package/sandbox/dist/core/autofix-service-query.js +69 -0
  27. package/sandbox/dist/core/autofix-test-policy.js +11 -0
  28. package/sandbox/dist/core/autofix-verification.js +355 -0
  29. package/sandbox/dist/core/bot-logins.js +14 -0
  30. package/sandbox/dist/core/check-run.js +158 -0
  31. package/sandbox/dist/core/datadog-metrics.js +113 -0
  32. package/sandbox/dist/core/environment.js +49 -0
  33. package/sandbox/dist/core/git-auth.js +17 -0
  34. package/sandbox/dist/core/github-history.js +4 -0
  35. package/sandbox/dist/core/github-issues.js +18 -0
  36. package/sandbox/dist/core/label-parsing.js +1 -0
  37. package/sandbox/dist/core/langfuse-observation.js +33 -0
  38. package/sandbox/dist/core/langfuse-session.js +39 -0
  39. package/sandbox/dist/core/logger-span-processor.js +90 -0
  40. package/sandbox/dist/core/logging.js +18 -0
  41. package/sandbox/dist/core/pi-autofix-extension.js +188 -0
  42. package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
  43. package/sandbox/dist/core/pi-progress-logging.js +51 -0
  44. package/sandbox/dist/core/pi-tool-output-filter-extension.js +65 -0
  45. package/sandbox/dist/core/pi.js +870 -0
  46. package/sandbox/dist/core/pii-scrubber.js +31 -0
  47. package/sandbox/dist/core/prompt-template.js +63 -0
  48. package/sandbox/dist/core/reactions.js +36 -0
  49. package/sandbox/dist/core/regexp.js +3 -0
  50. package/sandbox/dist/core/repository-map.js +1 -0
  51. package/sandbox/dist/core/repository-resolver.js +1 -0
  52. package/sandbox/dist/core/retry.js +29 -0
  53. package/sandbox/dist/core/s3-utils.js +42 -0
  54. package/sandbox/dist/core/shared.js +124 -0
  55. package/sandbox/dist/core/snapshot-helpers.js +17 -0
  56. package/sandbox/dist/core/span-test-helpers.js +13 -0
  57. package/sandbox/dist/core/tool-environment.js +83 -0
  58. package/sandbox/dist/core/tool-output.js +18 -0
  59. package/sandbox/dist/core/tracing-setup.js +92 -0
  60. package/sandbox/dist/core/tracing.js +81 -0
  61. package/sandbox/dist/main.js +51 -0
  62. package/sandbox/dist/providers/config.js +23 -0
  63. package/sandbox/dist/providers/credentials.js +40 -0
  64. package/sandbox/dist/providers/gemini.js +28 -0
  65. package/sandbox/dist/providers/helpers.js +253 -0
  66. package/sandbox/dist/providers/index.js +27 -0
  67. package/sandbox/dist/providers/openai.js +23 -0
  68. package/sandbox/dist/providers/openrouter-pi.js +25 -0
  69. package/sandbox/dist/providers/pi-review.js +301 -0
  70. package/sandbox/dist/providers/process.js +34 -0
  71. package/sandbox/dist/providers/schemas.js +61 -0
  72. package/sandbox/dist/providers/types.js +4 -0
  73. package/sandbox/dist/tasks/chat/chat.js +440 -0
  74. package/sandbox/dist/tasks/chat/prompt.js +20 -0
  75. package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +304 -0
  76. package/sandbox/dist/tasks/issue-summarize/context.js +21 -0
  77. package/sandbox/dist/tasks/issue-summarize/media.js +335 -0
  78. package/sandbox/dist/tasks/issue-summarize/model.js +230 -0
  79. package/sandbox/dist/tasks/issue-summarize/output.js +45 -0
  80. package/sandbox/dist/tasks/issue-summarize/prompt.js +11 -0
  81. package/sandbox/dist/tasks/issue-summarize/summarize.js +238 -0
  82. package/sandbox/dist/tasks/issue-summarize/thread.js +123 -0
  83. package/sandbox/dist/tasks/issue-summarize/types.js +1 -0
  84. package/sandbox/dist/tasks/issue-triage/assignee-review.js +114 -0
  85. package/sandbox/dist/tasks/issue-triage/autofix-capabilities.js +22 -0
  86. package/sandbox/dist/tasks/issue-triage/autofix-checkpoint.js +122 -0
  87. package/sandbox/dist/tasks/issue-triage/autofix-policy.js +36 -0
  88. package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
  89. package/sandbox/dist/tasks/issue-triage/context.js +57 -0
  90. package/sandbox/dist/tasks/issue-triage/diff-validator.js +102 -0
  91. package/sandbox/dist/tasks/issue-triage/fix-attempt.js +617 -0
  92. package/sandbox/dist/tasks/issue-triage/fix-context.js +5 -0
  93. package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +263 -0
  94. package/sandbox/dist/tasks/issue-triage/fix-failure-reasons.js +2 -0
  95. package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +526 -0
  96. package/sandbox/dist/tasks/issue-triage/fix-loop/effects.types.js +1 -0
  97. package/sandbox/dist/tasks/issue-triage/fix-loop/fake-effects.js +65 -0
  98. package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +108 -0
  99. package/sandbox/dist/tasks/issue-triage/fix-loop/recovery.js +512 -0
  100. package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +233 -0
  101. package/sandbox/dist/tasks/issue-triage/fix-loop/state-machine.js +171 -0
  102. package/sandbox/dist/tasks/issue-triage/github-request.js +2 -0
  103. package/sandbox/dist/tasks/issue-triage/hero-group-map.js +206 -0
  104. package/sandbox/dist/tasks/issue-triage/markers.js +6 -0
  105. package/sandbox/dist/tasks/issue-triage/model.js +124 -0
  106. package/sandbox/dist/tasks/issue-triage/output.js +164 -0
  107. package/sandbox/dist/tasks/issue-triage/pr-creator.js +585 -0
  108. package/sandbox/dist/tasks/issue-triage/pr-readiness.js +25 -0
  109. package/sandbox/dist/tasks/issue-triage/prompt.js +63 -0
  110. package/sandbox/dist/tasks/issue-triage/repo-config.js +47 -0
  111. package/sandbox/dist/tasks/issue-triage/repo-prompts.js +77 -0
  112. package/sandbox/dist/tasks/issue-triage/report-fixture.js +53 -0
  113. package/sandbox/dist/tasks/issue-triage/report-history.js +113 -0
  114. package/sandbox/dist/tasks/issue-triage/report-schema.js +63 -0
  115. package/sandbox/dist/tasks/issue-triage/report.js +111 -0
  116. package/sandbox/dist/tasks/issue-triage/resolution.js +30 -0
  117. package/sandbox/dist/tasks/issue-triage/routing.js +169 -0
  118. package/sandbox/dist/tasks/issue-triage/sandbox-environment.js +59 -0
  119. package/sandbox/dist/tasks/issue-triage/shared.js +86 -0
  120. package/sandbox/dist/tasks/issue-triage/side-effects.js +102 -0
  121. package/sandbox/dist/tasks/issue-triage/triage.js +442 -0
  122. package/sandbox/dist/tasks/issue-triage/types.js +1 -0
  123. package/sandbox/dist/tasks/persist-logs/context.js +19 -0
  124. package/sandbox/dist/tasks/persist-logs/dd-proxy.js +116 -0
  125. package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +156 -0
  126. package/sandbox/dist/tasks/persist-logs/persist-logs.js +491 -0
  127. package/sandbox/dist/tasks/refresh-s3-urls/refresh-s3-urls.js +214 -0
  128. package/sandbox/dist/tasks/review/check-run.js +326 -0
  129. package/sandbox/dist/tasks/review/comment-mapper.js +46 -0
  130. package/sandbox/dist/tasks/review/conversation-context.js +477 -0
  131. package/sandbox/dist/tasks/review/diff.js +95 -0
  132. package/sandbox/dist/tasks/review/engines/dedupe.js +291 -0
  133. package/sandbox/dist/tasks/review/engines/multi-focus.js +419 -0
  134. package/sandbox/dist/tasks/review/engines/shared.js +87 -0
  135. package/sandbox/dist/tasks/review/engines/types.js +1 -0
  136. package/sandbox/dist/tasks/review/finding-output.js +18 -0
  137. package/sandbox/dist/tasks/review/format.js +17 -0
  138. package/sandbox/dist/tasks/review/github-comment-format.js +94 -0
  139. package/sandbox/dist/tasks/review/local-git.js +374 -0
  140. package/sandbox/dist/tasks/review/local.js +118 -0
  141. package/sandbox/dist/tasks/review/multi-focus-prompt.js +59 -0
  142. package/sandbox/dist/tasks/review/prompt.js +81 -0
  143. package/sandbox/dist/tasks/review/review-feedback.js +48 -0
  144. package/sandbox/dist/tasks/review/review-mode-github.js +58 -0
  145. package/sandbox/dist/tasks/review/review-mode-policy.js +232 -0
  146. package/sandbox/dist/tasks/review/review-mode-types.js +1 -0
  147. package/sandbox/dist/tasks/review/review-mode.js +296 -0
  148. package/sandbox/dist/tasks/review/review-summary.js +123 -0
  149. package/sandbox/dist/tasks/review/review.js +391 -0
  150. package/sandbox/dist/tasks/review/runner.js +255 -0
  151. package/sandbox/dist/tasks/review/severity.js +5 -0
  152. package/sandbox/dist/tasks/review/summary-model.js +271 -0
  153. package/sandbox/dist/tasks/review/thinking-level.js +14 -0
  154. package/sandbox/dist/tasks/review/translation-review-policy.js +37 -0
  155. package/sandbox/dist/tasks/review/types.js +1 -0
  156. package/sandbox/dist/tasks/review-slice-plan/context.js +66 -0
  157. package/sandbox/dist/tasks/review-slice-plan/plan.js +210 -0
  158. package/sandbox/dist/tasks/review-slice-plan/prompt.js +59 -0
  159. package/sandbox/dist/tasks/review-slice-plan/review-slice-plan.js +367 -0
  160. package/sandbox/dist/tasks/sentry-triage/store.js +133 -0
  161. package/sandbox/dist/tasks/sentry-triage/triage.js +217 -0
  162. package/sandbox/node_modules/doistbot-logging/dist/index.d.ts +23 -0
  163. package/sandbox/node_modules/doistbot-logging/dist/index.js +51 -0
  164. package/sandbox/node_modules/doistbot-logging/package.json +12 -0
  165. package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
  166. package/sandbox/node_modules/doistbot-repo-config/dist/index.js +197 -0
  167. package/sandbox/node_modules/doistbot-repo-config/package.json +12 -0
  168. package/sandbox/src/review/prompts/ai-internal-tools.md +254 -0
  169. package/sandbox/src/review/prompts/backend-repository-standards.md +102 -0
  170. package/sandbox/src/review/prompts/observability-standard.md +169 -0
  171. package/sandbox/src/review/prompts/outline-map.json +26 -0
  172. package/sandbox/src/review/prompts/secrets-management.md +286 -0
  173. package/sandbox/src/review/prompts/service-production-readiness.md +122 -0
  174. package/sandbox/src/review/prompts/workload-placement.md +345 -0
  175. package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +16 -0
  176. package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +32 -0
  177. package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +20 -0
  178. package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +13 -0
  179. package/sandbox/src/tasks/review/prompts/review-focus-prompts/security.md +18 -0
  180. package/sandbox/src/tasks/review/prompts/review-focus-prompts/standards.md +21 -0
  181. package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +19 -0
  182. package/sandbox/src/tasks/review/prompts/review-mode.md +40 -0
  183. package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +142 -0
  184. package/scripts/restore-runtime-assets.mjs +21 -0
  185. package/README.md +0 -3
@@ -0,0 +1,225 @@
1
+ import chalk from 'chalk';
2
+ import { createPiAuthRuntime, deleteGeminiApiKey, deleteOpenRouterApiKey, getAuthStatus, loginOpenAICodex, logoutOpenAICodex, saveGeminiApiKey, saveOpenRouterApiKey, } from '../auth.js';
3
+ import { getEnvApiKeys } from '../env.js';
4
+ import { FILE_STORE_PATH } from '../file-store.js';
5
+ import { promptHiddenInput, promptMultiSelect } from '../input.js';
6
+ import { getSecureStoreLabel } from '../secure-store.js';
7
+ import { writeLine } from '../terminal.js';
8
+ const AUTH_LOGOUT_PROVIDER_CONFIGS = [
9
+ {
10
+ clear: async ({ logoutOpenAICodex }) => {
11
+ await logoutOpenAICodex();
12
+ return '✓ OpenAI stored auth cleared.';
13
+ },
14
+ envWarning: () => undefined,
15
+ name: 'OpenAI',
16
+ value: 'openai',
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
+ },
31
+ {
32
+ clear: async ({ deleteOpenRouterApiKey }) => {
33
+ const deleted = await deleteOpenRouterApiKey();
34
+ return deleted
35
+ ? '✓ OpenRouter stored API key cleared.'
36
+ : 'OpenRouter had no stored API key to clear.';
37
+ },
38
+ envWarning: (env) => getEnvApiKeys(env).openrouter
39
+ ? 'OpenRouter is still configured from OPENROUTER_API_KEY.'
40
+ : undefined,
41
+ name: 'OpenRouter',
42
+ value: 'openrouter',
43
+ },
44
+ ];
45
+ const AUTH_LOGOUT_PROVIDERS = AUTH_LOGOUT_PROVIDER_CONFIGS.map(({ value }) => value);
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
+ }
59
+ export async function runAuthSetup(dependencies = {}) {
60
+ const deps = {
61
+ createPiAuthRuntime,
62
+ getAuthStatus,
63
+ isInteractive: process.stdin.isTTY === true,
64
+ loginOpenAICodex,
65
+ promptHiddenInput,
66
+ saveGeminiApiKey,
67
+ saveOpenRouterApiKey,
68
+ writeLine,
69
+ ...dependencies,
70
+ };
71
+ const runtime = await deps.createPiAuthRuntime();
72
+ const status = await deps.getAuthStatus({ runtime });
73
+ deps.writeLine('1. OpenAI Codex');
74
+ deps.writeLine();
75
+ if (status.openai.configured && !status.openai.refreshFailed) {
76
+ deps.writeLine('✓ OpenAI already configured (pi-auth).');
77
+ }
78
+ else {
79
+ if (!deps.isInteractive) {
80
+ throw new Error('Cannot configure OpenAI Codex auth in non-interactive mode.');
81
+ }
82
+ if (status.openai.refreshFailed) {
83
+ deps.writeLine('OpenAI session could not be refreshed (expired, revoked, or offline). Reconnecting.');
84
+ }
85
+ await deps.loginOpenAICodex(runtime);
86
+ deps.writeLine();
87
+ deps.writeLine('✓ OpenAI configured.');
88
+ }
89
+ deps.writeLine();
90
+ deps.writeLine(`2. Gemini API key ${formatOptionalLabel()}`);
91
+ deps.writeLine();
92
+ if (status.gemini.source === 'env') {
93
+ deps.writeLine('✓ Gemini already configured from GEMINI_API_KEY.');
94
+ }
95
+ else if (status.gemini.source === 'keychain') {
96
+ deps.writeLine(`✓ Gemini already configured in the ${getSecureStoreLabel()}.`);
97
+ }
98
+ else if (status.gemini.source === 'file') {
99
+ deps.writeLine(`✓ Gemini already configured (${FILE_STORE_PATH}).`);
100
+ }
101
+ else {
102
+ if (!deps.isInteractive) {
103
+ deps.writeLine('Gemini not configured. Skipping optional Gemini provider.');
104
+ }
105
+ else {
106
+ deps.writeLine(`Doistbot can use Gemini via Google Vertex. Paste a Gemini API key, or ${formatSkipInstruction()}.`);
107
+ const geminiApiKey = (await deps.promptHiddenInput(API_KEY_PROMPT)).trim();
108
+ if (!geminiApiKey) {
109
+ deps.writeLine();
110
+ deps.writeLine('Gemini skipped. You can add it later with `doistbot auth`.');
111
+ }
112
+ else {
113
+ const backend = await deps.saveGeminiApiKey(geminiApiKey);
114
+ deps.writeLine();
115
+ if (backend === 'file') {
116
+ deps.writeLine(`Note: ${getSecureStoreLabel()} unavailable; key stored in ${FILE_STORE_PATH} with 0600 permissions.`);
117
+ }
118
+ deps.writeLine('✓ Gemini configured.');
119
+ }
120
+ }
121
+ }
122
+ deps.writeLine();
123
+ deps.writeLine(`3. OpenRouter API key ${formatOptionalLabel()}`);
124
+ deps.writeLine();
125
+ if (status.openrouter.source === 'env') {
126
+ deps.writeLine('✓ OpenRouter already configured from OPENROUTER_API_KEY.');
127
+ return;
128
+ }
129
+ if (status.openrouter.source === 'keychain') {
130
+ deps.writeLine(`✓ OpenRouter already configured in the ${getSecureStoreLabel()}.`);
131
+ return;
132
+ }
133
+ if (status.openrouter.source === 'file') {
134
+ deps.writeLine(`✓ OpenRouter already configured (${FILE_STORE_PATH}).`);
135
+ return;
136
+ }
137
+ if (!deps.isInteractive) {
138
+ deps.writeLine('OpenRouter not configured. Skipping optional OpenRouter provider.');
139
+ return;
140
+ }
141
+ deps.writeLine(`Doistbot can use OpenRouter for DeepSeek, Grok, and other open models. Paste an OpenRouter API key, or ${formatSkipInstruction()}.`);
142
+ const openRouterApiKey = (await deps.promptHiddenInput(API_KEY_PROMPT)).trim();
143
+ if (!openRouterApiKey) {
144
+ deps.writeLine();
145
+ deps.writeLine('OpenRouter skipped. You can add it later with `doistbot auth`.');
146
+ return;
147
+ }
148
+ const backend = await deps.saveOpenRouterApiKey(openRouterApiKey);
149
+ deps.writeLine();
150
+ if (backend === 'file') {
151
+ deps.writeLine(`Note: ${getSecureStoreLabel()} unavailable; key stored in ${FILE_STORE_PATH} with 0600 permissions.`);
152
+ }
153
+ deps.writeLine('✓ OpenRouter configured.');
154
+ }
155
+ export async function runAuthStatus() {
156
+ const status = await getAuthStatus();
157
+ writeLine(`OpenAI: ${formatOpenAIStatus(status.openai)}`);
158
+ writeLine(`Gemini ${formatOptionalLabel()}: ${status.gemini.configured ? `configured (${status.gemini.source ?? 'unknown'})` : 'not configured'}`);
159
+ writeLine(`OpenRouter ${formatOptionalLabel()}: ${status.openrouter.configured ? `configured (${status.openrouter.source ?? 'unknown'})` : 'not configured'}`);
160
+ }
161
+ function formatOpenAIStatus(status) {
162
+ if (!status.configured) {
163
+ return 'not configured';
164
+ }
165
+ const base = `configured (${status.source ?? 'unknown'})`;
166
+ return status.refreshFailed
167
+ ? `${base} — session could not be refreshed; check your connection or run \`doistbot auth login\``
168
+ : base;
169
+ }
170
+ export async function runAuthLogout(target, dependencies = {}) {
171
+ const deps = {
172
+ deleteGeminiApiKey,
173
+ deleteOpenRouterApiKey,
174
+ env: process.env,
175
+ isInteractive: process.stdin.isTTY === true,
176
+ logoutOpenAICodex,
177
+ promptProviderSelection: promptLogoutProviderSelection,
178
+ writeLine,
179
+ ...dependencies,
180
+ };
181
+ const providers = await resolveLogoutProviders(target, deps);
182
+ const providerConfigs = providers.map(getAuthLogoutProviderConfig);
183
+ const clearMessages = await Promise.all(providerConfigs.map((config) => config.clear(deps)));
184
+ for (const message of clearMessages) {
185
+ deps.writeLine(message);
186
+ }
187
+ for (const config of providerConfigs) {
188
+ const warning = config.envWarning(deps.env);
189
+ if (warning) {
190
+ deps.writeLine(warning);
191
+ }
192
+ }
193
+ }
194
+ async function resolveLogoutProviders(target, dependencies) {
195
+ if (target === undefined) {
196
+ if (!dependencies.isInteractive) {
197
+ throw new Error(`Provider required in non-interactive mode. Use \`doistbot auth logout ${AUTH_LOGOUT_PROVIDER_USAGE}\`.`);
198
+ }
199
+ return await dependencies.promptProviderSelection();
200
+ }
201
+ const normalized = target.toLowerCase();
202
+ if (normalized === 'all') {
203
+ return [...AUTH_LOGOUT_PROVIDERS];
204
+ }
205
+ if (isAuthLogoutProvider(normalized)) {
206
+ return [normalized];
207
+ }
208
+ throw new Error(`Usage: doistbot auth logout [${AUTH_LOGOUT_PROVIDER_USAGE}]`);
209
+ }
210
+ function isAuthLogoutProvider(value) {
211
+ return AUTH_LOGOUT_PROVIDERS.includes(value);
212
+ }
213
+ function getAuthLogoutProviderConfig(provider) {
214
+ const config = AUTH_LOGOUT_PROVIDER_CONFIGS.find(({ value }) => value === provider);
215
+ if (!config) {
216
+ throw new Error(`Unsupported auth logout provider: ${provider}`);
217
+ }
218
+ return config;
219
+ }
220
+ async function promptLogoutProviderSelection() {
221
+ return await promptMultiSelect({
222
+ message: 'Select providers to log out:',
223
+ choices: AUTH_LOGOUT_PROVIDER_CONFIGS.map(({ name, value }) => ({ name, value })),
224
+ });
225
+ }
@@ -0,0 +1,39 @@
1
+ import { getAuthStatus } from '../auth.js';
2
+ import { formatMissingRequiredCliAuthProviders, getMissingRequiredCliAuthProviders, } from '../config.js';
3
+ import { checkGit, checkPiDependency, checkRepository, checkSandboxBuild } from '../runtime.js';
4
+ import { renderDiagnosticCheck } from '../terminal.js';
5
+ export function getConfiguredProviders({ status, }) {
6
+ return [
7
+ status.openai.configured && !status.openai.refreshFailed
8
+ ? `OpenAI (${status.openai.source ?? 'configured'})`
9
+ : undefined,
10
+ status.gemini.configured
11
+ ? `Gemini optional (${status.gemini.source ?? 'configured'})`
12
+ : undefined,
13
+ status.openrouter.configured
14
+ ? `OpenRouter optional (${status.openrouter.source ?? 'configured'})`
15
+ : undefined,
16
+ ].filter((provider) => provider !== undefined);
17
+ }
18
+ export function getMissingRequiredProviders({ status, }) {
19
+ return getMissingRequiredCliAuthProviders(status);
20
+ }
21
+ export async function checkAuth({ env = process.env, getStatus = getAuthStatus, } = {}) {
22
+ const status = await getStatus({ env });
23
+ const configured = getConfiguredProviders({ status });
24
+ const missingRequiredProviders = getMissingRequiredProviders({ status });
25
+ if (missingRequiredProviders.length === 0) {
26
+ return { ok: true, message: `${configured.join(', ')} configured` };
27
+ }
28
+ return {
29
+ ok: false,
30
+ message: formatMissingRequiredCliAuthProviders(missingRequiredProviders),
31
+ };
32
+ }
33
+ export async function runDoctor() {
34
+ await renderDiagnosticCheck('git', checkGit);
35
+ await renderDiagnosticCheck('repository', checkRepository);
36
+ await renderDiagnosticCheck('pi', checkPiDependency);
37
+ await renderDiagnosticCheck('sandbox build', checkSandboxBuild);
38
+ await renderDiagnosticCheck('auth', checkAuth);
39
+ }
@@ -0,0 +1,25 @@
1
+ const REVIEW_USAGE_COMMANDS = [
2
+ 'doistbot review [--base <ref>] [--models <models>]',
3
+ 'doistbot review staged [--models <models>]',
4
+ 'db review [--base <ref>] [--models <models>]',
5
+ 'db review staged [--models <models>]',
6
+ ];
7
+ const REVIEW_SETUP_COMMANDS = [
8
+ 'doistbot review',
9
+ 'doistbot review staged',
10
+ 'db review',
11
+ 'db review staged',
12
+ ];
13
+ export function formatReviewUsage() {
14
+ return `Usage: ${formatCommandList(REVIEW_USAGE_COMMANDS)}`;
15
+ }
16
+ export function formatSetupCompleteMessage() {
17
+ return `Setup complete. Run ${formatCommandList(REVIEW_SETUP_COMMANDS)} from a git repository.`;
18
+ }
19
+ function formatCommandList(commands) {
20
+ const quotedCommands = commands.map((command) => `\`${command}\``);
21
+ if (quotedCommands.length <= 2) {
22
+ return quotedCommands.join(' or ');
23
+ }
24
+ return `${quotedCommands.slice(0, -1).join(', ')}, or ${quotedCommands.at(-1)}`;
25
+ }
@@ -0,0 +1,311 @@
1
+ import { formatMissingRequiredCliAuthProviders, getApiKeys, getCliReviewRouting, getMissingRequiredCliReviewAuthProviders, } from '../config.js';
2
+ import { getRepositoryRoot, loadSandboxFormatModule, loadSandboxReviewModule } from '../runtime.js';
3
+ import { formatReviewProgress, renderAlert, renderMarkdown, withSpinner, writeLine, } from '../terminal.js';
4
+ const LOCAL_REVIEW_SOURCE_STAGED = 'staged';
5
+ const JSON_SCHEMA_VERSION = 1;
6
+ const PROVIDER_DISPLAY_NAMES = {
7
+ deepseek: 'DeepSeek',
8
+ gemini: 'Gemini',
9
+ grok: 'Grok',
10
+ zai: 'Z.ai',
11
+ openai: 'OpenAI',
12
+ };
13
+ function stripProviderFailurePrefix(error) {
14
+ return error
15
+ .replace(/^Pi\s+\w+\s+request failed:\s*/i, '')
16
+ .replace(/^Pi\s+\w+\s+request timed out after\s*/i, 'Timed out after ')
17
+ .trim();
18
+ }
19
+ function getReadableProviderFailure(failure) {
20
+ const error = stripProviderFailurePrefix(failure.error);
21
+ const normalized = error.toLowerCase();
22
+ if (failure.model === 'openai' &&
23
+ (normalized.includes('no api key found for openai') ||
24
+ normalized.includes('no api key found for openai-codex') ||
25
+ normalized.includes('no api key for provider') ||
26
+ normalized.includes('use /login') ||
27
+ normalized.includes('invalid api key') ||
28
+ normalized.includes('incorrect api key') ||
29
+ normalized.includes('refresh_token_invalidated') ||
30
+ normalized.includes('unauthorized') ||
31
+ /\b401\b/.test(normalized))) {
32
+ return 'OpenAI Codex is not authenticated. Run `doistbot auth login` to reconnect OpenAI.';
33
+ }
34
+ if (failure.model === 'gemini' &&
35
+ (normalized.includes('429') ||
36
+ normalized.includes('resource_exhausted') ||
37
+ normalized.includes('quota') ||
38
+ normalized.includes('rate-limit') ||
39
+ normalized.includes('rate limit'))) {
40
+ return 'Gemini is rate-limited or out of quota. Check your Google Gemini quota or wait for quota to reset.';
41
+ }
42
+ if (failure.model === 'gemini' &&
43
+ (normalized.includes('api_key_invalid') ||
44
+ normalized.includes('api key not valid') ||
45
+ normalized.includes('invalid_argument') ||
46
+ normalized.includes('invalid api key') ||
47
+ normalized.includes('no api key found for google') ||
48
+ normalized.includes('no api key found for gemini') ||
49
+ normalized.includes('no api key for provider'))) {
50
+ return 'Gemini API key is invalid or missing. Run `doistbot auth` and paste a valid Gemini key.';
51
+ }
52
+ const firstLine = error
53
+ .split(/\r?\n/)
54
+ .find((line) => line.trim())
55
+ ?.trim();
56
+ return `${PROVIDER_DISPLAY_NAMES[failure.model]} failed: ${firstLine || 'unknown error'}`;
57
+ }
58
+ function getReadableProviderFailureEntries(failures) {
59
+ const messagesByModel = new Map();
60
+ for (const failure of failures) {
61
+ if (!messagesByModel.has(failure.model)) {
62
+ messagesByModel.set(failure.model, getReadableProviderFailure(failure));
63
+ }
64
+ }
65
+ return Array.from(messagesByModel, ([model, message]) => ({ model, message }));
66
+ }
67
+ function getReadableProviderFailures(failures) {
68
+ return getReadableProviderFailureEntries(failures).map(({ message }) => message);
69
+ }
70
+ function formatProviderFailureMessage(failures) {
71
+ const messages = getReadableProviderFailures(failures);
72
+ if (messages.length === 0) {
73
+ return 'Review failed: no model returned a usable review.';
74
+ }
75
+ return `Review failed because no provider returned a usable review:\n${messages
76
+ .map((message) => `- ${message}`)
77
+ .join('\n')}`;
78
+ }
79
+ function formatPartialProviderFailureMessage(failures) {
80
+ const messages = getReadableProviderFailures(failures);
81
+ if (messages.length === 0) {
82
+ return '';
83
+ }
84
+ return `Review completed with provider warnings:\n${messages
85
+ .map((message) => `- ${message}`)
86
+ .join('\n')}`;
87
+ }
88
+ function handleSkippedReview(result) {
89
+ switch (result.reason) {
90
+ case 'no_diff':
91
+ case 'empty_diff':
92
+ writeLine('No local changes to review.');
93
+ return;
94
+ case 'no_models':
95
+ writeLine('No review models are configured. Run `doistbot doctor` for details.');
96
+ return;
97
+ case 'no_review':
98
+ throw new Error(formatProviderFailureMessage(result.providerFailures));
99
+ default:
100
+ writeLine(`Review skipped: ${result.reason}`);
101
+ }
102
+ }
103
+ function formatReviewOutput({ formatReviewForTerminal, review, }) {
104
+ return formatReviewForTerminal({ review });
105
+ }
106
+ function getJsonStatus(result) {
107
+ function mapping(statusMapping, canTrustCleanReview = statusMapping.status === 'reviewed') {
108
+ return { mapping: statusMapping, canTrustCleanReview };
109
+ }
110
+ if (result.reviewResult.status === 'skipped') {
111
+ switch (result.reviewResult.reason) {
112
+ case 'empty_review':
113
+ case 'no_models':
114
+ case 'no_review':
115
+ return mapping({
116
+ status: 'unverified',
117
+ verified: false,
118
+ unverifiedReason: result.reviewResult.reason,
119
+ }, false);
120
+ default:
121
+ return mapping({
122
+ status: 'skipped',
123
+ verified: false,
124
+ skipReason: result.reviewResult.reason,
125
+ }, false);
126
+ }
127
+ }
128
+ return mapping({ status: 'reviewed', verified: true });
129
+ }
130
+ function finalizeJsonStatus({ initialStatus, findingCount, providerFailureCount, }) {
131
+ if (initialStatus.canTrustCleanReview && providerFailureCount > 0 && findingCount === 0) {
132
+ return { status: 'unverified', verified: false, unverifiedReason: 'provider_failures' };
133
+ }
134
+ return initialStatus.mapping;
135
+ }
136
+ function getReviewForJson(result) {
137
+ if (result.reviewResult.status === 'reviewed') {
138
+ return result.reviewResult.review;
139
+ }
140
+ return { summary: '', comments: [] };
141
+ }
142
+ function hasLocation(finding) {
143
+ return typeof finding.path === 'string' && typeof finding.line === 'number';
144
+ }
145
+ function formatReviewJsonFinding(finding) {
146
+ return {
147
+ body: finding.body,
148
+ severity: finding.severity,
149
+ ...(hasLocation(finding)
150
+ ? {
151
+ location: {
152
+ path: finding.path,
153
+ line: finding.line,
154
+ ...(finding.side ? { side: finding.side } : {}),
155
+ },
156
+ }
157
+ : {}),
158
+ };
159
+ }
160
+ function getReviewFindingsForJson(review) {
161
+ if (review.findings) {
162
+ return review.findings;
163
+ }
164
+ return review.comments.map((comment) => ({
165
+ body: comment.body,
166
+ severity: null,
167
+ placement: 'inline',
168
+ path: comment.path,
169
+ line: comment.line,
170
+ side: comment.side,
171
+ }));
172
+ }
173
+ function getReviewFindingCount(review) {
174
+ return getReviewFindingsForJson(review).length;
175
+ }
176
+ export function formatReviewForJson(result) {
177
+ const review = getReviewForJson(result);
178
+ const reviewFindings = getReviewFindingsForJson(review);
179
+ const findings = reviewFindings.map(formatReviewJsonFinding);
180
+ const statusMapping = finalizeJsonStatus({
181
+ initialStatus: getJsonStatus(result),
182
+ findingCount: findings.length,
183
+ providerFailureCount: result.reviewResult.providerFailures.length,
184
+ });
185
+ return {
186
+ schemaVersion: JSON_SCHEMA_VERSION,
187
+ ...statusMapping,
188
+ reviewSource: result.source,
189
+ repository: {
190
+ owner: result.owner,
191
+ repo: result.repo,
192
+ branch: result.branch,
193
+ baseBranch: result.baseBranch,
194
+ },
195
+ review: {
196
+ count: findings.length,
197
+ findings,
198
+ },
199
+ providerFailures: getReadableProviderFailureEntries(result.reviewResult.providerFailures),
200
+ ...(review.reviewMode ? { reviewMode: review.reviewMode } : {}),
201
+ };
202
+ }
203
+ async function getReviewAuthCheck(models) {
204
+ if (models !== undefined) {
205
+ const configuredApiKeys = await getApiKeys(undefined, undefined, models);
206
+ const missingModels = models.filter((model) => configuredApiKeys[model] === undefined);
207
+ return {
208
+ apiKeys: configuredApiKeys,
209
+ missingModels,
210
+ missingProviders: missingModels.map((model) => PROVIDER_DISPLAY_NAMES[model]),
211
+ };
212
+ }
213
+ const configuredApiKeys = await getApiKeys();
214
+ const missingRequiredProviders = getMissingRequiredCliReviewAuthProviders(configuredApiKeys);
215
+ return {
216
+ apiKeys: configuredApiKeys,
217
+ missingModels: missingRequiredProviders.includes('OpenAI') ? ['openai'] : [],
218
+ missingProviders: missingRequiredProviders,
219
+ };
220
+ }
221
+ function getMissingAuthProviderFailures(missingModels) {
222
+ return missingModels.map((model) => ({
223
+ model,
224
+ message: `${PROVIDER_DISPLAY_NAMES[model]} authentication unavailable; run \`doistbot auth login\``,
225
+ }));
226
+ }
227
+ function formatMissingAuthForJson({ source, missingModels, }) {
228
+ return {
229
+ schemaVersion: JSON_SCHEMA_VERSION,
230
+ status: 'unverified',
231
+ verified: false,
232
+ unverifiedReason: 'missing_auth',
233
+ reviewSource: source,
234
+ repository: {
235
+ owner: 'unknown',
236
+ repo: 'unknown',
237
+ branch: 'unknown',
238
+ baseBranch: 'unknown',
239
+ },
240
+ review: {
241
+ count: 0,
242
+ findings: [],
243
+ },
244
+ providerFailures: getMissingAuthProviderFailures(missingModels),
245
+ };
246
+ }
247
+ async function runLocalReviewForCli({ source, apiKeys, models, onProgress, }) {
248
+ const reviewModule = await loadSandboxReviewModule();
249
+ return await reviewModule.runLocalReview({
250
+ workspaceDir: getRepositoryRoot(),
251
+ source,
252
+ apiKeys,
253
+ ...getCliReviewRouting(apiKeys, models),
254
+ suppressLogs: true,
255
+ onProgress,
256
+ });
257
+ }
258
+ export async function runReview({ source, json = false, models } = {
259
+ source: 'base',
260
+ }) {
261
+ const authCheck = await getReviewAuthCheck(models);
262
+ if (authCheck.missingProviders.length > 0) {
263
+ if (json) {
264
+ writeLine(JSON.stringify(formatMissingAuthForJson({
265
+ source,
266
+ missingModels: authCheck.missingModels,
267
+ }), null, 2));
268
+ process.exitCode = 1;
269
+ return;
270
+ }
271
+ throw new Error(formatMissingRequiredCliAuthProviders(authCheck.missingProviders));
272
+ }
273
+ if (json) {
274
+ const result = await runLocalReviewForCli({ source, apiKeys: authCheck.apiKeys, models });
275
+ const output = formatReviewForJson(result);
276
+ writeLine(JSON.stringify(output, null, 2));
277
+ if (output.status === 'unverified') {
278
+ process.exitCode = 1;
279
+ }
280
+ return;
281
+ }
282
+ const result = await withSpinner(source === LOCAL_REVIEW_SOURCE_STAGED
283
+ ? 'Reviewing staged changes...'
284
+ : 'Reviewing local changes...', async (status) => await runLocalReviewForCli({
285
+ source,
286
+ apiKeys: authCheck.apiKeys,
287
+ models,
288
+ onProgress: status.enabled
289
+ ? (event) => status.update(formatReviewProgress(event))
290
+ : undefined,
291
+ }));
292
+ if (result.reviewResult.status === 'skipped') {
293
+ handleSkippedReview(result.reviewResult);
294
+ return;
295
+ }
296
+ const formatModule = await loadSandboxFormatModule();
297
+ if (result.reviewResult.providerFailures.length > 0 &&
298
+ getReviewFindingCount(result.reviewResult.review) === 0) {
299
+ throw new Error(formatProviderFailureMessage(result.reviewResult.providerFailures));
300
+ }
301
+ const output = formatReviewOutput({
302
+ formatReviewForTerminal: formatModule.formatReviewForTerminal,
303
+ review: result.reviewResult.review,
304
+ });
305
+ writeLine(renderMarkdown(output || 'No findings.'));
306
+ if (result.reviewResult.providerFailures.length > 0) {
307
+ writeLine();
308
+ writeLine();
309
+ writeLine(renderAlert(formatPartialProviderFailureMessage(result.reviewResult.providerFailures)));
310
+ }
311
+ }
@@ -0,0 +1,14 @@
1
+ import { checkGit, checkPiDependency, checkSandboxBuild } from '../runtime.js';
2
+ import { renderDiagnosticCheck, writeLine } from '../terminal.js';
3
+ import { runAuthSetup } from './auth.js';
4
+ import { formatSetupCompleteMessage } from './review-usage.js';
5
+ export async function runSetup() {
6
+ writeLine('Checking Doistbot CLI prerequisites...');
7
+ await renderDiagnosticCheck('git', checkGit);
8
+ await renderDiagnosticCheck('pi', checkPiDependency);
9
+ await renderDiagnosticCheck('sandbox build', checkSandboxBuild);
10
+ writeLine();
11
+ await runAuthSetup();
12
+ writeLine();
13
+ writeLine(formatSetupCompleteMessage());
14
+ }