@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.
- package/dist/actions/auth.js +225 -0
- package/dist/actions/doctor.js +39 -0
- package/dist/actions/review-usage.js +25 -0
- package/dist/actions/review.js +311 -0
- package/dist/actions/setup.js +14 -0
- package/dist/auth.js +202 -0
- package/dist/config.js +145 -0
- package/dist/env.js +10 -0
- package/dist/file-store.js +112 -0
- package/dist/index.js +164 -0
- package/dist/input.js +40 -0
- package/dist/runtime.js +105 -0
- package/dist/secure-store.js +148 -0
- package/dist/terminal.js +185 -0
- package/dist/test-utils/secure-store-mocks.js +29 -0
- package/dist/update-cache-entrypoint.js +16 -0
- package/dist/update-check.js +235 -0
- package/package.json +65 -5
- package/sandbox/_node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/_node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/_node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/_node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/_node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/dist/core/autofix-resources.js +58 -0
- package/sandbox/dist/core/autofix-service-query.js +69 -0
- package/sandbox/dist/core/autofix-test-policy.js +11 -0
- package/sandbox/dist/core/autofix-verification.js +355 -0
- package/sandbox/dist/core/bot-logins.js +14 -0
- package/sandbox/dist/core/check-run.js +158 -0
- package/sandbox/dist/core/datadog-metrics.js +113 -0
- package/sandbox/dist/core/environment.js +49 -0
- package/sandbox/dist/core/git-auth.js +17 -0
- package/sandbox/dist/core/github-history.js +4 -0
- package/sandbox/dist/core/github-issues.js +18 -0
- package/sandbox/dist/core/label-parsing.js +1 -0
- package/sandbox/dist/core/langfuse-observation.js +33 -0
- package/sandbox/dist/core/langfuse-session.js +39 -0
- package/sandbox/dist/core/logger-span-processor.js +90 -0
- package/sandbox/dist/core/logging.js +18 -0
- package/sandbox/dist/core/pi-autofix-extension.js +188 -0
- package/sandbox/dist/core/pi-execution-limits-extension.js +79 -0
- package/sandbox/dist/core/pi-progress-logging.js +51 -0
- package/sandbox/dist/core/pi-tool-output-filter-extension.js +65 -0
- package/sandbox/dist/core/pi.js +870 -0
- package/sandbox/dist/core/pii-scrubber.js +31 -0
- package/sandbox/dist/core/prompt-template.js +63 -0
- package/sandbox/dist/core/reactions.js +36 -0
- package/sandbox/dist/core/regexp.js +3 -0
- package/sandbox/dist/core/repository-map.js +1 -0
- package/sandbox/dist/core/repository-resolver.js +1 -0
- package/sandbox/dist/core/retry.js +29 -0
- package/sandbox/dist/core/s3-utils.js +42 -0
- package/sandbox/dist/core/shared.js +124 -0
- package/sandbox/dist/core/snapshot-helpers.js +17 -0
- package/sandbox/dist/core/span-test-helpers.js +13 -0
- package/sandbox/dist/core/tool-environment.js +83 -0
- package/sandbox/dist/core/tool-output.js +18 -0
- package/sandbox/dist/core/tracing-setup.js +92 -0
- package/sandbox/dist/core/tracing.js +81 -0
- package/sandbox/dist/main.js +51 -0
- package/sandbox/dist/providers/config.js +23 -0
- package/sandbox/dist/providers/credentials.js +40 -0
- package/sandbox/dist/providers/gemini.js +28 -0
- package/sandbox/dist/providers/helpers.js +253 -0
- package/sandbox/dist/providers/index.js +27 -0
- package/sandbox/dist/providers/openai.js +23 -0
- package/sandbox/dist/providers/openrouter-pi.js +25 -0
- package/sandbox/dist/providers/pi-review.js +301 -0
- package/sandbox/dist/providers/process.js +34 -0
- package/sandbox/dist/providers/schemas.js +61 -0
- package/sandbox/dist/providers/types.js +4 -0
- package/sandbox/dist/tasks/chat/chat.js +440 -0
- package/sandbox/dist/tasks/chat/prompt.js +20 -0
- package/sandbox/dist/tasks/issue-fix-retry/fix-retry.js +304 -0
- package/sandbox/dist/tasks/issue-summarize/context.js +21 -0
- package/sandbox/dist/tasks/issue-summarize/media.js +335 -0
- package/sandbox/dist/tasks/issue-summarize/model.js +230 -0
- package/sandbox/dist/tasks/issue-summarize/output.js +45 -0
- package/sandbox/dist/tasks/issue-summarize/prompt.js +11 -0
- package/sandbox/dist/tasks/issue-summarize/summarize.js +238 -0
- package/sandbox/dist/tasks/issue-summarize/thread.js +123 -0
- package/sandbox/dist/tasks/issue-summarize/types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/assignee-review.js +114 -0
- package/sandbox/dist/tasks/issue-triage/autofix-capabilities.js +22 -0
- package/sandbox/dist/tasks/issue-triage/autofix-checkpoint.js +122 -0
- package/sandbox/dist/tasks/issue-triage/autofix-policy.js +36 -0
- package/sandbox/dist/tasks/issue-triage/clone.js +40 -0
- package/sandbox/dist/tasks/issue-triage/context.js +57 -0
- package/sandbox/dist/tasks/issue-triage/diff-validator.js +102 -0
- package/sandbox/dist/tasks/issue-triage/fix-attempt.js +617 -0
- package/sandbox/dist/tasks/issue-triage/fix-context.js +5 -0
- package/sandbox/dist/tasks/issue-triage/fix-dispatch.js +263 -0
- package/sandbox/dist/tasks/issue-triage/fix-failure-reasons.js +2 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.js +526 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/effects.types.js +1 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/fake-effects.js +65 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/promotion-task.js +108 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/recovery.js +512 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/runners.js +233 -0
- package/sandbox/dist/tasks/issue-triage/fix-loop/state-machine.js +171 -0
- package/sandbox/dist/tasks/issue-triage/github-request.js +2 -0
- package/sandbox/dist/tasks/issue-triage/hero-group-map.js +206 -0
- package/sandbox/dist/tasks/issue-triage/markers.js +6 -0
- package/sandbox/dist/tasks/issue-triage/model.js +124 -0
- package/sandbox/dist/tasks/issue-triage/output.js +164 -0
- package/sandbox/dist/tasks/issue-triage/pr-creator.js +585 -0
- package/sandbox/dist/tasks/issue-triage/pr-readiness.js +25 -0
- package/sandbox/dist/tasks/issue-triage/prompt.js +63 -0
- package/sandbox/dist/tasks/issue-triage/repo-config.js +47 -0
- package/sandbox/dist/tasks/issue-triage/repo-prompts.js +77 -0
- package/sandbox/dist/tasks/issue-triage/report-fixture.js +53 -0
- package/sandbox/dist/tasks/issue-triage/report-history.js +113 -0
- package/sandbox/dist/tasks/issue-triage/report-schema.js +63 -0
- package/sandbox/dist/tasks/issue-triage/report.js +111 -0
- package/sandbox/dist/tasks/issue-triage/resolution.js +30 -0
- package/sandbox/dist/tasks/issue-triage/routing.js +169 -0
- package/sandbox/dist/tasks/issue-triage/sandbox-environment.js +59 -0
- package/sandbox/dist/tasks/issue-triage/shared.js +86 -0
- package/sandbox/dist/tasks/issue-triage/side-effects.js +102 -0
- package/sandbox/dist/tasks/issue-triage/triage.js +442 -0
- package/sandbox/dist/tasks/issue-triage/types.js +1 -0
- package/sandbox/dist/tasks/persist-logs/context.js +19 -0
- package/sandbox/dist/tasks/persist-logs/dd-proxy.js +116 -0
- package/sandbox/dist/tasks/persist-logs/llm-query-planner.js +156 -0
- package/sandbox/dist/tasks/persist-logs/persist-logs.js +491 -0
- package/sandbox/dist/tasks/refresh-s3-urls/refresh-s3-urls.js +214 -0
- package/sandbox/dist/tasks/review/check-run.js +326 -0
- package/sandbox/dist/tasks/review/comment-mapper.js +46 -0
- package/sandbox/dist/tasks/review/conversation-context.js +477 -0
- package/sandbox/dist/tasks/review/diff.js +95 -0
- package/sandbox/dist/tasks/review/engines/dedupe.js +291 -0
- package/sandbox/dist/tasks/review/engines/multi-focus.js +419 -0
- package/sandbox/dist/tasks/review/engines/shared.js +87 -0
- package/sandbox/dist/tasks/review/engines/types.js +1 -0
- package/sandbox/dist/tasks/review/finding-output.js +18 -0
- package/sandbox/dist/tasks/review/format.js +17 -0
- package/sandbox/dist/tasks/review/github-comment-format.js +94 -0
- package/sandbox/dist/tasks/review/local-git.js +374 -0
- package/sandbox/dist/tasks/review/local.js +118 -0
- package/sandbox/dist/tasks/review/multi-focus-prompt.js +59 -0
- package/sandbox/dist/tasks/review/prompt.js +81 -0
- package/sandbox/dist/tasks/review/review-feedback.js +48 -0
- package/sandbox/dist/tasks/review/review-mode-github.js +58 -0
- package/sandbox/dist/tasks/review/review-mode-policy.js +232 -0
- package/sandbox/dist/tasks/review/review-mode-types.js +1 -0
- package/sandbox/dist/tasks/review/review-mode.js +296 -0
- package/sandbox/dist/tasks/review/review-summary.js +123 -0
- package/sandbox/dist/tasks/review/review.js +391 -0
- package/sandbox/dist/tasks/review/runner.js +255 -0
- package/sandbox/dist/tasks/review/severity.js +5 -0
- package/sandbox/dist/tasks/review/summary-model.js +271 -0
- package/sandbox/dist/tasks/review/thinking-level.js +14 -0
- package/sandbox/dist/tasks/review/translation-review-policy.js +37 -0
- package/sandbox/dist/tasks/review/types.js +1 -0
- package/sandbox/dist/tasks/review-slice-plan/context.js +66 -0
- package/sandbox/dist/tasks/review-slice-plan/plan.js +210 -0
- package/sandbox/dist/tasks/review-slice-plan/prompt.js +59 -0
- package/sandbox/dist/tasks/review-slice-plan/review-slice-plan.js +367 -0
- package/sandbox/dist/tasks/sentry-triage/store.js +133 -0
- package/sandbox/dist/tasks/sentry-triage/triage.js +217 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.d.ts +23 -0
- package/sandbox/node_modules/doistbot-logging/dist/index.js +51 -0
- package/sandbox/node_modules/doistbot-logging/package.json +12 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.d.ts +93 -0
- package/sandbox/node_modules/doistbot-repo-config/dist/index.js +197 -0
- package/sandbox/node_modules/doistbot-repo-config/package.json +12 -0
- package/sandbox/src/review/prompts/ai-internal-tools.md +254 -0
- package/sandbox/src/review/prompts/backend-repository-standards.md +102 -0
- package/sandbox/src/review/prompts/observability-standard.md +169 -0
- package/sandbox/src/review/prompts/outline-map.json +26 -0
- package/sandbox/src/review/prompts/secrets-management.md +286 -0
- package/sandbox/src/review/prompts/service-production-readiness.md +122 -0
- package/sandbox/src/review/prompts/workload-placement.md +345 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/efficiency.md +16 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/general.md +32 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/quality.md +20 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/reuse.md +13 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/security.md +18 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/standards.md +21 -0
- package/sandbox/src/tasks/review/prompts/review-focus-prompts/tests.md +19 -0
- package/sandbox/src/tasks/review/prompts/review-mode.md +40 -0
- package/sandbox/src/tasks/review/prompts/review-multi-focus-base-prompt.md +142 -0
- package/scripts/restore-runtime-assets.mjs +21 -0
- package/README.md +0 -3
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { withLangfuseTaskContext } from './core/langfuse-session.js';
|
|
2
|
+
import { setupTracing } from './core/tracing-setup.js';
|
|
3
|
+
// Register the OTel SDK before any task module loads — withPhase/startToolSpan
|
|
4
|
+
// are no-ops until a tracer provider is registered.
|
|
5
|
+
setupTracing();
|
|
6
|
+
const taskType = process.env.TASK_TYPE ?? 'review';
|
|
7
|
+
// Tasks run (or kick off) on import, so the wrapper's async context flows into
|
|
8
|
+
// every span the task creates.
|
|
9
|
+
await withLangfuseTaskContext(async () => {
|
|
10
|
+
switch (taskType) {
|
|
11
|
+
case 'chat':
|
|
12
|
+
await import('./tasks/chat/chat.js');
|
|
13
|
+
break;
|
|
14
|
+
case 'issue-summarize':
|
|
15
|
+
await import('./tasks/issue-summarize/summarize.js');
|
|
16
|
+
break;
|
|
17
|
+
case 'sentry-triage': {
|
|
18
|
+
const { main } = await import('./tasks/sentry-triage/triage.js');
|
|
19
|
+
await main();
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
case 'issue-triage':
|
|
23
|
+
await import('./tasks/issue-triage/triage.js');
|
|
24
|
+
break;
|
|
25
|
+
case 'issue-fix-retry':
|
|
26
|
+
await import('./tasks/issue-fix-retry/fix-retry.js');
|
|
27
|
+
break;
|
|
28
|
+
case 'fix-loop-promotion':
|
|
29
|
+
await import('./tasks/issue-triage/fix-loop/promotion-task.js');
|
|
30
|
+
break;
|
|
31
|
+
case 'persist-logs': {
|
|
32
|
+
const { main } = await import('./tasks/persist-logs/persist-logs.js');
|
|
33
|
+
await main();
|
|
34
|
+
break;
|
|
35
|
+
}
|
|
36
|
+
case 'refresh-s3-urls': {
|
|
37
|
+
const { main } = await import('./tasks/refresh-s3-urls/refresh-s3-urls.js');
|
|
38
|
+
await main();
|
|
39
|
+
break;
|
|
40
|
+
}
|
|
41
|
+
case 'review':
|
|
42
|
+
await import('./tasks/review/review.js');
|
|
43
|
+
break;
|
|
44
|
+
case 'review-slice-plan':
|
|
45
|
+
await import('./tasks/review-slice-plan/review-slice-plan.js');
|
|
46
|
+
break;
|
|
47
|
+
default:
|
|
48
|
+
process.stderr.write(`Unknown TASK_TYPE: ${taskType}\n`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export const DEFAULT_PROVIDER_TIMEOUT_MS = 10 * 60 * 1000;
|
|
2
|
+
function parseTimeoutMs(value) {
|
|
3
|
+
if (!value) {
|
|
4
|
+
return null;
|
|
5
|
+
}
|
|
6
|
+
const parsed = Number.parseInt(value, 10);
|
|
7
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
8
|
+
return null;
|
|
9
|
+
}
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
export function resolveProviderTimeoutMs(explicitTimeoutMs) {
|
|
13
|
+
if (typeof explicitTimeoutMs === 'number' &&
|
|
14
|
+
Number.isFinite(explicitTimeoutMs) &&
|
|
15
|
+
explicitTimeoutMs > 0) {
|
|
16
|
+
return explicitTimeoutMs;
|
|
17
|
+
}
|
|
18
|
+
const sharedTimeout = parseTimeoutMs(process.env.REVIEW_MODEL_TIMEOUT_MS);
|
|
19
|
+
if (sharedTimeout != null) {
|
|
20
|
+
return sharedTimeout;
|
|
21
|
+
}
|
|
22
|
+
return DEFAULT_PROVIDER_TIMEOUT_MS;
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { parsePiAuthProviderId } from '../core/pi.js';
|
|
2
|
+
export function resolveReviewProviderCredential(credential) {
|
|
3
|
+
if (typeof credential === 'string') {
|
|
4
|
+
const apiKey = credential.trim();
|
|
5
|
+
if (!apiKey) {
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
const piAuthProviderId = parsePiAuthProviderId(apiKey);
|
|
9
|
+
return {
|
|
10
|
+
apiKey,
|
|
11
|
+
...(piAuthProviderId ? { piProviderId: piAuthProviderId } : {}),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
if (!credential) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
const apiKey = credential.apiKey.trim();
|
|
18
|
+
if (!apiKey) {
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
const piAuthProviderId = parsePiAuthProviderId(apiKey);
|
|
22
|
+
const piProviderId = piAuthProviderId ?? credential.piProviderId;
|
|
23
|
+
return {
|
|
24
|
+
apiKey,
|
|
25
|
+
...(piProviderId ? { piProviderId } : {}),
|
|
26
|
+
...(credential.modelName ? { modelName: credential.modelName } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
export function hasReviewProviderCredential(credential) {
|
|
30
|
+
return resolveReviewProviderCredential(credential) !== undefined;
|
|
31
|
+
}
|
|
32
|
+
export function getReviewProviderCredential(apiKeys, provider) {
|
|
33
|
+
return resolveReviewProviderCredential(apiKeys[provider]);
|
|
34
|
+
}
|
|
35
|
+
export function createOpenRouterCredential(apiKey) {
|
|
36
|
+
return {
|
|
37
|
+
apiKey,
|
|
38
|
+
piProviderId: 'openrouter',
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini provider for the multi-model code review system.
|
|
3
|
+
* Uses the Pi harness instead of the Gemini CLI.
|
|
4
|
+
*/
|
|
5
|
+
import { DEFAULT_OPENROUTER_MODELS, DEFAULT_PI_MODELS } from '../core/pi.js';
|
|
6
|
+
import { resolveProviderTimeoutMs } from './config.js';
|
|
7
|
+
import { extractJsonFromResponse } from './helpers.js';
|
|
8
|
+
import { runPiReviewProvider } from './pi-review.js';
|
|
9
|
+
export class GeminiProvider {
|
|
10
|
+
name = 'gemini';
|
|
11
|
+
async review(prompt, config) {
|
|
12
|
+
const modelName = config.modelName ??
|
|
13
|
+
(config.piProviderId === 'openrouter'
|
|
14
|
+
? DEFAULT_OPENROUTER_MODELS.gemini
|
|
15
|
+
: DEFAULT_PI_MODELS.gemini) ??
|
|
16
|
+
DEFAULT_PI_MODELS.gemini;
|
|
17
|
+
return await runPiReviewProvider({
|
|
18
|
+
provider: this.name,
|
|
19
|
+
modelName,
|
|
20
|
+
prompt,
|
|
21
|
+
config: {
|
|
22
|
+
...config,
|
|
23
|
+
timeout: resolveProviderTimeoutMs(config.timeout),
|
|
24
|
+
},
|
|
25
|
+
parseOutput: (output) => JSON.parse(extractJsonFromResponse(output)),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared helper functions for provider implementations.
|
|
3
|
+
*/
|
|
4
|
+
import { randomUUID } from 'crypto';
|
|
5
|
+
import { logger } from '../core/logging.js';
|
|
6
|
+
import { isRawReviewResult } from './schemas.js';
|
|
7
|
+
const RAW_MODEL_LOG_CHUNK_CHARS = 8000;
|
|
8
|
+
const REVIEW_LOG_ENGINE = 'multi_focus';
|
|
9
|
+
const PROVIDER_ERROR_DETAILS_MAX_DEPTH = 8;
|
|
10
|
+
function isRecord(value) {
|
|
11
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
12
|
+
}
|
|
13
|
+
function parseJsonValue(value) {
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(value.trim());
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return undefined;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function readProviderErrorDetails(error) {
|
|
22
|
+
const rawError = error instanceof Error ? error.message : String(error);
|
|
23
|
+
const details = { rawError };
|
|
24
|
+
function visit(value, depth) {
|
|
25
|
+
if (depth > PROVIDER_ERROR_DETAILS_MAX_DEPTH) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (typeof value === 'string') {
|
|
29
|
+
const trimmed = value.trim();
|
|
30
|
+
const parsed = parseJsonValue(trimmed);
|
|
31
|
+
if (isRecord(parsed)) {
|
|
32
|
+
visit(parsed, depth + 1);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (!details.reason && trimmed) {
|
|
36
|
+
details.reason = trimmed;
|
|
37
|
+
}
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!isRecord(value)) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (value.error !== undefined) {
|
|
44
|
+
visit(value.error, depth + 1);
|
|
45
|
+
}
|
|
46
|
+
if (typeof value.message === 'string') {
|
|
47
|
+
visit(value.message, depth + 1);
|
|
48
|
+
}
|
|
49
|
+
if (details.errorCode === undefined &&
|
|
50
|
+
(typeof value.code === 'number' || typeof value.code === 'string')) {
|
|
51
|
+
details.errorCode = value.code;
|
|
52
|
+
}
|
|
53
|
+
if (details.status === undefined && typeof value.status === 'string') {
|
|
54
|
+
details.status = value.status;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
visit(rawError, 0);
|
|
58
|
+
return details;
|
|
59
|
+
}
|
|
60
|
+
function redactSensitiveStrings(content) {
|
|
61
|
+
return content
|
|
62
|
+
.replace(/(sk-[A-Za-z0-9_-]{10,})/g, '[REDACTED_OPENAI_KEY]')
|
|
63
|
+
.replace(/(AIza[0-9A-Za-z_-]{20,})/g, '[REDACTED_GEMINI_KEY]')
|
|
64
|
+
.replace(/(anthropic_[A-Za-z0-9_-]{10,})/g, '[REDACTED_ANTHROPIC_KEY]')
|
|
65
|
+
.replace(/(Bearer\s+)[A-Za-z0-9._-]+/gi, '$1[REDACTED_TOKEN]');
|
|
66
|
+
}
|
|
67
|
+
function splitIntoChunks(content, chunkChars) {
|
|
68
|
+
if (!content) {
|
|
69
|
+
return [];
|
|
70
|
+
}
|
|
71
|
+
const chunks = [];
|
|
72
|
+
for (let index = 0; index < content.length; index += chunkChars) {
|
|
73
|
+
chunks.push(content.slice(index, index + chunkChars));
|
|
74
|
+
}
|
|
75
|
+
return chunks;
|
|
76
|
+
}
|
|
77
|
+
export function buildRawModelOutputEntries(content) {
|
|
78
|
+
const text = redactSensitiveStrings(content);
|
|
79
|
+
const chunks = splitIntoChunks(text, RAW_MODEL_LOG_CHUNK_CHARS);
|
|
80
|
+
const chunkCount = chunks.length;
|
|
81
|
+
return chunks.map((chunk, index) => ({
|
|
82
|
+
chunk,
|
|
83
|
+
chunkCount,
|
|
84
|
+
chunkIndex: index + 1,
|
|
85
|
+
loggedLength: text.length,
|
|
86
|
+
originalLength: content.length,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
export function logRawModelOutput({ model, stage, success, status, durationMs, stdout, stderr, }) {
|
|
90
|
+
const streams = [
|
|
91
|
+
{ name: 'stdout', value: stdout },
|
|
92
|
+
{ name: 'stderr', value: stderr },
|
|
93
|
+
];
|
|
94
|
+
for (const stream of streams) {
|
|
95
|
+
const output = stream.value ?? '';
|
|
96
|
+
if (!output) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const entries = buildRawModelOutputEntries(output);
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
logger.info('Review model raw output', {
|
|
102
|
+
model,
|
|
103
|
+
engine: REVIEW_LOG_ENGINE,
|
|
104
|
+
stage,
|
|
105
|
+
success,
|
|
106
|
+
status: status ?? null,
|
|
107
|
+
durationMs,
|
|
108
|
+
stream: stream.name,
|
|
109
|
+
chunkIndex: entry.chunkIndex,
|
|
110
|
+
chunkCount: entry.chunkCount,
|
|
111
|
+
originalLength: entry.originalLength,
|
|
112
|
+
loggedLength: entry.loggedLength,
|
|
113
|
+
output: entry.chunk,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Parses raw findings from a model response into normalized ReviewFinding objects.
|
|
120
|
+
*/
|
|
121
|
+
export function parseRawFindings(comments, modelName) {
|
|
122
|
+
return comments
|
|
123
|
+
.filter((comment) => {
|
|
124
|
+
const path = typeof comment.path === 'string' ? comment.path.trim() : '';
|
|
125
|
+
const line = Number(comment.line);
|
|
126
|
+
const body = typeof comment.body === 'string' ? comment.body.trim() : '';
|
|
127
|
+
return path !== '' && Number.isInteger(line) && body !== '';
|
|
128
|
+
})
|
|
129
|
+
.map((comment) => {
|
|
130
|
+
let side = 'RIGHT';
|
|
131
|
+
if (typeof comment.side === 'string') {
|
|
132
|
+
const upperSide = comment.side.toUpperCase();
|
|
133
|
+
if (upperSide === 'LEFT' || upperSide === 'RIGHT') {
|
|
134
|
+
side = upperSide;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return {
|
|
138
|
+
id: randomUUID(),
|
|
139
|
+
path: comment.path.trim(),
|
|
140
|
+
line: Number(comment.line),
|
|
141
|
+
side,
|
|
142
|
+
body: comment.body.trim(),
|
|
143
|
+
sourceModel: modelName,
|
|
144
|
+
...(comment.translationResolution
|
|
145
|
+
? { translationResolution: comment.translationResolution }
|
|
146
|
+
: {}),
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Normalizes structured model output into a single provider response shape.
|
|
152
|
+
*/
|
|
153
|
+
export function normalizeParsedModelOutput(parsed, modelName) {
|
|
154
|
+
if (isRawReviewResult(parsed)) {
|
|
155
|
+
return {
|
|
156
|
+
summary: parsed.summary,
|
|
157
|
+
findings: parseRawFindings(parsed.comments, modelName),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
throw new Error('Model output did not match review schema');
|
|
161
|
+
}
|
|
162
|
+
function extractFencedJsonCandidates(content) {
|
|
163
|
+
const matches = content.match(/```json\s*([\s\S]*?)\s*```/gi);
|
|
164
|
+
if (!matches) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
return matches
|
|
168
|
+
.map((block) => block
|
|
169
|
+
.replace(/^```json\s*/i, '')
|
|
170
|
+
.replace(/\s*```$/, '')
|
|
171
|
+
.trim())
|
|
172
|
+
.filter(Boolean);
|
|
173
|
+
}
|
|
174
|
+
function extractBalancedJsonCandidates(content) {
|
|
175
|
+
const candidates = [];
|
|
176
|
+
let depth = 0;
|
|
177
|
+
let start = -1;
|
|
178
|
+
let inString = false;
|
|
179
|
+
let escaping = false;
|
|
180
|
+
for (let index = 0; index < content.length; index++) {
|
|
181
|
+
const char = content[index];
|
|
182
|
+
if (char === undefined) {
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (inString) {
|
|
186
|
+
if (escaping) {
|
|
187
|
+
escaping = false;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (char === '\\') {
|
|
191
|
+
escaping = true;
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (char === '"') {
|
|
195
|
+
inString = false;
|
|
196
|
+
}
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (char === '"') {
|
|
200
|
+
inString = true;
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
if (char === '{') {
|
|
204
|
+
if (depth === 0) {
|
|
205
|
+
start = index;
|
|
206
|
+
}
|
|
207
|
+
depth += 1;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
if (char === '}') {
|
|
211
|
+
if (depth === 0) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
depth -= 1;
|
|
215
|
+
if (depth === 0 && start !== -1) {
|
|
216
|
+
candidates.push(content.slice(start, index + 1));
|
|
217
|
+
start = -1;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return candidates;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Returns JSON-looking candidates from a response string in parser preference order.
|
|
225
|
+
* Fenced JSON blocks are preferred over balanced objects because models often include
|
|
226
|
+
* code snippets or object-shaped prose before the final fenced response.
|
|
227
|
+
*/
|
|
228
|
+
export function extractJsonCandidatesFromResponse(content) {
|
|
229
|
+
return [
|
|
230
|
+
...extractFencedJsonCandidates(content).reverse(),
|
|
231
|
+
...extractBalancedJsonCandidates(content).reverse(),
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Extracts JSON from a response string that may contain additional text.
|
|
236
|
+
* Prefers valid fenced JSON blocks, then valid balanced JSON objects.
|
|
237
|
+
* Throws when no valid JSON candidate can be parsed.
|
|
238
|
+
*/
|
|
239
|
+
export function extractJsonFromResponse(content) {
|
|
240
|
+
for (const candidate of extractJsonCandidatesFromResponse(content)) {
|
|
241
|
+
if (!candidate) {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
JSON.parse(candidate);
|
|
246
|
+
return candidate;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
throw new Error('No valid JSON object found in model response');
|
|
253
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider factory and exports for the multi-model code review system.
|
|
3
|
+
*/
|
|
4
|
+
import { GeminiProvider } from './gemini.js';
|
|
5
|
+
import { OpenAIProvider } from './openai.js';
|
|
6
|
+
import { OpenRouterPiProvider } from './openrouter-pi.js';
|
|
7
|
+
export { GeminiProvider } from './gemini.js';
|
|
8
|
+
export { OpenAIProvider } from './openai.js';
|
|
9
|
+
export { OpenRouterPiProvider } from './openrouter-pi.js';
|
|
10
|
+
export * from './types.js';
|
|
11
|
+
const providers = {
|
|
12
|
+
deepseek: new OpenRouterPiProvider('deepseek'),
|
|
13
|
+
gemini: new GeminiProvider(),
|
|
14
|
+
grok: new OpenRouterPiProvider('grok'),
|
|
15
|
+
zai: new OpenRouterPiProvider('zai'),
|
|
16
|
+
openai: new OpenAIProvider(),
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Gets a provider instance by name.
|
|
20
|
+
*/
|
|
21
|
+
export function getProvider(name) {
|
|
22
|
+
const provider = providers[name];
|
|
23
|
+
if (!provider) {
|
|
24
|
+
throw new Error(`Unknown provider: ${name}`);
|
|
25
|
+
}
|
|
26
|
+
return provider;
|
|
27
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenAI provider for the multi-model code review system.
|
|
3
|
+
* Uses the Pi harness instead of the Codex CLI.
|
|
4
|
+
*/
|
|
5
|
+
import { DEFAULT_PI_MODELS } from '../core/pi.js';
|
|
6
|
+
import { resolveProviderTimeoutMs } from './config.js';
|
|
7
|
+
import { extractJsonFromResponse } from './helpers.js';
|
|
8
|
+
import { runPiReviewProvider } from './pi-review.js';
|
|
9
|
+
export class OpenAIProvider {
|
|
10
|
+
name = 'openai';
|
|
11
|
+
async review(prompt, config) {
|
|
12
|
+
return await runPiReviewProvider({
|
|
13
|
+
provider: this.name,
|
|
14
|
+
modelName: config.modelName ?? DEFAULT_PI_MODELS.openai,
|
|
15
|
+
prompt,
|
|
16
|
+
config: {
|
|
17
|
+
...config,
|
|
18
|
+
timeout: resolveProviderTimeoutMs(config.timeout),
|
|
19
|
+
},
|
|
20
|
+
parseOutput: (output) => JSON.parse(extractJsonFromResponse(output)),
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { DEFAULT_OPENROUTER_MODELS, DEFAULT_PI_MODELS } from '../core/pi.js';
|
|
2
|
+
import { resolveProviderTimeoutMs } from './config.js';
|
|
3
|
+
import { extractJsonFromResponse } from './helpers.js';
|
|
4
|
+
import { runPiReviewProvider } from './pi-review.js';
|
|
5
|
+
export class OpenRouterPiProvider {
|
|
6
|
+
name;
|
|
7
|
+
fallbackModelName;
|
|
8
|
+
constructor(name, fallbackModelName = DEFAULT_OPENROUTER_MODELS[name] ??
|
|
9
|
+
DEFAULT_PI_MODELS[name]) {
|
|
10
|
+
this.name = name;
|
|
11
|
+
this.fallbackModelName = fallbackModelName;
|
|
12
|
+
}
|
|
13
|
+
async review(prompt, config) {
|
|
14
|
+
return await runPiReviewProvider({
|
|
15
|
+
provider: this.name,
|
|
16
|
+
modelName: config.modelName ?? this.fallbackModelName,
|
|
17
|
+
prompt,
|
|
18
|
+
config: {
|
|
19
|
+
...config,
|
|
20
|
+
timeout: resolveProviderTimeoutMs(config.timeout),
|
|
21
|
+
},
|
|
22
|
+
parseOutput: (output) => JSON.parse(extractJsonFromResponse(output)),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
}
|