co-maintainer 0.5.1 → 0.5.3
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/package.json +2 -4
- package/dist/src/ai/anthropic.js +6 -1
- package/dist/src/ai/openai.js +18 -19
- package/dist/src/ai/openrouter.js +13 -19
- package/dist/src/ai/provider.d.ts +9 -0
- package/dist/src/ai/provider.js +29 -1
- package/dist/src/cli/commands/clear.d.ts +1 -0
- package/dist/src/cli/commands/clear.js +152 -0
- package/dist/src/cli/commands/registry.d.ts +4 -0
- package/dist/src/cli/commands/registry.js +48 -0
- package/dist/src/cli/commands/uninstall.d.ts +1 -0
- package/dist/src/cli/commands/uninstall.js +43 -0
- package/dist/src/cli/main.js +10 -0
- package/dist/src/config.d.ts +14 -0
- package/dist/src/config.js +38 -1
- package/dist/src/knowledge/synthesis.js +3 -0
- package/dist/src/local/review_lock.js +2 -2
- package/dist/src/server/api/knowledge.d.ts +7 -0
- package/dist/src/server/api/knowledge.js +68 -0
- package/dist/src/server/api/repos.js +5 -0
- package/dist/src/server/app.js +4 -0
- package/dist/src/server/pages/client.d.ts +1 -1
- package/dist/src/server/pages/client.js +1 -1
- package/dist/src/server/pages/knowledge.js +20 -1
- package/dist/src/server/pages/settings.js +18 -0
- package/dist/src/services/clear.d.ts +70 -0
- package/dist/src/services/clear.js +310 -0
- package/dist/src/store/app_db.d.ts +3 -0
- package/dist/src/store/app_db.js +5 -0
- package/dist/src/store/cache_db.d.ts +11 -0
- package/dist/src/store/cache_db.js +60 -1
- package/dist/src/store/repos.d.ts +3 -0
- package/dist/src/store/repos.js +8 -0
- package/dist/src/util/runtime.d.ts +3 -0
- package/dist/src/util/runtime.js +6 -1
- package/package.json +2 -4
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "co-maintainer",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.3",
|
|
4
4
|
"description": "An imitator of yours, A PR review tool like co-maintainer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -59,11 +59,9 @@
|
|
|
59
59
|
"dx:scenario": "node scripts/dx_scenario.ts",
|
|
60
60
|
"dashboard:smoke": "node scripts/dashboard_smoke.ts"
|
|
61
61
|
},
|
|
62
|
-
"dependencies": {
|
|
63
|
-
"marked": "^15.0.7"
|
|
64
|
-
},
|
|
65
62
|
"devDependencies": {
|
|
66
63
|
"@types/node": "^24.0.0",
|
|
64
|
+
"marked": "^15.0.7",
|
|
67
65
|
"oxfmt": "^0.68.0",
|
|
68
66
|
"oxlint": "^1.83.0",
|
|
69
67
|
"playwright": "^1.63.0",
|
package/dist/src/ai/anthropic.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getEnv } from "../util/runtime.js";
|
|
2
|
+
import { outputLimitError } from "./provider.js";
|
|
2
3
|
import { CliError, EXIT_RUNTIME, EXIT_USAGE, networkFailure, } from "../cli/error.js";
|
|
3
4
|
/** Required on every Messages request and the only version the errors below
|
|
4
5
|
* are written against. */
|
|
@@ -197,8 +198,12 @@ export function parseMessagesBody(json, model) {
|
|
|
197
198
|
}
|
|
198
199
|
}
|
|
199
200
|
const usage = json.usage;
|
|
201
|
+
const text = texts.join("");
|
|
202
|
+
if (json.stop_reason === "max_tokens" && !text.trim() && !toolCalls.length) {
|
|
203
|
+
throw outputLimitError("anthropic", model);
|
|
204
|
+
}
|
|
200
205
|
return {
|
|
201
|
-
text
|
|
206
|
+
text,
|
|
202
207
|
...(toolCalls.length ? { toolCalls } : {}),
|
|
203
208
|
// Tokens read from or written to the prompt cache are input too, and with
|
|
204
209
|
// the guides cached they are most of it.
|
package/dist/src/ai/openai.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { getEnv } from "../util/runtime.js";
|
|
2
|
+
import { outputLimitError, withoutRefusedPart } from "./provider.js";
|
|
2
3
|
import { CliError, EXIT_RUNTIME, EXIT_USAGE, networkFailure, } from "../cli/error.js";
|
|
3
4
|
/** `CM_OPENAI_URL` points at a local fake in tests. Unset means the real
|
|
4
5
|
* endpoint, so production behavior is unchanged. */
|
|
@@ -145,8 +146,12 @@ function parseResponsesBody(json, model) {
|
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
148
|
const usage = json.usage;
|
|
149
|
+
const text = texts.join("");
|
|
150
|
+
if (json.status === "incomplete" && !text.trim() && !toolCalls.length) {
|
|
151
|
+
throw outputLimitError("openai", model);
|
|
152
|
+
}
|
|
148
153
|
return {
|
|
149
|
-
text
|
|
154
|
+
text,
|
|
150
155
|
...(toolCalls.length ? { toolCalls } : {}),
|
|
151
156
|
tokensIn: Number(usage?.input_tokens ?? 0),
|
|
152
157
|
tokensOut: Number(usage?.output_tokens ?? 0),
|
|
@@ -184,25 +189,19 @@ export class OpenAiProvider {
|
|
|
184
189
|
}
|
|
185
190
|
}
|
|
186
191
|
async complete(request) {
|
|
187
|
-
let
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const detail = await response.text();
|
|
193
|
-
if (/text\.format|json_schema|structured/i.test(detail)) {
|
|
194
|
-
const { responseFormat: _dropped, ...withoutSchema } = request;
|
|
195
|
-
response = await this.post(responsesBody(this.model, withoutSchema));
|
|
196
|
-
if (response.ok) {
|
|
197
|
-
return parseResponsesBody((await response.json()), this.model);
|
|
198
|
-
}
|
|
199
|
-
throw openAiError(this.model, response.status, await response.text());
|
|
192
|
+
let current = request;
|
|
193
|
+
for (;;) {
|
|
194
|
+
const response = await this.post(responsesBody(this.model, current));
|
|
195
|
+
if (response.ok) {
|
|
196
|
+
return parseResponsesBody((await response.json()), this.model);
|
|
200
197
|
}
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
198
|
+
const detail = await response.text();
|
|
199
|
+
const lighter = response.status === 400
|
|
200
|
+
? withoutRefusedPart(current, detail, /text\.format|json_schema|structured/i)
|
|
201
|
+
: undefined;
|
|
202
|
+
if (!lighter)
|
|
203
|
+
throw openAiError(this.model, response.status, detail);
|
|
204
|
+
current = lighter;
|
|
205
205
|
}
|
|
206
|
-
return parseResponsesBody((await response.json()), this.model);
|
|
207
206
|
}
|
|
208
207
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { chatBody, parseChatResponse } from "./provider.js";
|
|
1
|
+
import { chatBody, parseChatResponse, withoutRefusedPart } from "./provider.js";
|
|
2
2
|
import { getEnv } from "../util/runtime.js";
|
|
3
3
|
import { CliError, EXIT_RUNTIME, EXIT_USAGE, networkFailure, } from "../cli/error.js";
|
|
4
4
|
/** `CM_OPENROUTER_URL` points the provider at a local fake in tests. It is
|
|
@@ -75,25 +75,19 @@ export class OpenRouterProvider {
|
|
|
75
75
|
return openRouterError(this.model, status, body, this.host);
|
|
76
76
|
}
|
|
77
77
|
async complete(request) {
|
|
78
|
-
let
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
const detail = await response.text();
|
|
84
|
-
if (/response_format|json_schema|structured/i.test(detail)) {
|
|
85
|
-
const { responseFormat: _dropped, ...withoutSchema } = request;
|
|
86
|
-
response = await this.post(chatBody(this.model, withoutSchema));
|
|
87
|
-
if (response.ok) {
|
|
88
|
-
return parseChatResponse((await response.json()), this.host.provider, this.model);
|
|
89
|
-
}
|
|
90
|
-
throw this.fail(response.status, await response.text());
|
|
78
|
+
let current = request;
|
|
79
|
+
for (;;) {
|
|
80
|
+
const response = await this.post(chatBody(this.model, current));
|
|
81
|
+
if (response.ok) {
|
|
82
|
+
return parseChatResponse((await response.json()), this.host.provider, this.model);
|
|
91
83
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
84
|
+
const detail = await response.text();
|
|
85
|
+
const lighter = response.status === 400
|
|
86
|
+
? withoutRefusedPart(current, detail, /response_format|json_schema|structured/i)
|
|
87
|
+
: undefined;
|
|
88
|
+
if (!lighter)
|
|
89
|
+
throw this.fail(response.status, detail);
|
|
90
|
+
current = lighter;
|
|
96
91
|
}
|
|
97
|
-
return parseChatResponse((await response.json()), this.host.provider, this.model);
|
|
98
92
|
}
|
|
99
93
|
}
|
|
@@ -7,5 +7,14 @@ export { providerKeyEnv } from "../config.ts";
|
|
|
7
7
|
/** Hetzner was dropped in 0.5.1. Naming it beats a generic "must be one of". */
|
|
8
8
|
export declare function rejectRetiredProvider(value: string | undefined): void;
|
|
9
9
|
export declare function createAiProvider(options: Options, model: string): AiProvider | undefined;
|
|
10
|
+
/** A 400 that names one optional part of the request earns one retry without
|
|
11
|
+
* it: the JSON schema, which the prompt asks for anyway, or "thinking off",
|
|
12
|
+
* which a model that always thinks refuses. Undefined when nothing is left to
|
|
13
|
+
* drop, so the caller reports the error. */
|
|
14
|
+
export declare function withoutRefusedPart(request: AiRequest, detail: string, schemaPattern: RegExp): AiRequest | undefined;
|
|
15
|
+
/** The model stopped at its output limit before writing a word. Thinking counts
|
|
16
|
+
* toward that limit, so a thinking model can spend all of it there. Without
|
|
17
|
+
* this the empty answer read as a normal one and failed later as "not JSON". */
|
|
18
|
+
export declare function outputLimitError(provider: string, model: string): Error;
|
|
10
19
|
export declare function parseChatResponse(json: Json, provider: AiResponse["provider"], model: string): AiResponse;
|
|
11
20
|
export declare function chatBody(model: string, request: AiRequest): Json;
|
package/dist/src/ai/provider.js
CHANGED
|
@@ -38,6 +38,28 @@ export function createAiProvider(options, model) {
|
|
|
38
38
|
}
|
|
39
39
|
die(`${options.ai} support is not finished yet. Use openrouter or none.`);
|
|
40
40
|
}
|
|
41
|
+
/** A 400 that names one optional part of the request earns one retry without
|
|
42
|
+
* it: the JSON schema, which the prompt asks for anyway, or "thinking off",
|
|
43
|
+
* which a model that always thinks refuses. Undefined when nothing is left to
|
|
44
|
+
* drop, so the caller reports the error. */
|
|
45
|
+
export function withoutRefusedPart(request, detail, schemaPattern) {
|
|
46
|
+
if (request.responseFormat && schemaPattern.test(detail)) {
|
|
47
|
+
const { responseFormat: _dropped, ...rest } = request;
|
|
48
|
+
return rest;
|
|
49
|
+
}
|
|
50
|
+
if (request.reasoningEffort === "none" &&
|
|
51
|
+
/reasoning|effort|thinking/i.test(detail)) {
|
|
52
|
+
const { reasoningEffort: _dropped, ...rest } = request;
|
|
53
|
+
return rest;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
/** The model stopped at its output limit before writing a word. Thinking counts
|
|
58
|
+
* toward that limit, so a thinking model can spend all of it there. Without
|
|
59
|
+
* this the empty answer read as a normal one and failed later as "not JSON". */
|
|
60
|
+
export function outputLimitError(provider, model) {
|
|
61
|
+
return new Error(`${provider} model ${model} reached its output limit before writing an answer. Thinking counts toward that limit.`);
|
|
62
|
+
}
|
|
41
63
|
export function parseChatResponse(json, provider, model) {
|
|
42
64
|
const choice = Array.isArray(json.choices)
|
|
43
65
|
? json.choices[0]
|
|
@@ -61,8 +83,14 @@ export function parseChatResponse(json, provider, model) {
|
|
|
61
83
|
})
|
|
62
84
|
.filter((call) => call.id && call.function.name)
|
|
63
85
|
: undefined;
|
|
86
|
+
const text = String(message?.content ?? "");
|
|
87
|
+
if (choice?.finish_reason === "length" &&
|
|
88
|
+
!text.trim() &&
|
|
89
|
+
!toolCalls?.length) {
|
|
90
|
+
throw outputLimitError(provider, model);
|
|
91
|
+
}
|
|
64
92
|
return {
|
|
65
|
-
text
|
|
93
|
+
text,
|
|
66
94
|
...(toolCalls?.length ? { toolCalls } : {}),
|
|
67
95
|
tokensIn: Number(usage?.prompt_tokens ?? 0),
|
|
68
96
|
tokensOut: Number(usage?.completion_tokens ?? 0),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runClear(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
/** `co-maintainer clear`: delete what `init` produced.
|
|
2
|
+
*
|
|
3
|
+
* A repository goes back to "never inited": guides, the knowledge stamp, the
|
|
4
|
+
* skill state and the settings `init` remembered are gone, so the next build
|
|
5
|
+
* is `init`, not `sync`. `--include-cache` reaches further and takes the
|
|
6
|
+
* fetched evidence too, which makes that next init a cold build.
|
|
7
|
+
*
|
|
8
|
+
* `all` touches every repository on the machine, so it asks first. Pending
|
|
9
|
+
* jobs are asked about separately because they would otherwise rebuild the
|
|
10
|
+
* knowledge after it was cleared. */
|
|
11
|
+
import { CliError, EXIT_USAGE } from "../error.js";
|
|
12
|
+
import { askConfirm } from "../prompt.js";
|
|
13
|
+
import { canPrompt } from "../../tools/codegraph.js";
|
|
14
|
+
import { liveLockPid } from "../../store/app_db.js";
|
|
15
|
+
import { unknownOptionMessage } from "./registry.js";
|
|
16
|
+
import { cancelOrphanedJobs, cancelQueuedJobs, clearRepos, clearRepo, isSafeRepoName, listStoredRepos, repoJobs, withAppDbIfPresent, } from "../../services/clear.js";
|
|
17
|
+
/** "3 guides, 1 cache row, saved settings" without a trailing separator. */
|
|
18
|
+
function joinParts(parts) {
|
|
19
|
+
return parts.join(", ");
|
|
20
|
+
}
|
|
21
|
+
function repoParts(result) {
|
|
22
|
+
const parts = [];
|
|
23
|
+
if (result.guides > 0)
|
|
24
|
+
parts.push(`${result.guides} guide file(s)`);
|
|
25
|
+
if (result.cacheRows > 0)
|
|
26
|
+
parts.push(`${result.cacheRows} cache row(s)`);
|
|
27
|
+
if (result.clones)
|
|
28
|
+
parts.push("the clone");
|
|
29
|
+
if (result.worktrees)
|
|
30
|
+
parts.push("the worktrees");
|
|
31
|
+
if (result.settings)
|
|
32
|
+
parts.push("saved settings");
|
|
33
|
+
return parts;
|
|
34
|
+
}
|
|
35
|
+
function printRepo(result) {
|
|
36
|
+
const parts = repoParts(result);
|
|
37
|
+
if (parts.length === 0) {
|
|
38
|
+
console.log(`Nothing to clear for ${result.repo}.`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
console.log(`Cleared ${result.repo}: ${joinParts(parts)}.`);
|
|
42
|
+
}
|
|
43
|
+
function printAll(result) {
|
|
44
|
+
if (result.repos.length === 0) {
|
|
45
|
+
console.log("Nothing to clear.");
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
for (const repo of result.repos) {
|
|
49
|
+
const parts = repoParts(repo);
|
|
50
|
+
if (parts.length > 0) {
|
|
51
|
+
console.log(`Cleared ${repo.repo}: ${joinParts(parts)}.`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const extra = [];
|
|
55
|
+
if (result.cacheFile)
|
|
56
|
+
extra.push("cache.db");
|
|
57
|
+
if (result.clones)
|
|
58
|
+
extra.push("every clone");
|
|
59
|
+
if (result.worktrees)
|
|
60
|
+
extra.push("every worktree");
|
|
61
|
+
if (result.settings)
|
|
62
|
+
extra.push("saved settings");
|
|
63
|
+
const tail = extra.length > 0 ? ` Also cleared ${joinParts(extra)}.` : "";
|
|
64
|
+
console.log(`Cleared ${result.repos.length} repositories.${tail}`);
|
|
65
|
+
}
|
|
66
|
+
/** What the pending-job question should say. */
|
|
67
|
+
function jobSummary(repos) {
|
|
68
|
+
const jobs = repoJobs(repos);
|
|
69
|
+
const parts = [];
|
|
70
|
+
if (jobs.queued.length > 0)
|
|
71
|
+
parts.push(`${jobs.queued.length} queued`);
|
|
72
|
+
if (jobs.running.length > 0)
|
|
73
|
+
parts.push(`${jobs.running.length} unfinished`);
|
|
74
|
+
if (parts.length === 0)
|
|
75
|
+
return undefined;
|
|
76
|
+
return `This target has ${joinParts(parts)} job(s).`;
|
|
77
|
+
}
|
|
78
|
+
/** Pending jobs would rebuild the knowledge this command is deleting. The
|
|
79
|
+
* question is asked once for the whole target. Without a terminal (or with
|
|
80
|
+
* `--yes`) they are canceled: keeping them would contradict the clear. */
|
|
81
|
+
async function handlePendingJobs(repos, assumeYes) {
|
|
82
|
+
await withAppDbIfPresent(async () => {
|
|
83
|
+
const summary = jobSummary(repos);
|
|
84
|
+
if (summary === undefined)
|
|
85
|
+
return;
|
|
86
|
+
const cancelThem = !canPrompt()
|
|
87
|
+
? true
|
|
88
|
+
: assumeYes || (await askConfirm(`${summary} Cancel them?`));
|
|
89
|
+
if (!cancelThem) {
|
|
90
|
+
console.log("Pending jobs were kept. They can rebuild the knowledge later.");
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const canceled = cancelQueuedJobs(repos) + cancelOrphanedJobs(repos);
|
|
94
|
+
if (canceled > 0)
|
|
95
|
+
console.log(`Canceled ${canceled} job(s).`);
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
export async function runClear(args) {
|
|
99
|
+
let includeCache = false;
|
|
100
|
+
let yes = false;
|
|
101
|
+
const positionals = [];
|
|
102
|
+
for (const arg of args) {
|
|
103
|
+
if (arg === "--include-cache") {
|
|
104
|
+
includeCache = true;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (arg === "--yes") {
|
|
108
|
+
yes = true;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (arg.startsWith("-")) {
|
|
112
|
+
throw new CliError("usage", unknownOptionMessage(arg, "clear"));
|
|
113
|
+
}
|
|
114
|
+
positionals.push(arg);
|
|
115
|
+
}
|
|
116
|
+
const target = positionals[0];
|
|
117
|
+
if (positionals.length !== 1 || !target) {
|
|
118
|
+
throw new CliError("usage", "clear needs one target: owner/repo or all.", "Usage: co-maintainer clear <owner/repo|all> [--include-cache] [--yes]");
|
|
119
|
+
}
|
|
120
|
+
const all = target === "all";
|
|
121
|
+
if (!all && !isSafeRepoName(target)) {
|
|
122
|
+
throw new CliError("usage", "Repository must look like owner/repo");
|
|
123
|
+
}
|
|
124
|
+
// A live serve owns guides and caches: clearing underneath it would confuse
|
|
125
|
+
// webhooks and remote clients mid-flight. The same guard rollback uses.
|
|
126
|
+
const pid = await liveLockPid();
|
|
127
|
+
if (pid !== undefined) {
|
|
128
|
+
throw new CliError("serve_already_running", `A co-maintainer serve process (pid ${pid}) is using this data directory.`, "Stop it, then run clear again.", EXIT_USAGE);
|
|
129
|
+
}
|
|
130
|
+
const repos = all ? await listStoredRepos() : [target];
|
|
131
|
+
if (all && !yes) {
|
|
132
|
+
if (!canPrompt()) {
|
|
133
|
+
throw new CliError("confirmation_required", "clear all needs your confirmation.", "Run it in a terminal, or pass --yes.", EXIT_USAGE);
|
|
134
|
+
}
|
|
135
|
+
const scope = includeCache
|
|
136
|
+
? "guides, settings, cached evidence, clones and worktrees"
|
|
137
|
+
: "guides and saved settings";
|
|
138
|
+
console.log(`This deletes the ${scope} of ${repos.length} repositories on this machine.`);
|
|
139
|
+
if (!(await askConfirm("Clear them?"))) {
|
|
140
|
+
console.log("Nothing changed.");
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
await handlePendingJobs(repos, yes);
|
|
145
|
+
// The snapshot the jobs were negotiated against is the one that gets
|
|
146
|
+
// cleared, so the printed count matches what actually went.
|
|
147
|
+
if (all)
|
|
148
|
+
printAll(await clearRepos(repos, { includeCache }));
|
|
149
|
+
else
|
|
150
|
+
printRepo(await clearRepo(target, { includeCache }));
|
|
151
|
+
console.log(`Run co-maintainer init ${all ? "owner/repo" : target} to build the guides again.`);
|
|
152
|
+
}
|
|
@@ -30,6 +30,10 @@ export declare function findCommand(name: string): CommandSpec | undefined;
|
|
|
30
30
|
/** Commands listed in the global help. A hidden alias or command is resolvable
|
|
31
31
|
* but invisible. */
|
|
32
32
|
export declare function visibleCommands(): CommandSpec[];
|
|
33
|
+
/** The visible command names in help order. Tests and scripts iterate this
|
|
34
|
+
* instead of repeating the list, so a new command cannot be forgotten in one
|
|
35
|
+
* place and present in another. */
|
|
36
|
+
export declare function visibleCommandNames(): string[];
|
|
33
37
|
/** Levenshtein distance, iterative and allocation-light: the help path runs it
|
|
34
38
|
* on every unknown token, and the strings are short. */
|
|
35
39
|
export declare function levenshtein(a: string, b: string): number;
|
|
@@ -430,6 +430,48 @@ export const COMMANDS = [
|
|
|
430
430
|
"co-maintainer view owner/repo --remote",
|
|
431
431
|
],
|
|
432
432
|
},
|
|
433
|
+
{
|
|
434
|
+
name: "clear",
|
|
435
|
+
summary: "Delete the generated guides and saved settings for a repository, or for every repository.",
|
|
436
|
+
usage: ["co-maintainer clear <owner/repo|all> [options]"],
|
|
437
|
+
groups: [
|
|
438
|
+
{
|
|
439
|
+
title: "Clear",
|
|
440
|
+
flags: [
|
|
441
|
+
{
|
|
442
|
+
name: "include-cache",
|
|
443
|
+
description: "Also delete the fetched evidence: pull request listings, AI job cache, costs, local review carry-over, clones and worktrees. The next init is then a cold build.",
|
|
444
|
+
},
|
|
445
|
+
{
|
|
446
|
+
name: "yes",
|
|
447
|
+
description: "Skip the confirmation `all` asks. Needed when there is no terminal.",
|
|
448
|
+
},
|
|
449
|
+
],
|
|
450
|
+
},
|
|
451
|
+
],
|
|
452
|
+
examples: [
|
|
453
|
+
"co-maintainer clear owner/repo",
|
|
454
|
+
"co-maintainer clear owner/repo --include-cache",
|
|
455
|
+
"co-maintainer clear all --include-cache --yes",
|
|
456
|
+
],
|
|
457
|
+
},
|
|
458
|
+
{
|
|
459
|
+
name: "uninstall",
|
|
460
|
+
summary: "Delete every co-maintainer data file on this machine.",
|
|
461
|
+
usage: ["co-maintainer uninstall [--yes]"],
|
|
462
|
+
groups: [
|
|
463
|
+
{
|
|
464
|
+
title: "Uninstall",
|
|
465
|
+
flags: [
|
|
466
|
+
{
|
|
467
|
+
name: "yes",
|
|
468
|
+
description: "Skip the confirmation. Needed when there is no terminal.",
|
|
469
|
+
},
|
|
470
|
+
],
|
|
471
|
+
},
|
|
472
|
+
],
|
|
473
|
+
examples: ["co-maintainer uninstall", "co-maintainer uninstall --yes"],
|
|
474
|
+
},
|
|
433
475
|
{
|
|
434
476
|
name: "set",
|
|
435
477
|
summary: "Persist defaults and secrets to the user config file.",
|
|
@@ -644,6 +686,12 @@ export function findCommand(name) {
|
|
|
644
686
|
export function visibleCommands() {
|
|
645
687
|
return COMMANDS.filter((command) => !command.hidden);
|
|
646
688
|
}
|
|
689
|
+
/** The visible command names in help order. Tests and scripts iterate this
|
|
690
|
+
* instead of repeating the list, so a new command cannot be forgotten in one
|
|
691
|
+
* place and present in another. */
|
|
692
|
+
export function visibleCommandNames() {
|
|
693
|
+
return visibleCommands().map((command) => command.name);
|
|
694
|
+
}
|
|
647
695
|
/** Levenshtein distance, iterative and allocation-light: the help path runs it
|
|
648
696
|
* on every unknown token, and the strings are short. */
|
|
649
697
|
export function levenshtein(a, b) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runUninstall(args: string[]): Promise<void>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** `co-maintainer uninstall`: delete every data file this machine holds.
|
|
2
|
+
*
|
|
3
|
+
* The npm package cannot remove itself while the CLI runs from it, so the
|
|
4
|
+
* command deletes the config, databases, guides, clones, worktrees and tools
|
|
5
|
+
* and then prints the npm command that finishes the job. */
|
|
6
|
+
import { CliError, EXIT_USAGE } from "../error.js";
|
|
7
|
+
import { askConfirm } from "../prompt.js";
|
|
8
|
+
import { canPrompt } from "../../tools/codegraph.js";
|
|
9
|
+
import { liveLockPid } from "../../store/app_db.js";
|
|
10
|
+
import { unknownOptionMessage } from "./registry.js";
|
|
11
|
+
import { uninstallData } from "../../services/clear.js";
|
|
12
|
+
export async function runUninstall(args) {
|
|
13
|
+
for (const arg of args) {
|
|
14
|
+
if (arg !== "--yes") {
|
|
15
|
+
throw new CliError("usage", unknownOptionMessage(arg, "uninstall"));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const pid = await liveLockPid();
|
|
19
|
+
if (pid !== undefined) {
|
|
20
|
+
throw new CliError("serve_already_running", `A co-maintainer serve process (pid ${pid}) is using this data directory.`, "Stop it, then run uninstall again.", EXIT_USAGE);
|
|
21
|
+
}
|
|
22
|
+
if (!args.includes("--yes")) {
|
|
23
|
+
if (!canPrompt()) {
|
|
24
|
+
throw new CliError("confirmation_required", "Uninstall needs your confirmation.", "Run it in a terminal, or pass --yes.", EXIT_USAGE);
|
|
25
|
+
}
|
|
26
|
+
console.log("This deletes every co-maintainer data file: config.json, the databases, guides, clones, worktrees and tools.");
|
|
27
|
+
if (!(await askConfirm("Uninstall?"))) {
|
|
28
|
+
console.log("Nothing changed.");
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
const removed = await uninstallData();
|
|
33
|
+
if (removed.paths.length === 0) {
|
|
34
|
+
console.log("Nothing to remove.");
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
console.log("Removed:");
|
|
38
|
+
for (const path of removed.paths)
|
|
39
|
+
console.log(` ${path}`);
|
|
40
|
+
}
|
|
41
|
+
console.log("The CLI itself is still installed. Remove it with:");
|
|
42
|
+
console.log(" npm uninstall -g co-maintainer");
|
|
43
|
+
}
|
package/dist/src/cli/main.js
CHANGED
|
@@ -6,6 +6,8 @@ import { runRollback } from "./commands/rollback.js";
|
|
|
6
6
|
import { runSet } from "./commands/set.js";
|
|
7
7
|
import { runConfig } from "./commands/config.js";
|
|
8
8
|
import { runView } from "./commands/view.js";
|
|
9
|
+
import { runClear } from "./commands/clear.js";
|
|
10
|
+
import { runUninstall } from "./commands/uninstall.js";
|
|
9
11
|
import { runProbe } from "./commands/probe.js";
|
|
10
12
|
import { runReviewFromCli } from "./commands/review.js";
|
|
11
13
|
import { runInitOrRemake } from "../services/setup.js";
|
|
@@ -68,6 +70,14 @@ export async function run(args) {
|
|
|
68
70
|
await runView(args.slice(1));
|
|
69
71
|
return;
|
|
70
72
|
}
|
|
73
|
+
if (args[0] === "clear") {
|
|
74
|
+
await runClear(args.slice(1));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (args[0] === "uninstall") {
|
|
78
|
+
await runUninstall(args.slice(1));
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
71
81
|
if (args[0] === "serve") {
|
|
72
82
|
await runServe(args.slice(1));
|
|
73
83
|
return;
|
package/dist/src/config.d.ts
CHANGED
|
@@ -142,8 +142,16 @@ export declare function repoSlug(repo: string): string;
|
|
|
142
142
|
* Windows is the roaming profile and gets synced across machines. */
|
|
143
143
|
export declare function clonesDir(): string;
|
|
144
144
|
export declare function cloneDir(repo: string): string;
|
|
145
|
+
/** The directory holding one repository's PR worktrees. `clear` removes it
|
|
146
|
+
* whole, so the layout lives in one place instead of at each call site. */
|
|
147
|
+
export declare function repoWorktreesDir(repo: string): string;
|
|
148
|
+
/** Every repository's worktrees, for `clear all` and `uninstall`. */
|
|
149
|
+
export declare function worktreesDir(): string;
|
|
145
150
|
export declare function worktreeDir(repo: string, pr: number): string;
|
|
146
151
|
export declare function cacheDbPath(): string;
|
|
152
|
+
/** Transient pid files that keep one local review per repository root.
|
|
153
|
+
* `uninstall` still takes them: they are co-maintainer data too. */
|
|
154
|
+
export declare function locksDir(): string;
|
|
147
155
|
export declare function loadEnvFile(path: string): void;
|
|
148
156
|
export declare function readConfig(): UserConfig;
|
|
149
157
|
/** The App private key to sign with. An inline `githubAppPrivateKey` wins,
|
|
@@ -158,6 +166,12 @@ export declare function appPrivateKeyFileMissing(config: Pick<UserConfig, "githu
|
|
|
158
166
|
/** Merges `patch` into `repos[repo]` in config.json, dropping undefined
|
|
159
167
|
* fields. Never call this with a token or other secret. */
|
|
160
168
|
export declare function writeRepoConfig(repo: string, patch: RepoConfig): Promise<void>;
|
|
169
|
+
/** Drops `repos[repo]` from config.json. `clear` uses this so the repository
|
|
170
|
+
* goes back to "never inited": the next run resolves its flags again. Returns
|
|
171
|
+
* whether there was an entry to drop. */
|
|
172
|
+
export declare function removeRepoConfig(repo: string): Promise<boolean>;
|
|
173
|
+
/** Drops every per-repo entry. `clear all` forgets every repository it knows. */
|
|
174
|
+
export declare function removeAllRepoConfig(): Promise<boolean>;
|
|
161
175
|
/** Merges `patch` into the top-level (global, cross-repo) config —
|
|
162
176
|
* used by `co-maintainer set`. This is the only path allowed to persist
|
|
163
177
|
* a token. */
|
package/dist/src/config.js
CHANGED
|
@@ -117,12 +117,26 @@ export function clonesDir() {
|
|
|
117
117
|
export function cloneDir(repo) {
|
|
118
118
|
return `${clonesDir()}/${repoSlug(repo)}`;
|
|
119
119
|
}
|
|
120
|
+
/** The directory holding one repository's PR worktrees. `clear` removes it
|
|
121
|
+
* whole, so the layout lives in one place instead of at each call site. */
|
|
122
|
+
export function repoWorktreesDir(repo) {
|
|
123
|
+
return `${getCacheDir()}/co-maintainer/wt/${repoSlug(repo)}`;
|
|
124
|
+
}
|
|
125
|
+
/** Every repository's worktrees, for `clear all` and `uninstall`. */
|
|
126
|
+
export function worktreesDir() {
|
|
127
|
+
return `${getCacheDir()}/co-maintainer/wt`;
|
|
128
|
+
}
|
|
120
129
|
export function worktreeDir(repo, pr) {
|
|
121
|
-
return `${
|
|
130
|
+
return `${repoWorktreesDir(repo)}/${pr}`;
|
|
122
131
|
}
|
|
123
132
|
export function cacheDbPath() {
|
|
124
133
|
return `${getCacheDir()}/co-maintainer/cache.db`;
|
|
125
134
|
}
|
|
135
|
+
/** Transient pid files that keep one local review per repository root.
|
|
136
|
+
* `uninstall` still takes them: they are co-maintainer data too. */
|
|
137
|
+
export function locksDir() {
|
|
138
|
+
return `${getCacheDir()}/co-maintainer/locks`;
|
|
139
|
+
}
|
|
126
140
|
export function loadEnvFile(path) {
|
|
127
141
|
const contents = readTextFileSync(path);
|
|
128
142
|
for (const line of contents.split(/\r?\n/)) {
|
|
@@ -200,6 +214,29 @@ export async function writeRepoConfig(repo, patch) {
|
|
|
200
214
|
repos[repo] = Object.fromEntries(Object.entries({ ...repos[repo], ...patch }).filter(([, value]) => value !== undefined));
|
|
201
215
|
await writeConfig({ ...config, repos });
|
|
202
216
|
}
|
|
217
|
+
/** Drops `repos[repo]` from config.json. `clear` uses this so the repository
|
|
218
|
+
* goes back to "never inited": the next run resolves its flags again. Returns
|
|
219
|
+
* whether there was an entry to drop. */
|
|
220
|
+
export async function removeRepoConfig(repo) {
|
|
221
|
+
const config = readConfig();
|
|
222
|
+
if (!config.repos || !(repo in config.repos))
|
|
223
|
+
return false;
|
|
224
|
+
const repos = { ...config.repos };
|
|
225
|
+
delete repos[repo];
|
|
226
|
+
await writeConfig({
|
|
227
|
+
...config,
|
|
228
|
+
repos: Object.keys(repos).length > 0 ? repos : undefined,
|
|
229
|
+
});
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
/** Drops every per-repo entry. `clear all` forgets every repository it knows. */
|
|
233
|
+
export async function removeAllRepoConfig() {
|
|
234
|
+
const config = readConfig();
|
|
235
|
+
if (!config.repos || Object.keys(config.repos).length === 0)
|
|
236
|
+
return false;
|
|
237
|
+
await writeConfig({ ...config, repos: undefined });
|
|
238
|
+
return true;
|
|
239
|
+
}
|
|
203
240
|
/** Merges `patch` into the top-level (global, cross-repo) config —
|
|
204
241
|
* used by `co-maintainer set`. This is the only path allowed to persist
|
|
205
242
|
* a token. */
|
|
@@ -157,6 +157,9 @@ function extractRequest(prompt, maxTokens) {
|
|
|
157
157
|
job: "extract_unit",
|
|
158
158
|
prompt,
|
|
159
159
|
maxTokens,
|
|
160
|
+
// Pulling facts out of a text needs no thinking, and on a thinking model
|
|
161
|
+
// the thinking used the whole limit and left the answer empty.
|
|
162
|
+
reasoningEffort: "none",
|
|
160
163
|
system: "Extract only repository-specific facts. Never invent files, commands, or process.",
|
|
161
164
|
};
|
|
162
165
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { locksDir } from "../config.js";
|
|
2
2
|
import { ReviewCliError } from "./git_ops.js";
|
|
3
3
|
import { isNotFound, isProcessAlive, mkdir, readTextFile, remove, removeSync, writeTextFile, } from "../util/runtime.js";
|
|
4
4
|
async function lockPath(gitRoot) {
|
|
@@ -7,7 +7,7 @@ async function lockPath(gitRoot) {
|
|
|
7
7
|
.map((b) => b.toString(16).padStart(2, "0"))
|
|
8
8
|
.join("")
|
|
9
9
|
.slice(0, 16);
|
|
10
|
-
const dir =
|
|
10
|
+
const dir = locksDir();
|
|
11
11
|
await mkdir(dir, { recursive: true });
|
|
12
12
|
return `${dir}/${hex}.lock`;
|
|
13
13
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** `undefined` means the clear may go ahead. A 409 carries enough detail for
|
|
2
|
+
* the page to ask the right question: whether to abort reviews and queued
|
|
3
|
+
* jobs, or to wait out a sync. */
|
|
4
|
+
export declare function negotiateJobs(repos: string[], abort: boolean): Response | undefined;
|
|
5
|
+
export declare function handleKnowledgeRoute(request: Request, url: URL): Promise<Response>;
|
|
6
|
+
/** Shared by the per-repo route, which knows exactly one repository. */
|
|
7
|
+
export declare function clearOneRepo(fullName: string, url: URL): Promise<Response>;
|