co-maintainer 0.5.0 → 0.5.1-beta.2
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/LICENSE +21 -21
- package/README.md +5 -3
- package/dist/package.json +1 -1
- package/dist/src/ai/anthropic.d.ts +55 -0
- package/dist/src/ai/anthropic.js +262 -0
- package/dist/src/ai/batch.d.ts +2 -2
- package/dist/src/ai/batch.js +1 -1
- package/dist/src/ai/estimate.d.ts +4 -0
- package/dist/src/ai/estimate.js +17 -2
- package/dist/src/ai/mermaid_loop.js +7 -4
- package/dist/src/ai/openai.d.ts +45 -0
- package/dist/src/ai/openai.js +206 -0
- package/dist/src/ai/opencode.d.ts +10 -0
- package/dist/src/ai/opencode.js +76 -0
- package/dist/src/ai/openrouter.d.ts +23 -7
- package/dist/src/ai/openrouter.js +39 -23
- package/dist/src/ai/pricing.js +1 -1
- package/dist/src/ai/provider.d.ts +9 -0
- package/dist/src/ai/provider.js +42 -3
- package/dist/src/ai/verify.d.ts +9 -1
- package/dist/src/ai/verify.js +81 -3
- package/dist/src/cli/args.d.ts +1 -2
- package/dist/src/cli/args.js +38 -26
- package/dist/src/cli/commands/config.d.ts +1 -1
- package/dist/src/cli/commands/config.js +10 -4
- package/dist/src/cli/commands/probe.js +3 -11
- package/dist/src/cli/commands/registry.d.ts +1 -9
- package/dist/src/cli/commands/registry.js +75 -10
- package/dist/src/cli/commands/review.js +18 -13
- package/dist/src/cli/commands/rollback.d.ts +1 -0
- package/dist/src/cli/commands/rollback.js +42 -0
- package/dist/src/cli/commands/serve.js +2 -3
- package/dist/src/cli/commands/set.js +71 -7
- package/dist/src/cli/commands/view.js +10 -9
- package/dist/src/cli/error.d.ts +4 -4
- package/dist/src/cli/error.js +4 -4
- package/dist/src/cli/main.d.ts +4 -4
- package/dist/src/cli/main.js +13 -8
- package/dist/src/cli/prompt.js +1 -1
- package/dist/src/cli/review_args.d.ts +9 -3
- package/dist/src/cli/review_args.js +28 -5
- package/dist/src/cli/review_github.d.ts +31 -0
- package/dist/src/cli/review_github.js +128 -0
- package/dist/src/cli/review_result.d.ts +27 -16
- package/dist/src/cli/review_result.js +7 -7
- package/dist/src/config.d.ts +28 -6
- package/dist/src/config.js +15 -1
- package/dist/src/git/default_branch.d.ts +1 -1
- package/dist/src/git/default_branch.js +1 -1
- package/dist/src/github/app.js +2 -2
- package/dist/src/github/app_manifest.d.ts +1 -7
- package/dist/src/github/app_manifest.js +2 -7
- package/dist/src/github/client.d.ts +4 -1
- package/dist/src/github/client.js +14 -2
- package/dist/src/github/gh.js +10 -7
- package/dist/src/github/pat.js +4 -4
- package/dist/src/github/permissions.d.ts +36 -0
- package/dist/src/github/permissions.js +334 -0
- package/dist/src/knowledge/facts.js +1 -1
- package/dist/src/knowledge/guide.d.ts +3 -3
- package/dist/src/knowledge/guide.js +5 -5
- package/dist/src/knowledge/probe.js +1 -2
- package/dist/src/knowledge/sections.d.ts +3 -3
- package/dist/src/knowledge/sections.js +3 -3
- package/dist/src/knowledge/skill.d.ts +1 -1
- package/dist/src/knowledge/skill.js +4 -4
- package/dist/src/knowledge/synthesis.d.ts +1 -1
- package/dist/src/knowledge/synthesis.js +5 -5
- package/dist/src/knowledge/types.d.ts +1 -1
- package/dist/src/local/codegraph_exclude.d.ts +1 -1
- package/dist/src/local/codegraph_exclude.js +1 -1
- package/dist/src/local/codegraph_prepare.js +1 -1
- package/dist/src/local/git_ops.d.ts +1 -1
- package/dist/src/local/git_ops.js +1 -1
- package/dist/src/local/git_parse.d.ts +2 -2
- package/dist/src/local/git_parse.js +2 -2
- package/dist/src/local/git_revision.d.ts +1 -1
- package/dist/src/local/git_revision.js +1 -1
- package/dist/src/local/git_untracked.d.ts +1 -1
- package/dist/src/local/git_untracked.js +1 -1
- package/dist/src/local/review_local.js +17 -14
- package/dist/src/local/review_lock.d.ts +1 -1
- package/dist/src/local/review_lock.js +1 -1
- package/dist/src/pr/checkout.js +2 -2
- package/dist/src/pr/findings.js +1 -1
- package/dist/src/pr/findings_json.d.ts +2 -2
- package/dist/src/pr/findings_json.js +1 -1
- package/dist/src/pr/github_revision.js +1 -1
- package/dist/src/pr/review_copy.d.ts +2 -2
- package/dist/src/pr/review_copy.js +2 -2
- package/dist/src/pr/reviewer.d.ts +6 -0
- package/dist/src/pr/reviewer.js +49 -39
- package/dist/src/remote/client.d.ts +4 -0
- package/dist/src/remote/client.js +84 -23
- package/dist/src/remote/fixtures.js +1 -1
- package/dist/src/remote/http.d.ts +10 -1
- package/dist/src/remote/http.js +15 -3
- package/dist/src/remote/schema.d.ts +3 -3
- package/dist/src/remote/schema.js +3 -3
- package/dist/src/remote/server/body.d.ts +1 -1
- package/dist/src/remote/server/body.js +1 -1
- package/dist/src/remote/server/byok_keys.d.ts +15 -0
- package/dist/src/remote/server/byok_keys.js +40 -0
- package/dist/src/remote/server/guides.js +2 -2
- package/dist/src/remote/server/routes.js +8 -1
- package/dist/src/remote/validate.js +15 -1
- package/dist/src/review/blocking.d.ts +2 -2
- package/dist/src/review/blocking.js +2 -2
- package/dist/src/review/carry_over.d.ts +2 -2
- package/dist/src/review/carry_over.js +2 -2
- package/dist/src/review/guides.js +1 -1
- package/dist/src/server/api/remote_tokens.js +2 -0
- package/dist/src/server/api/repos.js +51 -0
- package/dist/src/server/api/settings.js +10 -0
- package/dist/src/server/errors.d.ts +1 -1
- package/dist/src/server/http.js +1 -1
- package/dist/src/server/pages/activity.d.ts +2 -0
- package/dist/src/server/pages/activity.js +18 -4
- package/dist/src/server/pages/add_repo.js +61 -1
- package/dist/src/server/pages/analytics.js +18 -8
- package/dist/src/server/pages/cost.d.ts +21 -0
- package/dist/src/server/pages/cost.js +72 -0
- package/dist/src/server/pages/pr.js +5 -4
- package/dist/src/server/pages/repo.js +3 -2
- package/dist/src/server/pages/repo_prs.js +3 -2
- package/dist/src/server/pages/repo_remote.js +3 -2
- package/dist/src/server/pages/repo_settings.js +326 -12
- package/dist/src/server/pages/router.js +3 -2
- package/dist/src/server/pages/settings.js +50 -6
- package/dist/src/server/pages/styles.d.ts +1 -1
- package/dist/src/server/pages/styles.js +1 -1
- package/dist/src/services/credentials.js +8 -11
- package/dist/src/services/dashboard.d.ts +22 -9
- package/dist/src/services/dashboard.js +16 -2
- package/dist/src/services/probe.js +1 -1
- package/dist/src/services/remote_review.js +142 -35
- package/dist/src/services/remote_review_abort.d.ts +5 -1
- package/dist/src/services/remote_review_abort.js +9 -1
- package/dist/src/services/review.d.ts +1 -1
- package/dist/src/services/review.js +19 -5
- package/dist/src/services/review_policy.d.ts +116 -0
- package/dist/src/services/review_policy.js +489 -0
- package/dist/src/services/setup.d.ts +4 -3
- package/dist/src/services/setup.js +18 -16
- package/dist/src/services/setup_checklist.js +2 -2
- package/dist/src/services/webhook.d.ts +23 -0
- package/dist/src/services/webhook.js +141 -34
- package/dist/src/store/app_db.d.ts +5 -0
- package/dist/src/store/app_db.js +69 -13
- package/dist/src/store/backup.d.ts +31 -0
- package/dist/src/store/backup.js +158 -0
- package/dist/src/store/migrations.d.ts +1 -1
- package/dist/src/store/migrations.js +21 -1
- package/dist/src/store/remote_review_inputs.d.ts +3 -0
- package/dist/src/store/remote_review_inputs.js +3 -3
- package/dist/src/store/remote_tokens.d.ts +1 -1
- package/dist/src/store/remote_tokens.js +1 -1
- package/dist/src/store/repos.d.ts +2 -2
- package/dist/src/store/repos.js +1 -1
- package/dist/src/store/reviews.d.ts +21 -13
- package/dist/src/store/reviews.js +60 -15
- package/dist/src/store/rows.d.ts +10 -0
- package/dist/src/tools/codegraph.d.ts +4 -4
- package/dist/src/tools/codegraph.js +5 -5
- package/dist/src/tools/codegraph_exec.d.ts +1 -1
- package/dist/src/tools/codegraph_exec.js +1 -1
- package/dist/src/types.d.ts +12 -6
- package/dist/src/util/cost.d.ts +52 -0
- package/dist/src/util/cost.js +70 -0
- package/dist/src/util/log.d.ts +1 -1
- package/dist/src/util/log.js +2 -2
- package/dist/src/util/redact.js +2 -0
- package/dist/src/util/run_summary.d.ts +7 -2
- package/dist/src/util/run_summary.js +28 -2
- package/dist/src/util/time.d.ts +1 -1
- package/dist/src/util/time.js +1 -1
- package/package.json +1 -1
|
@@ -2,10 +2,10 @@ import type { RepoRow } from "./rows.ts";
|
|
|
2
2
|
export declare function activateRepo(fullName: string, installationId: number | undefined): void;
|
|
3
3
|
export declare function deactivateRepo(fullName: string): void;
|
|
4
4
|
export declare function deactivateReposForInstallation(installationId: number): void;
|
|
5
|
-
export declare function updateRepoSettings(fullName: string, patch: Partial<Pick<RepoRow, "auto_review" | "review_scope" | "skip_drafts" | "skip_bots" | "use_codegraph">>): void;
|
|
5
|
+
export declare function updateRepoSettings(fullName: string, patch: Partial<Pick<RepoRow, "auto_review" | "review_scope" | "skip_drafts" | "skip_bots" | "use_codegraph" | "review_policy_json">>): void;
|
|
6
6
|
export declare function markKnowledgeBuilt(fullName: string, baseSha: string): void;
|
|
7
7
|
export declare function setInstallationId(fullName: string, installationId: number): void;
|
|
8
8
|
export declare function getRepo(fullName: string): RepoRow | undefined;
|
|
9
|
-
/** Case-insensitive match on `repos.full_name
|
|
9
|
+
/** Case-insensitive match on `repos.full_name`. */
|
|
10
10
|
export declare function findRepoByFullName(fullName: string): RepoRow | undefined;
|
|
11
11
|
export declare function listActiveRepos(): RepoRow[];
|
package/dist/src/store/repos.js
CHANGED
|
@@ -46,7 +46,7 @@ export function getRepo(fullName) {
|
|
|
46
46
|
.prepare(`SELECT * FROM repos WHERE full_name = ?`)
|
|
47
47
|
.get(fullName);
|
|
48
48
|
}
|
|
49
|
-
/** Case-insensitive match on `repos.full_name
|
|
49
|
+
/** Case-insensitive match on `repos.full_name`. */
|
|
50
50
|
export function findRepoByFullName(fullName) {
|
|
51
51
|
return getAppDb()
|
|
52
52
|
.prepare(`SELECT * FROM repos WHERE lower(full_name) = lower(?)`)
|
|
@@ -26,18 +26,29 @@ export declare function insertReview(row: {
|
|
|
26
26
|
}): void;
|
|
27
27
|
/** Written immediately before and after the `POST .../reviews` call, with
|
|
28
28
|
* nothing else run between either write and the call. */
|
|
29
|
-
export declare function setReviewStatus(id: string, status: ReviewRow["status"], patch?: Partial<Pick<ReviewRow, "posted_review_id" | "findings_count" | "tokens_in" | "tokens_out" | "cost" | "duration_ms" | "check_run_id" | "posted_fallback" | "open_count" | "closed_count" | "subject_id" | "guide_built_at">>): void;
|
|
29
|
+
export declare function setReviewStatus(id: string, status: ReviewRow["status"], patch?: Partial<Pick<ReviewRow, "posted_review_id" | "findings_count" | "tokens_in" | "tokens_out" | "cost" | "cost_status" | "cost_note" | "billed_to" | "duration_ms" | "check_run_id" | "posted_fallback" | "open_count" | "closed_count" | "subject_id" | "guide_built_at">>): void;
|
|
30
30
|
export declare function setReviewScope(id: string, scope: string, baseSha: string): void;
|
|
31
31
|
export declare function getReview(id: string): ReviewRow | undefined;
|
|
32
32
|
export declare function getReviewByJobId(jobId: string): ReviewRow | undefined;
|
|
33
33
|
/** Newest round first. The PR detail page reads this directly. */
|
|
34
34
|
export declare function listReviewsForPr(repo: string, prNumber: number): ReviewRow[];
|
|
35
35
|
export declare function latestPostedReview(repo: string, prNumber: number): ReviewRow | undefined;
|
|
36
|
-
|
|
36
|
+
/** `cost` adds up only the costs that are known and billed to the server.
|
|
37
|
+
* A review with an unknown cost is counted in `unknownCount` instead of
|
|
38
|
+
* being added as zero. `knownCount` tells "no cost yet" apart from "every
|
|
39
|
+
* review cost exactly $0". BYOK is kept apart, never inside `cost`. */
|
|
40
|
+
export type CostTotals = {
|
|
41
|
+
cost: number;
|
|
42
|
+
knownCount: number;
|
|
43
|
+
unknownCount: number;
|
|
44
|
+
byokUsd: number;
|
|
45
|
+
byokKnownCount: number;
|
|
46
|
+
byokUnknownCount: number;
|
|
47
|
+
};
|
|
48
|
+
export type ReviewStats = CostTotals & {
|
|
37
49
|
reviews: number;
|
|
38
50
|
pullRequests: number;
|
|
39
51
|
findings: number;
|
|
40
|
-
cost: number;
|
|
41
52
|
tokensIn: number;
|
|
42
53
|
tokensOut: number;
|
|
43
54
|
failed: number;
|
|
@@ -49,22 +60,21 @@ export declare function reviewStats(sinceIso: string, repo?: string, untilIso?:
|
|
|
49
60
|
export declare function reviewStatsByRepo(sinceIso: string): (ReviewStats & {
|
|
50
61
|
repo: string;
|
|
51
62
|
})[];
|
|
52
|
-
export
|
|
63
|
+
export type DayStats = CostTotals & {
|
|
53
64
|
day: string;
|
|
54
65
|
reviews: number;
|
|
55
66
|
findings: number;
|
|
56
|
-
|
|
57
|
-
|
|
67
|
+
};
|
|
68
|
+
export declare function reviewStatsByDay(sinceIso: string): DayStats[];
|
|
58
69
|
export declare function reviewStatsByModel(sinceIso: string): (ReviewStats & {
|
|
59
70
|
model: string;
|
|
60
71
|
})[];
|
|
61
72
|
export declare function listRecentReviews(limit: number, offset?: number): ReviewRow[];
|
|
62
73
|
export declare function countReviews(): number;
|
|
63
|
-
export type PullSummary = {
|
|
74
|
+
export type PullSummary = CostTotals & {
|
|
64
75
|
pr_number: number;
|
|
65
76
|
last_reviewed: string;
|
|
66
77
|
findings: number;
|
|
67
|
-
cost: number;
|
|
68
78
|
review_count: number;
|
|
69
79
|
};
|
|
70
80
|
export declare function listPullSummaries(repo: string, limit: number, offset: number): {
|
|
@@ -72,15 +82,13 @@ export declare function listPullSummaries(repo: string, limit: number, offset: n
|
|
|
72
82
|
total: number;
|
|
73
83
|
};
|
|
74
84
|
export declare function listRemoteReviewsForRepo(repo: string, limit: number, offset?: number, sinceIso?: string): ReviewRow[];
|
|
75
|
-
export declare function remoteTokenUsageSince(tokenId: string, sinceIso: string): {
|
|
85
|
+
export declare function remoteTokenUsageSince(tokenId: string, sinceIso: string): CostTotals & {
|
|
76
86
|
reviews: number;
|
|
77
|
-
cost: number;
|
|
78
87
|
};
|
|
79
|
-
export declare function reviewStatsByRemoteToken(sinceIso: string): {
|
|
88
|
+
export declare function reviewStatsByRemoteToken(sinceIso: string): (CostTotals & {
|
|
80
89
|
tokenId: string;
|
|
81
90
|
tokenName: string;
|
|
82
91
|
reviews: number;
|
|
83
|
-
cost: number;
|
|
84
92
|
findings: number;
|
|
85
|
-
}[];
|
|
93
|
+
})[];
|
|
86
94
|
export declare function listLatestReviewsForRepo(repo: string, limit: number): ReviewRow[];
|
|
@@ -4,16 +4,20 @@ export function insertRemoteReview(row) {
|
|
|
4
4
|
getAppDb()
|
|
5
5
|
.prepare(`INSERT INTO reviews
|
|
6
6
|
(id, kind, subject_id, repo, pr_number, branch, token_id, token_name,
|
|
7
|
-
job_id, head_sha, base_sha, scope, model, status, created_at, round
|
|
8
|
-
|
|
7
|
+
job_id, head_sha, base_sha, scope, model, status, created_at, round,
|
|
8
|
+
cost_status, cost_note, billed_to)
|
|
9
|
+
VALUES (?, 'remote', ?, ?, NULL, ?, ?, ?, ?, NULL, NULL, ?, ?, 'queued', ?, 1,
|
|
10
|
+
'unknown', 'not_recorded', 'server')`)
|
|
9
11
|
.run(row.id, row.subjectId, row.repo, row.branch, row.tokenId, row.tokenName, row.jobId, row.scope, row.model, nowIso());
|
|
10
12
|
}
|
|
11
13
|
export function insertReview(row) {
|
|
12
14
|
getAppDb()
|
|
13
15
|
.prepare(`INSERT INTO reviews
|
|
14
16
|
(id, kind, subject_id, repo, pr_number, job_id, head_sha, base_sha, scope, model,
|
|
15
|
-
status, created_at, round, "trigger", guide_built_at
|
|
16
|
-
|
|
17
|
+
status, created_at, round, "trigger", guide_built_at,
|
|
18
|
+
cost_status, cost_note, billed_to)
|
|
19
|
+
VALUES (?, 'pr', ?, ?, ?, ?, ?, ?, ?, ?, 'drafting', ?, ?, ?, ?,
|
|
20
|
+
'unknown', 'not_recorded', 'server')`)
|
|
17
21
|
.run(row.id, row.subjectId ?? null, row.repo, row.prNumber, row.jobId, row.headSha, row.baseSha, row.scope, row.model, nowIso(), row.round ?? 1, row.trigger ?? null, row.guideBuiltAt ?? null);
|
|
18
22
|
}
|
|
19
23
|
/** Written immediately before and after the `POST .../reviews` call, with
|
|
@@ -28,13 +32,22 @@ export function setReviewStatus(id, status, patch = {}) {
|
|
|
28
32
|
tokens_in = COALESCE(?, tokens_in),
|
|
29
33
|
tokens_out = COALESCE(?, tokens_out),
|
|
30
34
|
cost = COALESCE(?, cost),
|
|
35
|
+
cost_status = COALESCE(?, cost_status),
|
|
36
|
+
cost_note = CASE WHEN ? THEN ? ELSE cost_note END,
|
|
37
|
+
billed_to = COALESCE(?, billed_to),
|
|
31
38
|
duration_ms = COALESCE(?, duration_ms),
|
|
32
39
|
check_run_id = COALESCE(?, check_run_id),
|
|
33
40
|
posted_fallback = COALESCE(?, posted_fallback),
|
|
34
41
|
subject_id = COALESCE(?, subject_id),
|
|
35
42
|
guide_built_at = COALESCE(?, guide_built_at)
|
|
36
43
|
WHERE id = ?`)
|
|
37
|
-
.run(status, patch.posted_review_id ?? null, patch.findings_count ?? null, patch.open_count ?? null, patch.closed_count ?? null, patch.tokens_in ?? null, patch.tokens_out ?? null, patch.cost ?? null, patch.
|
|
44
|
+
.run(status, patch.posted_review_id ?? null, patch.findings_count ?? null, patch.open_count ?? null, patch.closed_count ?? null, patch.tokens_in ?? null, patch.tokens_out ?? null, patch.cost ?? null, patch.cost_status ?? null,
|
|
45
|
+
// `cost_note` travels with `cost_status`: when the status changes to
|
|
46
|
+
// known, the note (e.g. a stale "not recorded yet") must clear to
|
|
47
|
+
// null, which `COALESCE(?, cost_note)` cannot do since a provided
|
|
48
|
+
// `null` and an omitted field bind the same way. The flag makes the
|
|
49
|
+
// two cases distinguishable.
|
|
50
|
+
patch.cost_status !== undefined, patch.cost_note ?? null, patch.billed_to ?? null, patch.duration_ms ?? null, patch.check_run_id ?? null, patch.posted_fallback ?? null, patch.subject_id ?? null, patch.guide_built_at ?? null, id);
|
|
38
51
|
}
|
|
39
52
|
export function setReviewScope(id, scope, baseSha) {
|
|
40
53
|
getAppDb()
|
|
@@ -64,13 +77,33 @@ export function latestPostedReview(repo, prNumber) {
|
|
|
64
77
|
ORDER BY round DESC, created_at DESC LIMIT 1`)
|
|
65
78
|
.get(repo, prNumber);
|
|
66
79
|
}
|
|
80
|
+
const COST_STATUS = `COALESCE(cost_status, CASE WHEN cost IS NULL THEN 'unknown' ELSE 'known' END)`;
|
|
81
|
+
const BILLED = `COALESCE(billed_to, 'server')`;
|
|
82
|
+
const bucket = (status, billed, value) => `COALESCE(SUM(CASE WHEN ${COST_STATUS} = '${status}' AND ${BILLED} = '${billed}' THEN ${value} END), 0)`;
|
|
83
|
+
/** The six figures of `CostTotals`, ready to sit in any SELECT list. */
|
|
84
|
+
const COST_COLUMNS = `${bucket("known", "server", "cost")} AS cost,
|
|
85
|
+
${bucket("known", "server", "1")} AS knownCount,
|
|
86
|
+
${bucket("unknown", "server", "1")} AS unknownCount,
|
|
87
|
+
${bucket("known", "byok", "cost")} AS byokUsd,
|
|
88
|
+
${bucket("known", "byok", "1")} AS byokKnownCount,
|
|
89
|
+
${bucket("unknown", "byok", "1")} AS byokUnknownCount`;
|
|
90
|
+
function asCostTotals(row) {
|
|
91
|
+
return {
|
|
92
|
+
cost: Number(row?.cost ?? 0),
|
|
93
|
+
knownCount: Number(row?.knownCount ?? 0),
|
|
94
|
+
unknownCount: Number(row?.unknownCount ?? 0),
|
|
95
|
+
byokUsd: Number(row?.byokUsd ?? 0),
|
|
96
|
+
byokKnownCount: Number(row?.byokKnownCount ?? 0),
|
|
97
|
+
byokUnknownCount: Number(row?.byokUnknownCount ?? 0),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
67
100
|
/** Every aggregate the dashboard reads comes from this one column list, so a
|
|
68
101
|
* new figure lands on the totals, the per repo table and the trend window at
|
|
69
102
|
* the same time. */
|
|
70
103
|
const STATS_COLUMNS = `COUNT(*) AS reviews,
|
|
71
104
|
COUNT(DISTINCT pr_number) AS pullRequests,
|
|
72
105
|
COALESCE(SUM(findings_count), 0) AS findings,
|
|
73
|
-
|
|
106
|
+
${COST_COLUMNS},
|
|
74
107
|
COALESCE(SUM(tokens_in), 0) AS tokensIn,
|
|
75
108
|
COALESCE(SUM(tokens_out), 0) AS tokensOut,
|
|
76
109
|
COALESCE(SUM(status = 'failed'), 0) AS failed,
|
|
@@ -80,7 +113,7 @@ function asStats(row) {
|
|
|
80
113
|
reviews: Number(row?.reviews ?? 0),
|
|
81
114
|
pullRequests: Number(row?.pullRequests ?? 0),
|
|
82
115
|
findings: Number(row?.findings ?? 0),
|
|
83
|
-
|
|
116
|
+
...asCostTotals(row),
|
|
84
117
|
tokensIn: Number(row?.tokensIn ?? 0),
|
|
85
118
|
tokensOut: Number(row?.tokensOut ?? 0),
|
|
86
119
|
failed: Number(row?.failed ?? 0),
|
|
@@ -118,11 +151,20 @@ export function reviewStatsByDay(sinceIso) {
|
|
|
118
151
|
.prepare(`SELECT substr(created_at, 1, 10) AS day,
|
|
119
152
|
COUNT(*) AS reviews,
|
|
120
153
|
COALESCE(SUM(findings_count), 0) AS findings,
|
|
121
|
-
|
|
154
|
+
${COST_COLUMNS}
|
|
122
155
|
FROM reviews WHERE created_at >= ?
|
|
123
156
|
GROUP BY day
|
|
124
157
|
ORDER BY day`)
|
|
125
|
-
.all(sinceIso)
|
|
158
|
+
.all(sinceIso)
|
|
159
|
+
.map((row) => {
|
|
160
|
+
const value = row;
|
|
161
|
+
return {
|
|
162
|
+
day: value.day,
|
|
163
|
+
reviews: Number(value.reviews),
|
|
164
|
+
findings: Number(value.findings),
|
|
165
|
+
...asCostTotals(value),
|
|
166
|
+
};
|
|
167
|
+
});
|
|
126
168
|
}
|
|
127
169
|
export function reviewStatsByModel(sinceIso) {
|
|
128
170
|
return getAppDb()
|
|
@@ -150,14 +192,17 @@ export function listPullSummaries(repo, limit, offset) {
|
|
|
150
192
|
.prepare(`SELECT pr_number,
|
|
151
193
|
MAX(created_at) AS last_reviewed,
|
|
152
194
|
COALESCE(SUM(findings_count), 0) AS findings,
|
|
153
|
-
|
|
195
|
+
${COST_COLUMNS},
|
|
154
196
|
COUNT(*) AS review_count
|
|
155
197
|
FROM reviews WHERE repo = ?
|
|
156
198
|
GROUP BY pr_number
|
|
157
199
|
ORDER BY last_reviewed DESC
|
|
158
200
|
LIMIT ? OFFSET ?`)
|
|
159
201
|
.all(repo, limit, offset);
|
|
160
|
-
return {
|
|
202
|
+
return {
|
|
203
|
+
items: items.map((item) => ({ ...item, ...asCostTotals(item) })),
|
|
204
|
+
total,
|
|
205
|
+
};
|
|
161
206
|
}
|
|
162
207
|
export function listRemoteReviewsForRepo(repo, limit, offset = 0, sinceIso) {
|
|
163
208
|
if (sinceIso) {
|
|
@@ -177,20 +222,20 @@ export function listRemoteReviewsForRepo(repo, limit, offset = 0, sinceIso) {
|
|
|
177
222
|
}
|
|
178
223
|
export function remoteTokenUsageSince(tokenId, sinceIso) {
|
|
179
224
|
const row = getAppDb()
|
|
180
|
-
.prepare(`SELECT COUNT(*) AS reviews,
|
|
225
|
+
.prepare(`SELECT COUNT(*) AS reviews, ${COST_COLUMNS}
|
|
181
226
|
FROM reviews
|
|
182
227
|
WHERE token_id = ? AND kind = 'remote' AND created_at >= ?`)
|
|
183
228
|
.get(tokenId, sinceIso);
|
|
184
229
|
return {
|
|
185
230
|
reviews: Number(row?.reviews ?? 0),
|
|
186
|
-
|
|
231
|
+
...asCostTotals(row),
|
|
187
232
|
};
|
|
188
233
|
}
|
|
189
234
|
export function reviewStatsByRemoteToken(sinceIso) {
|
|
190
235
|
return getAppDb()
|
|
191
236
|
.prepare(`SELECT token_id, token_name,
|
|
192
237
|
COUNT(*) AS reviews,
|
|
193
|
-
|
|
238
|
+
${COST_COLUMNS},
|
|
194
239
|
COALESCE(SUM(findings_count), 0) AS findings
|
|
195
240
|
FROM reviews
|
|
196
241
|
WHERE kind = 'remote' AND created_at >= ? AND token_id IS NOT NULL
|
|
@@ -201,7 +246,7 @@ export function reviewStatsByRemoteToken(sinceIso) {
|
|
|
201
246
|
tokenId: row.token_id,
|
|
202
247
|
tokenName: row.token_name,
|
|
203
248
|
reviews: Number(row.reviews),
|
|
204
|
-
|
|
249
|
+
...asCostTotals(row),
|
|
205
250
|
findings: Number(row.findings),
|
|
206
251
|
}));
|
|
207
252
|
}
|
package/dist/src/store/rows.d.ts
CHANGED
|
@@ -24,6 +24,9 @@ export type RepoRow = {
|
|
|
24
24
|
settings: string;
|
|
25
25
|
created_at: string;
|
|
26
26
|
use_codegraph: number;
|
|
27
|
+
/** JSON: `"legacy"`, a template name, or a policy object. `null` takes the
|
|
28
|
+
* server default. */
|
|
29
|
+
review_policy_json: string | null;
|
|
27
30
|
};
|
|
28
31
|
export type JobRow = {
|
|
29
32
|
id: string;
|
|
@@ -69,6 +72,10 @@ export type ReviewRow = {
|
|
|
69
72
|
tokens_in: number;
|
|
70
73
|
tokens_out: number;
|
|
71
74
|
cost: number | null;
|
|
75
|
+
/** `null` on a row written by 0.5.0 after this column was added. */
|
|
76
|
+
cost_status: "known" | "unknown" | null;
|
|
77
|
+
cost_note: string | null;
|
|
78
|
+
billed_to: "server" | "byok" | null;
|
|
72
79
|
duration_ms: number | null;
|
|
73
80
|
posted_review_id: string | null;
|
|
74
81
|
status: string;
|
|
@@ -176,4 +183,7 @@ export type RemoteReviewInputRow = {
|
|
|
176
183
|
request_id: string;
|
|
177
184
|
token_id: string;
|
|
178
185
|
created_at: string;
|
|
186
|
+
/** `'byok'` when the client's own key paid for the review, `'server'`
|
|
187
|
+
* otherwise. A null (pre-0.5.1 row) is treated as `'server'`. */
|
|
188
|
+
billed_to?: string | null;
|
|
179
189
|
};
|
|
@@ -33,7 +33,7 @@ export declare const CODEGRAPH_APPROX_SIZE = "~250 MB";
|
|
|
33
33
|
/** A prompt may only be shown when there is a human on the other end of stdin
|
|
34
34
|
* *and* stdout, and no CI marker. A closed stdin (`</dev/null`), a piped
|
|
35
35
|
* output, or `CI=1` must all take the non-interactive path: asking then would
|
|
36
|
-
* hang on a question nobody can answer
|
|
36
|
+
* hang on a question nobody can answer. `stdout` is checked because a
|
|
37
37
|
* prompt printed into a pipe is invisible.
|
|
38
38
|
*
|
|
39
39
|
* `streams` is injectable so the tests can stand in for a TTY without a real
|
|
@@ -46,12 +46,12 @@ export declare function canPrompt(streams?: {
|
|
|
46
46
|
isTTY?: boolean;
|
|
47
47
|
};
|
|
48
48
|
}, env?: Record<string, string | undefined>): boolean;
|
|
49
|
-
/** The one line a non-interactive run prints instead of asking
|
|
49
|
+
/** The one line a non-interactive run prints instead of asking. It names
|
|
50
50
|
* the reason and both ways forward, so a CI log explains itself. */
|
|
51
51
|
export declare function skippedNotice(version?: string): string;
|
|
52
52
|
/** What the install prompt says: the package and version, where it comes from,
|
|
53
53
|
* what it is for, the approximate size, and the exact directory. A bare
|
|
54
|
-
* "Install ...? [y/N]" left the user guessing
|
|
54
|
+
* "Install ...? [y/N]" left the user guessing. */
|
|
55
55
|
export declare function installPrompt(version: string, root: string): string;
|
|
56
56
|
export declare function runCommand(command: string, args: string[]): Promise<CommandResult>;
|
|
57
57
|
/** One version per directory, so several can sit side by side and an upgrade
|
|
@@ -83,7 +83,7 @@ export declare function installHint(version?: string, root?: string): string;
|
|
|
83
83
|
* to the binary, or exits: co-maintainer indexes the repository with it, so
|
|
84
84
|
* declining is a decision not to run this command, not a degraded mode. */
|
|
85
85
|
export declare function ensureCodegraph(options?: EnsureOptions): Promise<string>;
|
|
86
|
-
/** Review paths must not call `Deno.exit
|
|
86
|
+
/** Review paths must not call `Deno.exit`. */
|
|
87
87
|
export declare function ensureCodegraphForReview(options?: EnsureOptions): Promise<{
|
|
88
88
|
path: string;
|
|
89
89
|
} | {
|
|
@@ -17,7 +17,7 @@ export const CODEGRAPH_APPROX_SIZE = "~250 MB";
|
|
|
17
17
|
/** A prompt may only be shown when there is a human on the other end of stdin
|
|
18
18
|
* *and* stdout, and no CI marker. A closed stdin (`</dev/null`), a piped
|
|
19
19
|
* output, or `CI=1` must all take the non-interactive path: asking then would
|
|
20
|
-
* hang on a question nobody can answer
|
|
20
|
+
* hang on a question nobody can answer. `stdout` is checked because a
|
|
21
21
|
* prompt printed into a pipe is invisible.
|
|
22
22
|
*
|
|
23
23
|
* `streams` is injectable so the tests can stand in for a TTY without a real
|
|
@@ -28,7 +28,7 @@ export function canPrompt(streams = {
|
|
|
28
28
|
}, env = process.env) {
|
|
29
29
|
return (streams.stdin?.isTTY === true && streams.stdout?.isTTY === true && !env.CI);
|
|
30
30
|
}
|
|
31
|
-
/** The one line a non-interactive run prints instead of asking
|
|
31
|
+
/** The one line a non-interactive run prints instead of asking. It names
|
|
32
32
|
* the reason and both ways forward, so a CI log explains itself. */
|
|
33
33
|
export function skippedNotice(version = CODEGRAPH_VERSION) {
|
|
34
34
|
return (`codegraph ${version} is not installed, reviewing without it. ` +
|
|
@@ -36,7 +36,7 @@ export function skippedNotice(version = CODEGRAPH_VERSION) {
|
|
|
36
36
|
}
|
|
37
37
|
/** What the install prompt says: the package and version, where it comes from,
|
|
38
38
|
* what it is for, the approximate size, and the exact directory. A bare
|
|
39
|
-
* "Install ...? [y/N]" left the user guessing
|
|
39
|
+
* "Install ...? [y/N]" left the user guessing. */
|
|
40
40
|
export function installPrompt(version, root) {
|
|
41
41
|
return (`co-maintainer uses codegraph ${version} (${CODEGRAPH_PACKAGE} from npm, ${CODEGRAPH_APPROX_SIZE}) ` +
|
|
42
42
|
"to follow calls between files while reviewing.\n" +
|
|
@@ -165,7 +165,7 @@ export async function ensureCodegraph(options = {}) {
|
|
|
165
165
|
log(`[codegraph] ready at ${after.path}`);
|
|
166
166
|
return after.path;
|
|
167
167
|
}
|
|
168
|
-
/** Review paths must not call `Deno.exit
|
|
168
|
+
/** Review paths must not call `Deno.exit`. */
|
|
169
169
|
export async function ensureCodegraphForReview(options = {}) {
|
|
170
170
|
const root = options.root ?? toolsDir();
|
|
171
171
|
const run = options.run ?? runCommand;
|
|
@@ -179,7 +179,7 @@ export async function ensureCodegraphForReview(options = {}) {
|
|
|
179
179
|
}
|
|
180
180
|
if (!options.allowInstall) {
|
|
181
181
|
// Non-interactive: do not ask, review without it. The caller logs the
|
|
182
|
-
// reason, so the run continues instead of hanging or exiting
|
|
182
|
+
// reason, so the run continues instead of hanging or exiting.
|
|
183
183
|
if (!interactive) {
|
|
184
184
|
return { reason: skippedNotice() };
|
|
185
185
|
}
|
|
@@ -7,7 +7,7 @@ export type ExecOptions = {
|
|
|
7
7
|
env?: Record<string, string>;
|
|
8
8
|
};
|
|
9
9
|
export type CodegraphRunner = (binary: string, args: string[], worktree: string) => Promise<CommandResult>;
|
|
10
|
-
/** Run the codegraph CLI in a repo root
|
|
10
|
+
/** Run the codegraph CLI in a repo root.
|
|
11
11
|
*
|
|
12
12
|
* On Windows the CLI is a `.cmd`/`.ps1` shim, and `node:child_process.spawn`
|
|
13
13
|
* cannot execute those without a shell — it fails with `EINVAL`. `Deno.Command`
|
|
@@ -3,7 +3,7 @@ export const LOCAL_CODEGRAPH_DIR = ".co-maintainer-codegraph";
|
|
|
3
3
|
export const SERVER_CODEGRAPH_DIR = ".codegraph";
|
|
4
4
|
const TOOL_TIMEOUT_MS = 60_000;
|
|
5
5
|
const INDEX_COMMANDS = new Set(["init", "sync", "index", "unlock"]);
|
|
6
|
-
/** Run the codegraph CLI in a repo root
|
|
6
|
+
/** Run the codegraph CLI in a repo root.
|
|
7
7
|
*
|
|
8
8
|
* On Windows the CLI is a `.cmd`/`.ps1` shim, and `node:child_process.spawn`
|
|
9
9
|
* cannot execute those without a shell — it fails with `EINVAL`. `Deno.Command`
|
package/dist/src/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Cross-layer contracts only. A type owned by one layer (Fact, Source,
|
|
2
2
|
* PullRequest, State) lives next to the code that owns it instead, in
|
|
3
|
-
* `knowledge/types.ts
|
|
3
|
+
* `knowledge/types.ts`. */
|
|
4
4
|
export type Json = Record<string, unknown>;
|
|
5
5
|
export type Options = {
|
|
6
6
|
command: "probe" | "init" | "remake" | "review";
|
|
@@ -24,7 +24,7 @@ export type Options = {
|
|
|
24
24
|
envPath?: string;
|
|
25
25
|
auth: "gh" | "pat";
|
|
26
26
|
githubPat?: string;
|
|
27
|
-
ai: "none" | "openrouter" | "
|
|
27
|
+
ai: "none" | "openrouter" | "openai" | "anthropic" | "opencode-zen" | "opencode-go";
|
|
28
28
|
aiToken?: string;
|
|
29
29
|
lowModel?: string;
|
|
30
30
|
highModel?: string;
|
|
@@ -44,9 +44,9 @@ export type Options = {
|
|
|
44
44
|
maxPullRequestChangeLines?: number;
|
|
45
45
|
maxComments?: number;
|
|
46
46
|
/** `probe --run`: after printing the plan, run the recommended init in the
|
|
47
|
-
* same process
|
|
47
|
+
* same process. */
|
|
48
48
|
run?: boolean;
|
|
49
|
-
/** How a review decides a blocking finding
|
|
49
|
+
/** How a review decides a blocking finding. `model` keeps the
|
|
50
50
|
* 0.4.13 behavior, `severity` uses a fixed P0/P1 threshold. */
|
|
51
51
|
reviewBlocking?: "model" | "severity";
|
|
52
52
|
};
|
|
@@ -85,7 +85,7 @@ export type AiRequest = {
|
|
|
85
85
|
job: string;
|
|
86
86
|
/** A JSON schema the response must satisfy, sent as OpenRouter's
|
|
87
87
|
* `response_format`. Providers without structured output ignore it, so the
|
|
88
|
-
* model is also asked for a fenced JSON block in the prompt
|
|
88
|
+
* model is also asked for a fenced JSON block in the prompt. */
|
|
89
89
|
responseFormat?: Json;
|
|
90
90
|
reasoningEffort?: "max" | "xhigh" | "high" | "medium" | "low" | "none";
|
|
91
91
|
};
|
|
@@ -95,8 +95,14 @@ export type AiResponse = {
|
|
|
95
95
|
tokensIn: number;
|
|
96
96
|
tokensOut: number;
|
|
97
97
|
cost?: number;
|
|
98
|
+
/** Set on a response merged from several calls, so the caller can tell a
|
|
99
|
+
* partial cost from a missing one. */
|
|
100
|
+
costCalls?: {
|
|
101
|
+
known: number;
|
|
102
|
+
unknown: number;
|
|
103
|
+
};
|
|
98
104
|
model: string;
|
|
99
|
-
provider: "openrouter" | "
|
|
105
|
+
provider: "openrouter" | "openai" | "anthropic" | "opencode-zen" | "opencode-go";
|
|
100
106
|
};
|
|
101
107
|
export type AiProvider = {
|
|
102
108
|
supportsTools?: boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** What an AI run cost, and how sure we are. A missing cost is never `$0`:
|
|
2
|
+
* `known` includes a real zero (a free model), `unknown` carries a reason.
|
|
3
|
+
* The reason texts live here so the CLI, the JSON and the dashboard say the
|
|
4
|
+
* same thing. */
|
|
5
|
+
export type CostReason = "provider_did_not_report" | "partial" | "recorded_before_0_5_1" | "not_recorded";
|
|
6
|
+
export type CostStatus = "known" | "unknown";
|
|
7
|
+
export type BilledTo = "server" | "byok";
|
|
8
|
+
export type CostOutcome = {
|
|
9
|
+
status: "known";
|
|
10
|
+
usd: number;
|
|
11
|
+
} | {
|
|
12
|
+
status: "unknown";
|
|
13
|
+
reason: CostReason;
|
|
14
|
+
};
|
|
15
|
+
/** How many AI calls reported a cost and how many did not. */
|
|
16
|
+
export type CostCalls = {
|
|
17
|
+
known: number;
|
|
18
|
+
unknown: number;
|
|
19
|
+
};
|
|
20
|
+
export type CostTally = {
|
|
21
|
+
cost: number;
|
|
22
|
+
knownCalls: number;
|
|
23
|
+
unknownCalls: number;
|
|
24
|
+
};
|
|
25
|
+
export declare function emptyTally(): CostTally;
|
|
26
|
+
/** A response merged from several calls carries its own `costCalls`. A plain
|
|
27
|
+
* response is one call. */
|
|
28
|
+
export declare function addResponseCost(tally: CostTally, response: {
|
|
29
|
+
cost?: number;
|
|
30
|
+
costCalls?: CostCalls;
|
|
31
|
+
}): void;
|
|
32
|
+
/** Every call reported: known. None did: the provider does not report. Some
|
|
33
|
+
* did: partial, and the known part is dropped so a total never looks
|
|
34
|
+
* complete. No call at all: the run ended before a cost was recorded. */
|
|
35
|
+
export declare function settle(tally: CostTally): CostOutcome;
|
|
36
|
+
/** `provider` names the service in the first sentence, as in "OpenAI did not
|
|
37
|
+
* report the cost for this review." */
|
|
38
|
+
export declare function costReasonText(reason: CostReason, provider?: string): string;
|
|
39
|
+
/** The cost part of a review JSON `usage` object. `costUsd` stays `null`
|
|
40
|
+
* when the cost is unknown, as it always did. */
|
|
41
|
+
export declare function costUsage(tally: CostTally): {
|
|
42
|
+
costUsd: number | null;
|
|
43
|
+
costStatus: CostStatus;
|
|
44
|
+
costNote: CostReason | null;
|
|
45
|
+
billedTo: BilledTo;
|
|
46
|
+
};
|
|
47
|
+
/** The columns `setReviewStatus` writes for a finished review. */
|
|
48
|
+
export declare function costColumns(outcome: CostOutcome): {
|
|
49
|
+
cost: number | undefined;
|
|
50
|
+
cost_status: CostStatus;
|
|
51
|
+
cost_note: CostReason | null;
|
|
52
|
+
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/** What an AI run cost, and how sure we are. A missing cost is never `$0`:
|
|
2
|
+
* `known` includes a real zero (a free model), `unknown` carries a reason.
|
|
3
|
+
* The reason texts live here so the CLI, the JSON and the dashboard say the
|
|
4
|
+
* same thing. */
|
|
5
|
+
export function emptyTally() {
|
|
6
|
+
return { cost: 0, knownCalls: 0, unknownCalls: 0 };
|
|
7
|
+
}
|
|
8
|
+
/** A response merged from several calls carries its own `costCalls`. A plain
|
|
9
|
+
* response is one call. */
|
|
10
|
+
export function addResponseCost(tally, response) {
|
|
11
|
+
const calls = response.costCalls ??
|
|
12
|
+
(response.cost === undefined
|
|
13
|
+
? { known: 0, unknown: 1 }
|
|
14
|
+
: { known: 1, unknown: 0 });
|
|
15
|
+
tally.knownCalls += calls.known;
|
|
16
|
+
tally.unknownCalls += calls.unknown;
|
|
17
|
+
tally.cost += response.cost ?? 0;
|
|
18
|
+
}
|
|
19
|
+
/** Every call reported: known. None did: the provider does not report. Some
|
|
20
|
+
* did: partial, and the known part is dropped so a total never looks
|
|
21
|
+
* complete. No call at all: the run ended before a cost was recorded. */
|
|
22
|
+
export function settle(tally) {
|
|
23
|
+
if (tally.knownCalls === 0 && tally.unknownCalls === 0) {
|
|
24
|
+
return { status: "unknown", reason: "not_recorded" };
|
|
25
|
+
}
|
|
26
|
+
if (tally.unknownCalls === 0)
|
|
27
|
+
return { status: "known", usd: tally.cost };
|
|
28
|
+
return {
|
|
29
|
+
status: "unknown",
|
|
30
|
+
reason: tally.knownCalls === 0 ? "provider_did_not_report" : "partial",
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
const REASON_TEXT = {
|
|
34
|
+
provider_did_not_report: "The provider did not report the cost for this review.",
|
|
35
|
+
partial: "Some AI calls in this review did not report a cost.",
|
|
36
|
+
recorded_before_0_5_1: "Recorded before 0.5.1, the cost was not saved.",
|
|
37
|
+
not_recorded: "The review ended before its cost was recorded.",
|
|
38
|
+
};
|
|
39
|
+
/** `provider` names the service in the first sentence, as in "OpenAI did not
|
|
40
|
+
* report the cost for this review." */
|
|
41
|
+
export function costReasonText(reason, provider) {
|
|
42
|
+
if (reason === "provider_did_not_report" && provider) {
|
|
43
|
+
return `${provider} did not report the cost for this review.`;
|
|
44
|
+
}
|
|
45
|
+
return REASON_TEXT[reason];
|
|
46
|
+
}
|
|
47
|
+
/** The cost part of a review JSON `usage` object. `costUsd` stays `null`
|
|
48
|
+
* when the cost is unknown, as it always did. */
|
|
49
|
+
export function costUsage(tally) {
|
|
50
|
+
const outcome = settle(tally);
|
|
51
|
+
return outcome.status === "known"
|
|
52
|
+
? {
|
|
53
|
+
costUsd: outcome.usd,
|
|
54
|
+
costStatus: "known",
|
|
55
|
+
costNote: null,
|
|
56
|
+
billedTo: "server",
|
|
57
|
+
}
|
|
58
|
+
: {
|
|
59
|
+
costUsd: null,
|
|
60
|
+
costStatus: "unknown",
|
|
61
|
+
costNote: outcome.reason,
|
|
62
|
+
billedTo: "server",
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/** The columns `setReviewStatus` writes for a finished review. */
|
|
66
|
+
export function costColumns(outcome) {
|
|
67
|
+
return outcome.status === "known"
|
|
68
|
+
? { cost: outcome.usd, cost_status: "known", cost_note: null }
|
|
69
|
+
: { cost: undefined, cost_status: "unknown", cost_note: outcome.reason };
|
|
70
|
+
}
|
package/dist/src/util/log.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* with `withLogSink`), it also reaches `job_logs` and any live SSE
|
|
3
3
|
* subscriber, without threading a logger through every function signature. */
|
|
4
4
|
type Sink = (phase: string, message: string) => void;
|
|
5
|
-
/**
|
|
5
|
+
/** Progress and logs go to stderr during a CLI review. */
|
|
6
6
|
export declare function withCliLogsToStderr<T>(fn: () => Promise<T>): Promise<T>;
|
|
7
7
|
export declare function withLogSink<T>(sink: Sink, fn: () => Promise<T>): Promise<T>;
|
|
8
8
|
export declare function log(phase: string, message: string): void;
|
package/dist/src/util/log.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
2
|
const sinkStorage = new AsyncLocalStorage();
|
|
3
3
|
let cliLogsToStderr = false;
|
|
4
|
-
/**
|
|
4
|
+
/** Progress and logs go to stderr during a CLI review. */
|
|
5
5
|
export function withCliLogsToStderr(fn) {
|
|
6
6
|
const prev = cliLogsToStderr;
|
|
7
7
|
cliLogsToStderr = true;
|
|
@@ -32,7 +32,7 @@ export function startHeartbeat(phase) {
|
|
|
32
32
|
const timer = setInterval(() => {
|
|
33
33
|
log("progress", `still running ${label()} · ${Math.round((Date.now() - started) / 1000)}s elapsed`);
|
|
34
34
|
}, 15_000);
|
|
35
|
-
// Never keep the process alive on its own
|
|
35
|
+
// Never keep the process alive on its own. A long operation holds
|
|
36
36
|
// the loop open through its own pending async work (a fetch, a child), so the
|
|
37
37
|
// heartbeat still fires; but once that work is gone the timer must not be the
|
|
38
38
|
// reason the process cannot drain and apply its exit code. Every call site
|
package/dist/src/util/redact.js
CHANGED
|
@@ -4,6 +4,8 @@ const PATTERNS = [
|
|
|
4
4
|
/gh[pousr]_[A-Za-z0-9]{20,}/g, // ghp_, gho_, ghu_, ghs_, ghr_ (classic GitHub PATs and tokens)
|
|
5
5
|
/github_pat_[A-Za-z0-9_]{20,}/g, // fine-grained GitHub PATs
|
|
6
6
|
/sk-or-[A-Za-z0-9-]{10,}/g, // OpenRouter keys
|
|
7
|
+
/sk-ant-[A-Za-z0-9_-]{10,}/g, // Anthropic keys
|
|
8
|
+
/sk-[A-Za-z0-9_-]{20,}/g, // OpenAI keys, old `sk-` and new `sk-proj-` alike
|
|
7
9
|
/cmr_[A-Za-z0-9_-]{20,}/g, // remote review bearer tokens
|
|
8
10
|
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g,
|
|
9
11
|
];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
/** The one-line cost and time summary every AI command ends with
|
|
1
|
+
/** The one-line cost and time summary every AI command ends with.
|
|
2
2
|
*
|
|
3
|
-
*
|
|
3
|
+
* The JSON already carried `usage.costUsd` and a duration, but the human
|
|
4
4
|
* output had neither and `init` printed no total at all, so the only place to
|
|
5
5
|
* see what a run cost was the OpenRouter dashboard. This renders the same
|
|
6
6
|
* numbers as a single stderr line:
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* report a cost the dollar field is `cost unknown` rather than `$0.0000`.
|
|
13
13
|
*/
|
|
14
14
|
import type { AiMetrics } from "../services/setup.ts";
|
|
15
|
+
import { type CostTally } from "./cost.ts";
|
|
15
16
|
export type RunSummary = {
|
|
16
17
|
/** Wall-clock milliseconds, or null when only the AI time is known. */
|
|
17
18
|
durationMs: number | null;
|
|
@@ -19,6 +20,8 @@ export type RunSummary = {
|
|
|
19
20
|
tokensOut: number;
|
|
20
21
|
/** `null` when any call left the cost unknown, matching `usage.costUsd`. */
|
|
21
22
|
costUsd: number | null;
|
|
23
|
+
/** Why the cost is unknown, as a sentence. */
|
|
24
|
+
costNote?: string;
|
|
22
25
|
};
|
|
23
26
|
/** `21.0s`, `1.3s`, `2m 03s`; one decimal below a minute, whole seconds above. */
|
|
24
27
|
export declare function formatDuration(ms: number): string;
|
|
@@ -31,3 +34,5 @@ export declare function formatRunSummary(summary: RunSummary): string;
|
|
|
31
34
|
export declare function printRunSummary(summary: RunSummary): void;
|
|
32
35
|
/** The metrics half of a summary, so call sites do not repeat the spread. */
|
|
33
36
|
export declare function summaryFromMetrics(metrics: AiMetrics, durationMs: number | null): RunSummary;
|
|
37
|
+
/** `0.0016` for a known cost, `unknown` otherwise. For the timing log line. */
|
|
38
|
+
export declare function costLabel(metrics: CostTally): string;
|