march-cli 0.1.34 → 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 (72) hide show
  1. package/package.json +12 -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/lifecycle/runner-lifecycle.mjs +16 -0
  19. package/src/agent/lifecycle/runtime-restart-tool.mjs +22 -0
  20. package/src/agent/runner/provider-quota-runtime.mjs +38 -0
  21. package/src/agent/runner.mjs +14 -14
  22. package/src/agent/runtime/remote-runner-client.mjs +9 -15
  23. package/src/agent/runtime/runner-ipc-target.mjs +10 -22
  24. package/src/agent/runtime/runner-process-client.mjs +101 -24
  25. package/src/agent/runtime/runner-runtime-host.mjs +2 -0
  26. package/src/agent/runtime/state/runner-state.mjs +81 -0
  27. package/src/agent/runtime/ui-event-bridge.mjs +2 -0
  28. package/src/agent/session/session-options.mjs +2 -1
  29. package/src/agent/tools.mjs +6 -1
  30. package/src/cli/args.mjs +14 -3
  31. package/src/cli/commands/catalog/visible-commands.mjs +5 -0
  32. package/src/cli/commands/help-command.mjs +1 -7
  33. package/src/cli/commands/registry/slash-command-registry.mjs +296 -0
  34. package/src/cli/commands/status-command.mjs +61 -35
  35. package/src/cli/input/autocomplete.mjs +2 -25
  36. package/src/cli/repl-loop.mjs +24 -41
  37. package/src/cli/slash-commands.mjs +19 -185
  38. package/src/cli/startup/app-runtime.mjs +201 -0
  39. package/src/cli/startup/configured-command.mjs +9 -0
  40. package/src/cli/startup/early-command.mjs +29 -0
  41. package/src/cli/turn/turn-input-preparer.mjs +41 -0
  42. package/src/context/system-core/base.md +5 -0
  43. package/src/main.mjs +47 -242
  44. package/src/provider/quota/codex.mjs +278 -0
  45. package/src/provider/quota/index.mjs +46 -0
  46. package/src/provider/quota/transport-observer.mjs +99 -0
  47. package/src/web-ui/command.mjs +112 -0
  48. package/src/web-ui/index.html +12 -0
  49. package/src/web-ui/runtime-host.mjs +188 -0
  50. package/src/web-ui/server.mjs +140 -0
  51. package/src/web-ui/session-manager.mjs +111 -0
  52. package/src/web-ui/src/App.tsx +7 -0
  53. package/src/web-ui/src/components/AppShell.tsx +48 -0
  54. package/src/web-ui/src/components/Composer.tsx +47 -0
  55. package/src/web-ui/src/components/FileExplorer.tsx +46 -0
  56. package/src/web-ui/src/components/RightSidebar.tsx +115 -0
  57. package/src/web-ui/src/components/SessionTimeline.tsx +31 -0
  58. package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +109 -0
  59. package/src/web-ui/src/components/timeline/TimelineList.tsx +14 -0
  60. package/src/web-ui/src/fileTreeAdapter.ts +51 -0
  61. package/src/web-ui/src/main.tsx +11 -0
  62. package/src/web-ui/src/mockData.ts +87 -0
  63. package/src/web-ui/src/model.ts +82 -0
  64. package/src/web-ui/src/runtime/client.ts +81 -0
  65. package/src/web-ui/src/runtime/runtimeTimeline.ts +88 -0
  66. package/src/web-ui/src/runtime/useWebRuntime.ts +144 -0
  67. package/src/web-ui/src/styles/shell.css +166 -0
  68. package/src/web-ui/src/styles/tokens.css +116 -0
  69. package/src/web-ui/src/timelineAdapter.ts +43 -0
  70. package/src/web-ui/src/vite-env.d.ts +1 -0
  71. package/src/web-ui/tsconfig.json +20 -0
  72. package/src/web-ui/vite.config.mjs +11 -0
@@ -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,16 @@
1
+ export function createRunnerLifecycle() {
2
+ let pendingAction = null;
3
+ return {
4
+ requestRuntimeRestart({ reason = "" } = {}) {
5
+ pendingAction = { type: "restart_runtime", reason };
6
+ },
7
+ takePendingAction() {
8
+ const action = pendingAction;
9
+ pendingAction = null;
10
+ return action;
11
+ },
12
+ clearPendingAction() {
13
+ pendingAction = null;
14
+ },
15
+ };
16
+ }
@@ -0,0 +1,22 @@
1
+ import { defineTool } from "@earendil-works/pi-coding-agent";
2
+ import { Type } from "typebox";
3
+ import { toolText } from "../tool-result.mjs";
4
+
5
+ export function createRuntimeRestartTool({ lifecycle }) {
6
+ return defineTool({
7
+ name: "request_runtime_restart",
8
+ label: "Request Runtime Restart",
9
+ description: "Request March to restart the runtime after the current turn so the next turn loads updated runner/tool code from disk.",
10
+ parameters: Type.Object({
11
+ reason: Type.Optional(Type.String({ description: "Why the runtime needs to restart" })),
12
+ }),
13
+ execute: async (_toolCallId, params = {}) => {
14
+ const reason = String(params.reason ?? "").trim();
15
+ lifecycle?.requestRuntimeRestart?.({ reason });
16
+ return toolText(
17
+ "March runtime restart requested. The current turn will finish first; the next turn will use the latest code from disk.",
18
+ { lifecycleAction: { type: "restart_runtime", reason } },
19
+ );
20
+ },
21
+ });
22
+ }
@@ -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
+ }