pudu-ai 0.2.18 → 0.2.21
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/README.es.md +85 -0
- package/README.md +19 -2
- package/dist/cli/brew-ZRR7D4XL.js +80 -0
- package/dist/cli/chunk-6C5EPKRI.js +1597 -0
- package/dist/cli/chunk-QGEXKMQ5.js +501 -0
- package/dist/cli/chunk-SS7OXXQQ.js +82 -0
- package/dist/cli/index.js +191 -2991
- package/dist/cli/render-3UBPZNF4.js +812 -0
- package/dist/cli/repo-cli-4GSG5MD3.js +255 -0
- package/package.json +3 -2
- package/python/pudu_agent/__init__.py +1 -0
- package/python/pudu_agent/__main__.py +4 -0
- package/python/pudu_agent/__pycache__/__init__.cpython-312.pyc +0 -0
- package/python/pudu_agent/__pycache__/__main__.cpython-312.pyc +0 -0
- package/python/pudu_agent/__pycache__/ast_search.cpython-312.pyc +0 -0
- package/python/pudu_agent/__pycache__/protocol.cpython-312.pyc +0 -0
- package/python/pudu_agent/__pycache__/search.cpython-312.pyc +0 -0
- package/python/pudu_agent/ast_search.py +153 -0
- package/python/pudu_agent/protocol.py +96 -0
- package/python/pudu_agent/search.py +342 -0
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import {
|
|
2
|
+
na,
|
|
3
|
+
t
|
|
4
|
+
} from "./chunk-QGEXKMQ5.js";
|
|
5
|
+
import {
|
|
6
|
+
commandExists,
|
|
7
|
+
runCommand
|
|
8
|
+
} from "./chunk-SS7OXXQQ.js";
|
|
9
|
+
|
|
10
|
+
// src/agent-lab/repo-cli.ts
|
|
11
|
+
import path2 from "node:path";
|
|
12
|
+
|
|
13
|
+
// src/agent-lab/python-bridge.ts
|
|
14
|
+
import { existsSync } from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import { fileURLToPath } from "node:url";
|
|
17
|
+
function hasEngine(dir) {
|
|
18
|
+
return existsSync(path.join(dir, "pudu_agent", "__main__.py"));
|
|
19
|
+
}
|
|
20
|
+
function pythonRoot() {
|
|
21
|
+
const starts = [path.dirname(fileURLToPath(import.meta.url)), process.cwd()];
|
|
22
|
+
for (const start of starts) {
|
|
23
|
+
let dir = start;
|
|
24
|
+
for (let i = 0; i < 8; i++) {
|
|
25
|
+
const candidate = path.join(dir, "python");
|
|
26
|
+
if (hasEngine(candidate)) return candidate;
|
|
27
|
+
const parent = path.dirname(dir);
|
|
28
|
+
if (parent === dir) break;
|
|
29
|
+
dir = parent;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "python");
|
|
33
|
+
}
|
|
34
|
+
async function findPython() {
|
|
35
|
+
return await commandExists("python3") ?? await commandExists("python");
|
|
36
|
+
}
|
|
37
|
+
async function invokePuduAgent(request, timeout = 3e4) {
|
|
38
|
+
const python = await findPython();
|
|
39
|
+
if (!python) {
|
|
40
|
+
throw new Error("python3 not found on PATH");
|
|
41
|
+
}
|
|
42
|
+
const root = pythonRoot();
|
|
43
|
+
const payload = `${JSON.stringify(request)}
|
|
44
|
+
`;
|
|
45
|
+
const result = await runCommand(python, ["-m", "pudu_agent"], {
|
|
46
|
+
cwd: root,
|
|
47
|
+
env: { ...process.env, PYTHONPATH: root, PYTHONUNBUFFERED: "1" },
|
|
48
|
+
input: payload,
|
|
49
|
+
timeout
|
|
50
|
+
});
|
|
51
|
+
return { ...result, python };
|
|
52
|
+
}
|
|
53
|
+
function parseJsonStdout(stdout) {
|
|
54
|
+
const text = stdout.trim();
|
|
55
|
+
if (!text) {
|
|
56
|
+
throw new Error("Python engine returned empty stdout");
|
|
57
|
+
}
|
|
58
|
+
const start = text.indexOf("{");
|
|
59
|
+
const slice = start >= 0 ? text.slice(start) : text;
|
|
60
|
+
return JSON.parse(slice);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/agent-lab/schema.ts
|
|
64
|
+
import { z } from "zod";
|
|
65
|
+
var metricOriginSchema = z.enum(["MEASURED", "ESTIMATED", "DERIVED"]);
|
|
66
|
+
var searchIntentSchema = z.enum([
|
|
67
|
+
"TEXT",
|
|
68
|
+
"STRUCTURAL",
|
|
69
|
+
"RELATIONSHIP",
|
|
70
|
+
"IMPACT",
|
|
71
|
+
"SEMANTIC",
|
|
72
|
+
"UNKNOWN"
|
|
73
|
+
]);
|
|
74
|
+
var toolAvailabilitySchema = z.object({
|
|
75
|
+
available: z.boolean(),
|
|
76
|
+
path: z.string().nullable(),
|
|
77
|
+
version: z.string().nullable()
|
|
78
|
+
});
|
|
79
|
+
var searchMatchSchema = z.object({
|
|
80
|
+
file: z.string(),
|
|
81
|
+
line: z.number().nullable(),
|
|
82
|
+
column: z.number().nullable(),
|
|
83
|
+
endLine: z.number().nullable(),
|
|
84
|
+
endColumn: z.number().nullable(),
|
|
85
|
+
text: z.string(),
|
|
86
|
+
strategy: z.enum(["rg", "ast-grep"]),
|
|
87
|
+
language: z.string().nullable(),
|
|
88
|
+
metavariables: z.record(z.string())
|
|
89
|
+
});
|
|
90
|
+
var searchResultSchema = z.object({
|
|
91
|
+
schemaVersion: z.literal(1),
|
|
92
|
+
ok: z.boolean(),
|
|
93
|
+
op: z.literal("search"),
|
|
94
|
+
repo: z.string(),
|
|
95
|
+
query: z.string().nullable(),
|
|
96
|
+
structuralPattern: z.string().nullable(),
|
|
97
|
+
intent: searchIntentSchema,
|
|
98
|
+
strategy: z.enum(["rg", "ast-grep", "graph", "hybrid"]),
|
|
99
|
+
matches: z.array(searchMatchSchema),
|
|
100
|
+
tools: z.object({
|
|
101
|
+
rg: toolAvailabilitySchema,
|
|
102
|
+
astGrep: toolAvailabilitySchema,
|
|
103
|
+
graph: toolAvailabilitySchema
|
|
104
|
+
}),
|
|
105
|
+
errors: z.array(
|
|
106
|
+
z.object({
|
|
107
|
+
tool: z.string(),
|
|
108
|
+
message: z.string(),
|
|
109
|
+
origin: metricOriginSchema
|
|
110
|
+
})
|
|
111
|
+
),
|
|
112
|
+
metrics: z.object({
|
|
113
|
+
durationMs: z.number(),
|
|
114
|
+
matchCount: z.number(),
|
|
115
|
+
rgQueries: z.number(),
|
|
116
|
+
astQueries: z.number(),
|
|
117
|
+
graphQueries: z.number(),
|
|
118
|
+
origin: metricOriginSchema
|
|
119
|
+
}),
|
|
120
|
+
unavailable: z.array(z.string())
|
|
121
|
+
});
|
|
122
|
+
function parseSearchResult(value) {
|
|
123
|
+
return searchResultSchema.parse(value);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/agent-lab/search.ts
|
|
127
|
+
async function searchRepo(request) {
|
|
128
|
+
const invoked = await invokePuduAgent({
|
|
129
|
+
op: "search",
|
|
130
|
+
repo: request.repo,
|
|
131
|
+
query: request.query ?? null,
|
|
132
|
+
structuralPattern: request.structuralPattern ?? null,
|
|
133
|
+
intent: request.intent ?? null,
|
|
134
|
+
globs: request.globs ?? [],
|
|
135
|
+
limit: request.limit ?? 100,
|
|
136
|
+
timeoutMs: request.timeoutMs ?? 2e4
|
|
137
|
+
});
|
|
138
|
+
if (invoked.timedOut) {
|
|
139
|
+
throw new Error("Python search engine timed out");
|
|
140
|
+
}
|
|
141
|
+
let parsed;
|
|
142
|
+
try {
|
|
143
|
+
parsed = parseJsonStdout(invoked.stdout);
|
|
144
|
+
} catch {
|
|
145
|
+
const err = invoked.stderr.trim() || invoked.stdout.trim() || "invalid JSON from python -m pudu_agent";
|
|
146
|
+
throw new Error(err);
|
|
147
|
+
}
|
|
148
|
+
if (typeof parsed === "object" && parsed && "op" in parsed && parsed.op !== "search") {
|
|
149
|
+
throw new Error(parsed.error ?? "unexpected Python response");
|
|
150
|
+
}
|
|
151
|
+
return parseSearchResult(parsed);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// src/agent-lab/repo-cli.ts
|
|
155
|
+
var INTENTS = ["TEXT", "STRUCTURAL", "RELATIONSHIP", "IMPACT", "SEMANTIC", "UNKNOWN"];
|
|
156
|
+
function print(value, json) {
|
|
157
|
+
if (json) {
|
|
158
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}
|
|
159
|
+
`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
process.stdout.write(`${typeof value === "string" ? value : JSON.stringify(value, null, 2)}
|
|
163
|
+
`);
|
|
164
|
+
}
|
|
165
|
+
function asIntent(raw) {
|
|
166
|
+
if (!raw) return void 0;
|
|
167
|
+
const value = raw.trim().toUpperCase();
|
|
168
|
+
return INTENTS.find((item) => item === value);
|
|
169
|
+
}
|
|
170
|
+
function formatSearchText(result) {
|
|
171
|
+
const lines = [
|
|
172
|
+
t("repoSearchTitle"),
|
|
173
|
+
"",
|
|
174
|
+
t("aboutRepoSearch"),
|
|
175
|
+
"",
|
|
176
|
+
`Intent ${result.intent}`,
|
|
177
|
+
`Strategy ${result.strategy}`,
|
|
178
|
+
`Repo ${result.repo}`,
|
|
179
|
+
`Matches ${result.metrics.matchCount} (${result.metrics.origin})`,
|
|
180
|
+
`Duration ${result.metrics.durationMs} ms (${result.metrics.origin})`,
|
|
181
|
+
`rg ${result.tools.rg.available ? na(result.tools.rg.version) : t("repoToolMissing", { tool: "rg" })}`,
|
|
182
|
+
`ast-grep ${result.tools.astGrep.available ? na(result.tools.astGrep.version) : t("repoToolMissing", { tool: "ast-grep" })}`
|
|
183
|
+
];
|
|
184
|
+
if (result.unavailable.length) {
|
|
185
|
+
lines.push(`Unavailable ${result.unavailable.join(", ")}`);
|
|
186
|
+
}
|
|
187
|
+
for (const err of result.errors) {
|
|
188
|
+
lines.push(`Error ${err.tool}: ${err.message} (${err.origin})`);
|
|
189
|
+
}
|
|
190
|
+
lines.push("");
|
|
191
|
+
if (!result.matches.length) {
|
|
192
|
+
lines.push(t("repoNoMatches"));
|
|
193
|
+
return lines.join("\n");
|
|
194
|
+
}
|
|
195
|
+
for (const match of result.matches) {
|
|
196
|
+
const loc = `${match.file}:${na(match.line)}`;
|
|
197
|
+
const snippet = match.text.replace(/\s+/g, " ").slice(0, 160);
|
|
198
|
+
lines.push(`${loc} [${match.strategy}] ${snippet}`);
|
|
199
|
+
}
|
|
200
|
+
return lines.join("\n");
|
|
201
|
+
}
|
|
202
|
+
async function runRepoCommand(args) {
|
|
203
|
+
const sub = args.positional[0];
|
|
204
|
+
if (!sub || sub === "help") {
|
|
205
|
+
print(t("repoHelp"), args.json);
|
|
206
|
+
return 0;
|
|
207
|
+
}
|
|
208
|
+
if (sub !== "search") {
|
|
209
|
+
process.stderr.write(`${t("repoUnknownSubcommand", { sub })}
|
|
210
|
+
`);
|
|
211
|
+
process.stderr.write(`${t("repoHelp")}
|
|
212
|
+
`);
|
|
213
|
+
return 1;
|
|
214
|
+
}
|
|
215
|
+
const python = await findPython();
|
|
216
|
+
if (!python) {
|
|
217
|
+
process.stderr.write(`${t("repoPythonMissing")}
|
|
218
|
+
`);
|
|
219
|
+
return 1;
|
|
220
|
+
}
|
|
221
|
+
const queryParts = args.positional.slice(1);
|
|
222
|
+
const query = queryParts.join(" ").trim() || void 0;
|
|
223
|
+
if (!query && !args.structural) {
|
|
224
|
+
process.stderr.write(`${t("repoNeedQuery")}
|
|
225
|
+
`);
|
|
226
|
+
return 1;
|
|
227
|
+
}
|
|
228
|
+
const intent = asIntent(args.intent);
|
|
229
|
+
const repo = path2.resolve(args.repo ?? process.cwd());
|
|
230
|
+
try {
|
|
231
|
+
const result = await searchRepo({
|
|
232
|
+
repo,
|
|
233
|
+
query,
|
|
234
|
+
structuralPattern: args.structural,
|
|
235
|
+
intent,
|
|
236
|
+
globs: args.globs,
|
|
237
|
+
limit: args.limit
|
|
238
|
+
});
|
|
239
|
+
print(args.json ? result : formatSearchText(result), args.json);
|
|
240
|
+
return result.ok ? 0 : 1;
|
|
241
|
+
} catch (error) {
|
|
242
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
243
|
+
if (args.json) {
|
|
244
|
+
print({ ok: false, error: message }, true);
|
|
245
|
+
} else {
|
|
246
|
+
process.stderr.write(`${message}
|
|
247
|
+
`);
|
|
248
|
+
}
|
|
249
|
+
return 1;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
export {
|
|
253
|
+
formatSearchText,
|
|
254
|
+
runRepoCommand
|
|
255
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pudu-ai",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.21",
|
|
4
4
|
"description": "Pudu-AI — local AI hardware & benchmark lab for the terminal",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
"files": [
|
|
33
33
|
"dist",
|
|
34
34
|
"bin",
|
|
35
|
+
"python",
|
|
35
36
|
"README.md",
|
|
36
37
|
"LICENSE"
|
|
37
38
|
],
|
|
@@ -39,7 +40,7 @@
|
|
|
39
40
|
"node": ">=20"
|
|
40
41
|
},
|
|
41
42
|
"scripts": {
|
|
42
|
-
"build": "npx --yes esbuild src/cli/index.ts --bundle --platform=node --format=esm --
|
|
43
|
+
"build": "rm -rf dist && npx --yes esbuild src/cli/index.ts --bundle --platform=node --format=esm --splitting --outdir=dist/cli --outbase=src/cli --packages=external --jsx=automatic && node scripts/add-shebang.mjs",
|
|
43
44
|
"dev": "tsx src/cli/index.ts",
|
|
44
45
|
"pudu-ai": "tsx src/cli/index.ts",
|
|
45
46
|
"start": "node dist/cli/index.js",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PROTOCOL_VERSION = 1
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _which() -> str | None:
|
|
11
|
+
return shutil.which("ast-grep") or shutil.which("sg")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _version(binary: str) -> str | None:
|
|
15
|
+
try:
|
|
16
|
+
proc = subprocess.run(
|
|
17
|
+
[binary, "--version"],
|
|
18
|
+
capture_output=True,
|
|
19
|
+
text=True,
|
|
20
|
+
timeout=5,
|
|
21
|
+
check=False,
|
|
22
|
+
)
|
|
23
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
24
|
+
return None
|
|
25
|
+
line = (proc.stdout or proc.stderr or "").strip().splitlines()
|
|
26
|
+
return line[0] if line else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def tool_info() -> dict[str, Any]:
|
|
30
|
+
path = _which()
|
|
31
|
+
return {
|
|
32
|
+
"available": path is not None,
|
|
33
|
+
"path": path,
|
|
34
|
+
"version": _version(path) if path else None,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _match_from_ast(obj: dict[str, Any]) -> dict[str, Any] | None:
|
|
39
|
+
if not isinstance(obj, dict):
|
|
40
|
+
return None
|
|
41
|
+
file_path = obj.get("file")
|
|
42
|
+
text = obj.get("text")
|
|
43
|
+
if not isinstance(file_path, str):
|
|
44
|
+
return None
|
|
45
|
+
if not isinstance(text, str):
|
|
46
|
+
text = obj.get("lines") if isinstance(obj.get("lines"), str) else ""
|
|
47
|
+
rng = obj.get("range") or {}
|
|
48
|
+
start = rng.get("start") or {}
|
|
49
|
+
end = rng.get("end") or {}
|
|
50
|
+
start_line = start.get("line")
|
|
51
|
+
start_col = start.get("column")
|
|
52
|
+
end_line = end.get("line")
|
|
53
|
+
end_col = end.get("column")
|
|
54
|
+
metavars: dict[str, str] = {}
|
|
55
|
+
meta = obj.get("metaVariables") or {}
|
|
56
|
+
single = meta.get("single") if isinstance(meta, dict) else {}
|
|
57
|
+
if isinstance(single, dict):
|
|
58
|
+
for key, value in single.items():
|
|
59
|
+
if isinstance(value, dict) and isinstance(value.get("text"), str):
|
|
60
|
+
metavars[str(key)] = value["text"]
|
|
61
|
+
language = obj.get("language")
|
|
62
|
+
return {
|
|
63
|
+
"file": file_path,
|
|
64
|
+
"line": start_line + 1 if isinstance(start_line, int) else None,
|
|
65
|
+
"column": start_col + 1 if isinstance(start_col, int) else None,
|
|
66
|
+
"endLine": end_line + 1 if isinstance(end_line, int) else None,
|
|
67
|
+
"endColumn": end_col + 1 if isinstance(end_col, int) else None,
|
|
68
|
+
"text": text.rstrip("\n") if isinstance(text, str) else "",
|
|
69
|
+
"strategy": "ast-grep",
|
|
70
|
+
"language": language if isinstance(language, str) else None,
|
|
71
|
+
"metavariables": metavars,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_ast_grep_stdout(stdout: str) -> list[dict[str, Any]]:
|
|
76
|
+
text = (stdout or "").strip()
|
|
77
|
+
if not text:
|
|
78
|
+
return []
|
|
79
|
+
matches: list[dict[str, Any]] = []
|
|
80
|
+
try:
|
|
81
|
+
parsed = json.loads(text)
|
|
82
|
+
if isinstance(parsed, list):
|
|
83
|
+
for item in parsed:
|
|
84
|
+
match = _match_from_ast(item) if isinstance(item, dict) else None
|
|
85
|
+
if match:
|
|
86
|
+
matches.append(match)
|
|
87
|
+
return matches
|
|
88
|
+
if isinstance(parsed, dict):
|
|
89
|
+
match = _match_from_ast(parsed)
|
|
90
|
+
return [match] if match else []
|
|
91
|
+
except ValueError:
|
|
92
|
+
pass
|
|
93
|
+
for line in text.splitlines():
|
|
94
|
+
line = line.strip()
|
|
95
|
+
if not line:
|
|
96
|
+
continue
|
|
97
|
+
try:
|
|
98
|
+
obj = json.loads(line)
|
|
99
|
+
except ValueError:
|
|
100
|
+
continue
|
|
101
|
+
match = _match_from_ast(obj) if isinstance(obj, dict) else None
|
|
102
|
+
if match:
|
|
103
|
+
matches.append(match)
|
|
104
|
+
return matches
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run_ast_grep(
|
|
108
|
+
repo: Path,
|
|
109
|
+
pattern: str,
|
|
110
|
+
globs: list[str],
|
|
111
|
+
limit: int,
|
|
112
|
+
timeout_s: float,
|
|
113
|
+
) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
|
114
|
+
binary = _which()
|
|
115
|
+
if not binary:
|
|
116
|
+
return [], [{"tool": "ast-grep", "message": "ast-grep not found on PATH", "origin": "MEASURED"}]
|
|
117
|
+
args = [binary, "run", "-p", pattern, "--json=stream", str(repo)]
|
|
118
|
+
for glob in globs:
|
|
119
|
+
args.extend(["--globs", glob])
|
|
120
|
+
try:
|
|
121
|
+
proc = subprocess.run(
|
|
122
|
+
args,
|
|
123
|
+
capture_output=True,
|
|
124
|
+
text=True,
|
|
125
|
+
timeout=timeout_s,
|
|
126
|
+
check=False,
|
|
127
|
+
)
|
|
128
|
+
except subprocess.TimeoutExpired:
|
|
129
|
+
return [], [{"tool": "ast-grep", "message": "ast-grep timed out", "origin": "MEASURED"}]
|
|
130
|
+
except OSError as exc:
|
|
131
|
+
return [], [{"tool": "ast-grep", "message": str(exc), "origin": "MEASURED"}]
|
|
132
|
+
|
|
133
|
+
stdout = proc.stdout or ""
|
|
134
|
+
if proc.returncode not in (0, 1) and not stdout.strip():
|
|
135
|
+
retry = [binary, "run", "-p", pattern, "--json", str(repo)]
|
|
136
|
+
try:
|
|
137
|
+
proc = subprocess.run(
|
|
138
|
+
retry,
|
|
139
|
+
capture_output=True,
|
|
140
|
+
text=True,
|
|
141
|
+
timeout=timeout_s,
|
|
142
|
+
check=False,
|
|
143
|
+
)
|
|
144
|
+
stdout = proc.stdout or ""
|
|
145
|
+
except (OSError, subprocess.TimeoutExpired) as exc:
|
|
146
|
+
return [], [{"tool": "ast-grep", "message": str(exc), "origin": "MEASURED"}]
|
|
147
|
+
|
|
148
|
+
matches = parse_ast_grep_stdout(stdout)[:limit]
|
|
149
|
+
errors: list[dict[str, Any]] = []
|
|
150
|
+
if proc.returncode not in (0, 1) and not matches:
|
|
151
|
+
err = (proc.stderr or "").strip() or f"ast-grep exit {proc.returncode}"
|
|
152
|
+
errors.append({"tool": "ast-grep", "message": err, "origin": "MEASURED"})
|
|
153
|
+
return matches, errors
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
PROTOCOL_VERSION = 1
|
|
8
|
+
|
|
9
|
+
INTENTS = ("TEXT", "STRUCTURAL", "RELATIONSHIP", "IMPACT", "SEMANTIC", "UNKNOWN")
|
|
10
|
+
|
|
11
|
+
IMPACT_HINTS = (
|
|
12
|
+
"impact of",
|
|
13
|
+
"what breaks",
|
|
14
|
+
"if i change",
|
|
15
|
+
"if i modify",
|
|
16
|
+
"affected tests",
|
|
17
|
+
"what can break",
|
|
18
|
+
)
|
|
19
|
+
RELATIONSHIP_HINTS = (
|
|
20
|
+
"who calls",
|
|
21
|
+
"callers of",
|
|
22
|
+
"depends on",
|
|
23
|
+
"dependencies of",
|
|
24
|
+
"path between",
|
|
25
|
+
"connected to",
|
|
26
|
+
"imports of",
|
|
27
|
+
"what depends",
|
|
28
|
+
)
|
|
29
|
+
SEMANTIC_HINTS = ("similar to", "like this code", "meaning of")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def read_request(raw: str | None = None) -> dict[str, Any]:
|
|
33
|
+
text = sys.stdin.read() if raw is None else raw
|
|
34
|
+
text = text.strip()
|
|
35
|
+
if not text:
|
|
36
|
+
raise ValueError("empty request")
|
|
37
|
+
data = json.loads(text)
|
|
38
|
+
if not isinstance(data, dict):
|
|
39
|
+
raise ValueError("request must be a JSON object")
|
|
40
|
+
return data
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def write_response(payload: dict[str, Any]) -> None:
|
|
44
|
+
sys.stdout.write(json.dumps(payload, ensure_ascii=False))
|
|
45
|
+
sys.stdout.write("\n")
|
|
46
|
+
sys.stdout.flush()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def error_response(message: str, **extra: Any) -> dict[str, Any]:
|
|
50
|
+
body: dict[str, Any] = {
|
|
51
|
+
"schemaVersion": PROTOCOL_VERSION,
|
|
52
|
+
"ok": False,
|
|
53
|
+
"error": message,
|
|
54
|
+
}
|
|
55
|
+
body.update(extra)
|
|
56
|
+
return body
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def classify_intent(
|
|
60
|
+
query: str | None,
|
|
61
|
+
structural_pattern: str | None,
|
|
62
|
+
explicit: str | None,
|
|
63
|
+
) -> str:
|
|
64
|
+
if explicit:
|
|
65
|
+
value = explicit.strip().upper()
|
|
66
|
+
if value in INTENTS and value != "UNKNOWN":
|
|
67
|
+
return value
|
|
68
|
+
if structural_pattern:
|
|
69
|
+
return "STRUCTURAL"
|
|
70
|
+
q = (query or "").strip()
|
|
71
|
+
if any(token in q for token in ("$$$", "$")):
|
|
72
|
+
return "STRUCTURAL"
|
|
73
|
+
low = q.lower()
|
|
74
|
+
if any(hint in low for hint in IMPACT_HINTS):
|
|
75
|
+
return "IMPACT"
|
|
76
|
+
if any(hint in low for hint in RELATIONSHIP_HINTS):
|
|
77
|
+
return "RELATIONSHIP"
|
|
78
|
+
if any(hint in low for hint in SEMANTIC_HINTS):
|
|
79
|
+
return "SEMANTIC"
|
|
80
|
+
if explicit and explicit.strip().upper() == "UNKNOWN":
|
|
81
|
+
return "UNKNOWN"
|
|
82
|
+
if q:
|
|
83
|
+
return "TEXT"
|
|
84
|
+
return "UNKNOWN"
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def strategies_for(intent: str) -> list[str]:
|
|
88
|
+
if intent == "TEXT":
|
|
89
|
+
return ["rg"]
|
|
90
|
+
if intent == "STRUCTURAL":
|
|
91
|
+
return ["ast-grep"]
|
|
92
|
+
if intent in {"RELATIONSHIP", "IMPACT"}:
|
|
93
|
+
return ["graph"]
|
|
94
|
+
if intent == "SEMANTIC":
|
|
95
|
+
return []
|
|
96
|
+
return ["rg", "ast-grep"]
|