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.
Files changed (53) hide show
  1. package/README.md +16 -19
  2. package/dist/code-mode.d.ts +1 -1
  3. package/dist/code-mode.js +1 -1
  4. package/dist/codemode/connector.d.ts +18 -3
  5. package/dist/codemode/connector.js +85 -31
  6. package/dist/codemode/dispatch.d.ts +13 -1
  7. package/dist/codemode/dispatch.js +87 -24
  8. package/dist/codemode/guest-api.d.ts +16 -0
  9. package/dist/codemode/guest-api.js +194 -0
  10. package/dist/codemode/guest-worker.mjs +287 -0
  11. package/dist/codemode/index.d.ts +4 -3
  12. package/dist/codemode/index.js +4 -3
  13. package/dist/codemode/native.d.ts +1 -1
  14. package/dist/codemode/native.js +1 -1
  15. package/dist/codemode/runner.d.ts +13 -9
  16. package/dist/codemode/runner.js +411 -213
  17. package/dist/codemode/session-pool.d.ts +6 -1
  18. package/dist/codemode/session-pool.js +125 -32
  19. package/dist/codemode/types.d.ts +42 -2
  20. package/dist/codemode/types.js +40 -15
  21. package/dist/codemode/worker.d.ts +1 -1
  22. package/dist/codemode/worker.js +25 -2
  23. package/dist/host/commands.d.ts +6 -0
  24. package/dist/host/commands.js +49 -0
  25. package/dist/host/results.d.ts +123 -0
  26. package/dist/host/results.js +126 -0
  27. package/dist/host/tools.d.ts +28 -0
  28. package/dist/host/tools.js +802 -0
  29. package/dist/index.d.ts +7 -34
  30. package/dist/index.js +5 -543
  31. package/dist/runtime/config.d.ts +36 -0
  32. package/dist/runtime/config.js +98 -0
  33. package/dist/runtime/freshness.d.ts +43 -0
  34. package/dist/runtime/freshness.js +446 -0
  35. package/dist/runtime/index-health.d.ts +16 -0
  36. package/dist/runtime/index-health.js +111 -0
  37. package/dist/runtime/runtime.d.ts +48 -0
  38. package/dist/runtime/runtime.js +265 -0
  39. package/dist/runtime/sqlite.d.ts +15 -0
  40. package/dist/runtime/sqlite.js +63 -0
  41. package/dist/runtime/types.d.ts +55 -0
  42. package/dist/runtime/types.js +25 -0
  43. package/dist/ui/card.d.ts +66 -0
  44. package/dist/ui/card.js +375 -0
  45. package/dist/ui/present.d.ts +89 -0
  46. package/dist/ui/present.js +391 -0
  47. package/package.json +8 -7
  48. package/dist/codemode/sandbox-worker.d.ts +0 -1
  49. package/dist/codemode/sandbox-worker.js +0 -204
  50. package/dist/present.d.ts +0 -70
  51. package/dist/present.js +0 -260
  52. package/dist/runtime.d.ts +0 -137
  53. 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;