pi-ast-sgrep 1.3.2 → 2.0.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.
@@ -0,0 +1,166 @@
1
+ /** Neat asgrep tool chrome for the Pi TUI and the model-visible content. */
2
+ export const ASGREP_PROMPT_SNIPPET = "Search this repo by intent, symbol, callers, defs, pattern, or chain (in-process asgrep; use without being asked)";
3
+ export const ASGREP_PROMPT_GUIDELINES = [
4
+ "For any code lookup (find a function, callers, defs, intent, structural pattern, or imports), call asgrep or asgrep_search immediately. Do not wait for the user to mention ast-sgrep.",
5
+ "Prefer the asgrep Code Mode tool when you need more than one lookup, filtering, or parallel work. Write JavaScript that calls asgrep.search / semantic / chain / defs / callers / imports / indexStatus / indexRepo and return a small shaped value.",
6
+ "Use grep only for exact log strings, filenames, or config keys. Use Pi write/edit to change files; asgrep does not mutate source.",
7
+ ];
8
+ function paint(theme, role, text, bold = false) {
9
+ const body = bold && theme ? theme.bold(text) : text;
10
+ return theme ? theme.fg(role, body) : body;
11
+ }
12
+ function hitLocation(hit) {
13
+ const file = String(hit.file ?? hit.path ?? "");
14
+ const line = hit.start_line ?? hit.line ?? hit.lines;
15
+ if (typeof line === "number")
16
+ return `${file}:${line}`;
17
+ if (typeof line === "string" && line.length > 0)
18
+ return `${file}:${line}`;
19
+ if (typeof hit.ref === "string" && hit.ref.length > 0)
20
+ return hit.ref;
21
+ return file || "?";
22
+ }
23
+ function hitLabel(hit) {
24
+ const symbol = typeof hit.symbol === "string" ? hit.symbol : "";
25
+ const kind = typeof hit.kind === "string" ? hit.kind : "";
26
+ const preview = typeof hit.preview === "string" ? hit.preview.replace(/\s+/g, " ").trim() : "";
27
+ return [symbol, kind, preview && preview.length < 80 ? preview : ""].filter(Boolean).join(" ");
28
+ }
29
+ function header(theme, verb, bits) {
30
+ return [paint(theme, "toolTitle", "asgrep", true), paint(theme, "accent", verb), ...bits.filter((bit) => Boolean(bit))].join(" · ");
31
+ }
32
+ export function formatSearchCall(params, theme) {
33
+ return header(theme, "search", [
34
+ params.query ? JSON.stringify(params.query) : undefined,
35
+ params.mode ?? "natural",
36
+ params.limit !== undefined ? `limit ${params.limit}` : undefined,
37
+ params.excerptLines ? `excerpt ${params.excerptLines}` : undefined,
38
+ ]);
39
+ }
40
+ export function formatIndexCall(force, theme) {
41
+ return header(theme, force ? "reindex" : "index", []);
42
+ }
43
+ export function formatStatusCall(theme) {
44
+ return header(theme, "status", []);
45
+ }
46
+ export function formatCodemodeCall(code, theme) {
47
+ const preview = code.trim().replace(/\s+/g, " ").slice(0, 80);
48
+ return header(theme, "codemode", [`${preview}${code.trim().length > 80 ? "…" : ""}`]);
49
+ }
50
+ export function formatSearchResult(response, meta, theme) {
51
+ const hits = Array.isArray(response.hits) ? response.hits : [];
52
+ const title = header(theme, meta.command, [
53
+ meta.query ? JSON.stringify(meta.query) : undefined,
54
+ meta.mode,
55
+ `${hits.length} hit${hits.length === 1 ? "" : "s"}`,
56
+ meta.activationMs !== undefined ? `${meta.activationMs < 10 ? meta.activationMs.toFixed(2) : meta.activationMs.toFixed(1)}ms` : undefined,
57
+ meta.backend,
58
+ ]);
59
+ const rows = hits.slice(0, 24).map((hit) => {
60
+ const loc = hitLocation(hit);
61
+ const label = hitLabel(hit);
62
+ return paint(theme, "toolOutput", label ? ` ${loc} ${label}` : ` ${loc}`);
63
+ });
64
+ if (hits.length > 24) {
65
+ rows.push(paint(theme, "muted", ` … ${hits.length - 24} more`));
66
+ }
67
+ return [title, ...rows].join("\n");
68
+ }
69
+ export function formatStatusResult(response, theme) {
70
+ const state = typeof response.status === "string" ? response.status
71
+ : typeof response.index_status === "string" ? response.index_status
72
+ : response.ok ? "ok" : "failed";
73
+ const counts = response.counts && typeof response.counts === "object"
74
+ ? Object.entries(response.counts).map(([key, value]) => `${key}=${String(value)}`).join(" ")
75
+ : "";
76
+ const backend = typeof response.backend === "string" ? response.backend : "";
77
+ const title = header(theme, "status", [state, counts, backend]);
78
+ return title;
79
+ }
80
+ export function formatIndexResult(command, response, theme) {
81
+ const count = typeof response.count === "number" ? response.count
82
+ : typeof response.total === "number" ? response.total
83
+ : undefined;
84
+ const tail = count === undefined ? "done" : `${count} file${count === 1 ? "" : "s"}`;
85
+ return header(theme, command, [tail]);
86
+ }
87
+ function compactValue(value) {
88
+ if (value === null || value === undefined)
89
+ return String(value);
90
+ if (typeof value !== "object")
91
+ return String(value);
92
+ if (Array.isArray(value))
93
+ return `${value.length} item${value.length === 1 ? "" : "s"}`;
94
+ const json = JSON.stringify(value);
95
+ return json.length <= 80 ? json : `${json.slice(0, 79)}…`;
96
+ }
97
+ export function formatCodemodeResult(value, meta = {}, theme) {
98
+ if (value && typeof value === "object" && Array.isArray(value.hits)) {
99
+ const searchMeta = { command: "codemode" };
100
+ if (meta.wallMs !== undefined)
101
+ searchMeta.activationMs = meta.wallMs;
102
+ if (meta.backend !== undefined)
103
+ searchMeta.backend = meta.backend;
104
+ return formatSearchResult(value, searchMeta, theme);
105
+ }
106
+ const bits = [];
107
+ if (value && typeof value === "object") {
108
+ const record = value;
109
+ if (typeof record.hit_count === "number")
110
+ bits.push(`${record.hit_count} hit${record.hit_count === 1 ? "" : "s"}`);
111
+ else if (typeof record.node_count === "number")
112
+ bits.push(`${record.node_count} node${record.node_count === 1 ? "" : "s"}`);
113
+ }
114
+ if (meta.backend === "napi")
115
+ bits.push("in-process");
116
+ else if (meta.backend === "cli")
117
+ bits.push("cli-sticky");
118
+ if (meta.stats && meta.stats.calls > 0) {
119
+ const via = (meta.stats.stickyCalls ?? 0) > 0
120
+ ? `native ${meta.stats.stickyCalls}`
121
+ : meta.stats.batchedCalls > 0
122
+ ? `batched ${meta.stats.batchedCalls}`
123
+ : meta.stats.parallelSpawnCalls > 0
124
+ ? `parallel-spawn ${meta.stats.parallelSpawnCalls}`
125
+ : `${meta.stats.calls} call${meta.stats.calls === 1 ? "" : "s"}`;
126
+ bits.push(via);
127
+ if (meta.stats.waves > 1)
128
+ bits.push(`${meta.stats.waves} waves`);
129
+ }
130
+ if (meta.wallMs !== undefined)
131
+ bits.push(`${meta.wallMs}ms`);
132
+ const title = header(theme, "codemode", bits);
133
+ if (value && typeof value === "object" && !Array.isArray(value)) {
134
+ const rows = Object.entries(value).slice(0, 16).map(([key, entry]) => paint(theme, "toolOutput", ` ${key}: ${compactValue(entry)}`));
135
+ return [title, ...rows].join("\n");
136
+ }
137
+ if (Array.isArray(value)) {
138
+ return [title, paint(theme, "toolOutput", ` ${value.length} value${value.length === 1 ? "" : "s"}`)].join("\n");
139
+ }
140
+ return `${title}\n${paint(theme, "toolOutput", ` ${compactValue(value)}`)}`;
141
+ }
142
+ /** Minimal pi-tui Text stand-in so we do not take a TUI package dependency. */
143
+ export class AsgrepText {
144
+ #text;
145
+ constructor(text = "") {
146
+ this.#text = text;
147
+ }
148
+ setText(text) {
149
+ this.#text = text;
150
+ }
151
+ invalidate() { }
152
+ render(_width) {
153
+ return this.#text.length === 0 ? [""] : this.#text.split("\n");
154
+ }
155
+ }
156
+ export function presentText(formatted, last) {
157
+ if (last instanceof AsgrepText) {
158
+ last.setText(formatted);
159
+ return last;
160
+ }
161
+ if (last && typeof last === "object" && last !== null && "setText" in last && typeof last.setText === "function") {
162
+ last.setText(formatted);
163
+ return last;
164
+ }
165
+ return new AsgrepText(formatted);
166
+ }
package/dist/runtime.d.ts CHANGED
@@ -1,8 +1,9 @@
1
+ import { type FSWatcher } from "node:fs";
1
2
  import { resolveBinary } from "ast-sgrep";
2
- export declare const RUNTIME_VERSION = "1.3.2";
3
+ export declare const RUNTIME_VERSION = "2.0.0";
3
4
  export declare const MACHINE_SCHEMA_VERSION = "1.0.0";
4
5
  export declare const CONFIG_SCHEMA_VERSION: 1;
5
- export declare const INDEX_FORMAT_VERSION: 5;
6
+ export declare const INDEX_FORMAT_VERSION: 12;
6
7
  export declare const DEFAULT_TIMEOUT_MS = 30000;
7
8
  export declare const DEFAULT_MAX_OUTPUT_BYTES: number;
8
9
  export declare const DEFAULT_REFRESH_INTERVAL_MS = 30000;
@@ -83,11 +84,26 @@ export interface FreshnessRuntime {
83
84
  resolveRoot(context: RuntimeContext): Promise<string>;
84
85
  inspectIndexCompatibility?(context: RuntimeContext): Promise<IndexHealth>;
85
86
  rebuildIncompatibleIndex?(context: RuntimeContext, options?: RunOptions): Promise<MachineEnvelope>;
87
+ /** Absolute database path whose SQLite/derived writes are owned by this runtime. */
88
+ resolveIndexPath?(root: string): string;
89
+ /**
90
+ * Optional warm native call (session sticky pool). When present, freshness
91
+ * prefers this over cold `run` for status/index — same Searcher as Code Mode.
92
+ */
93
+ nativeCall?(tool: string, args: Record<string, unknown>, context: RuntimeContext, options?: RunOptions): Promise<MachineEnvelope>;
94
+ /** Enable low-latency external filesystem change detection for real runtimes. */
95
+ watchExternalChanges?: boolean;
86
96
  }
87
97
  export interface FreshnessCoordinatorOptions {
88
98
  refreshIntervalMs?: number;
89
99
  now?: () => number;
100
+ watchFactory?: FreshnessWatchFactory;
90
101
  }
102
+ export type FreshnessWatchFactory = (root: string, options: {
103
+ recursive: true;
104
+ persistent: false;
105
+ encoding: "utf8";
106
+ }, listener: (eventType: "rename" | "change", filename: string | null) => void) => FSWatcher;
91
107
  export type IndexHealth = "ready" | "missing" | "incompatible";
92
108
  export declare class FreshnessCoordinator {
93
109
  #private;
@@ -95,16 +111,27 @@ export declare class FreshnessCoordinator {
95
111
  markAffectedPath(path: string, cwd: string): void;
96
112
  markRootDirty(root: string): void;
97
113
  ensureFresh(runtime: FreshnessRuntime, context: RuntimeContext, options?: RunOptions): Promise<string>;
114
+ shutdown(): void;
98
115
  }
99
116
  export declare class AstSgrepRuntime {
100
117
  #private;
101
118
  private readonly pi;
119
+ readonly watchExternalChanges = true;
102
120
  readonly config: ReturnType<typeof resolveConfig>;
103
121
  constructor(pi: PiExec, sources?: ConfigSources, dependencies?: RuntimeDependencies);
104
122
  resolveRoot(context: RuntimeContext): Promise<string>;
123
+ resolveIndexPath(root: string): string;
105
124
  inspectIndexCompatibility(context: RuntimeContext): Promise<IndexHealth>;
106
125
  rebuildIncompatibleIndex(context: RuntimeContext, options?: RunOptions): Promise<MachineEnvelope>;
107
126
  run(args: readonly string[], context: RuntimeContext, options?: RunOptions): Promise<MachineEnvelope>;
127
+ /** Absolute path to the native binary (for sticky serve / stdin batch spawn). */
128
+ resolveBinaryPath(options?: {
129
+ env?: NodeJS.ProcessEnv;
130
+ }): string;
131
+ /** Merged process env for native Code Mode workers. */
132
+ nativeEnv(options?: {
133
+ env?: NodeJS.ProcessEnv;
134
+ }): NodeJS.ProcessEnv;
108
135
  checkCompatibility(context: RuntimeContext, options?: RunOptions): Promise<MachineEnvelope>;
109
136
  }
110
137
  export {};