pi-ast-sgrep 2.0.2 → 2.2.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/README.md +16 -19
- package/dist/code-mode.d.ts +1 -1
- package/dist/code-mode.js +1 -1
- package/dist/codemode/connector.d.ts +18 -3
- package/dist/codemode/connector.js +85 -31
- package/dist/codemode/dispatch.d.ts +13 -1
- package/dist/codemode/dispatch.js +87 -24
- package/dist/codemode/guest-api.d.ts +16 -0
- package/dist/codemode/guest-api.js +194 -0
- package/dist/codemode/guest-worker.mjs +287 -0
- package/dist/codemode/index.d.ts +4 -3
- package/dist/codemode/index.js +4 -3
- package/dist/codemode/native.d.ts +1 -1
- package/dist/codemode/native.js +1 -1
- package/dist/codemode/runner.d.ts +13 -9
- package/dist/codemode/runner.js +411 -213
- package/dist/codemode/session-pool.d.ts +6 -1
- package/dist/codemode/session-pool.js +125 -32
- package/dist/codemode/types.d.ts +42 -2
- package/dist/codemode/types.js +40 -15
- package/dist/codemode/worker.d.ts +1 -1
- package/dist/codemode/worker.js +25 -2
- package/dist/host/commands.d.ts +6 -0
- package/dist/host/commands.js +49 -0
- package/dist/host/results.d.ts +123 -0
- package/dist/host/results.js +126 -0
- package/dist/host/tools.d.ts +28 -0
- package/dist/host/tools.js +802 -0
- package/dist/index.d.ts +7 -34
- package/dist/index.js +5 -543
- package/dist/runtime/config.d.ts +36 -0
- package/dist/runtime/config.js +98 -0
- package/dist/runtime/freshness.d.ts +43 -0
- package/dist/runtime/freshness.js +446 -0
- package/dist/runtime/index-health.d.ts +16 -0
- package/dist/runtime/index-health.js +111 -0
- package/dist/runtime/runtime.d.ts +48 -0
- package/dist/runtime/runtime.js +265 -0
- package/dist/runtime/sqlite.d.ts +15 -0
- package/dist/runtime/sqlite.js +63 -0
- package/dist/runtime/types.d.ts +55 -0
- package/dist/runtime/types.js +25 -0
- package/dist/ui/card.d.ts +66 -0
- package/dist/ui/card.js +375 -0
- package/dist/ui/present.d.ts +89 -0
- package/dist/ui/present.js +391 -0
- package/package.json +8 -7
- package/dist/codemode/sandbox-worker.d.ts +0 -1
- package/dist/codemode/sandbox-worker.js +0 -204
- package/dist/present.d.ts +0 -70
- package/dist/present.js +0 -260
- package/dist/runtime.d.ts +0 -137
- package/dist/runtime.js +0 -799
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool-result plumbing shared by tools.ts and commands.ts: bounded text,
|
|
3
|
+
* success/failure envelopes, freshness-timeout classification, reporting.
|
|
4
|
+
*/
|
|
5
|
+
import { isClosedWorkerError } from "../codemode/index.js";
|
|
6
|
+
import { RuntimeError } from "../runtime/types.js";
|
|
7
|
+
import { formatEditResult, formatIndexResult, formatReadResult, formatSearchResult, formatStatusResult } from "../ui/present.js";
|
|
8
|
+
export const MAX_CONTENT_CHARS = 8_000;
|
|
9
|
+
export function bounded(text) {
|
|
10
|
+
return text.length <= MAX_CONTENT_CHARS ? text : `${text.slice(0, MAX_CONTENT_CHARS - 1)}…`;
|
|
11
|
+
}
|
|
12
|
+
export function success(command, response, extra = {}) {
|
|
13
|
+
const text = command === "status"
|
|
14
|
+
? formatStatusResult(response)
|
|
15
|
+
: command === "index" || command === "reindex"
|
|
16
|
+
? formatIndexResult(command, response)
|
|
17
|
+
: command === "edit"
|
|
18
|
+
? formatEditResult(response)
|
|
19
|
+
: command === "read"
|
|
20
|
+
? formatReadResult(response)
|
|
21
|
+
: formatSearchResult(response, { command, ...extra });
|
|
22
|
+
// Notes qualify the answer the agent is about to trust (stale index, empty
|
|
23
|
+
// index): they stay in the model-visible text, not only in the details bag.
|
|
24
|
+
const body = (extra.notes ?? []).length > 0
|
|
25
|
+
? `${text}\n${(extra.notes ?? []).map((note) => ` ! ${note}`).join("\n")}`
|
|
26
|
+
: text;
|
|
27
|
+
return {
|
|
28
|
+
content: [{ type: "text", text: bounded(body) }],
|
|
29
|
+
// The tool execute owns its machine command: normalize the envelope's
|
|
30
|
+
// command (native catalog names like index_status/index_repo must surface
|
|
31
|
+
// as the machine commands status/index/reindex).
|
|
32
|
+
details: { ok: true, command, response: { ...response, command }, ...extra },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Failure families the native session raises as *text*: the NAPI boundary
|
|
37
|
+
* carries `err.to_string()`, so the code has to be reconstructed from the
|
|
38
|
+
* message instead of being read off a struct. Everything here maps to
|
|
39
|
+
* OPERATIONAL_ERROR — a real answer about the index, not a mystery failure.
|
|
40
|
+
*
|
|
41
|
+
* Keep this list bounded and message-precise: an unrecognised failure stays
|
|
42
|
+
* UNEXPECTED_ERROR, which is honest, while a false positive would hide one.
|
|
43
|
+
*/
|
|
44
|
+
const OPERATIONAL_FAILURES = [
|
|
45
|
+
{
|
|
46
|
+
pattern: /database is locked|database table is locked/i,
|
|
47
|
+
hint: "another process holds the index write lock (a concurrent asgrep index build); retry in a few seconds, or scope the search with in:/fileFilter",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
pattern: /index changed while preparing|retry the rebuild/i,
|
|
51
|
+
hint: "the index changed while this query was preparing; retry once the running index build finishes",
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
pattern: /index is empty|index does not exist|failed to open index|failed to resolve index path|index schema version|unsupported schema|newer than supported/i,
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
pattern: /database disk image is malformed|file is not a database|not a database/i,
|
|
58
|
+
hint: "the index file is damaged; run /asgrep-reindex to rebuild it",
|
|
59
|
+
},
|
|
60
|
+
{ pattern: /unable to open database file|no such table/i },
|
|
61
|
+
];
|
|
62
|
+
export function errorDetails(cause, signal) {
|
|
63
|
+
if (signal?.aborted) {
|
|
64
|
+
return { code: "CANCELLED", message: "cancelled", details: {} };
|
|
65
|
+
}
|
|
66
|
+
if (cause instanceof RuntimeError) {
|
|
67
|
+
return { code: cause.code, message: cause.message, details: cause.details };
|
|
68
|
+
}
|
|
69
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
70
|
+
if (/timed out after \d+ms|timeout after \d+ms|exceeded \d+ms/i.test(message)) {
|
|
71
|
+
return { code: "TIMEOUT", message, details: {} };
|
|
72
|
+
}
|
|
73
|
+
if (isClosedWorkerError(cause)) {
|
|
74
|
+
return {
|
|
75
|
+
code: "SESSION_CLOSED",
|
|
76
|
+
message: "asgrep session closed; retry the search",
|
|
77
|
+
details: {},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
// An aborted call is not a mystery failure: the caller's deadline or cancel
|
|
81
|
+
// fired. (Checked after timeout so a timed-out abort still reads as TIMEOUT.)
|
|
82
|
+
if ((cause instanceof Error && cause.name === "AbortError") || /aborted|was cancelled|operation cancelled/i.test(message)) {
|
|
83
|
+
return { code: "CANCELLED", message: "cancelled", details: {} };
|
|
84
|
+
}
|
|
85
|
+
for (const family of OPERATIONAL_FAILURES) {
|
|
86
|
+
if (!family.pattern.test(message))
|
|
87
|
+
continue;
|
|
88
|
+
return {
|
|
89
|
+
code: "OPERATIONAL_ERROR",
|
|
90
|
+
message,
|
|
91
|
+
details: family.hint ? { hint: family.hint } : {},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return { code: "UNEXPECTED_ERROR", message, details: {} };
|
|
95
|
+
}
|
|
96
|
+
export function isFreshnessTimeout(cause, userSignal) {
|
|
97
|
+
if (userSignal?.aborted)
|
|
98
|
+
return false;
|
|
99
|
+
if (cause instanceof RuntimeError && (cause.code === "TIMEOUT" || cause.code === "CANCELLED"))
|
|
100
|
+
return true;
|
|
101
|
+
if (isClosedWorkerError(cause))
|
|
102
|
+
return true;
|
|
103
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
104
|
+
return /timed out after \d+ms|timeout after \d+ms|exceeded \d+ms/i.test(message);
|
|
105
|
+
}
|
|
106
|
+
/** Leading or mid-query `in:path` scope used to bound a fresh-directory index. */
|
|
107
|
+
export function extractInPath(query) {
|
|
108
|
+
const match = /(?:^|\s)in:([^\s]+)/.exec(query);
|
|
109
|
+
const path = match?.[1];
|
|
110
|
+
if (!path || path.split(/[/\\]/u).includes(".."))
|
|
111
|
+
return undefined;
|
|
112
|
+
return path;
|
|
113
|
+
}
|
|
114
|
+
export function failure(command, cause, signal) {
|
|
115
|
+
const error = errorDetails(cause, signal);
|
|
116
|
+
return {
|
|
117
|
+
content: [{ type: "text", text: bounded(`${command} failed [${error.code}]: ${error.message}`) }],
|
|
118
|
+
details: { ok: false, command, error },
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
export function report(onUpdate, command, phase) {
|
|
122
|
+
onUpdate?.({
|
|
123
|
+
content: [{ type: "text", text: `${command} ${phase}` }],
|
|
124
|
+
details: { command, phase },
|
|
125
|
+
});
|
|
126
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The four registered pi tools: asgrep (Code Mode), asgrep_search,
|
|
3
|
+
* asgrep_index, asgrep_status — plus the session pool, freshness wiring,
|
|
4
|
+
* and workspace event hooks that serve them.
|
|
5
|
+
*/
|
|
6
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { type FreshnessLike, type RuntimeLike } from "./results.js";
|
|
8
|
+
export declare const DEFAULT_LIMIT = 8;
|
|
9
|
+
/**
|
|
10
|
+
* pi ships `read`, `edit`, `write`, `bash`, `grep`, `find`, `ls` built in, so on
|
|
11
|
+
* a normal Pi host our one-shot file tools would be paid for twice and never
|
|
12
|
+
* needed. They stay REGISTERED — an MCP-style host, a `--no-builtin-tools`
|
|
13
|
+
* session, or a host that drops the built-ins still gets them — but they are
|
|
14
|
+
* left out of the active set when the host already provides read+edit. Pi only
|
|
15
|
+
* sends ACTIVE tools (schema, snippet, guidelines) to the model, so this is the
|
|
16
|
+
* difference between ~296 tokens per request and nothing.
|
|
17
|
+
*
|
|
18
|
+
* ASGREP_KEEP_FILE_TOOLS=1 pins them active regardless.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Tools that MUTATE the index never ride the warm session: its calls are
|
|
22
|
+
* serialized, so a write there blocks every read queued behind it.
|
|
23
|
+
*
|
|
24
|
+
* Exported so the routing contract is testable without a live session.
|
|
25
|
+
*/
|
|
26
|
+
export declare function writesOffSession(tool: string): boolean;
|
|
27
|
+
export declare function hostProvidesFileTools(pi: ExtensionAPI, env?: NodeJS.ProcessEnv): boolean;
|
|
28
|
+
export declare function registerAstSgrepTools(pi: ExtensionAPI, runtime?: RuntimeLike, freshness?: FreshnessLike): void;
|