@doist/doistbot-cli 1.0.6 → 1.0.8
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 +43 -11
- package/dist/actions/doctor.js +4 -2
- package/dist/actions/review.js +11 -14
- package/dist/auth.js +23 -6
- package/dist/config.js +58 -12
- package/package.json +3 -3
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +1 -1
- package/sandbox/dist/core/pi.js +31 -1
- package/sandbox/dist/core/prompt-template.js +45 -3
- package/sandbox/dist/core/repository-map.js +24 -0
- package/sandbox/dist/core/tracing.js +9 -0
- package/sandbox/dist/providers/helpers.js +51 -0
- package/sandbox/dist/providers/pi-review.js +7 -1
- package/sandbox/dist/tasks/chat/chat.js +20 -8
- package/sandbox/dist/tasks/issue-summarize/model.js +119 -48
- package/sandbox/dist/tasks/issue-summarize/prompt.js +2 -2
- package/sandbox/dist/tasks/issue-summarize/summarize.js +104 -88
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +3 -2
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +2 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +2 -2
- package/sandbox/dist/tasks/issue-triage/prompt.js +2 -2
- package/sandbox/dist/tasks/issue-triage/triage.js +2 -2
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +3 -3
- package/sandbox/dist/tasks/review/engines/dedupe.js +107 -42
- package/sandbox/dist/tasks/review/engines/multi-focus.js +229 -119
- package/sandbox/dist/tasks/review/engines/shared.js +36 -1
- package/sandbox/dist/tasks/review/github-comment-format.js +12 -4
- package/sandbox/dist/tasks/review/local.js +6 -1
- package/sandbox/dist/tasks/review/review-summary.js +3 -0
- package/sandbox/dist/tasks/review/review.js +253 -186
- package/sandbox/dist/tasks/review/runner.js +10 -5
- package/sandbox/dist/tasks/review/summary-model.js +40 -18
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +1 -1
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +1 -1
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +1 -0
package/dist/actions/auth.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
1
2
|
import { deleteGeminiApiKey, deleteOpenRouterApiKey, getAuthStatus, loginOpenAICodex, logoutOpenAICodex, saveGeminiApiKey, saveOpenRouterApiKey, } from '../auth.js';
|
|
2
3
|
import { getEnvApiKeys } from '../env.js';
|
|
3
4
|
import { FILE_STORE_PATH } from '../file-store.js';
|
|
@@ -43,6 +44,18 @@ const AUTH_LOGOUT_PROVIDER_CONFIGS = [
|
|
|
43
44
|
];
|
|
44
45
|
const AUTH_LOGOUT_PROVIDERS = AUTH_LOGOUT_PROVIDER_CONFIGS.map(({ value }) => value);
|
|
45
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
|
+
}
|
|
46
59
|
export async function runAuthSetup(dependencies = {}) {
|
|
47
60
|
const deps = {
|
|
48
61
|
getAuthStatus,
|
|
@@ -57,19 +70,22 @@ export async function runAuthSetup(dependencies = {}) {
|
|
|
57
70
|
const status = await deps.getAuthStatus();
|
|
58
71
|
deps.writeLine('1. OpenAI Codex');
|
|
59
72
|
deps.writeLine();
|
|
60
|
-
if (status.openai.configured) {
|
|
73
|
+
if (status.openai.configured && !status.openai.refreshFailed) {
|
|
61
74
|
deps.writeLine('✓ OpenAI already configured (pi-auth).');
|
|
62
75
|
}
|
|
63
76
|
else {
|
|
64
77
|
if (!deps.isInteractive) {
|
|
65
78
|
throw new Error('Cannot configure OpenAI Codex auth in non-interactive mode.');
|
|
66
79
|
}
|
|
80
|
+
if (status.openai.refreshFailed) {
|
|
81
|
+
deps.writeLine('OpenAI session could not be refreshed (expired, revoked, or offline). Reconnecting.');
|
|
82
|
+
}
|
|
67
83
|
await deps.loginOpenAICodex();
|
|
68
84
|
deps.writeLine();
|
|
69
85
|
deps.writeLine('✓ OpenAI configured.');
|
|
70
86
|
}
|
|
71
87
|
deps.writeLine();
|
|
72
|
-
deps.writeLine(
|
|
88
|
+
deps.writeLine(`2. Gemini API key ${formatOptionalLabel()}`);
|
|
73
89
|
deps.writeLine();
|
|
74
90
|
if (status.gemini.source === 'env') {
|
|
75
91
|
deps.writeLine('✓ Gemini already configured from GEMINI_API_KEY.');
|
|
@@ -85,8 +101,8 @@ export async function runAuthSetup(dependencies = {}) {
|
|
|
85
101
|
deps.writeLine('Gemini not configured. Skipping optional Gemini provider.');
|
|
86
102
|
}
|
|
87
103
|
else {
|
|
88
|
-
deps.writeLine(
|
|
89
|
-
const geminiApiKey = (await deps.promptHiddenInput(
|
|
104
|
+
deps.writeLine(`Doistbot can use Gemini via Google Vertex. Paste a Gemini API key, or ${formatSkipInstruction()}.`);
|
|
105
|
+
const geminiApiKey = (await deps.promptHiddenInput(API_KEY_PROMPT)).trim();
|
|
90
106
|
if (!geminiApiKey) {
|
|
91
107
|
deps.writeLine();
|
|
92
108
|
deps.writeLine('Gemini skipped. You can add it later with `doistbot auth`.');
|
|
@@ -102,7 +118,7 @@ export async function runAuthSetup(dependencies = {}) {
|
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
120
|
deps.writeLine();
|
|
105
|
-
deps.writeLine(
|
|
121
|
+
deps.writeLine(`3. OpenRouter API key ${formatOptionalLabel()}`);
|
|
106
122
|
deps.writeLine();
|
|
107
123
|
if (status.openrouter.source === 'env') {
|
|
108
124
|
deps.writeLine('✓ OpenRouter already configured from OPENROUTER_API_KEY.');
|
|
@@ -117,10 +133,17 @@ export async function runAuthSetup(dependencies = {}) {
|
|
|
117
133
|
return;
|
|
118
134
|
}
|
|
119
135
|
if (!deps.isInteractive) {
|
|
120
|
-
|
|
136
|
+
deps.writeLine('OpenRouter not configured. Skipping optional OpenRouter provider.');
|
|
137
|
+
return;
|
|
121
138
|
}
|
|
122
|
-
deps.writeLine(
|
|
123
|
-
const
|
|
139
|
+
deps.writeLine(`Doistbot can use OpenRouter for DeepSeek, Z.ai, and other open models. Paste an OpenRouter API key, or ${formatSkipInstruction()}.`);
|
|
140
|
+
const openRouterApiKey = (await deps.promptHiddenInput(API_KEY_PROMPT)).trim();
|
|
141
|
+
if (!openRouterApiKey) {
|
|
142
|
+
deps.writeLine();
|
|
143
|
+
deps.writeLine('OpenRouter skipped. You can add it later with `doistbot auth`.');
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const backend = await deps.saveOpenRouterApiKey(openRouterApiKey);
|
|
124
147
|
deps.writeLine();
|
|
125
148
|
if (backend === 'file') {
|
|
126
149
|
deps.writeLine(`Note: ${getSecureStoreLabel()} unavailable; key stored in ${FILE_STORE_PATH} with 0600 permissions.`);
|
|
@@ -129,9 +152,18 @@ export async function runAuthSetup(dependencies = {}) {
|
|
|
129
152
|
}
|
|
130
153
|
export async function runAuthStatus() {
|
|
131
154
|
const status = await getAuthStatus();
|
|
132
|
-
writeLine(`OpenAI: ${
|
|
133
|
-
writeLine(`Gemini (
|
|
134
|
-
writeLine(`OpenRouter: ${status.openrouter.configured ? `configured (${status.openrouter.source ?? 'unknown'})` : 'not configured'}`);
|
|
155
|
+
writeLine(`OpenAI: ${formatOpenAIStatus(status.openai)}`);
|
|
156
|
+
writeLine(`Gemini ${formatOptionalLabel()}: ${status.gemini.configured ? `configured (${status.gemini.source ?? 'unknown'})` : 'not configured'}`);
|
|
157
|
+
writeLine(`OpenRouter ${formatOptionalLabel()}: ${status.openrouter.configured ? `configured (${status.openrouter.source ?? 'unknown'})` : 'not configured'}`);
|
|
158
|
+
}
|
|
159
|
+
function formatOpenAIStatus(status) {
|
|
160
|
+
if (!status.configured) {
|
|
161
|
+
return 'not configured';
|
|
162
|
+
}
|
|
163
|
+
const base = `configured (${status.source ?? 'unknown'})`;
|
|
164
|
+
return status.refreshFailed
|
|
165
|
+
? `${base} — session could not be refreshed; check your connection or run \`doistbot auth login\``
|
|
166
|
+
: base;
|
|
135
167
|
}
|
|
136
168
|
export async function runAuthLogout(target, dependencies = {}) {
|
|
137
169
|
const deps = {
|
package/dist/actions/doctor.js
CHANGED
|
@@ -4,12 +4,14 @@ import { checkGit, checkPiDependency, checkRepository, checkSandboxBuild } from
|
|
|
4
4
|
import { renderDiagnosticCheck } from '../terminal.js';
|
|
5
5
|
export function getConfiguredProviders({ status, }) {
|
|
6
6
|
return [
|
|
7
|
-
status.openai.configured
|
|
7
|
+
status.openai.configured && !status.openai.refreshFailed
|
|
8
|
+
? `OpenAI (${status.openai.source ?? 'configured'})`
|
|
9
|
+
: undefined,
|
|
8
10
|
status.gemini.configured
|
|
9
11
|
? `Gemini optional (${status.gemini.source ?? 'configured'})`
|
|
10
12
|
: undefined,
|
|
11
13
|
status.openrouter.configured
|
|
12
|
-
? `OpenRouter (${status.openrouter.source ?? 'configured'})`
|
|
14
|
+
? `OpenRouter optional (${status.openrouter.source ?? 'configured'})`
|
|
13
15
|
: undefined,
|
|
14
16
|
].filter((provider) => provider !== undefined);
|
|
15
17
|
}
|
package/dist/actions/review.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatMissingRequiredCliAuthProviders, getApiKeys, getMissingRequiredCliReviewAuthProviders, } from '../config.js';
|
|
1
|
+
import { formatMissingRequiredCliAuthProviders, getApiKeys, getCliReviewRouting, getMissingRequiredCliReviewAuthProviders, } from '../config.js';
|
|
2
2
|
import { getRepositoryRoot, loadSandboxFormatModule, loadSandboxReviewModule } from '../runtime.js';
|
|
3
3
|
import { formatReviewProgress, renderAlert, renderMarkdown, withSpinner, writeLine, } from '../terminal.js';
|
|
4
4
|
const LOCAL_REVIEW_SOURCE_STAGED = 'staged';
|
|
@@ -21,10 +21,14 @@ function getReadableProviderFailure(failure) {
|
|
|
21
21
|
if (failure.model === 'openai' &&
|
|
22
22
|
(normalized.includes('no api key found for openai') ||
|
|
23
23
|
normalized.includes('no api key found for openai-codex') ||
|
|
24
|
+
normalized.includes('no api key for provider') ||
|
|
24
25
|
normalized.includes('use /login') ||
|
|
25
26
|
normalized.includes('invalid api key') ||
|
|
26
|
-
normalized.includes('incorrect api key')
|
|
27
|
-
|
|
27
|
+
normalized.includes('incorrect api key') ||
|
|
28
|
+
normalized.includes('refresh_token_invalidated') ||
|
|
29
|
+
normalized.includes('unauthorized') ||
|
|
30
|
+
/\b401\b/.test(normalized))) {
|
|
31
|
+
return 'OpenAI Codex is not authenticated. Run `doistbot auth login` to reconnect OpenAI.';
|
|
28
32
|
}
|
|
29
33
|
if (failure.model === 'gemini' &&
|
|
30
34
|
(normalized.includes('429') ||
|
|
@@ -40,7 +44,8 @@ function getReadableProviderFailure(failure) {
|
|
|
40
44
|
normalized.includes('invalid_argument') ||
|
|
41
45
|
normalized.includes('invalid api key') ||
|
|
42
46
|
normalized.includes('no api key found for google') ||
|
|
43
|
-
normalized.includes('no api key found for gemini')
|
|
47
|
+
normalized.includes('no api key found for gemini') ||
|
|
48
|
+
normalized.includes('no api key for provider'))) {
|
|
44
49
|
return 'Gemini API key is invalid or missing. Run `doistbot auth` and paste a valid Gemini key.';
|
|
45
50
|
}
|
|
46
51
|
const firstLine = error
|
|
@@ -205,16 +210,7 @@ function getMissingAuthProviderFailures(missingProviders) {
|
|
|
205
210
|
if (missingProviders.includes('OpenAI')) {
|
|
206
211
|
failures.push({
|
|
207
212
|
model: 'openai',
|
|
208
|
-
message: 'OpenAI
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
if (missingProviders.includes('OpenRouter')) {
|
|
212
|
-
failures.push({
|
|
213
|
-
model: 'deepseek',
|
|
214
|
-
message: 'OpenRouter not configured; run `doistbot auth`',
|
|
215
|
-
}, {
|
|
216
|
-
model: 'zai',
|
|
217
|
-
message: 'OpenRouter not configured; run `doistbot auth`',
|
|
213
|
+
message: 'OpenAI authentication unavailable; run `doistbot auth login`',
|
|
218
214
|
});
|
|
219
215
|
}
|
|
220
216
|
return failures;
|
|
@@ -245,6 +241,7 @@ async function runLocalReviewForCli({ source, apiKeys, onProgress, }) {
|
|
|
245
241
|
workspaceDir: getRepositoryRoot(),
|
|
246
242
|
source,
|
|
247
243
|
apiKeys,
|
|
244
|
+
...getCliReviewRouting(apiKeys),
|
|
248
245
|
suppressLogs: true,
|
|
249
246
|
onProgress,
|
|
250
247
|
});
|
package/dist/auth.js
CHANGED
|
@@ -28,9 +28,22 @@ export function getBrowserOpenCommand(platform, url) {
|
|
|
28
28
|
}
|
|
29
29
|
return { command: 'xdg-open', args: [url] };
|
|
30
30
|
}
|
|
31
|
-
|
|
31
|
+
/**
|
|
32
|
+
* Check that the stored OAuth session is usable, not just present.
|
|
33
|
+
* getApiKey returns the cached access token when unexpired; when expired it
|
|
34
|
+
* attempts one cross-process-locked refresh, persists the result, and returns
|
|
35
|
+
* undefined when the refresh fails (revoked session, no network) while
|
|
36
|
+
* preserving the stored credentials. Unexpected storage errors propagate.
|
|
37
|
+
*/
|
|
38
|
+
export async function getOpenAICodexAuthState(storage) {
|
|
32
39
|
const resolvedStorage = storage ?? (await createPiAuthStorage());
|
|
33
|
-
|
|
40
|
+
if (resolvedStorage.get(OPENAI_CODEX_PROVIDER_ID)?.type !== 'oauth') {
|
|
41
|
+
return 'missing';
|
|
42
|
+
}
|
|
43
|
+
const apiKey = await resolvedStorage.getApiKey(OPENAI_CODEX_PROVIDER_ID, {
|
|
44
|
+
includeFallback: false,
|
|
45
|
+
});
|
|
46
|
+
return apiKey ? 'ready' : 'refresh-failed';
|
|
34
47
|
}
|
|
35
48
|
export async function loginOpenAICodex(storage) {
|
|
36
49
|
const resolvedStorage = storage ?? (await createPiAuthStorage());
|
|
@@ -105,11 +118,15 @@ export async function getAuthStatus({ env = process.env, storage, geminiStore =
|
|
|
105
118
|
const openRouterStored = envApiKeys.openrouter
|
|
106
119
|
? { value: undefined }
|
|
107
120
|
: await readStoredOpenRouterApiKey(openRouterStore).catch(() => ({ value: undefined }));
|
|
108
|
-
const
|
|
121
|
+
const openAICodexAuthState = await getOpenAICodexAuthState(storage);
|
|
109
122
|
return {
|
|
110
|
-
openai:
|
|
111
|
-
? { configured:
|
|
112
|
-
: {
|
|
123
|
+
openai: openAICodexAuthState === 'missing'
|
|
124
|
+
? { configured: false, source: undefined }
|
|
125
|
+
: {
|
|
126
|
+
configured: true,
|
|
127
|
+
source: 'pi-auth',
|
|
128
|
+
refreshFailed: openAICodexAuthState === 'refresh-failed',
|
|
129
|
+
},
|
|
113
130
|
gemini: envApiKeys.gemini
|
|
114
131
|
? { configured: true, source: 'env' }
|
|
115
132
|
: {
|
package/dist/config.js
CHANGED
|
@@ -1,13 +1,35 @@
|
|
|
1
|
-
import { getStoredGeminiApiKey, getStoredOpenRouterApiKey,
|
|
1
|
+
import { getOpenAICodexAuthState, getStoredGeminiApiKey, getStoredOpenRouterApiKey, PI_AUTH_API_KEY, } from './auth.js';
|
|
2
2
|
import { getEnvApiKeys } from './env.js';
|
|
3
|
-
const REQUIRED_CLI_AUTH_PROVIDERS = [
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
const REQUIRED_CLI_AUTH_PROVIDERS = [{ provider: 'openai', label: 'OpenAI' }];
|
|
4
|
+
const ALL_REVIEW_FOCUSES = [
|
|
5
|
+
'general',
|
|
6
|
+
'quality',
|
|
7
|
+
'efficiency',
|
|
8
|
+
'tests',
|
|
9
|
+
'standards',
|
|
6
10
|
];
|
|
11
|
+
const OPENROUTER_REVIEW_MODELS = ['deepseek', 'zai'];
|
|
12
|
+
const CLI_GEMINI_REVIEW_MODEL = 'gemini-3.5-flash';
|
|
13
|
+
function createCliGeminiCredential(apiKey) {
|
|
14
|
+
return {
|
|
15
|
+
apiKey,
|
|
16
|
+
modelName: CLI_GEMINI_REVIEW_MODEL,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
function createCliGeminiSummaryModel(credential) {
|
|
20
|
+
if (!credential) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
provider: 'gemini',
|
|
25
|
+
credential,
|
|
26
|
+
modelName: CLI_GEMINI_REVIEW_MODEL,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
7
29
|
export async function getApiKeys(env = process.env, auth = {
|
|
8
30
|
getStoredGeminiApiKey,
|
|
9
31
|
getStoredOpenRouterApiKey,
|
|
10
|
-
|
|
32
|
+
getOpenAICodexAuthState,
|
|
11
33
|
}) {
|
|
12
34
|
const envApiKeys = getEnvApiKeys(env);
|
|
13
35
|
const storedGeminiApiKey = envApiKeys.gemini
|
|
@@ -16,36 +38,60 @@ export async function getApiKeys(env = process.env, auth = {
|
|
|
16
38
|
const storedOpenRouterApiKey = envApiKeys.openrouter
|
|
17
39
|
? undefined
|
|
18
40
|
: await auth.getStoredOpenRouterApiKey().catch(() => undefined);
|
|
19
|
-
const
|
|
41
|
+
const openAICodexAuthState = await auth.getOpenAICodexAuthState();
|
|
20
42
|
const openRouterApiKey = envApiKeys.openrouter || storedOpenRouterApiKey;
|
|
21
43
|
const apiKeys = {};
|
|
22
44
|
const geminiApiKey = envApiKeys.gemini || storedGeminiApiKey;
|
|
23
45
|
if (geminiApiKey) {
|
|
24
|
-
apiKeys.gemini = geminiApiKey;
|
|
46
|
+
apiKeys.gemini = createCliGeminiCredential(geminiApiKey);
|
|
25
47
|
}
|
|
26
48
|
if (openRouterApiKey) {
|
|
27
49
|
apiKeys.deepseek = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
|
|
28
50
|
apiKeys.zai = { apiKey: openRouterApiKey, piProviderId: 'openrouter' };
|
|
29
51
|
}
|
|
30
|
-
if (
|
|
52
|
+
if (openAICodexAuthState === 'ready') {
|
|
31
53
|
apiKeys.openai = PI_AUTH_API_KEY;
|
|
32
54
|
}
|
|
33
55
|
return apiKeys;
|
|
34
56
|
}
|
|
35
57
|
export function getMissingRequiredCliAuthProviders(status) {
|
|
36
|
-
return REQUIRED_CLI_AUTH_PROVIDERS.filter(({ provider }) =>
|
|
58
|
+
return REQUIRED_CLI_AUTH_PROVIDERS.filter(({ provider }) => {
|
|
59
|
+
const providerStatus = status[provider];
|
|
60
|
+
return !providerStatus.configured || providerStatus.refreshFailed === true;
|
|
61
|
+
}).map(({ label }) => label);
|
|
37
62
|
}
|
|
38
63
|
export function getMissingRequiredCliReviewAuthProviders(apiKeys) {
|
|
39
64
|
return REQUIRED_CLI_AUTH_PROVIDERS.filter(({ provider }) => {
|
|
40
65
|
switch (provider) {
|
|
41
66
|
case 'openai':
|
|
42
67
|
return !apiKeys.openai;
|
|
43
|
-
case 'openrouter':
|
|
44
|
-
return !(apiKeys.deepseek && apiKeys.zai);
|
|
45
68
|
}
|
|
46
69
|
return false;
|
|
47
70
|
}).map(({ label }) => label);
|
|
48
71
|
}
|
|
49
72
|
export function formatMissingRequiredCliAuthProviders(missingProviders) {
|
|
50
|
-
return `${missingProviders.join(' and ')} not configured; run \`doistbot auth\``;
|
|
73
|
+
return `${missingProviders.join(' and ')} authentication unavailable (not configured, or the session could not be refreshed); run \`doistbot auth login\``;
|
|
74
|
+
}
|
|
75
|
+
export function getCliReviewRouting(apiKeys) {
|
|
76
|
+
const hasGemini = Boolean(apiKeys.gemini);
|
|
77
|
+
const hasOpenRouter = Boolean(apiKeys.deepseek && apiKeys.zai);
|
|
78
|
+
if (hasGemini && hasOpenRouter) {
|
|
79
|
+
return {
|
|
80
|
+
reviewModelsEnabled: ['openai', ...OPENROUTER_REVIEW_MODELS],
|
|
81
|
+
geminiFallbackFocuses: ALL_REVIEW_FOCUSES,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
const focusModels = [
|
|
85
|
+
'openai',
|
|
86
|
+
...(hasGemini ? ['gemini'] : []),
|
|
87
|
+
...(hasOpenRouter ? OPENROUTER_REVIEW_MODELS : []),
|
|
88
|
+
];
|
|
89
|
+
return {
|
|
90
|
+
reviewModelsEnabled: focusModels,
|
|
91
|
+
reviewModelsByFocus: Object.fromEntries(ALL_REVIEW_FOCUSES.map((focus) => [focus, focusModels])),
|
|
92
|
+
geminiFallbackFocuses: [],
|
|
93
|
+
...(hasGemini && !hasOpenRouter
|
|
94
|
+
? { reviewSummaryModel: createCliGeminiSummaryModel(apiKeys.gemini) }
|
|
95
|
+
: {}),
|
|
96
|
+
};
|
|
51
97
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doist/doistbot-cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Local self-review CLI for Doistbot",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -42,8 +42,8 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@inquirer/checkbox": "5.2.1",
|
|
44
44
|
"@inquirer/password": "5.1.1",
|
|
45
|
-
"@earendil-works/pi-ai": "0.
|
|
46
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
45
|
+
"@earendil-works/pi-ai": "0.80.6",
|
|
46
|
+
"@earendil-works/pi-coding-agent": "0.80.6",
|
|
47
47
|
"@napi-rs/keyring": "1.3.0",
|
|
48
48
|
"ajv": "8.20.0",
|
|
49
49
|
"chalk": "4.1.2",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Logger } from 'doistbot-logging';
|
|
2
2
|
import type { Octokit } from 'octokit';
|
|
3
3
|
export declare const CONFIG_FILE_PATH = ".github/doistbot.yml";
|
|
4
|
-
export declare const DEFAULT_REVIEW_MODELS: readonly ["
|
|
4
|
+
export declare const DEFAULT_REVIEW_MODELS: readonly ["openai", "deepseek", "zai"];
|
|
5
5
|
export type DefaultReviewModel = (typeof DEFAULT_REVIEW_MODELS)[number];
|
|
6
6
|
export type RepoConfig = {
|
|
7
7
|
review?: {
|
|
@@ -2,7 +2,7 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { load as yamlLoad } from 'js-yaml';
|
|
4
4
|
export const CONFIG_FILE_PATH = '.github/doistbot.yml';
|
|
5
|
-
export const DEFAULT_REVIEW_MODELS = ['
|
|
5
|
+
export const DEFAULT_REVIEW_MODELS = ['openai', 'deepseek', 'zai'];
|
|
6
6
|
function parseRepoConfigContent(content) {
|
|
7
7
|
return yamlLoad(content);
|
|
8
8
|
}
|
package/sandbox/dist/core/pi.js
CHANGED
|
@@ -13,13 +13,15 @@ export const DEFAULT_PI_MODELS = {
|
|
|
13
13
|
deepseek: 'deepseek-v4-pro',
|
|
14
14
|
gemini: 'gemini-3.1-pro-preview',
|
|
15
15
|
zai: 'glm5.2',
|
|
16
|
-
openai: 'gpt-5.
|
|
16
|
+
openai: 'gpt-5.6-terra',
|
|
17
17
|
};
|
|
18
18
|
export const DEFAULT_OPENROUTER_MODELS = {
|
|
19
19
|
deepseek: 'deepseek/deepseek-v4-pro',
|
|
20
20
|
gemini: 'google/gemini-3.1-pro-preview',
|
|
21
21
|
zai: 'z-ai/glm-5.2',
|
|
22
22
|
};
|
|
23
|
+
// Gemini flash fallback model.
|
|
24
|
+
export const DEFAULT_GEMINI_FLASH_MODEL = 'gemini-3.5-flash';
|
|
23
25
|
export const MAX_PI_THINKING_BY_PROVIDER = {
|
|
24
26
|
deepseek: 'high',
|
|
25
27
|
gemini: 'high',
|
|
@@ -205,6 +207,15 @@ function extractAssistantMessageResult(message) {
|
|
|
205
207
|
? { error: errorMessage.trim() }
|
|
206
208
|
: null;
|
|
207
209
|
}
|
|
210
|
+
function readStopReason(message) {
|
|
211
|
+
if (!message || typeof message !== 'object') {
|
|
212
|
+
return undefined;
|
|
213
|
+
}
|
|
214
|
+
const stopReason = message.stopReason;
|
|
215
|
+
// pi's stopReason follows the PiStopReason contract; cast rather than
|
|
216
|
+
// re-validate against a runtime list that would duplicate the union.
|
|
217
|
+
return typeof stopReason === 'string' ? stopReason : undefined;
|
|
218
|
+
}
|
|
208
219
|
function maybeEmitProgress({ event, onProgress, lastThinkingAt, lastRespondingAt, lastToolUpdateAt, toolStartedAt, }) {
|
|
209
220
|
if (!onProgress) {
|
|
210
221
|
return;
|
|
@@ -352,6 +363,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
352
363
|
let stderr = '';
|
|
353
364
|
let lastAssistantText = '';
|
|
354
365
|
let lastAssistantError = '';
|
|
366
|
+
let lastAssistantStopReason;
|
|
355
367
|
let sessionHeaderSeen = false;
|
|
356
368
|
const lastThinkingAt = { value: 0 };
|
|
357
369
|
const lastRespondingAt = { value: 0 };
|
|
@@ -426,6 +438,14 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
426
438
|
}
|
|
427
439
|
}
|
|
428
440
|
if (parsed.type === 'message_end') {
|
|
441
|
+
// Read the stop reason regardless of whether the
|
|
442
|
+
// message carried text or an error — a terminal
|
|
443
|
+
// "length"/"error"/"aborted" ending must surface
|
|
444
|
+
// even when there's no usable text.
|
|
445
|
+
const stopReason = readStopReason(parsed.message);
|
|
446
|
+
if (stopReason) {
|
|
447
|
+
lastAssistantStopReason = stopReason;
|
|
448
|
+
}
|
|
429
449
|
const result = extractAssistantMessageResult(parsed.message);
|
|
430
450
|
if (result?.text) {
|
|
431
451
|
lastAssistantText = result.text;
|
|
@@ -437,6 +457,10 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
437
457
|
}
|
|
438
458
|
if (parsed.type === 'agent_end' && Array.isArray(parsed.messages)) {
|
|
439
459
|
for (const message of parsed.messages) {
|
|
460
|
+
const stopReason = readStopReason(message);
|
|
461
|
+
if (stopReason) {
|
|
462
|
+
lastAssistantStopReason = stopReason;
|
|
463
|
+
}
|
|
440
464
|
const result = extractAssistantMessageResult(message);
|
|
441
465
|
if (result?.text) {
|
|
442
466
|
lastAssistantText = result.text;
|
|
@@ -475,6 +499,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
475
499
|
durationMs: Date.now() - startedAt,
|
|
476
500
|
timedOut,
|
|
477
501
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
502
|
+
stopReason: lastAssistantStopReason,
|
|
478
503
|
});
|
|
479
504
|
});
|
|
480
505
|
child.on('close', (code, signal) => {
|
|
@@ -489,6 +514,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
489
514
|
durationMs: Date.now() - startedAt,
|
|
490
515
|
timedOut: true,
|
|
491
516
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
517
|
+
stopReason: lastAssistantStopReason,
|
|
492
518
|
});
|
|
493
519
|
return;
|
|
494
520
|
}
|
|
@@ -499,6 +525,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
499
525
|
durationMs: Date.now() - startedAt,
|
|
500
526
|
timedOut: false,
|
|
501
527
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
528
|
+
stopReason: lastAssistantStopReason,
|
|
502
529
|
});
|
|
503
530
|
return;
|
|
504
531
|
}
|
|
@@ -509,6 +536,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
509
536
|
durationMs: Date.now() - startedAt,
|
|
510
537
|
timedOut: false,
|
|
511
538
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
539
|
+
stopReason: lastAssistantStopReason,
|
|
512
540
|
});
|
|
513
541
|
return;
|
|
514
542
|
}
|
|
@@ -522,6 +550,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
522
550
|
durationMs: Date.now() - startedAt,
|
|
523
551
|
timedOut: false,
|
|
524
552
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
553
|
+
stopReason: lastAssistantStopReason,
|
|
525
554
|
});
|
|
526
555
|
return;
|
|
527
556
|
}
|
|
@@ -530,6 +559,7 @@ export function createPiInvoker({ provider, apiKey, piProviderId, cwd, extraEnv,
|
|
|
530
559
|
output,
|
|
531
560
|
durationMs: Date.now() - startedAt,
|
|
532
561
|
toolUsage: toSortedToolUsage(toolUsageByName),
|
|
562
|
+
stopReason: lastAssistantStopReason,
|
|
533
563
|
});
|
|
534
564
|
});
|
|
535
565
|
});
|
|
@@ -1,15 +1,57 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
3
4
|
import { logger } from './logging.js';
|
|
5
|
+
const promptCache = new Map();
|
|
6
|
+
const resolveCache = new Map();
|
|
7
|
+
/** Swap the `dist` directory nearest to the file (not just the first `/dist/` in the path) for `src`. */
|
|
8
|
+
function distDirToSrcDir(dir) {
|
|
9
|
+
const distSeg = `${path.sep}dist${path.sep}`;
|
|
10
|
+
const idx = dir.lastIndexOf(distSeg);
|
|
11
|
+
if (idx === -1) {
|
|
12
|
+
return dir;
|
|
13
|
+
}
|
|
14
|
+
return `${dir.slice(0, idx)}${path.sep}src${path.sep}${dir.slice(idx + distSeg.length)}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a prompt file living in `<module>/prompts/`, relative to the calling
|
|
18
|
+
* module. At runtime the module lives under `dist/`, but the prompts are shipped
|
|
19
|
+
* under `src/`, so we map `dist`->`src`. The `dist` candidate is tried first so a
|
|
20
|
+
* future build step that copies prompts into `dist/` works without code changes.
|
|
21
|
+
*/
|
|
22
|
+
export function resolvePromptFile(moduleUrl, fileName) {
|
|
23
|
+
const byFileName = resolveCache.get(moduleUrl) ?? new Map();
|
|
24
|
+
resolveCache.set(moduleUrl, byFileName);
|
|
25
|
+
const cached = byFileName.get(fileName);
|
|
26
|
+
if (cached !== undefined) {
|
|
27
|
+
return cached;
|
|
28
|
+
}
|
|
29
|
+
const dir = path.dirname(fileURLToPath(moduleUrl));
|
|
30
|
+
const candidates = [
|
|
31
|
+
path.join(dir, 'prompts', fileName),
|
|
32
|
+
path.join(distDirToSrcDir(dir), 'prompts', fileName),
|
|
33
|
+
];
|
|
34
|
+
const resolved = candidates.find(fs.existsSync) ?? candidates[candidates.length - 1];
|
|
35
|
+
byFileName.set(fileName, resolved);
|
|
36
|
+
return resolved;
|
|
37
|
+
}
|
|
4
38
|
export function loadPromptTemplateFile({ fileName, promptLabel, }) {
|
|
5
|
-
|
|
39
|
+
if (!path.isAbsolute(fileName)) {
|
|
40
|
+
throw new Error(`${promptLabel} prompt template path must be absolute (resolve it with resolvePromptFile first), got: ${fileName}`);
|
|
41
|
+
}
|
|
42
|
+
const cached = promptCache.get(fileName);
|
|
43
|
+
if (cached !== undefined) {
|
|
44
|
+
return cached;
|
|
45
|
+
}
|
|
6
46
|
try {
|
|
7
|
-
|
|
47
|
+
const contents = fs.readFileSync(fileName, 'utf8');
|
|
48
|
+
promptCache.set(fileName, contents);
|
|
49
|
+
return contents;
|
|
8
50
|
}
|
|
9
51
|
catch (error) {
|
|
10
52
|
logger.error(`Failed to load ${promptLabel} prompt template`, {
|
|
11
53
|
error: error instanceof Error ? error.message : String(error),
|
|
12
|
-
promptPath,
|
|
54
|
+
promptPath: fileName,
|
|
13
55
|
});
|
|
14
56
|
throw error;
|
|
15
57
|
}
|
|
@@ -64,6 +64,16 @@ const TODOIST_REPOSITORIES = [
|
|
|
64
64
|
product: 'Todoist',
|
|
65
65
|
surfaces: ['android', 'ios'],
|
|
66
66
|
}),
|
|
67
|
+
createRepositoryTarget({
|
|
68
|
+
repo: 'todoist-mcp',
|
|
69
|
+
product: 'Todoist',
|
|
70
|
+
surfaces: ['mcp'],
|
|
71
|
+
}),
|
|
72
|
+
createRepositoryTarget({
|
|
73
|
+
repo: 'todoist-cli',
|
|
74
|
+
product: 'Todoist',
|
|
75
|
+
surfaces: ['cli'],
|
|
76
|
+
}),
|
|
67
77
|
];
|
|
68
78
|
const TWIST_REPOSITORIES = [
|
|
69
79
|
createRepositoryTarget({
|
|
@@ -126,6 +136,16 @@ const TWIST_REPOSITORIES = [
|
|
|
126
136
|
product: 'Twist',
|
|
127
137
|
surfaces: ['desktop'],
|
|
128
138
|
}),
|
|
139
|
+
createRepositoryTarget({
|
|
140
|
+
repo: 'twist-ai',
|
|
141
|
+
product: 'Twist',
|
|
142
|
+
surfaces: ['mcp'],
|
|
143
|
+
}),
|
|
144
|
+
createRepositoryTarget({
|
|
145
|
+
repo: 'twist-cli',
|
|
146
|
+
product: 'Twist',
|
|
147
|
+
surfaces: ['cli'],
|
|
148
|
+
}),
|
|
129
149
|
];
|
|
130
150
|
function dedupeRepositories(repositories) {
|
|
131
151
|
const unique = new Map();
|
|
@@ -145,8 +165,10 @@ export const APP_TO_REPO_SELECTORS = {
|
|
|
145
165
|
all: (repositories) => dedupeRepositories(repositories),
|
|
146
166
|
android: (repositories) => selectBySurface(repositories, ['android']),
|
|
147
167
|
backend: (repositories) => selectBySurface(repositories, ['backend']),
|
|
168
|
+
cli: (repositories) => selectBySurface(repositories, ['cli']),
|
|
148
169
|
desktop: (repositories) => selectBySurface(repositories, ['desktop']),
|
|
149
170
|
ios: (repositories) => selectBySurface(repositories, ['ios']),
|
|
171
|
+
mcp: (repositories) => selectBySurface(repositories, ['mcp']),
|
|
150
172
|
web: (repositories) => selectBySurface(repositories, ['web']),
|
|
151
173
|
};
|
|
152
174
|
export const TEAM_TO_REPO_SELECTORS = {
|
|
@@ -179,8 +201,10 @@ export const APP_LABEL_ALIASES = {
|
|
|
179
201
|
all: 'all',
|
|
180
202
|
android: 'android',
|
|
181
203
|
backend: 'backend',
|
|
204
|
+
cli: 'cli',
|
|
182
205
|
desktop: 'desktop',
|
|
183
206
|
ios: 'ios',
|
|
207
|
+
mcp: 'mcp',
|
|
184
208
|
web: 'web',
|
|
185
209
|
};
|
|
186
210
|
export const TEAM_LABEL_ALIASES = {
|
|
@@ -69,3 +69,12 @@ export function recordDecision(name, meta) {
|
|
|
69
69
|
export function recordEvent(name, meta) {
|
|
70
70
|
emitPointInTime('event', name, meta);
|
|
71
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Maps a doistbot provider key to the OTel `gen_ai.provider.name` value
|
|
74
|
+
* (docs/sandbox-instrumentation.md). Only `gemini` has a standard name
|
|
75
|
+
* (`gcp.gemini`); the OpenRouter-routed keys (`zai`, `deepseek`) have none, so
|
|
76
|
+
* they pass through. Single source so every span site stays consistent.
|
|
77
|
+
*/
|
|
78
|
+
export function genAiProviderName(provider) {
|
|
79
|
+
return provider === 'gemini' ? 'gcp.gemini' : provider;
|
|
80
|
+
}
|