ndrstnd 0.1.0
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 +201 -0
- package/README.md +56 -0
- package/dist/server/agent.d.ts +51 -0
- package/dist/server/agent.js +77 -0
- package/dist/server/agent.js.map +1 -0
- package/dist/server/analysis-core.d.ts +93 -0
- package/dist/server/analysis-core.js +442 -0
- package/dist/server/analysis-core.js.map +1 -0
- package/dist/server/analyze.d.ts +55 -0
- package/dist/server/analyze.js +72 -0
- package/dist/server/analyze.js.map +1 -0
- package/dist/server/artifact.d.ts +8 -0
- package/dist/server/artifact.js +43 -0
- package/dist/server/artifact.js.map +1 -0
- package/dist/server/claude.d.ts +29 -0
- package/dist/server/claude.js +243 -0
- package/dist/server/claude.js.map +1 -0
- package/dist/server/cli-support.d.ts +7 -0
- package/dist/server/cli-support.js +13 -0
- package/dist/server/cli-support.js.map +1 -0
- package/dist/server/cli.d.ts +2 -0
- package/dist/server/cli.js +276 -0
- package/dist/server/cli.js.map +1 -0
- package/dist/server/codex.d.ts +34 -0
- package/dist/server/codex.js +256 -0
- package/dist/server/codex.js.map +1 -0
- package/dist/server/conversation.d.ts +9 -0
- package/dist/server/conversation.js +57 -0
- package/dist/server/conversation.js.map +1 -0
- package/dist/server/evidence-ordering.d.ts +13 -0
- package/dist/server/evidence-ordering.js +143 -0
- package/dist/server/evidence-ordering.js.map +1 -0
- package/dist/server/git-model.d.ts +15 -0
- package/dist/server/git-model.js +156 -0
- package/dist/server/git-model.js.map +1 -0
- package/dist/server/git.d.ts +29 -0
- package/dist/server/git.js +124 -0
- package/dist/server/git.js.map +1 -0
- package/dist/server/http.d.ts +14 -0
- package/dist/server/http.js +139 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/preview-fixture.d.ts +1 -0
- package/dist/server/preview-fixture.js +23 -0
- package/dist/server/preview-fixture.js.map +1 -0
- package/dist/server/review-presentation.d.ts +3 -0
- package/dist/server/review-presentation.js +14 -0
- package/dist/server/review-presentation.js.map +1 -0
- package/dist/server/skill.d.ts +14 -0
- package/dist/server/skill.js +94 -0
- package/dist/server/skill.js.map +1 -0
- package/dist/server/store.d.ts +36 -0
- package/dist/server/store.js +134 -0
- package/dist/server/store.js.map +1 -0
- package/dist/server/version.d.ts +2 -0
- package/dist/server/version.js +13 -0
- package/dist/server/version.js.map +1 -0
- package/dist/shared/analysis-schema.d.ts +325 -0
- package/dist/shared/analysis-schema.js +50 -0
- package/dist/shared/analysis-schema.js.map +1 -0
- package/dist/shared/domain.d.ts +33 -0
- package/dist/shared/domain.js +2 -0
- package/dist/shared/domain.js.map +1 -0
- package/dist/web/evidence-model.d.ts +36 -0
- package/dist/web/evidence-model.js +134 -0
- package/dist/web/evidence-model.js.map +1 -0
- package/dist/web/frozen-review-data.d.ts +3 -0
- package/dist/web/frozen-review-data.js +202 -0
- package/dist/web/frozen-review-data.js.map +1 -0
- package/dist/web/language.d.ts +4 -0
- package/dist/web/language.js +14 -0
- package/dist/web/language.js.map +1 -0
- package/dist/web/page.d.ts +10 -0
- package/dist/web/page.js +1038 -0
- package/dist/web/page.js.map +1 -0
- package/dist/web/review-data.d.ts +20 -0
- package/dist/web/review-data.js +2 -0
- package/dist/web/review-data.js.map +1 -0
- package/dist/web/test-plan-model.d.ts +27 -0
- package/dist/web/test-plan-model.js +89 -0
- package/dist/web/test-plan-model.js.map +1 -0
- package/package.json +47 -0
- package/src/skill-assets/ndrstnd/SKILL.md +22 -0
- package/src/skill-assets/ndrstnd/agents/openai.yaml +4 -0
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { mkdir, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createReviewPresentationData } from "./review-presentation.js";
|
|
5
|
+
import { renderArtifact } from "../web/page.js";
|
|
6
|
+
const artifactPrefix = "ndrstnd-";
|
|
7
|
+
const maxArtifactAgeMs = 7 * 24 * 60 * 60 * 1000;
|
|
8
|
+
export async function writeReviewArtifact(session, revision, options = {}) {
|
|
9
|
+
const directory = options.directory ?? defaultArtifactDirectory();
|
|
10
|
+
await mkdir(directory, { recursive: true, mode: 0o700 });
|
|
11
|
+
await cleanupArtifacts(directory, options.now);
|
|
12
|
+
const timestamp = (options.now ?? new Date()).toISOString().replace(/[:.]/g, "-");
|
|
13
|
+
const filePath = join(directory, `${artifactPrefix}${timestamp}-${revision.id.slice(0, 8)}.html`);
|
|
14
|
+
await writeFile(filePath, await renderArtifact(createReviewPresentationData(session, revision)), { encoding: "utf8", mode: 0o600 });
|
|
15
|
+
return filePath;
|
|
16
|
+
}
|
|
17
|
+
export async function cleanupArtifacts(directory = defaultArtifactDirectory(), now = new Date()) {
|
|
18
|
+
let entries;
|
|
19
|
+
try {
|
|
20
|
+
entries = await readdir(directory);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error.code === "ENOENT")
|
|
24
|
+
return;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
await Promise.all(entries.filter((entry) => entry.startsWith(artifactPrefix) && entry.endsWith(".html")).map(async (entry) => {
|
|
28
|
+
const path = join(directory, entry);
|
|
29
|
+
const metadata = await stat(path);
|
|
30
|
+
if (now.getTime() - metadata.mtime.getTime() > maxArtifactAgeMs)
|
|
31
|
+
await rm(path, { force: true });
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
export function defaultArtifactDirectory() {
|
|
35
|
+
if (process.env.NDRSTND_ARTIFACT_DIR !== undefined)
|
|
36
|
+
return process.env.NDRSTND_ARTIFACT_DIR;
|
|
37
|
+
if (process.platform === "darwin")
|
|
38
|
+
return join(homedir(), "Library", "Application Support", "ndrstnd", "artifacts");
|
|
39
|
+
if (process.platform === "win32")
|
|
40
|
+
return join(process.env.LOCALAPPDATA ?? homedir(), "ndrstnd", "artifacts");
|
|
41
|
+
return join(process.env.XDG_DATA_HOME ?? join(homedir(), ".local", "share"), "ndrstnd", "artifacts");
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=artifact.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"artifact.js","sourceRoot":"","sources":["../../src/server/artifact.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { AgentClient, AgentTextThread, AuthStatus } from "./agent.js";
|
|
2
|
+
/** Turns a stream-json event into reviewer-facing phrasing; undefined means the previous label still applies. */
|
|
3
|
+
export declare function describeClaudeEvent(event: Record<string, unknown>): string | undefined;
|
|
4
|
+
export declare function getClaudeAuthStatus(): Promise<AuthStatus>;
|
|
5
|
+
/**
|
|
6
|
+
* Runs analysis turns through `claude --print`. Each turn is one short-lived
|
|
7
|
+
* process; follow-up turns (validation repairs) resume the same Claude Code
|
|
8
|
+
* session by ID so the inspection context is kept without resending the prompt.
|
|
9
|
+
*/
|
|
10
|
+
export declare class ClaudeCodeClient implements AgentClient {
|
|
11
|
+
private readonly inactivityTimeoutMs;
|
|
12
|
+
private closed;
|
|
13
|
+
private readonly active;
|
|
14
|
+
constructor(inactivityTimeoutMs?: number);
|
|
15
|
+
startTextThread(cwd: string): Promise<AgentTextThread>;
|
|
16
|
+
close(): void;
|
|
17
|
+
private runPrintTurn;
|
|
18
|
+
/** Kept as an instance method so tests can substitute a scripted child process. */
|
|
19
|
+
private spawnClaude;
|
|
20
|
+
}
|
|
21
|
+
/** The command override lets tests exercise the timeout without a real claude binary. */
|
|
22
|
+
export declare function runClaudeCommand(args: string[], options?: {
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
command?: string;
|
|
25
|
+
}): Promise<{
|
|
26
|
+
stdout: string;
|
|
27
|
+
stderr: string;
|
|
28
|
+
exitCode: number | null;
|
|
29
|
+
}>;
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
/** Turns a stream-json event into reviewer-facing phrasing; undefined means the previous label still applies. */
|
|
3
|
+
export function describeClaudeEvent(event) {
|
|
4
|
+
if (event["type"] === "stream_event") {
|
|
5
|
+
const streamEvent = asObject(event["event"]);
|
|
6
|
+
return streamEvent["type"] === "content_block_delta" && asObject(streamEvent["delta"])["type"] === "text_delta" ? "drafting the narrative" : undefined;
|
|
7
|
+
}
|
|
8
|
+
if (event["type"] !== "assistant")
|
|
9
|
+
return undefined;
|
|
10
|
+
const content = asObject(event["message"])["content"];
|
|
11
|
+
const block = asObject(Array.isArray(content) ? content.at(-1) : undefined);
|
|
12
|
+
if (block["type"] === "text")
|
|
13
|
+
return "drafting the narrative";
|
|
14
|
+
if (block["type"] !== "tool_use")
|
|
15
|
+
return undefined;
|
|
16
|
+
const name = typeof block["name"] === "string" ? block["name"] : "";
|
|
17
|
+
const input = asObject(block["input"]);
|
|
18
|
+
if (name === "Bash" && typeof input["command"] === "string" && input["command"].trim() !== "")
|
|
19
|
+
return `running \`${compactText(input["command"])}\``;
|
|
20
|
+
if (name === "Read" && typeof input["file_path"] === "string")
|
|
21
|
+
return `reading ${compactText(input["file_path"])}`;
|
|
22
|
+
if (name === "Grep" || name === "Glob")
|
|
23
|
+
return "searching the repository";
|
|
24
|
+
return name === "" ? "inspecting the repository" : `using ${name}`;
|
|
25
|
+
}
|
|
26
|
+
function compactText(value) {
|
|
27
|
+
const collapsed = value.replace(/\s+/g, " ").trim();
|
|
28
|
+
return collapsed.length > 90 ? `${collapsed.slice(0, 87)}…` : collapsed;
|
|
29
|
+
}
|
|
30
|
+
export async function getClaudeAuthStatus() {
|
|
31
|
+
try {
|
|
32
|
+
const { stdout, stderr, exitCode } = await runClaudeCommand(["auth", "status", "--json"]);
|
|
33
|
+
const payload = parseJsonObject(stdout);
|
|
34
|
+
if (payload === undefined) {
|
|
35
|
+
throw new Error(exitCode === 0 ? "Claude Code returned an unreadable auth status." : `claude auth status exited with status ${exitCode}.${stderrHint(stderr)}`);
|
|
36
|
+
}
|
|
37
|
+
if (payload["loggedIn"] !== true)
|
|
38
|
+
return { state: "signed-out" };
|
|
39
|
+
const accountType = [payload["subscriptionType"], payload["authMethod"]].find((value) => typeof value === "string" && value !== "");
|
|
40
|
+
return { state: "signed-in", accountType: typeof accountType === "string" ? accountType : "unknown" };
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
return { state: "unreachable", reason: error instanceof Error ? error.message : String(error) };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Runs analysis turns through `claude --print`. Each turn is one short-lived
|
|
48
|
+
* process; follow-up turns (validation repairs) resume the same Claude Code
|
|
49
|
+
* session by ID so the inspection context is kept without resending the prompt.
|
|
50
|
+
*/
|
|
51
|
+
export class ClaudeCodeClient {
|
|
52
|
+
inactivityTimeoutMs;
|
|
53
|
+
closed = false;
|
|
54
|
+
active = new Set();
|
|
55
|
+
constructor(inactivityTimeoutMs = 300_000) {
|
|
56
|
+
this.inactivityTimeoutMs = inactivityTimeoutMs;
|
|
57
|
+
}
|
|
58
|
+
async startTextThread(cwd) {
|
|
59
|
+
let sessionId;
|
|
60
|
+
return {
|
|
61
|
+
send: async (prompt, onActivity) => {
|
|
62
|
+
const turn = await this.runPrintTurn(cwd, prompt, sessionId, onActivity);
|
|
63
|
+
sessionId = turn.sessionId ?? sessionId;
|
|
64
|
+
return turn.text;
|
|
65
|
+
},
|
|
66
|
+
// Print-mode turns end with their process; there is no server-side thread to archive.
|
|
67
|
+
close: async () => undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
close() {
|
|
71
|
+
this.closed = true;
|
|
72
|
+
for (const child of this.active)
|
|
73
|
+
child.kill();
|
|
74
|
+
this.active.clear();
|
|
75
|
+
}
|
|
76
|
+
runPrintTurn(cwd, prompt, resumeSessionId, onActivity) {
|
|
77
|
+
if (this.closed)
|
|
78
|
+
return Promise.reject(new Error("Claude Code client is not running."));
|
|
79
|
+
const args = [
|
|
80
|
+
"--print",
|
|
81
|
+
"--output-format", "stream-json",
|
|
82
|
+
"--verbose",
|
|
83
|
+
"--include-partial-messages",
|
|
84
|
+
// The analysis inspects the repository and must never edit it, run
|
|
85
|
+
// non-git commands, or reach configured MCP servers.
|
|
86
|
+
"--permission-mode", "dontAsk",
|
|
87
|
+
"--tools", "Read,Grep,Glob,Bash",
|
|
88
|
+
"--allowed-tools", "Read,Grep,Glob,Bash(git *)",
|
|
89
|
+
"--strict-mcp-config",
|
|
90
|
+
];
|
|
91
|
+
if (resumeSessionId !== undefined)
|
|
92
|
+
args.push("--resume", resumeSessionId);
|
|
93
|
+
const child = this.spawnClaude(cwd, args);
|
|
94
|
+
this.active.add(child);
|
|
95
|
+
let stdoutBuffer = "";
|
|
96
|
+
let stderrTail = "";
|
|
97
|
+
let draft = "";
|
|
98
|
+
let events = 0;
|
|
99
|
+
let sessionId;
|
|
100
|
+
let result;
|
|
101
|
+
return new Promise((resolve, reject) => {
|
|
102
|
+
let settled = false;
|
|
103
|
+
let timeout;
|
|
104
|
+
const settle = (outcome) => {
|
|
105
|
+
if (settled)
|
|
106
|
+
return;
|
|
107
|
+
settled = true;
|
|
108
|
+
clearTimeout(timeout);
|
|
109
|
+
this.active.delete(child);
|
|
110
|
+
child.kill();
|
|
111
|
+
outcome();
|
|
112
|
+
};
|
|
113
|
+
const failWith = (error) => settle(() => reject(error));
|
|
114
|
+
const armTimeout = () => {
|
|
115
|
+
// Inspecting a large branch takes many quiet-but-active tool turns, so
|
|
116
|
+
// time out on inactivity across any stream event rather than total duration.
|
|
117
|
+
clearTimeout(timeout);
|
|
118
|
+
timeout = setTimeout(() => failWith(new Error(`Claude Code analysis stalled: no CLI activity for ${Math.round(this.inactivityTimeoutMs / 1_000)}s after ${events} stream events and ${draft.length} draft characters.${stderrHint(stderrTail)}`)), this.inactivityTimeoutMs);
|
|
119
|
+
};
|
|
120
|
+
let label = "starting the analysis turn";
|
|
121
|
+
const handleEvent = (event) => {
|
|
122
|
+
events += 1;
|
|
123
|
+
armTimeout();
|
|
124
|
+
if (typeof event["session_id"] === "string" && sessionId === undefined)
|
|
125
|
+
sessionId = event["session_id"];
|
|
126
|
+
if (event["type"] === "stream_event") {
|
|
127
|
+
const streamEvent = asObject(event["event"]);
|
|
128
|
+
const delta = asObject(streamEvent["delta"]);
|
|
129
|
+
if (streamEvent["type"] === "content_block_delta" && delta["type"] === "text_delta" && typeof delta["text"] === "string") {
|
|
130
|
+
draft += delta["text"];
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (event["type"] === "result") {
|
|
134
|
+
const denials = Array.isArray(event["permission_denials"]) ? event["permission_denials"].length : 0;
|
|
135
|
+
result = {
|
|
136
|
+
text: typeof event["result"] === "string" ? event["result"] : "",
|
|
137
|
+
isError: event["is_error"] === true,
|
|
138
|
+
detail: `${typeof event["subtype"] === "string" ? event["subtype"] : "an unreported failure"}${denials > 0 ? ` after ${denials} denied tool request${denials === 1 ? "" : "s"}` : ""}`,
|
|
139
|
+
};
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
label = describeClaudeEvent(event) ?? label;
|
|
143
|
+
onActivity?.({ label, notifications: events, draftCharacters: draft.length });
|
|
144
|
+
};
|
|
145
|
+
const parseLine = (line) => {
|
|
146
|
+
if (line === "")
|
|
147
|
+
return;
|
|
148
|
+
try {
|
|
149
|
+
handleEvent(JSON.parse(line));
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Ignore non-JSON noise on stdout.
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
child.stdout.on("data", (chunk) => {
|
|
156
|
+
stdoutBuffer += chunk.toString("utf8");
|
|
157
|
+
for (;;) {
|
|
158
|
+
const newline = stdoutBuffer.indexOf("\n");
|
|
159
|
+
if (newline === -1)
|
|
160
|
+
return;
|
|
161
|
+
const line = stdoutBuffer.slice(0, newline).trim();
|
|
162
|
+
stdoutBuffer = stdoutBuffer.slice(newline + 1);
|
|
163
|
+
parseLine(line);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
child.stderr.on("data", (chunk) => {
|
|
167
|
+
stderrTail = `${stderrTail}${chunk.toString("utf8")}`.slice(-4_000);
|
|
168
|
+
});
|
|
169
|
+
child.once("error", (error) => failWith(new Error(`Claude Code could not run: ${error.message}${stderrHint(stderrTail)}`)));
|
|
170
|
+
child.once("close", (code) => {
|
|
171
|
+
// A final event is not guaranteed a trailing newline; drain it before judging the outcome.
|
|
172
|
+
parseLine(stdoutBuffer.trim());
|
|
173
|
+
stdoutBuffer = "";
|
|
174
|
+
const outcome = result;
|
|
175
|
+
if (outcome !== undefined && outcome.isError)
|
|
176
|
+
return failWith(new Error(`Claude Code analysis turn failed: ${outcome.text !== "" ? outcome.text : outcome.detail}.${stderrHint(stderrTail)}`));
|
|
177
|
+
if (outcome === undefined)
|
|
178
|
+
return failWith(new Error(`Claude Code exited with status ${code ?? "unknown"} before reporting a result.${stderrHint(stderrTail)}`));
|
|
179
|
+
settle(() => resolve({ text: outcome.text !== "" ? outcome.text : draft, sessionId }));
|
|
180
|
+
});
|
|
181
|
+
armTimeout();
|
|
182
|
+
// A process dying before it drains the prompt EPIPEs stdin; the close handler already reports that failure, so the stream error must not crash the CLI.
|
|
183
|
+
child.stdin.on("error", () => undefined);
|
|
184
|
+
child.stdin.end(prompt);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
/** Kept as an instance method so tests can substitute a scripted child process. */
|
|
188
|
+
spawnClaude(cwd, args) {
|
|
189
|
+
return spawn("claude", args, { cwd, stdio: "pipe" });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/** The command override lets tests exercise the timeout without a real claude binary. */
|
|
193
|
+
export function runClaudeCommand(args, options = {}) {
|
|
194
|
+
const timeoutMs = options.timeoutMs ?? 60_000;
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const child = spawn(options.command ?? "claude", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
197
|
+
let settled = false;
|
|
198
|
+
const timeout = setTimeout(() => {
|
|
199
|
+
settled = true;
|
|
200
|
+
child.kill();
|
|
201
|
+
reject(new Error(`claude ${args.join(" ")} did not answer within ${Math.round(timeoutMs / 1_000)}s.`));
|
|
202
|
+
}, timeoutMs);
|
|
203
|
+
timeout.unref();
|
|
204
|
+
const settle = (outcome) => {
|
|
205
|
+
if (settled)
|
|
206
|
+
return;
|
|
207
|
+
settled = true;
|
|
208
|
+
clearTimeout(timeout);
|
|
209
|
+
outcome();
|
|
210
|
+
};
|
|
211
|
+
let stdout = "";
|
|
212
|
+
let stderr = "";
|
|
213
|
+
child.stdout.on("data", (chunk) => {
|
|
214
|
+
stdout += chunk.toString("utf8");
|
|
215
|
+
});
|
|
216
|
+
child.stderr.on("data", (chunk) => {
|
|
217
|
+
stderr = `${stderr}${chunk.toString("utf8")}`.slice(-4_000);
|
|
218
|
+
});
|
|
219
|
+
child.once("error", (error) => settle(() => reject(new Error(`Claude Code could not run: ${error.message}`))));
|
|
220
|
+
child.once("close", (exitCode) => settle(() => resolve({ stdout, stderr, exitCode })));
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function parseJsonObject(text) {
|
|
224
|
+
const start = text.indexOf("{");
|
|
225
|
+
const end = text.lastIndexOf("}");
|
|
226
|
+
if (start === -1 || end <= start)
|
|
227
|
+
return undefined;
|
|
228
|
+
try {
|
|
229
|
+
const value = JSON.parse(text.slice(start, end + 1));
|
|
230
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function stderrHint(tail) {
|
|
237
|
+
const trimmed = tail.trim().slice(-600);
|
|
238
|
+
return trimmed === "" ? "" : ` Claude Code reported: ${trimmed}`;
|
|
239
|
+
}
|
|
240
|
+
function asObject(value) {
|
|
241
|
+
return value !== null && typeof value === "object" ? value : {};
|
|
242
|
+
}
|
|
243
|
+
//# sourceMappingURL=claude.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"claude.js","sourceRoot":"","sources":["../../src/server/claude.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAuC,MAAM,oBAAoB,CAAC;AAGhF,iHAAiH;AACjH,MAAM,UAAU,mBAAmB,CAAC,KAA8B;IAChE,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,cAAc,EAAE,CAAC;QACrC,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAC7C,OAAO,WAAW,CAAC,MAAM,CAAC,KAAK,qBAAqB,IAAI,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,SAAS,CAAC;IACzJ,CAAC;IACD,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,WAAW;QAAE,OAAO,SAAS,CAAC;IACpD,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAC5E,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,MAAM;QAAE,OAAO,wBAAwB,CAAC;IAC9D,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,UAAU;QAAE,OAAO,SAAS,CAAC;IACnD,MAAM,IAAI,GAAG,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IACpE,MAAM,KAAK,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;IACvC,IAAI,IAAI,KAAK,MAAM,IAAI,OAAO,KAAK,CAAC,SAAS,CAAC,KAAK,QAAQ,IAAI,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,aAAa,WAAW,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,IAAI,CAAC;IACrJ,IAAI,IAAI,KAAK,MAAM,IAAI,OAAO,KAAK,CAAC,WAAW,CAAC,KAAK,QAAQ;QAAE,OAAO,WAAW,WAAW,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,EAAE,CAAC;IACnH,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,MAAM;QAAE,OAAO,0BAA0B,CAAC;IAC1E,OAAO,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,2BAA2B,CAAC,CAAC,CAAC,SAAS,IAAI,EAAE,CAAC;AACrE,CAAC;AAED,SAAS,WAAW,CAAC,KAAa;IAChC,MAAM,SAAS,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;IACpD,OAAO,SAAS,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;AAC1E,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,IAAI,CAAC;QACH,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,gBAAgB,CAAC,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,CAAC,CAAC,CAAC;QAC1F,MAAM,OAAO,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QACxC,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;YAC1B,MAAM,IAAI,KAAK,CAAC,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,iDAAiD,CAAC,CAAC,CAAC,yCAAyC,QAAQ,IAAI,UAAU,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAClK,CAAC;QACD,IAAI,OAAO,CAAC,UAAU,CAAC,KAAK,IAAI;YAAE,OAAO,EAAE,KAAK,EAAE,YAAY,EAAE,CAAC;QACjE,MAAM,WAAW,GAAG,CAAC,OAAO,CAAC,kBAAkB,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,EAAE,CAAC,CAAC;QACpI,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,WAAW,EAAE,OAAO,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS,EAAE,CAAC;IACxG,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;IAClG,CAAC;AACH,CAAC;AAED;;;;GAIG;AACH,MAAM,OAAO,gBAAgB;IAIE;IAHrB,MAAM,GAAG,KAAK,CAAC;IACN,MAAM,GAAG,IAAI,GAAG,EAAkC,CAAC;IAEpE,YAA6B,sBAAsB,OAAO;QAA7B,wBAAmB,GAAnB,mBAAmB,CAAU;IAAG,CAAC;IAE9D,KAAK,CAAC,eAAe,CAAC,GAAW;QAC/B,IAAI,SAA6B,CAAC;QAClC,OAAO;YACL,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,EAAE;gBACjC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,UAAU,CAAC,CAAC;gBACzE,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,SAAS,CAAC;gBACxC,OAAO,IAAI,CAAC,IAAI,CAAC;YACnB,CAAC;YACD,sFAAsF;YACtF,KAAK,EAAE,KAAK,IAAI,EAAE,CAAC,SAAS;SAC7B,CAAC;IACJ,CAAC;IAED,KAAK;QACH,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;QACnB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM;YAAE,KAAK,CAAC,IAAI,EAAE,CAAC;QAC9C,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;IAEO,YAAY,CAAC,GAAW,EAAE,MAAc,EAAE,eAAmC,EAAE,UAA6C;QAClI,IAAI,IAAI,CAAC,MAAM;YAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC,CAAC;QACxF,MAAM,IAAI,GAAG;YACX,SAAS;YACT,iBAAiB,EAAE,aAAa;YAChC,WAAW;YACX,4BAA4B;YAC5B,mEAAmE;YACnE,qDAAqD;YACrD,mBAAmB,EAAE,SAAS;YAC9B,SAAS,EAAE,qBAAqB;YAChC,iBAAiB,EAAE,4BAA4B;YAC/C,qBAAqB;SACtB,CAAC;QACF,IAAI,eAAe,KAAK,SAAS;YAAE,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,eAAe,CAAC,CAAC;QAC1E,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;QAC1C,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QAEvB,IAAI,YAAY,GAAG,EAAE,CAAC;QACtB,IAAI,UAAU,GAAG,EAAE,CAAC;QACpB,IAAI,KAAK,GAAG,EAAE,CAAC;QACf,IAAI,MAAM,GAAG,CAAC,CAAC;QACf,IAAI,SAA6B,CAAC;QAClC,IAAI,MAAsE,CAAC;QAE3E,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,IAAI,OAAO,GAAG,KAAK,CAAC;YACpB,IAAI,OAAuB,CAAC;YAC5B,MAAM,MAAM,GAAG,CAAC,OAAmB,EAAE,EAAE;gBACrC,IAAI,OAAO;oBAAE,OAAO;gBACpB,OAAO,GAAG,IAAI,CAAC;gBACf,YAAY,CAAC,OAAO,CAAC,CAAC;gBACtB,IAAI,CAAC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBAC1B,KAAK,CAAC,IAAI,EAAE,CAAC;gBACb,OAAO,EAAE,CAAC;YACZ,CAAC,CAAC;YACF,MAAM,QAAQ,GAAG,CAAC,KAAY,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YAC/D,MAAM,UAAU,GAAG,GAAG,EAAE;gBACtB,uEAAuE;gBACvE,6EAA6E;gBAC7E,YAAY,CAAC,OAAO,CAAC,CAAC;gBACtB,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,qDAAqD,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,mBAAmB,GAAG,KAAK,CAAC,WAAW,MAAM,sBAAsB,KAAK,CAAC,MAAM,qBAAqB,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,IAAI,CAAC,mBAAmB,CAAC,CAAC;YAC/Q,CAAC,CAAC;YACF,IAAI,KAAK,GAAG,4BAA4B,CAAC;YACzC,MAAM,WAAW,GAAG,CAAC,KAA8B,EAAE,EAAE;gBACrD,MAAM,IAAI,CAAC,CAAC;gBACZ,UAAU,EAAE,CAAC;gBACb,IAAI,OAAO,KAAK,CAAC,YAAY,CAAC,KAAK,QAAQ,IAAI,SAAS,KAAK,SAAS;oBAAE,SAAS,GAAG,KAAK,CAAC,YAAY,CAAC,CAAC;gBACxG,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,cAAc,EAAE,CAAC;oBACrC,MAAM,WAAW,GAAG,QAAQ,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;oBAC7C,MAAM,KAAK,GAAG,QAAQ,CAAC,WAAW,CAAC,OAAO,CAAC,CAAC,CAAC;oBAC7C,IAAI,WAAW,CAAC,MAAM,CAAC,KAAK,qBAAqB,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,YAAY,IAAI,OAAO,KAAK,CAAC,MAAM,CAAC,KAAK,QAAQ,EAAE,CAAC;wBACzH,KAAK,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC;oBACzB,CAAC;gBACH,CAAC;gBACD,IAAI,KAAK,CAAC,MAAM,CAAC,KAAK,QAAQ,EAAE,CAAC;oBAC/B,MAAM,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;oBACpG,MAAM,GAAG;wBACP,IAAI,EAAE,OAAO,KAAK,CAAC,QAAQ,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE;wBAChE,OAAO,EAAE,KAAK,CAAC,UAAU,CAAC,KAAK,IAAI;wBACnC,MAAM,EAAE,GAAG,OAAO,KAAK,CAAC,SAAS,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,uBAAuB,GAAG,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,OAAO,uBAAuB,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,EAAE;qBACvL,CAAC;oBACF,OAAO;gBACT,CAAC;gBACD,KAAK,GAAG,mBAAmB,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC;gBAC5C,UAAU,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,MAAM,EAAE,eAAe,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;YAChF,CAAC,CAAC;YAEF,MAAM,SAAS,GAAG,CAAC,IAAY,EAAE,EAAE;gBACjC,IAAI,IAAI,KAAK,EAAE;oBAAE,OAAO;gBACxB,IAAI,CAAC;oBACH,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAA4B,CAAC,CAAC;gBAC3D,CAAC;gBAAC,MAAM,CAAC;oBACP,mCAAmC;gBACrC,CAAC;YACH,CAAC,CAAC;YACF,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;gBACxC,YAAY,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;gBACvC,SAAS,CAAC;oBACR,MAAM,OAAO,GAAG,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;oBAC3C,IAAI,OAAO,KAAK,CAAC,CAAC;wBAAE,OAAO;oBAC3B,MAAM,IAAI,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,EAAE,OAAO,CAAC,CAAC,IAAI,EAAE,CAAC;oBACnD,YAAY,GAAG,YAAY,CAAC,KAAK,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC;oBAC/C,SAAS,CAAC,IAAI,CAAC,CAAC;gBAClB,CAAC;YACH,CAAC,CAAC,CAAC;YACH,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;gBACxC,UAAU,GAAG,GAAG,UAAU,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;YACtE,CAAC,CAAC,CAAC;YACH,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,QAAQ,CAAC,IAAI,KAAK,CAAC,8BAA8B,KAAK,CAAC,OAAO,GAAG,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAC5H,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;gBAC3B,2FAA2F;gBAC3F,SAAS,CAAC,YAAY,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC/B,YAAY,GAAG,EAAE,CAAC;gBAClB,MAAM,OAAO,GAAG,MAAM,CAAC;gBACvB,IAAI,OAAO,KAAK,SAAS,IAAI,OAAO,CAAC,OAAO;oBAAE,OAAO,QAAQ,CAAC,IAAI,KAAK,CAAC,qCAAqC,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,IAAI,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;gBAC/L,IAAI,OAAO,KAAK,SAAS;oBAAE,OAAO,QAAQ,CAAC,IAAI,KAAK,CAAC,kCAAkC,IAAI,IAAI,SAAS,8BAA8B,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC;gBACjK,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;YACzF,CAAC,CAAC,CAAC;YACH,UAAU,EAAE,CAAC;YACb,wJAAwJ;YACxJ,KAAK,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;YACzC,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC1B,CAAC,CAAC,CAAC;IACL,CAAC;IAED,mFAAmF;IAC3E,WAAW,CAAC,GAAW,EAAE,IAAc;QAC7C,OAAO,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC,CAAC;IACvD,CAAC;CACF;AAED,yFAAyF;AACzF,MAAM,UAAU,gBAAgB,CAAC,IAAc,EAAE,UAAoD,EAAE;IACrG,MAAM,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,MAAM,CAAC;IAC9C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QACrC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,OAAO,IAAI,QAAQ,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,CAAC,CAAC;QAC9F,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,OAAO,GAAG,UAAU,CAAC,GAAG,EAAE;YAC9B,OAAO,GAAG,IAAI,CAAC;YACf,KAAK,CAAC,IAAI,EAAE,CAAC;YACb,MAAM,CAAC,IAAI,KAAK,CAAC,UAAU,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,0BAA0B,IAAI,CAAC,KAAK,CAAC,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACzG,CAAC,EAAE,SAAS,CAAC,CAAC;QACd,OAAO,CAAC,KAAK,EAAE,CAAC;QAChB,MAAM,MAAM,GAAG,CAAC,OAAmB,EAAE,EAAE;YACrC,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,YAAY,CAAC,OAAO,CAAC,CAAC;YACtB,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC;QACF,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,IAAI,MAAM,GAAG,EAAE,CAAC;QAChB,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACxC,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;QACnC,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE;YACxC,MAAM,GAAG,GAAG,MAAM,GAAG,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC;QAC9D,CAAC,CAAC,CAAC;QACH,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,8BAA8B,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAC/G,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,QAAQ,EAAE,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,CAAC;IACzF,CAAC,CAAC,CAAC;AACL,CAAC;AAED,SAAS,eAAe,CAAC,IAAY;IACnC,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAChC,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC;IAClC,IAAI,KAAK,KAAK,CAAC,CAAC,IAAI,GAAG,IAAI,KAAK;QAAE,OAAO,SAAS,CAAC;IACnD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,CAAC,CAAY,CAAC;QAChE,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAgC,CAAC,CAAC,CAAC,SAAS,CAAC;IAC7H,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC;IACxC,OAAO,OAAO,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,0BAA0B,OAAO,EAAE,CAAC;AACnE,CAAC;AAED,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,KAAgC,CAAC,CAAC,CAAC,EAAE,CAAC;AAC7F,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Chooses the platform opener; kept pure so the cmd.exe escaping stays testable. */
|
|
2
|
+
export declare function browserOpenCommand(url: string, platform: NodeJS.Platform): {
|
|
3
|
+
command: string;
|
|
4
|
+
args: string[];
|
|
5
|
+
};
|
|
6
|
+
/** cmd.exe parses &, |, <, >, ^, ( and ) even inside start arguments; a path containing them would otherwise split the command line. */
|
|
7
|
+
export declare function escapeCmdArgument(argument: string): string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Chooses the platform opener; kept pure so the cmd.exe escaping stays testable. */
|
|
2
|
+
export function browserOpenCommand(url, platform) {
|
|
3
|
+
if (platform === "darwin")
|
|
4
|
+
return { command: "open", args: [url] };
|
|
5
|
+
if (platform === "win32")
|
|
6
|
+
return { command: "cmd", args: ["/c", "start", "", escapeCmdArgument(url)] };
|
|
7
|
+
return { command: "xdg-open", args: [url] };
|
|
8
|
+
}
|
|
9
|
+
/** cmd.exe parses &, |, <, >, ^, ( and ) even inside start arguments; a path containing them would otherwise split the command line. */
|
|
10
|
+
export function escapeCmdArgument(argument) {
|
|
11
|
+
return argument.replace(/[&|^<>()]/g, "^$&");
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=cli-support.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli-support.js","sourceRoot":"","sources":["../../src/server/cli-support.ts"],"names":[],"mappings":"AAAA,qFAAqF;AACrF,MAAM,UAAU,kBAAkB,CAAC,GAAW,EAAE,QAAyB;IACvE,IAAI,QAAQ,KAAK,QAAQ;QAAE,OAAO,EAAE,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;IACnE,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,EAAE,EAAE,iBAAiB,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;IACvG,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC;AAC9C,CAAC;AAED,wIAAwI;AACxI,MAAM,UAAU,iBAAiB,CAAC,QAAgB;IAChD,OAAO,QAAQ,CAAC,OAAO,CAAC,YAAY,EAAE,KAAK,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from "node:child_process";
|
|
3
|
+
import { realpath } from "node:fs/promises";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { resolveReviewAgent, reviewAgents } from "./agent.js";
|
|
6
|
+
import { analyzeWithAgent, formatAnalysisHeartbeat } from "./analyze.js";
|
|
7
|
+
import { importConversation } from "./conversation.js";
|
|
8
|
+
import { GitReader, describeReviewScope, ensureArtifactDirectoryIgnored } from "./git.js";
|
|
9
|
+
import { ReviewStore, selectReusableRevision } from "./store.js";
|
|
10
|
+
import { installSkill, installedSkillIsStale } from "./skill.js";
|
|
11
|
+
import { writeReviewArtifact } from "./artifact.js";
|
|
12
|
+
import { browserOpenCommand } from "./cli-support.js";
|
|
13
|
+
import { packageVersion } from "./version.js";
|
|
14
|
+
import { pathToFileURL } from "node:url";
|
|
15
|
+
import { join, resolve } from "node:path";
|
|
16
|
+
const [command, ...args] = process.argv.slice(2);
|
|
17
|
+
if (command === undefined || command === "--help" || command === "-h") {
|
|
18
|
+
printHelp();
|
|
19
|
+
}
|
|
20
|
+
else if (command === "--version" || command === "-v" || command === "version") {
|
|
21
|
+
process.stdout.write(`ndrstnd ${await packageVersion()}\n`);
|
|
22
|
+
}
|
|
23
|
+
else if (command === "auth") {
|
|
24
|
+
await runAuth(args);
|
|
25
|
+
}
|
|
26
|
+
else if (command === "skill") {
|
|
27
|
+
await runSkill(args);
|
|
28
|
+
}
|
|
29
|
+
else if (command === "review") {
|
|
30
|
+
await runReview(args);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
fail(`Unknown command: ${command}`);
|
|
34
|
+
}
|
|
35
|
+
/** A typo in any flag must fail loudly; silently ignoring it would run the wrong review, agent, or install. */
|
|
36
|
+
function parseCommandArgs(command, args, valueOptions, booleanOptions, maxPositional) {
|
|
37
|
+
const values = new Map();
|
|
38
|
+
const flags = new Set();
|
|
39
|
+
const positional = [];
|
|
40
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
41
|
+
const argument = args[index];
|
|
42
|
+
if (valueOptions.includes(argument)) {
|
|
43
|
+
const value = args[index + 1];
|
|
44
|
+
if (value === undefined || value.startsWith("-"))
|
|
45
|
+
fail(`${argument} requires a value.`);
|
|
46
|
+
values.set(argument, value);
|
|
47
|
+
index += 1;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (booleanOptions.includes(argument)) {
|
|
51
|
+
flags.add(argument);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (argument.startsWith("-"))
|
|
55
|
+
fail(`Unknown ${command} option: ${argument}. Known options: ${[...valueOptions, ...booleanOptions].join(", ")}.`);
|
|
56
|
+
if (positional.length >= maxPositional)
|
|
57
|
+
fail(`Unexpected extra ${command} argument: ${argument}.`);
|
|
58
|
+
positional.push(argument);
|
|
59
|
+
}
|
|
60
|
+
return { positional, values, flags };
|
|
61
|
+
}
|
|
62
|
+
function wantsHelp(args) {
|
|
63
|
+
return args.includes("--help") || args.includes("-h");
|
|
64
|
+
}
|
|
65
|
+
function agentOptionFrom(values) {
|
|
66
|
+
const value = values.get("--agent");
|
|
67
|
+
if (value === undefined)
|
|
68
|
+
return undefined;
|
|
69
|
+
if (!reviewAgents.some((agent) => agent.id === value))
|
|
70
|
+
fail(`Unknown analysis agent: ${value}. Supported agents: ${reviewAgents.map((agent) => agent.id).join(", ")}.`);
|
|
71
|
+
return value;
|
|
72
|
+
}
|
|
73
|
+
async function runSkill(args) {
|
|
74
|
+
if (wantsHelp(args))
|
|
75
|
+
return printHelp();
|
|
76
|
+
const { positional, values, flags } = parseCommandArgs("skill", args, ["--agent"], ["--force"], 1);
|
|
77
|
+
if (positional[0] !== "install")
|
|
78
|
+
fail("Usage: ndrstnd skill install [--force] [--agent <codex|claude>]");
|
|
79
|
+
const installations = await installSkill(flags.has("--force"), agentOptionFrom(values)).catch((error) => fail(error instanceof Error ? error.message : String(error)));
|
|
80
|
+
for (const installation of installations) {
|
|
81
|
+
if (installation.status === "installed")
|
|
82
|
+
process.stdout.write(`Installed the ndrstnd skill for ${installation.agent.name} at ${installation.destination}.\n`);
|
|
83
|
+
else
|
|
84
|
+
process.stdout.write(`Skipped ${installation.agent.name}: ${installation.reason}.\n`);
|
|
85
|
+
}
|
|
86
|
+
if (!installations.some((installation) => installation.status === "installed")) {
|
|
87
|
+
fail("No skill was installed.");
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function runAuth(args) {
|
|
91
|
+
if (wantsHelp(args))
|
|
92
|
+
return printHelp();
|
|
93
|
+
const { positional, values } = parseCommandArgs("auth", args, ["--agent"], [], 1);
|
|
94
|
+
const action = positional[0] ?? "status";
|
|
95
|
+
const agentId = agentOptionFrom(values);
|
|
96
|
+
if (action === "status") {
|
|
97
|
+
const agents = agentId === undefined ? reviewAgents : reviewAgents.filter((agent) => agent.id === agentId);
|
|
98
|
+
for (const agent of agents) {
|
|
99
|
+
const status = await agent.getAuthStatus();
|
|
100
|
+
if (status.state === "signed-in")
|
|
101
|
+
process.stdout.write(`${agent.name} authentication is ready (${status.accountType}).\n`);
|
|
102
|
+
else if (status.state === "signed-out")
|
|
103
|
+
process.stdout.write(`${agent.name} is not signed in. Run \`ndrstnd auth login --agent ${agent.id}\`.\n`);
|
|
104
|
+
else
|
|
105
|
+
process.stdout.write(`${agent.name} authentication could not be checked: ${status.reason}\n`);
|
|
106
|
+
}
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (action === "login") {
|
|
110
|
+
const agent = await resolveReviewAgent(agentId).catch((error) => fail(error instanceof Error ? error.message : String(error)));
|
|
111
|
+
const beforeLogin = await agent.getAuthStatus();
|
|
112
|
+
if (beforeLogin.state === "signed-in") {
|
|
113
|
+
process.stdout.write(`${agent.name} authentication is already ready (${beforeLogin.accountType}).\n`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
await runAgentLogin(agent);
|
|
117
|
+
const afterLogin = await agent.getAuthStatus();
|
|
118
|
+
if (afterLogin.state === "signed-in")
|
|
119
|
+
process.stdout.write(`${agent.name} authentication is ready (${afterLogin.accountType}).\n`);
|
|
120
|
+
else
|
|
121
|
+
fail(`${agent.name} login completed but ndrstnd could not validate an authenticated connection.`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
fail(`Unknown auth action: ${action}`);
|
|
125
|
+
}
|
|
126
|
+
async function runAgentLogin(agent) {
|
|
127
|
+
const child = spawn(agent.command, agent.loginArgs, { stdio: "inherit" });
|
|
128
|
+
const exitCode = await new Promise((resolve, reject) => {
|
|
129
|
+
child.once("error", reject);
|
|
130
|
+
child.once("exit", resolve);
|
|
131
|
+
});
|
|
132
|
+
if (exitCode !== 0) {
|
|
133
|
+
process.exitCode = exitCode ?? 1;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async function runReview(args) {
|
|
137
|
+
if (wantsHelp(args))
|
|
138
|
+
return printHelp();
|
|
139
|
+
const { positional, values, flags } = parseCommandArgs("review", args, ["--base", "--repo", "--conversation", "--agent"], ["--uncommitted", "--no-open", "--fresh"], 1);
|
|
140
|
+
const targetArg = positional[0];
|
|
141
|
+
const uncommitted = flags.has("--uncommitted");
|
|
142
|
+
const explicitBase = values.get("--base");
|
|
143
|
+
if (uncommitted && explicitBase !== undefined)
|
|
144
|
+
fail("--uncommitted already reviews against HEAD; do not combine it with --base.");
|
|
145
|
+
if (uncommitted && targetArg !== undefined)
|
|
146
|
+
fail("--uncommitted reviews the checked-out branch; do not pass a branch.");
|
|
147
|
+
const noOpen = flags.has("--no-open");
|
|
148
|
+
const repoPath = await canonicalRepoPath(values.get("--repo"));
|
|
149
|
+
const baseRef = uncommitted ? "HEAD" : explicitBase;
|
|
150
|
+
const agent = await resolveReviewAgent(values.get("--agent")).catch((error) => fail(error instanceof Error ? error.message : String(error)));
|
|
151
|
+
const conversationPath = values.get("--conversation");
|
|
152
|
+
const store = openReviewStore();
|
|
153
|
+
try {
|
|
154
|
+
const conversation = conversationPath === undefined ? undefined : await importConversation(conversationPath);
|
|
155
|
+
await ensureArtifactDirectoryIgnored(repoPath);
|
|
156
|
+
const input = await new GitReader().collectReviewInput(repoPath, targetArg, baseRef);
|
|
157
|
+
const meaningfulFiles = input.files.filter((file) => file.signal === "meaningful").length;
|
|
158
|
+
const scope = await describeReviewScope(repoPath, input);
|
|
159
|
+
process.stdout.write(`Reviewing ${scope.targetLabel} against ${input.baseRef}${input.includesWorkingTree ? ", including uncommitted changes" : ""}: ${input.files.length} changed file${input.files.length === 1 ? "" : "s"} (${meaningfulFiles} meaningful).\n`);
|
|
160
|
+
if (baseRef === undefined && input.includesWorkingTree && scope.localCommitsIncluded > 0) {
|
|
161
|
+
process.stdout.write(`Warning: the inferred base ${input.baseRef} is ${scope.localCommitsIncluded} commit${scope.localCommitsIncluded === 1 ? "" : "s"} behind ${scope.targetLabel}, so those commits are included. Pass --base to narrow the review, or --uncommitted for only uncommitted changes.\n`);
|
|
162
|
+
}
|
|
163
|
+
if (await installedSkillIsStale()) {
|
|
164
|
+
process.stdout.write("The installed ndrstnd skill is older than this version; run `ndrstnd skill install --force` to refresh it.\n");
|
|
165
|
+
}
|
|
166
|
+
const session = store.getOrCreateSession(input, conversation);
|
|
167
|
+
let revision = flags.has("--fresh") ? undefined : selectReusableRevision(store.listRevisions(session.id), agent.id);
|
|
168
|
+
if (revision !== undefined) {
|
|
169
|
+
process.stdout.write(`Reusing the ${agent.name} analysis cached ${formatRevisionAge(revision.createdAt)}; pass --fresh to re-analyze.\n`);
|
|
170
|
+
}
|
|
171
|
+
if (revision === undefined) {
|
|
172
|
+
const auth = await agent.getAuthStatus();
|
|
173
|
+
if (auth.state === "signed-out")
|
|
174
|
+
throw new Error(`${agent.name} is not signed in, so ndrstnd cannot analyze this change. Run \`ndrstnd auth login --agent ${agent.id}\` first.`);
|
|
175
|
+
if (auth.state === "unreachable")
|
|
176
|
+
throw new Error(`${agent.name} could not be reached, so ndrstnd cannot analyze this change: ${auth.reason}`);
|
|
177
|
+
process.stdout.write(`Drafting the review narrative with ${agent.name}… This takes minutes on large branches; a heartbeat line prints every 15 seconds while the analysis is alive.\n`);
|
|
178
|
+
const heartbeat = startAnalysisHeartbeat(agent);
|
|
179
|
+
try {
|
|
180
|
+
const document = await analyzeWithAgent(agent, input, conversation, heartbeat.progress);
|
|
181
|
+
revision = store.createRevision(session.id, agent.id, "complete", document);
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
throw new Error(`${agent.name} analysis failed, so no review artifact was written: ${error instanceof Error ? error.message : String(error)} Nothing was persisted; re-run the same ndrstnd review command to retry.`);
|
|
185
|
+
}
|
|
186
|
+
finally {
|
|
187
|
+
heartbeat.stop();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
process.stdout.write(`merge-base=${input.mergeBase.slice(0, 12)} files=${input.files.length} meaningful-files=${meaningfulFiles} hunks=${input.hunks.length}${conversation === undefined ? "" : ` conversation=${conversation.messages.length}`}\n`);
|
|
191
|
+
const artifactPath = await writeReviewArtifact(session, revision, { directory: join(repoPath, ".ndrstnd") });
|
|
192
|
+
process.stdout.write(`ndrstnd artifact: ${artifactPath}\n`);
|
|
193
|
+
process.stdout.write("This self-contained file is in the Git-ignored .ndrstnd directory; delete it when the review is done.\n");
|
|
194
|
+
if (!noOpen)
|
|
195
|
+
openBrowser(pathToFileURL(artifactPath).href);
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
// fail() exits the process immediately, so the store must close first rather than in a finally.
|
|
199
|
+
store.close();
|
|
200
|
+
fail(error instanceof Error ? error.message : String(error));
|
|
201
|
+
}
|
|
202
|
+
store.close();
|
|
203
|
+
}
|
|
204
|
+
/** Resolving and de-symlinking the repository path keeps session identity independent of how the path was spelled. */
|
|
205
|
+
async function canonicalRepoPath(repoArgument) {
|
|
206
|
+
const requested = resolve(repoArgument ?? process.cwd());
|
|
207
|
+
try {
|
|
208
|
+
return await realpath(requested);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
fail(`The repository path ${requested} does not exist.`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
function formatRevisionAge(createdAt, now = Date.now()) {
|
|
215
|
+
const minutes = Math.max(0, Math.round((now - Date.parse(createdAt)) / 60_000));
|
|
216
|
+
if (minutes < 60)
|
|
217
|
+
return `${minutes} minute${minutes === 1 ? "" : "s"} ago`;
|
|
218
|
+
const hours = Math.round(minutes / 60);
|
|
219
|
+
if (hours < 48)
|
|
220
|
+
return `${hours} hour${hours === 1 ? "" : "s"} ago`;
|
|
221
|
+
const days = Math.round(hours / 24);
|
|
222
|
+
return `${days} day${days === 1 ? "" : "s"} ago`;
|
|
223
|
+
}
|
|
224
|
+
/** Prints liveness lines while the agent analyzes so a caller (human or agent) never mistakes a long turn for a hang. */
|
|
225
|
+
function startAnalysisHeartbeat(agent) {
|
|
226
|
+
// Defined here because the top-level command dispatch runs before module-level consts initialize.
|
|
227
|
+
const HEARTBEAT_INTERVAL_MS = 15_000;
|
|
228
|
+
const startedAt = Date.now();
|
|
229
|
+
let latest;
|
|
230
|
+
let lastEventAt = startedAt;
|
|
231
|
+
const timer = setInterval(() => {
|
|
232
|
+
process.stdout.write(`${formatAnalysisHeartbeat(agent.name, Date.now() - startedAt, latest, Date.now() - lastEventAt)}\n`);
|
|
233
|
+
}, HEARTBEAT_INTERVAL_MS);
|
|
234
|
+
timer.unref();
|
|
235
|
+
return {
|
|
236
|
+
progress: {
|
|
237
|
+
onActivity: (activity) => {
|
|
238
|
+
latest = activity;
|
|
239
|
+
lastEventAt = Date.now();
|
|
240
|
+
},
|
|
241
|
+
onRepair: (attempt, attempts, problem) => {
|
|
242
|
+
process.stdout.write(`${agent.name}'s draft failed validation; requesting repair turn ${attempt} of ${attempts}: ${problem}\n`);
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
stop: () => clearInterval(timer),
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function openReviewStore() {
|
|
249
|
+
try {
|
|
250
|
+
return new ReviewStore();
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
254
|
+
const hint = /NODE_MODULE_VERSION|compiled against a different Node\.js version/.test(message)
|
|
255
|
+
? ` The review store's native SQLite module was built for a different Node.js than the current ${process.versions.node}; run \`npm rebuild better-sqlite3\` in the ndrstnd installation or reinstall ndrstnd.`
|
|
256
|
+
: "";
|
|
257
|
+
fail(`The ndrstnd review store could not open: ${message}${hint}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
function openBrowser(url) {
|
|
261
|
+
const { command, args } = browserOpenCommand(url, process.platform);
|
|
262
|
+
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
263
|
+
// A machine without an opener (headless Linux) must not turn a finished review into a crash.
|
|
264
|
+
child.once("error", (error) => {
|
|
265
|
+
process.stdout.write(`Could not open a browser automatically (${error.message}); open the artifact path above manually.\n`);
|
|
266
|
+
});
|
|
267
|
+
child.unref();
|
|
268
|
+
}
|
|
269
|
+
function printHelp() {
|
|
270
|
+
process.stdout.write(`ndrstnd: understand agent-produced branch changes\n\nUsage:\n ndrstnd auth <status|login> [--agent <codex|claude>]\n ndrstnd skill install [--force] [--agent <codex|claude>]\n ndrstnd review [branch] [--base <branch>] [--uncommitted] [--repo <path>] [--conversation <path>] [--agent <codex|claude>] [--fresh] [--no-open]\n ndrstnd --version\n\nWithout a branch, ndrstnd reviews the checked-out branch including uncommitted changes.\n--uncommitted reviews only the uncommitted working-tree changes (an alias for --base HEAD).\n--agent picks the analysis agent; without it ndrstnd uses NDRSTND_AGENT, then the Codex or Claude Code session it runs inside, then the first installed CLI, preferring Codex.\n--fresh re-analyzes even when a cached analysis exists for the same input.\n`);
|
|
271
|
+
}
|
|
272
|
+
function fail(message) {
|
|
273
|
+
process.stderr.write(`${message}\n`);
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
276
|
+
//# sourceMappingURL=cli.js.map
|