@doist/doistbot-cli 1.0.3 → 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.
- package/dist/actions/auth.js +23 -23
- package/dist/actions/doctor.js +3 -5
- package/dist/actions/review-usage.js +6 -1
- package/dist/actions/review.js +7 -11
- package/dist/auth.js +17 -17
- package/dist/config.js +14 -9
- package/dist/env.js +1 -2
- package/dist/file-store.js +1 -1
- package/dist/index.js +19 -1
- package/dist/terminal.js +0 -7
- package/package.json +5 -5
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +8 -72
- package/sandbox/dist/core/bot-logins.js +0 -13
- package/sandbox/dist/core/datadog-metrics.js +0 -2
- package/sandbox/dist/core/pi.js +16 -13
- package/sandbox/dist/providers/config.js +1 -1
- package/sandbox/dist/providers/credentials.js +30 -0
- package/sandbox/dist/providers/gemini.js +4 -10
- package/sandbox/dist/providers/helpers.js +6 -21
- package/sandbox/dist/providers/index.js +0 -3
- package/sandbox/dist/providers/openai.js +1 -8
- package/sandbox/dist/providers/pi-review.js +16 -54
- package/sandbox/dist/providers/schemas.js +0 -39
- package/sandbox/dist/tasks/chat/chat.js +2 -1
- package/sandbox/dist/tasks/issue-summarize/context.js +1 -1
- package/sandbox/dist/tasks/issue-summarize/model.js +3 -1
- package/sandbox/dist/tasks/issue-summarize/summarize.js +1 -1
- package/sandbox/dist/tasks/issue-triage/context.js +1 -2
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +3 -3
- package/sandbox/dist/tasks/persist-logs/dd-proxy.js +2 -2
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +1 -1
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +1 -1
- package/sandbox/dist/tasks/review/conversation-context.js +13 -7
- package/sandbox/dist/tasks/review/engines/dedupe.js +18 -9
- package/sandbox/dist/tasks/review/engines/multi-focus.js +26 -31
- package/sandbox/dist/tasks/review/engines/shared.js +11 -59
- package/sandbox/dist/tasks/review/local-git.js +3 -1
- package/sandbox/dist/tasks/review/local.js +4 -29
- package/sandbox/dist/tasks/review/prompt.js +2 -49
- package/sandbox/dist/tasks/review/review-summary.js +82 -0
- package/sandbox/dist/tasks/review/review.js +23 -57
- package/sandbox/dist/tasks/review/runner.js +35 -51
- package/sandbox/dist/tasks/review/summary-model.js +11 -2
- package/sandbox/dist/tasks/review/thinking-level.js +0 -2
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +7 -21
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +8 -72
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/_node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/dist/providers/anthropic.js +0 -30
- package/sandbox/dist/tasks/review/author-format.js +0 -14
- package/sandbox/dist/tasks/review/author-profile.js +0 -67
- package/sandbox/dist/tasks/review/council/normalizer.js +0 -104
- package/sandbox/dist/tasks/review/council/orchestrator.js +0 -194
- package/sandbox/dist/tasks/review/council/provider-health.js +0 -31
- package/sandbox/dist/tasks/review/council/summary.js +0 -192
- package/sandbox/dist/tasks/review/council/types.js +0 -4
- package/sandbox/dist/tasks/review/council/voter.js +0 -384
- package/sandbox/dist/tasks/review/engines/council.js +0 -126
- package/sandbox/dist/tasks/review/engines/index.js +0 -12
- package/sandbox/dist/tasks/review/review-engine.js +0 -5
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.d.ts +0 -11
- package/sandbox/node_modules/doistbot-repo-config/dist/review-engine.js +0 -41
- package/sandbox/review-prompt.md +0 -209
- package/sandbox/vote-prompt.md +0 -69
package/dist/actions/auth.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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 ({
|
|
21
|
-
const deleted = await
|
|
20
|
+
clear: async ({ deleteOpenRouterApiKey }) => {
|
|
21
|
+
const deleted = await deleteOpenRouterApiKey();
|
|
22
22
|
return deleted
|
|
23
|
-
? '✓
|
|
24
|
-
: '
|
|
23
|
+
? '✓ OpenRouter stored API key cleared.'
|
|
24
|
+
: 'OpenRouter had no stored API key to clear.';
|
|
25
25
|
},
|
|
26
|
-
envWarning: (env) => getEnvApiKeys(env).
|
|
27
|
-
? '
|
|
26
|
+
envWarning: (env) => getEnvApiKeys(env).openrouter
|
|
27
|
+
? 'OpenRouter is still configured from OPENROUTER_API_KEY.'
|
|
28
28
|
: undefined,
|
|
29
|
-
name: '
|
|
30
|
-
value: '
|
|
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
|
-
|
|
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.
|
|
60
|
+
deps.writeLine('2. OpenRouter API key');
|
|
61
61
|
deps.writeLine();
|
|
62
|
-
if (status.
|
|
63
|
-
deps.writeLine('✓
|
|
62
|
+
if (status.openrouter.source === 'env') {
|
|
63
|
+
deps.writeLine('✓ OpenRouter already configured from OPENROUTER_API_KEY.');
|
|
64
64
|
return;
|
|
65
65
|
}
|
|
66
|
-
if (status.
|
|
67
|
-
deps.writeLine(`✓
|
|
66
|
+
if (status.openrouter.source === 'keychain') {
|
|
67
|
+
deps.writeLine(`✓ OpenRouter already configured in the ${getSecureStoreLabel()}.`);
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
70
|
-
if (status.
|
|
71
|
-
deps.writeLine(`✓
|
|
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
|
|
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
|
|
78
|
-
const backend = await deps.
|
|
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('✓
|
|
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(`
|
|
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
|
-
|
|
92
|
+
deleteOpenRouterApiKey,
|
|
93
93
|
env: process.env,
|
|
94
94
|
isInteractive: process.stdin.isTTY === true,
|
|
95
95
|
logoutOpenAICodex,
|
package/dist/actions/doctor.js
CHANGED
|
@@ -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.
|
|
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() {
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
const REVIEW_USAGE_COMMANDS = [
|
|
1
|
+
const REVIEW_USAGE_COMMANDS = [
|
|
2
|
+
'doistbot review [--base <ref>]',
|
|
3
|
+
'doistbot review staged',
|
|
4
|
+
'db review [--base <ref>]',
|
|
5
|
+
'db review staged',
|
|
6
|
+
];
|
|
2
7
|
const REVIEW_SETUP_COMMANDS = [
|
|
3
8
|
'doistbot review',
|
|
4
9
|
'doistbot review staged',
|
package/dist/actions/review.js
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
import { getApiKeys } from '../config.js';
|
|
2
2
|
import { getRepositoryRoot, loadSandboxFormatModule, loadSandboxReviewModule } from '../runtime.js';
|
|
3
3
|
import { formatReviewProgress, renderAlert, renderMarkdown, withSpinner, writeLine, } from '../terminal.js';
|
|
4
|
-
import { formatReviewUsage } from './review-usage.js';
|
|
5
4
|
const LOCAL_REVIEW_SOURCE_STAGED = 'staged';
|
|
6
5
|
const JSON_SCHEMA_VERSION = 1;
|
|
7
6
|
const PROVIDER_DISPLAY_NAMES = {
|
|
8
7
|
gemini: 'Gemini',
|
|
9
8
|
openai: 'OpenAI Codex',
|
|
10
|
-
anthropic: 'Anthropic',
|
|
11
9
|
};
|
|
12
10
|
function stripProviderFailurePrefix(error) {
|
|
13
11
|
return error
|
|
@@ -29,13 +27,15 @@ function getReadableProviderFailure(failure) {
|
|
|
29
27
|
normalized.includes('quota') ||
|
|
30
28
|
normalized.includes('rate-limit') ||
|
|
31
29
|
normalized.includes('rate limit'))) {
|
|
32
|
-
return 'Gemini is rate-limited or out of quota.
|
|
30
|
+
return 'Gemini via OpenRouter is rate-limited or out of quota. Check your OpenRouter credits, provider routing, or wait for quota to reset.';
|
|
33
31
|
}
|
|
34
32
|
if (failure.model === 'gemini' &&
|
|
35
33
|
(normalized.includes('api_key_invalid') ||
|
|
36
34
|
normalized.includes('api key not valid') ||
|
|
37
|
-
normalized.includes('invalid_argument')
|
|
38
|
-
|
|
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.';
|
|
39
39
|
}
|
|
40
40
|
const firstLine = error
|
|
41
41
|
.split(/\r?\n/)
|
|
@@ -197,12 +197,8 @@ async function runLocalReviewForCli({ source, onProgress, }) {
|
|
|
197
197
|
onProgress,
|
|
198
198
|
});
|
|
199
199
|
}
|
|
200
|
-
export async function runReview(
|
|
201
|
-
if (
|
|
202
|
-
throw new Error(formatReviewUsage());
|
|
203
|
-
}
|
|
204
|
-
const source = sourceArg === LOCAL_REVIEW_SOURCE_STAGED ? LOCAL_REVIEW_SOURCE_STAGED : 'base';
|
|
205
|
-
if (options.json === true) {
|
|
200
|
+
export async function runReview({ source, json = false } = { source: 'base' }) {
|
|
201
|
+
if (json) {
|
|
206
202
|
const result = await runLocalReviewForCli({ source });
|
|
207
203
|
const output = formatReviewForJson(result);
|
|
208
204
|
writeLine(JSON.stringify(output, null, 2));
|
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
|
|
9
|
-
export function
|
|
10
|
-
return createCompositeStore(createKeyringStore(
|
|
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
|
|
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
|
|
45
|
-
return (await
|
|
44
|
+
export async function getStoredOpenRouterApiKey(store = createOpenRouterKeyStore()) {
|
|
45
|
+
return (await readStoredOpenRouterApiKey(store)).value;
|
|
46
46
|
}
|
|
47
|
-
export async function
|
|
47
|
+
export async function saveOpenRouterApiKey(apiKey, store = createOpenRouterKeyStore()) {
|
|
48
48
|
const normalized = apiKey.trim();
|
|
49
49
|
if (!normalized) {
|
|
50
|
-
throw new Error('
|
|
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
|
|
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
|
|
62
|
+
export async function deleteOpenRouterApiKey(store = createOpenRouterKeyStore()) {
|
|
63
63
|
return await store.deleteSecret();
|
|
64
64
|
}
|
|
65
|
-
export async function getAuthStatus({ env = process.env, storage,
|
|
65
|
+
export async function getAuthStatus({ env = process.env, storage, openRouterStore = createOpenRouterKeyStore(), } = {}) {
|
|
66
66
|
const envApiKeys = getEnvApiKeys(env);
|
|
67
|
-
const
|
|
67
|
+
const openRouterStored = envApiKeys.openrouter
|
|
68
68
|
? { value: undefined }
|
|
69
|
-
: await
|
|
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
|
-
|
|
78
|
+
openrouter: envApiKeys.openrouter
|
|
79
79
|
? { configured: true, source: 'env' }
|
|
80
80
|
: {
|
|
81
|
-
configured:
|
|
82
|
-
source:
|
|
83
|
-
?
|
|
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 {
|
|
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
|
-
|
|
4
|
+
getStoredOpenRouterApiKey,
|
|
5
5
|
hasOpenAICodexAuth,
|
|
6
6
|
}) {
|
|
7
7
|
const envApiKeys = getEnvApiKeys(env);
|
|
8
|
-
const
|
|
8
|
+
const storedOpenRouterApiKey = envApiKeys.openrouter
|
|
9
9
|
? undefined
|
|
10
|
-
: await auth.
|
|
10
|
+
: await auth.getStoredOpenRouterApiKey().catch(() => undefined);
|
|
11
11
|
const openAICodexAuthConfigured = await auth.hasOpenAICodexAuth();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
}
|
package/dist/file-store.js
CHANGED
|
@@ -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
|
|
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/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Command, Option } from 'commander';
|
|
|
6
6
|
import { AUTH_LOGOUT_PROVIDER_USAGE, runAuthLogout, runAuthSetup, runAuthStatus, } from './actions/auth.js';
|
|
7
7
|
import { runDoctor } from './actions/doctor.js';
|
|
8
8
|
import { runReview } from './actions/review.js';
|
|
9
|
+
import { formatReviewUsage } from './actions/review-usage.js';
|
|
9
10
|
import { runSetup } from './actions/setup.js';
|
|
10
11
|
import { writeErrorLine } from './terminal.js';
|
|
11
12
|
import { notifyAndRefreshCliUpdateCache, notifyIfCliUpdateAvailable } from './update-check.js';
|
|
@@ -21,9 +22,25 @@ function getPackageVersion() {
|
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
const PACKAGE_VERSION = getPackageVersion();
|
|
25
|
+
const LOCAL_REVIEW_SOURCE_STAGED = 'staged';
|
|
24
26
|
function getUpdateCacheEntrypointPath() {
|
|
25
27
|
return fileURLToPath(new URL('./update-cache-entrypoint.js', import.meta.url));
|
|
26
28
|
}
|
|
29
|
+
function parseReviewInput(sourceArg, options = {}) {
|
|
30
|
+
if (sourceArg !== undefined && sourceArg !== LOCAL_REVIEW_SOURCE_STAGED) {
|
|
31
|
+
throw new Error(formatReviewUsage());
|
|
32
|
+
}
|
|
33
|
+
if (sourceArg === LOCAL_REVIEW_SOURCE_STAGED && options.base) {
|
|
34
|
+
throw new Error(formatReviewUsage());
|
|
35
|
+
}
|
|
36
|
+
if (options.base !== undefined) {
|
|
37
|
+
return { source: { kind: 'base-ref', ref: options.base }, json: options.json === true };
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
source: sourceArg === LOCAL_REVIEW_SOURCE_STAGED ? LOCAL_REVIEW_SOURCE_STAGED : 'base',
|
|
41
|
+
json: options.json === true,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
27
44
|
export function createProgram({ runAuthLogout: authLogout = runAuthLogout, runAuthSetup: authSetup = runAuthSetup, runAuthStatus: authStatus = runAuthStatus, runDoctor: doctor = runDoctor, runSetup: setup = runSetup, runReview: review = runReview, runAutomaticUpdateCheck: automaticUpdateCheck = async () => await notifyAndRefreshCliUpdateCache({
|
|
28
45
|
currentVersion: PACKAGE_VERSION,
|
|
29
46
|
updateCacheEntrypointPath: getUpdateCacheEntrypointPath(),
|
|
@@ -79,8 +96,9 @@ export function createProgram({ runAuthLogout: authLogout = runAuthLogout, runAu
|
|
|
79
96
|
withAutomaticUpdateCheck(program
|
|
80
97
|
.command('review [source]')
|
|
81
98
|
.description('Run a local self-review. Use `staged` to review only staged changes.')
|
|
99
|
+
.option('--base <ref>', 'Review local changes against a specific base ref')
|
|
82
100
|
.addOption(new Option('--json', 'Print machine-readable JSON instead of terminal-formatted markdown').hideHelp())
|
|
83
|
-
.action(async (source, options) => await review(source,
|
|
101
|
+
.action(async (source, options) => await review(parseReviewInput(source, options))));
|
|
84
102
|
program
|
|
85
103
|
.command('update')
|
|
86
104
|
.description('Check whether a newer Doistbot CLI version is available')
|
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.
|
|
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
|
|
44
|
-
"@inquirer/password": "5.
|
|
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.
|
|
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.
|
|
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
|
|
5
|
-
|
|
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
|
-
|
|
58
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
109
|
+
const hasRequiredModels = requiredModels.every((model) => models.includes(model));
|
|
110
|
+
return models.length > 0 && hasRequiredModels ? models : [...defaultModels];
|
|
174
111
|
}
|
|
175
|
-
export function
|
|
176
|
-
return [...
|
|
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
|
-
}
|