march-cli 0.1.35 → 0.1.36

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 (42) hide show
  1. package/package.json +1 -1
  2. package/src/agent/code-search/cache.mjs +133 -0
  3. package/src/agent/code-search/chunk-rules.mjs +107 -0
  4. package/src/agent/code-search/chunker.mjs +125 -0
  5. package/src/agent/code-search/engine.mjs +109 -0
  6. package/src/agent/code-search/languages.mjs +25 -0
  7. package/src/agent/code-search/parser-pool.mjs +29 -0
  8. package/src/agent/code-search/rerank.mjs +43 -0
  9. package/src/agent/code-search/retrieval/bm25.mjs +47 -0
  10. package/src/agent/code-search/retrieval/fusion.mjs +18 -0
  11. package/src/agent/code-search/retrieval/model2vec.mjs +96 -0
  12. package/src/agent/code-search/retrieval/safetensors.mjs +49 -0
  13. package/src/agent/code-search/retrieval/vector.mjs +107 -0
  14. package/src/agent/code-search/retrieval/wordpiece.mjs +82 -0
  15. package/src/agent/code-search/scanner.mjs +84 -0
  16. package/src/agent/code-search/tokenize.mjs +16 -0
  17. package/src/agent/code-search/tool.mjs +75 -0
  18. package/src/agent/runner/provider-quota-runtime.mjs +38 -0
  19. package/src/agent/runner.mjs +5 -0
  20. package/src/agent/runtime/remote-runner-client.mjs +2 -0
  21. package/src/agent/runtime/runner-ipc-target.mjs +7 -0
  22. package/src/agent/runtime/state/runner-state.mjs +1 -0
  23. package/src/agent/runtime/ui-event-bridge.mjs +2 -0
  24. package/src/agent/tools.mjs +3 -0
  25. package/src/cli/commands/registry/slash-command-registry.mjs +10 -7
  26. package/src/cli/commands/status-command.mjs +61 -35
  27. package/src/context/system-core/base.md +5 -0
  28. package/src/provider/quota/codex.mjs +278 -0
  29. package/src/provider/quota/index.mjs +46 -0
  30. package/src/provider/quota/transport-observer.mjs +99 -0
  31. package/src/web-ui/runtime-host.mjs +3 -0
  32. package/src/web-ui/server.mjs +1 -0
  33. package/src/web-ui/session-manager.mjs +2 -0
  34. package/src/web-ui/src/components/AppShell.tsx +1 -0
  35. package/src/web-ui/src/components/RightSidebar.tsx +47 -2
  36. package/src/web-ui/src/model.ts +20 -0
  37. package/src/web-ui/src/runtime/client.ts +8 -1
  38. package/src/web-ui/src/runtime/useWebRuntime.ts +13 -1
  39. package/src/web-ui/src/styles/shell.css +10 -0
  40. package/src/web-ui/dist/assets/index-BUmhnID4.css +0 -1
  41. package/src/web-ui/dist/assets/index-CtuqTjcB.js +0 -1845
  42. package/src/web-ui/dist/index.html +0 -13
@@ -0,0 +1,96 @@
1
+ import { existsSync } from "node:fs";
2
+ import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
3
+ import { basename, dirname, join } from "node:path";
4
+ import { readSafetensors } from "./safetensors.mjs";
5
+ import { normalizedVector } from "./vector.mjs";
6
+ import { WordPieceTokenizer } from "./wordpiece.mjs";
7
+
8
+ export const POTION_CODE_MODEL_ID = "minishlab/potion-code-16M";
9
+ const HF_RESOLVE_BASE = "https://huggingface.co";
10
+ const MODEL_FILES = ["config.json", "tokenizer.json", "model.safetensors"];
11
+
12
+ export class Model2VecVectorizer {
13
+ constructor({ modelDir, modelId = POTION_CODE_MODEL_ID, allowDownload = true } = {}) {
14
+ if (!modelDir) throw new Error("Model2VecVectorizer requires modelDir");
15
+ this.modelDir = modelDir;
16
+ this.modelId = modelId;
17
+ this.allowDownload = allowDownload;
18
+ this.id = `model2vec:${modelId}`;
19
+ this.dimensions = 256;
20
+ this.modelPromise = null;
21
+ }
22
+
23
+ async encode(texts) {
24
+ const model = await this.load();
25
+ return texts.map((text) => model.encode(text));
26
+ }
27
+
28
+ async load() {
29
+ this.modelPromise ??= this.loadModel();
30
+ return this.modelPromise;
31
+ }
32
+
33
+ async loadModel() {
34
+ await ensureModelFiles({ modelDir: this.modelDir, modelId: this.modelId, allowDownload: this.allowDownload });
35
+ const config = JSON.parse(await readFile(join(this.modelDir, "config.json"), "utf8"));
36
+ const tokenizerJson = JSON.parse(await readFile(join(this.modelDir, "tokenizer.json"), "utf8"));
37
+ const tensors = await readSafetensors(join(this.modelDir, "model.safetensors"));
38
+ const embeddings = tensors.getTensor(tensors.names.includes("embeddings") ? "embeddings" : "embedding.weight");
39
+ const weights = tensors.names.includes("weights") ? tensors.getTensor("weights") : null;
40
+ const mapping = tensors.names.includes("mapping") ? tensors.getTensor("mapping") : null;
41
+ const model = new LoadedModel2Vec({ config, tokenizerJson, embeddings, weights, mapping });
42
+ this.dimensions = model.dimensions;
43
+ return model;
44
+ }
45
+ }
46
+
47
+ export async function ensureModelFiles({ modelDir, modelId = POTION_CODE_MODEL_ID, allowDownload = true }) {
48
+ const missing = MODEL_FILES.filter((name) => !existsSync(join(modelDir, name)));
49
+ if (missing.length === 0) return;
50
+ if (!allowDownload) throw new Error(`Missing Model2Vec files in ${modelDir}: ${missing.join(", ")}`);
51
+ await mkdir(modelDir, { recursive: true });
52
+ for (const name of missing) await downloadModelFile({ modelDir, modelId, name });
53
+ }
54
+
55
+ class LoadedModel2Vec {
56
+ constructor({ config, tokenizerJson, embeddings, weights, mapping }) {
57
+ this.normalize = config?.normalize !== false;
58
+ this.tokenizer = new WordPieceTokenizer(tokenizerJson);
59
+ this.embeddings = embeddings.values;
60
+ this.vocabularySize = embeddings.shape[0];
61
+ this.dimensions = embeddings.shape[1];
62
+ this.weights = weights?.values ?? null;
63
+ this.mapping = mapping?.values ?? null;
64
+ }
65
+
66
+ encode(text) {
67
+ const ids = this.tokenizer.encode(text);
68
+ const values = new Float32Array(this.dimensions);
69
+ let count = 0;
70
+ for (const id of ids) {
71
+ const mapped = this.mapping ? this.mapping[id] : id;
72
+ if (mapped == null || mapped < 0 || mapped >= this.vocabularySize) continue;
73
+ const weight = this.weights?.[id] ?? 1;
74
+ const offset = mapped * this.dimensions;
75
+ for (let dim = 0; dim < this.dimensions; dim += 1) values[dim] += this.embeddings[offset + dim] * weight;
76
+ count += 1;
77
+ }
78
+ if (count > 0) for (let dim = 0; dim < this.dimensions; dim += 1) values[dim] /= count;
79
+ const vector = normalizedVector(values);
80
+ if (this.normalize && vector.norm > 0) {
81
+ for (let dim = 0; dim < values.length; dim += 1) values[dim] /= vector.norm;
82
+ return normalizedVector(values);
83
+ }
84
+ return vector;
85
+ }
86
+ }
87
+
88
+ async function downloadModelFile({ modelDir, modelId, name }) {
89
+ const url = `${HF_RESOLVE_BASE}/${modelId}/resolve/main/${name}`;
90
+ const response = await fetch(url, { redirect: "follow" });
91
+ if (!response.ok) throw new Error(`Failed to download ${modelId}/${name}: HTTP ${response.status}`);
92
+ const target = join(modelDir, name);
93
+ const tmpPath = join(dirname(target), `${basename(target)}.${process.pid}.tmp`);
94
+ await writeFile(tmpPath, new Uint8Array(await response.arrayBuffer()));
95
+ await rename(tmpPath, target);
96
+ }
@@ -0,0 +1,49 @@
1
+ import { readFile } from "node:fs/promises";
2
+
3
+ const HEADER_BYTES = 8;
4
+ const DTYPE_READERS = {
5
+ F32: { size: 4, read: (view, offset) => view.getFloat32(offset, true) },
6
+ F64: { size: 8, read: (view, offset) => view.getFloat64(offset, true) },
7
+ I32: { size: 4, read: (view, offset) => view.getInt32(offset, true) },
8
+ I64: { size: 8, read: (view, offset) => Number(view.getBigInt64(offset, true)) },
9
+ U32: { size: 4, read: (view, offset) => view.getUint32(offset, true) },
10
+ U64: { size: 8, read: (view, offset) => Number(view.getBigUint64(offset, true)) },
11
+ };
12
+
13
+ export async function readSafetensors(filePath) {
14
+ const buffer = await readFile(filePath);
15
+ return parseSafetensors(buffer);
16
+ }
17
+
18
+ export function parseSafetensors(buffer) {
19
+ const data = toArrayBuffer(buffer);
20
+ const headerLength = Number(new DataView(data, 0, HEADER_BYTES).getBigUint64(0, true));
21
+ const headerStart = HEADER_BYTES;
22
+ const headerEnd = headerStart + headerLength;
23
+ const headerJson = new TextDecoder().decode(new Uint8Array(data, headerStart, headerLength));
24
+ const header = JSON.parse(headerJson);
25
+ return {
26
+ names: Object.keys(header).filter((name) => name !== "__metadata__"),
27
+ getTensor(name) {
28
+ const descriptor = header[name];
29
+ if (!descriptor) throw new Error(`Missing safetensors tensor: ${name}`);
30
+ return readTensor(data, headerEnd, descriptor);
31
+ },
32
+ };
33
+ }
34
+
35
+ function readTensor(data, dataStart, descriptor) {
36
+ const reader = DTYPE_READERS[descriptor.dtype];
37
+ if (!reader) throw new Error(`Unsupported safetensors dtype: ${descriptor.dtype}`);
38
+ const [start, end] = descriptor.data_offsets;
39
+ const byteOffset = dataStart + start;
40
+ const length = (end - start) / reader.size;
41
+ const view = new DataView(data, byteOffset, end - start);
42
+ const values = new Float32Array(length);
43
+ for (let index = 0; index < length; index += 1) values[index] = reader.read(view, index * reader.size);
44
+ return { values, shape: descriptor.shape, dtype: descriptor.dtype };
45
+ }
46
+
47
+ function toArrayBuffer(buffer) {
48
+ return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
49
+ }
@@ -0,0 +1,107 @@
1
+ import { tokenize } from "../tokenize.mjs";
2
+
3
+ const DEFAULT_DIMENSIONS = 256;
4
+ const SEMANTIC_MIN_SCORE = 0.05;
5
+
6
+ export class LocalVectorIndex {
7
+ constructor(chunks, vectors, { vectorizer = defaultVectorizer } = {}) {
8
+ this.chunks = chunks;
9
+ this.dimensions = vectorizer.dimensions;
10
+ this.vectorizer = vectorizer;
11
+ this.vectors = vectors;
12
+ }
13
+
14
+ static async create(chunks, { vectorizer = defaultVectorizer } = {}) {
15
+ const vectors = await vectorizer.encode(chunks.map(chunkVectorText));
16
+ return new LocalVectorIndex(chunks, vectors, { vectorizer });
17
+ }
18
+
19
+ async search(query, { limit = 50 } = {}) {
20
+ const [queryVector] = await this.vectorizer.encode([query]);
21
+ return this.searchVector(queryVector, { limit });
22
+ }
23
+
24
+ searchVector(queryVector, { limit = 50 } = {}) {
25
+ if (queryVector.norm === 0) return [];
26
+ const scored = [];
27
+ for (let index = 0; index < this.vectors.length; index += 1) {
28
+ const score = cosineSimilarity(queryVector, this.vectors[index]);
29
+ if (score >= SEMANTIC_MIN_SCORE) scored.push({ chunk: this.chunks[index], score });
30
+ }
31
+ scored.sort((a, b) => b.score - a.score || a.chunk.file_path.localeCompare(b.chunk.file_path));
32
+ return scored.slice(0, limit);
33
+ }
34
+ }
35
+
36
+ export class HashingVectorizer {
37
+ constructor({ dimensions = DEFAULT_DIMENSIONS } = {}) {
38
+ this.id = `hashing-${dimensions}`;
39
+ this.dimensions = dimensions;
40
+ }
41
+
42
+ async encode(texts) {
43
+ return texts.map((text) => vectorizeText(text, this.dimensions));
44
+ }
45
+ }
46
+
47
+ export const defaultVectorizer = new HashingVectorizer();
48
+
49
+ function chunkVectorText(chunk) {
50
+ return [
51
+ chunk.file_path,
52
+ chunk.symbols.join(" "),
53
+ chunk.identifiers.join(" "),
54
+ chunk.content,
55
+ ].join("\n");
56
+ }
57
+
58
+ function vectorizeText(text, dimensions) {
59
+ const values = new Float32Array(dimensions);
60
+ const tokens = tokenize(text);
61
+ for (const token of tokens) {
62
+ addFeature(values, token, 1);
63
+ for (const gram of charTrigrams(token)) addFeature(values, gram, 0.35);
64
+ }
65
+ return normalizedVector(values);
66
+ }
67
+
68
+ export function normalizedVector(values) {
69
+ return { values, norm: vectorNorm(values) };
70
+ }
71
+
72
+ function addFeature(values, feature, weight) {
73
+ const hash = hashString(feature);
74
+ const index = hash % values.length;
75
+ const sign = hash & 1 ? 1 : -1;
76
+ values[index] += sign * weight;
77
+ }
78
+
79
+ function charTrigrams(token) {
80
+ const padded = `^${token}$`;
81
+ if (padded.length <= 3) return [padded];
82
+ const grams = [];
83
+ for (let index = 0; index <= padded.length - 3; index += 1) grams.push(padded.slice(index, index + 3));
84
+ return grams;
85
+ }
86
+
87
+ function cosineSimilarity(left, right) {
88
+ if (left.norm === 0 || right.norm === 0) return 0;
89
+ let dot = 0;
90
+ for (let index = 0; index < left.values.length; index += 1) dot += left.values[index] * right.values[index];
91
+ return dot / (left.norm * right.norm);
92
+ }
93
+
94
+ function vectorNorm(values) {
95
+ let sum = 0;
96
+ for (const value of values) sum += value * value;
97
+ return Math.sqrt(sum);
98
+ }
99
+
100
+ function hashString(value) {
101
+ let hash = 2166136261;
102
+ for (let index = 0; index < value.length; index += 1) {
103
+ hash ^= value.charCodeAt(index);
104
+ hash = Math.imul(hash, 16777619);
105
+ }
106
+ return hash >>> 0;
107
+ }
@@ -0,0 +1,82 @@
1
+ const MAX_INPUT_CHARS_PER_WORD = 100;
2
+
3
+ export class WordPieceTokenizer {
4
+ constructor(tokenizerJson) {
5
+ const model = tokenizerJson?.model ?? {};
6
+ this.vocab = model.vocab ?? {};
7
+ this.unkToken = model.unk_token ?? "[UNK]";
8
+ this.unkId = this.vocab[this.unkToken] ?? 1;
9
+ this.prefix = model.continuing_subword_prefix ?? "##";
10
+ this.lowercase = tokenizerJson?.normalizer?.lowercase !== false;
11
+ this.maxInputCharsPerWord = model.max_input_chars_per_word ?? MAX_INPUT_CHARS_PER_WORD;
12
+ }
13
+
14
+ encode(text, { maxLength = 512 } = {}) {
15
+ const ids = [];
16
+ for (const token of preTokenize(normalizeText(text, { lowercase: this.lowercase }))) {
17
+ for (const id of this.wordPiece(token)) {
18
+ if (id !== this.unkId) ids.push(id);
19
+ if (ids.length >= maxLength) return ids;
20
+ }
21
+ }
22
+ return ids;
23
+ }
24
+
25
+ wordPiece(token) {
26
+ if (!token) return [];
27
+ if (token.length > this.maxInputCharsPerWord) return [this.unkId];
28
+ const ids = [];
29
+ let start = 0;
30
+ while (start < token.length) {
31
+ let end = token.length;
32
+ let current = null;
33
+ while (start < end) {
34
+ const piece = start === 0 ? token.slice(start, end) : `${this.prefix}${token.slice(start, end)}`;
35
+ if (Object.hasOwn(this.vocab, piece)) {
36
+ current = piece;
37
+ break;
38
+ }
39
+ end -= 1;
40
+ }
41
+ if (current === null) return [this.unkId];
42
+ ids.push(this.vocab[current]);
43
+ start = end;
44
+ }
45
+ return ids;
46
+ }
47
+ }
48
+
49
+ function normalizeText(text, { lowercase }) {
50
+ let normalized = String(text ?? "").replace(/[\t\n\r]+/g, " ");
51
+ normalized = normalized.replace(/[\u0000-\u001f\u007f]/g, " ");
52
+ return lowercase ? normalized.toLowerCase() : normalized;
53
+ }
54
+
55
+ function preTokenize(text) {
56
+ const tokens = [];
57
+ let current = "";
58
+ for (const char of text) {
59
+ if (/\s/u.test(char)) {
60
+ pushCurrent();
61
+ } else if (isPunctuation(char)) {
62
+ pushCurrent();
63
+ tokens.push(char);
64
+ } else {
65
+ current += char;
66
+ }
67
+ }
68
+ pushCurrent();
69
+ return tokens;
70
+
71
+ function pushCurrent() {
72
+ if (current) tokens.push(current);
73
+ current = "";
74
+ }
75
+ }
76
+
77
+ function isPunctuation(char) {
78
+ const code = char.codePointAt(0);
79
+ if ((code >= 33 && code <= 47) || (code >= 58 && code <= 64)) return true;
80
+ if ((code >= 91 && code <= 96) || (code >= 123 && code <= 126)) return true;
81
+ return /\p{P}/u.test(char);
82
+ }
@@ -0,0 +1,84 @@
1
+ import { execFile } from "node:child_process";
2
+ import { lstat, readdir, readFile, stat } from "node:fs/promises";
3
+ import { isAbsolute, join, relative, resolve } from "node:path";
4
+ import { resolveRipgrepCommand } from "../../memory/markdown/ripgrep.mjs";
5
+ import { isSearchableTextPath, languageForPath } from "./languages.mjs";
6
+
7
+ const DEFAULT_MAX_FILES = 2_000;
8
+ const DEFAULT_MAX_FILE_BYTES = 512 * 1024;
9
+ const SKIP_DIRS = new Set([".git", "node_modules", "dist", "build", "coverage", ".next", ".turbo", ".cache"]);
10
+
11
+ export async function scanCodeFiles({ root, path = ".", maxFiles = DEFAULT_MAX_FILES, maxFileBytes = DEFAULT_MAX_FILE_BYTES } = {}) {
12
+ const rootPath = resolve(root);
13
+ const base = resolve(rootPath, path);
14
+ const baseRel = relative(rootPath, base);
15
+ if (baseRel.startsWith("..") || isAbsolute(baseRel)) throw new Error(`Search path escapes workspace: ${path}`);
16
+ let baseInfo;
17
+ try { baseInfo = await stat(base); } catch { return []; }
18
+ const candidates = baseInfo.isFile()
19
+ ? [relative(rootPath, base).replace(/\\/g, "/")]
20
+ : await listCandidateFiles(rootPath, base);
21
+ const files = [];
22
+ for (const relPath of [...candidates].sort()) {
23
+ if (files.length >= maxFiles) break;
24
+ if (!isSearchableTextPath(relPath)) continue;
25
+ const absPath = resolve(root, relPath);
26
+ let info;
27
+ try { info = await stat(absPath); } catch { continue; }
28
+ if (!info.isFile() || info.size > maxFileBytes) continue;
29
+ const content = await readUtf8Text(absPath);
30
+ if (content === null) continue;
31
+ files.push({
32
+ absPath,
33
+ relPath: relPath.replace(/\\/g, "/"),
34
+ language: languageForPath(relPath),
35
+ content,
36
+ size: info.size,
37
+ mtimeMs: info.mtimeMs,
38
+ });
39
+ }
40
+ return files;
41
+ }
42
+
43
+ async function listCandidateFiles(root, base) {
44
+ const fromRipgrep = await listRipgrepFiles(root, base);
45
+ if (fromRipgrep) return fromRipgrep;
46
+ return listFilesRecursively(root, base);
47
+ }
48
+
49
+ function listRipgrepFiles(root, base) {
50
+ return new Promise((resolveResult) => {
51
+ const command = resolveRipgrepCommand();
52
+ const args = ["--files", "--hidden", "--glob", "!.git/**", "--glob", "!node_modules/**"];
53
+ execFile(command, args, { cwd: base, maxBuffer: 50 * 1024 * 1024 }, (error, stdout) => {
54
+ if (error) return resolveResult(null);
55
+ const baseRel = relative(root, base).replace(/\\/g, "/");
56
+ const prefix = baseRel ? `${baseRel}/` : "";
57
+ resolveResult(stdout.split(/\r?\n/).filter(Boolean).map((file) => `${prefix}${file.replace(/\\/g, "/")}`));
58
+ });
59
+ });
60
+ }
61
+
62
+ async function listFilesRecursively(root, dir) {
63
+ let entries;
64
+ try { entries = await readdir(dir, { withFileTypes: true }); } catch { return []; }
65
+ const files = [];
66
+ for (const entry of entries) {
67
+ if (entry.isDirectory() && SKIP_DIRS.has(entry.name)) continue;
68
+ const absPath = join(dir, entry.name);
69
+ if (entry.isDirectory()) files.push(...await listFilesRecursively(root, absPath));
70
+ else if (entry.isFile()) files.push(relative(root, absPath).replace(/\\/g, "/"));
71
+ }
72
+ return files;
73
+ }
74
+
75
+ async function readUtf8Text(path) {
76
+ try {
77
+ if ((await lstat(path)).isSymbolicLink()) return null;
78
+ const buffer = await readFile(path);
79
+ if (buffer.includes(0)) return null;
80
+ return buffer.toString("utf8");
81
+ } catch {
82
+ return null;
83
+ }
84
+ }
@@ -0,0 +1,16 @@
1
+ const STOP_WORDS = new Set([
2
+ "a", "an", "and", "are", "as", "at", "be", "by", "for", "from", "how", "in", "is", "it", "of", "on", "or", "that", "the", "this", "to", "with",
3
+ ]);
4
+
5
+ export function tokenize(text) {
6
+ const raw = String(text ?? "")
7
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
8
+ .replace(/[_\-./\\:]+/g, " ")
9
+ .toLowerCase()
10
+ .match(/[a-z0-9]+/g) ?? [];
11
+ return raw.filter((token) => token.length > 1 && !STOP_WORDS.has(token));
12
+ }
13
+
14
+ export function uniqueTokens(text) {
15
+ return [...new Set(tokenize(text))];
16
+ }
@@ -0,0 +1,75 @@
1
+ import { join } from "node:path";
2
+ import { defineTool } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "typebox";
4
+ import { toolText } from "../tool-result.mjs";
5
+ import { CodeSearchIndexCache } from "./cache.mjs";
6
+ import { searchCode } from "./engine.mjs";
7
+ import { Model2VecVectorizer, POTION_CODE_MODEL_ID } from "./retrieval/model2vec.mjs";
8
+
9
+ const persistentCaches = new Map();
10
+
11
+ export function createCodeSearchTool({ engine, stateRoot = null }) {
12
+ const cache = stateRoot ? persistentCacheFor(stateRoot) : null;
13
+ return defineTool({
14
+ name: "code_search",
15
+ label: "Code Search",
16
+ description: "Native code-aware search over the workspace. Use it to locate relevant code snippets before reading full files; use grep for exact string confirmation.",
17
+ parameters: Type.Object({
18
+ query: Type.Optional(Type.String({ description: "Natural-language or symbol query" })),
19
+ path: Type.Optional(Type.String({ description: "Relative or absolute workspace path to search; default current workspace" })),
20
+ top_k: Type.Optional(Type.Number({ description: "Maximum results to return; default 5, max 20" })),
21
+ mode: Type.Optional(Type.Union([
22
+ Type.Literal("auto"),
23
+ Type.Literal("symbol"),
24
+ Type.Literal("lexical"),
25
+ Type.Literal("semantic"),
26
+ ], { description: "Search mode. auto uses BM25 + Model2Vec retrieval with RRF fusion." })),
27
+ include_tests: Type.Optional(Type.Boolean({ description: "Include test/spec paths without penalty; default false" })),
28
+ related_to: Type.Optional(Type.Object({
29
+ file_path: Type.String({ description: "Workspace-relative file path containing the known code" }),
30
+ line: Type.Number({ description: "Line inside the known code chunk" }),
31
+ }, { description: "Find code related to a known file location; query can optionally refine the relation" })),
32
+ }),
33
+ execute: async (_toolCallId, params) => executeCodeSearch({ engine, cache, ...params }),
34
+ });
35
+ }
36
+
37
+ export async function executeCodeSearch({ engine, cache, query, path = ".", top_k, mode = "auto", include_tests = false, related_to }) {
38
+ try {
39
+ const root = engine.cwd;
40
+ const searchPath = path === "." ? "." : engine.resolvePath(path);
41
+ const result = await searchCode({ root, query, path: searchPath, top_k, mode, include_tests, related_to, cache });
42
+ return toolText(formatSearchOutput(result), result);
43
+ } catch (err) {
44
+ return toolText(`Error running code_search: ${err.message}`, { error: true });
45
+ }
46
+ }
47
+
48
+ function persistentCacheFor(stateRoot) {
49
+ const storagePath = join(stateRoot, "code-search", "chunks.json");
50
+ const modelDir = join(stateRoot, "code-search", "models", POTION_CODE_MODEL_ID.replaceAll("/", "__"));
51
+ const cacheKey = `${storagePath}\n${modelDir}`;
52
+ let cache = persistentCaches.get(cacheKey);
53
+ if (!cache) {
54
+ cache = new CodeSearchIndexCache({
55
+ storagePath,
56
+ vectorizer: new Model2VecVectorizer({ modelDir }),
57
+ });
58
+ persistentCaches.set(cacheKey, cache);
59
+ }
60
+ return cache;
61
+ }
62
+
63
+ function formatSearchOutput({ results, stats }) {
64
+ const header = `--- code_search (${results.length} results, ${stats.files} files, ${stats.chunks} chunks, ${stats.mode}) ---`;
65
+ if (results.length === 0) return `${header}\nNo matching code snippets found.`;
66
+ const body = results.map((result, index) => [
67
+ `${index + 1}. ${result.file_path}:${result.start_line}-${result.end_line} score=${result.score} kind=${result.kind}${result.symbols.length ? ` symbols=${result.symbols.join(",")}` : ""}`,
68
+ fenceSnippet(result.snippet),
69
+ ].join("\n")).join("\n\n");
70
+ return `${header}\n${body}`;
71
+ }
72
+
73
+ function fenceSnippet(snippet) {
74
+ return "```\n" + snippet.trimEnd() + "\n```";
75
+ }
@@ -0,0 +1,38 @@
1
+ import { createProviderQuotaService } from "../../provider/quota/index.mjs";
2
+ import { installProviderQuotaTransportObserver, subscribeProviderQuotaTransport } from "../../provider/quota/transport-observer.mjs";
3
+
4
+ export function createRunnerProviderQuotaRuntime({ authStorage, getCurrentModel, ui } = {}) {
5
+ installProviderQuotaTransportObserver();
6
+ const providerQuota = createProviderQuotaService({ authStorage });
7
+ let lastSnapshot = null;
8
+ const unsubscribeTransport = subscribeProviderQuotaTransport((event) => {
9
+ const model = getCurrentModel?.();
10
+ if (!providerQuota.supports(model?.provider) || event.providerId !== model.provider) return;
11
+ const snapshot = event.source === "headers"
12
+ ? providerQuota.observeHeaders(event.headers, model)
13
+ : providerQuota.observeEvent(event.payload, model);
14
+ if (!snapshot) return;
15
+ lastSnapshot = snapshot;
16
+ ui?.providerQuotaSnapshot?.(lastSnapshot);
17
+ });
18
+
19
+ return {
20
+ getCachedProviderQuotaSnapshot() {
21
+ return lastSnapshot;
22
+ },
23
+ async getProviderQuotaSnapshot({ emit = false } = {}) {
24
+ const model = getCurrentModel?.();
25
+ if (!providerQuota.supports(model?.provider)) {
26
+ lastSnapshot = null;
27
+ if (emit) ui?.providerQuotaSnapshot?.(null);
28
+ return null;
29
+ }
30
+ lastSnapshot = await providerQuota.refresh(model);
31
+ if (emit) ui?.providerQuotaSnapshot?.(lastSnapshot);
32
+ return lastSnapshot;
33
+ },
34
+ disposeProviderQuotaRuntime() {
35
+ unsubscribeTransport();
36
+ },
37
+ };
38
+ }
@@ -27,6 +27,7 @@ import { registerSuperGrokProvider } from "../supergrok/provider.mjs";
27
27
  import { registerCustomProviders } from "../provider/custom-provider.mjs";
28
28
  import { injectHostedTools } from "../provider/hosted-tools.mjs";
29
29
  import { createRunnerLifecycle } from "./lifecycle/runner-lifecycle.mjs";
30
+ import { createRunnerProviderQuotaRuntime } from "./runner/provider-quota-runtime.mjs";
30
31
  export { MARCH_BASE_TOOL_NAMES, installModelPayloadDumper };
31
32
  export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
32
33
  export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
@@ -107,6 +108,8 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
107
108
  if (serviceTier === "priority" && selectedModel && isFastProvider(selectedModel.provider)) {
108
109
  _currentFastEntry = createFastModelEntry(selectedModel).model;
109
110
  }
111
+ const providerQuotaRuntime = createRunnerProviderQuotaRuntime({ authStorage: resolvedAuth, ui: runtimeUi,
112
+ getCurrentModel: () => _currentFastEntry ?? sessionBinding.get().model });
110
113
  return {
111
114
  engine,
112
115
  get session() { return sessionBinding.get(); },
@@ -176,6 +179,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
176
179
  return result;
177
180
  },
178
181
  getCurrentModel() { return _currentFastEntry ?? sessionBinding.get().model; },
182
+ ...providerQuotaRuntime,
179
183
  async setModel(model) {
180
184
  const activeSession = sessionBinding.get();
181
185
  const { baseId, isFast } = fromFastEntryModel(model);
@@ -249,6 +253,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
249
253
  () => shellRuntime?.dispose?.() ?? shellRuntime?.killAll?.(),
250
254
  () => lspService.dispose(),
251
255
  () => mcpClientManager?.disconnectAll?.(),
256
+ () => providerQuotaRuntime.disposeProviderQuotaRuntime(),
252
257
  () => detachRuntimeUi(),
253
258
  ]);
254
259
  },
@@ -26,6 +26,8 @@ export function createRemoteRunnerClient(peer, { initialState = null } = {}) {
26
26
  getScopedModels: () => state?.scopedModels ?? [],
27
27
  getConfiguredProviders: () => state?.configuredProviders ?? [],
28
28
  getSessionStats: () => state?.sessionStats ?? null,
29
+ getCachedProviderQuotaSnapshot: () => state?.providerQuota ?? null,
30
+ async getProviderQuotaSnapshot(options = {}) { return applyResultWithState(await peer.call("getProviderQuotaSnapshot", options)); },
29
31
  async refreshState() { return refreshState(); },
30
32
  getLastNotificationResult: () => peer.call("getLastNotificationResult"),
31
33
  notifyTest: (options) => peer.call("notifyTest", options),
@@ -37,6 +37,13 @@ export function createRunnerIpcTarget({ createRunnerImpl, runnerOptions = {} } =
37
37
  getSessionStats() {
38
38
  return getRunner().getSessionStats();
39
39
  },
40
+ async getProviderQuotaSnapshot(options = {}) {
41
+ const result = await getRunner().getProviderQuotaSnapshot(options);
42
+ return { result, state: getRunnerState(runner) };
43
+ },
44
+ getCachedProviderQuotaSnapshot() {
45
+ return getRunner().getCachedProviderQuotaSnapshot?.() ?? null;
46
+ },
40
47
  getState() {
41
48
  return getRunnerState(getRunner());
42
49
  },
@@ -22,6 +22,7 @@ export function createRunnerStateSnapshot(runner) {
22
22
  availableThinkingLevels: runner.getAvailableThinkingLevels?.() ?? [],
23
23
  canSwitchPiSession: runner.canSwitchPiSession?.() ?? false,
24
24
  sessionStats: runner.getSessionStats?.() ?? null,
25
+ providerQuota: runner.getCachedProviderQuotaSnapshot?.() ?? null,
25
26
  lspStatus: runner.getLspStatus?.() ?? null,
26
27
  extensionDiagnostics: runner.getExtensionDiagnostics?.() ?? [],
27
28
  extensionLifecycleState: runner.getExtensionLifecycleState?.() ?? null,
@@ -51,6 +51,7 @@ export function createRuntimeUiClient(eventBus) {
51
51
  status: (text) => eventBus.emit({ type: "status", text }),
52
52
  debugLines: (lines) => eventBus.emit({ type: "debug_lines", lines }),
53
53
  recall: ({ source, hints }) => eventBus.emit({ type: "recall", source, hints }),
54
+ providerQuotaSnapshot: (snapshot) => eventBus.emit({ type: "provider_quota_snapshot", snapshot }),
54
55
  editDiff: (path, diffLines) => eventBus.emit({ type: "edit_diff", path, diffLines }),
55
56
  requestPermission: (request) => eventBus.request({ type: "permission_request", ...request }),
56
57
  };
@@ -72,6 +73,7 @@ export function dispatchRuntimeUiEvent(ui, event) {
72
73
  case "status": return ui.status?.(event.text);
73
74
  case "debug_lines": return writeDebugLines(ui, event.lines);
74
75
  case "recall": return ui.recall?.({ source: event.source, hints: event.hints });
76
+ case "provider_quota_snapshot": return ui.providerQuotaSnapshot?.(event.snapshot);
75
77
  case "edit_diff": return ui.editDiff?.(event.path, event.diffLines);
76
78
  case "permission_request": return ui.requestPermission?.({ toolName: event.toolName, params: event.params, category: event.category });
77
79
  default: return undefined;
@@ -1,4 +1,5 @@
1
1
  import { createCommandExecTool } from "./command-exec-tool.mjs";
2
+ import { createCodeSearchTool } from "./code-search/tool.mjs";
2
3
  import { createContextStatsTool } from "./context-stats-tool.mjs";
3
4
  import { createEditFileTool } from "./file-edit-tool.mjs";
4
5
  import { createReadFileTool } from "./file-tools/read-file-tool.mjs";
@@ -15,6 +16,7 @@ import { createRuntimeRestartTool } from "./lifecycle/runtime-restart-tool.mjs";
15
16
 
16
17
  export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shellRuntime = null, lspService = null, mcpTools = [], webTools = [], lifecycle = null, permissionController = null, authStorage = null, projectMarchDir = null, stateRoot = null, getCurrentModel = null }) {
17
18
  const commandExecTool = createCommandExecTool({ cwd });
19
+ const codeSearchTool = createCodeSearchTool({ engine, stateRoot });
18
20
  const contextStatsTool = createContextStatsTool({ engine });
19
21
  const editFileTool = createEditFileTool({ engine, ui, lspService });
20
22
  const readFileTool = createReadFileTool({ engine });
@@ -30,6 +32,7 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shel
30
32
  screenTool,
31
33
  listWindowsTool,
32
34
  contextStatsTool,
35
+ codeSearchTool,
33
36
  commandExecTool,
34
37
  editFileTool,
35
38
  ...createShellTools(shellRuntime),