@spikedpunch/mast 0.2.0 → 0.4.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/MAST_SPEC.md +169 -44
- package/README.md +266 -20
- package/assets/prime.md +18 -0
- package/assets/signals.md +51 -0
- package/assets/skill.md +13 -15
- package/dist/ast/types.d.ts +137 -1
- package/dist/ast/types.d.ts.map +1 -1
- package/dist/cli/docs-cmd.d.ts +1 -0
- package/dist/cli/docs-cmd.d.ts.map +1 -1
- package/dist/cli/docs-cmd.js +2 -1
- package/dist/cli/docs-cmd.js.map +1 -1
- package/dist/cli/hook-cmd.d.ts +3 -0
- package/dist/cli/hook-cmd.d.ts.map +1 -0
- package/dist/cli/hook-cmd.js +13 -0
- package/dist/cli/hook-cmd.js.map +1 -0
- package/dist/cli/hook-state-dir.d.ts +24 -0
- package/dist/cli/hook-state-dir.d.ts.map +1 -0
- package/dist/cli/hook-state-dir.js +57 -0
- package/dist/cli/hook-state-dir.js.map +1 -0
- package/dist/cli/hook.d.ts +44 -0
- package/dist/cli/hook.d.ts.map +1 -0
- package/dist/cli/hook.js +200 -0
- package/dist/cli/hook.js.map +1 -0
- package/dist/cli/index.js +12 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/installed-artifacts.d.ts +23 -0
- package/dist/cli/installed-artifacts.d.ts.map +1 -0
- package/dist/cli/installed-artifacts.js +39 -0
- package/dist/cli/installed-artifacts.js.map +1 -0
- package/dist/cli/prime-cmd.d.ts +17 -0
- package/dist/cli/prime-cmd.d.ts.map +1 -0
- package/dist/cli/prime-cmd.js +70 -0
- package/dist/cli/prime-cmd.js.map +1 -0
- package/dist/cli/program.d.ts.map +1 -1
- package/dist/cli/program.js +6 -0
- package/dist/cli/program.js.map +1 -1
- package/dist/cli/query.d.ts +40 -1
- package/dist/cli/query.d.ts.map +1 -1
- package/dist/cli/query.js +36 -2
- package/dist/cli/query.js.map +1 -1
- package/dist/cli/search-cmd.d.ts +6 -1
- package/dist/cli/search-cmd.d.ts.map +1 -1
- package/dist/cli/search-cmd.js +9 -2
- package/dist/cli/search-cmd.js.map +1 -1
- package/dist/cli/serve.d.ts +6 -1
- package/dist/cli/serve.d.ts.map +1 -1
- package/dist/cli/serve.js +14 -4
- package/dist/cli/serve.js.map +1 -1
- package/dist/cli/setup-cmd.d.ts +34 -0
- package/dist/cli/setup-cmd.d.ts.map +1 -0
- package/dist/cli/setup-cmd.js +205 -0
- package/dist/cli/setup-cmd.js.map +1 -0
- package/dist/cli/setup-command.d.ts +32 -0
- package/dist/cli/setup-command.d.ts.map +1 -0
- package/dist/cli/setup-command.js +43 -0
- package/dist/cli/setup-command.js.map +1 -0
- package/dist/cli/setup-plan.d.ts +52 -0
- package/dist/cli/setup-plan.d.ts.map +1 -0
- package/dist/cli/setup-plan.js +221 -0
- package/dist/cli/setup-plan.js.map +1 -0
- package/dist/cli/setup-rules.d.ts +41 -0
- package/dist/cli/setup-rules.d.ts.map +1 -0
- package/dist/cli/setup-rules.js +76 -0
- package/dist/cli/setup-rules.js.map +1 -0
- package/dist/cli/setup-static.d.ts +13 -0
- package/dist/cli/setup-static.d.ts.map +1 -0
- package/dist/cli/setup-static.js +116 -0
- package/dist/cli/setup-static.js.map +1 -0
- package/dist/cli/skill-install.d.ts +2 -0
- package/dist/cli/skill-install.d.ts.map +1 -1
- package/dist/cli/skill-install.js +2 -0
- package/dist/cli/skill-install.js.map +1 -1
- package/dist/cli/status.d.ts +15 -0
- package/dist/cli/status.d.ts.map +1 -1
- package/dist/cli/status.js +32 -6
- package/dist/cli/status.js.map +1 -1
- package/dist/cli/upgrade-cmd.d.ts +5 -1
- package/dist/cli/upgrade-cmd.d.ts.map +1 -1
- package/dist/cli/upgrade-cmd.js +28 -3
- package/dist/cli/upgrade-cmd.js.map +1 -1
- package/dist/indexer/freshness.d.ts +10 -0
- package/dist/indexer/freshness.d.ts.map +1 -1
- package/dist/indexer/freshness.js +7 -1
- package/dist/indexer/freshness.js.map +1 -1
- package/dist/indexer/import-resolver.js +28 -16
- package/dist/indexer/import-resolver.js.map +1 -1
- package/dist/indexer/index.d.ts +38 -3
- package/dist/indexer/index.d.ts.map +1 -1
- package/dist/indexer/index.js +47 -4
- package/dist/indexer/index.js.map +1 -1
- package/dist/indexer/watcher.d.ts +94 -0
- package/dist/indexer/watcher.d.ts.map +1 -1
- package/dist/indexer/watcher.js +172 -19
- package/dist/indexer/watcher.js.map +1 -1
- package/dist/mcp/context.d.ts +16 -0
- package/dist/mcp/context.d.ts.map +1 -1
- package/dist/mcp/freshness-probe.d.ts +82 -0
- package/dist/mcp/freshness-probe.d.ts.map +1 -0
- package/dist/mcp/freshness-probe.js +111 -0
- package/dist/mcp/freshness-probe.js.map +1 -0
- package/dist/mcp/instructions.d.ts +7 -0
- package/dist/mcp/instructions.d.ts.map +1 -0
- package/dist/mcp/instructions.js +10 -0
- package/dist/mcp/instructions.js.map +1 -0
- package/dist/mcp/server.d.ts +54 -3
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +79 -4
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools/_helpers.d.ts +40 -0
- package/dist/mcp/tools/_helpers.d.ts.map +1 -1
- package/dist/mcp/tools/_helpers.js +27 -0
- package/dist/mcp/tools/_helpers.js.map +1 -1
- package/dist/mcp/tools/callers.d.ts.map +1 -1
- package/dist/mcp/tools/callers.js +6 -1
- package/dist/mcp/tools/callers.js.map +1 -1
- package/dist/mcp/tools/dependencies.d.ts.map +1 -1
- package/dist/mcp/tools/dependencies.js +2 -1
- package/dist/mcp/tools/dependencies.js.map +1 -1
- package/dist/mcp/tools/exports.d.ts.map +1 -1
- package/dist/mcp/tools/exports.js +2 -1
- package/dist/mcp/tools/exports.js.map +1 -1
- package/dist/mcp/tools/implementors.d.ts.map +1 -1
- package/dist/mcp/tools/implementors.js +2 -1
- package/dist/mcp/tools/implementors.js.map +1 -1
- package/dist/mcp/tools/project-skeleton.d.ts.map +1 -1
- package/dist/mcp/tools/project-skeleton.js +2 -1
- package/dist/mcp/tools/project-skeleton.js.map +1 -1
- package/dist/mcp/tools/reindex.d.ts.map +1 -1
- package/dist/mcp/tools/reindex.js +3 -0
- package/dist/mcp/tools/reindex.js.map +1 -1
- package/dist/mcp/tools/rename-impact.d.ts.map +1 -1
- package/dist/mcp/tools/rename-impact.js +4 -1
- package/dist/mcp/tools/rename-impact.js.map +1 -1
- package/dist/mcp/tools/search.d.ts.map +1 -1
- package/dist/mcp/tools/search.js +13 -2
- package/dist/mcp/tools/search.js.map +1 -1
- package/dist/mcp/tools/signature.d.ts.map +1 -1
- package/dist/mcp/tools/signature.js +2 -1
- package/dist/mcp/tools/signature.js.map +1 -1
- package/dist/mcp/tools/status.d.ts.map +1 -1
- package/dist/mcp/tools/status.js +8 -2
- package/dist/mcp/tools/status.js.map +1 -1
- package/dist/store/config.d.ts.map +1 -1
- package/dist/store/config.js +4 -5
- package/dist/store/config.js.map +1 -1
- package/dist/store/defaults.d.ts +9 -0
- package/dist/store/defaults.d.ts.map +1 -0
- package/dist/store/defaults.js +12 -0
- package/dist/store/defaults.js.map +1 -0
- package/package.json +5 -2
|
@@ -60,6 +60,62 @@ export interface WatchPathFilter {
|
|
|
60
60
|
* set of files a manual `mast index` run would.
|
|
61
61
|
*/
|
|
62
62
|
export declare function shouldWatchPath(filter: WatchPathFilter, absPath: string): boolean;
|
|
63
|
+
/** One directory entry, reduced to what reconciliation needs. */
|
|
64
|
+
export interface ListedEntry {
|
|
65
|
+
readonly name: string;
|
|
66
|
+
readonly isDirectory: boolean;
|
|
67
|
+
}
|
|
68
|
+
export interface UnwatchedEntries {
|
|
69
|
+
/** Every unknown file, including those inside unknown directories. */
|
|
70
|
+
readonly files: readonly string[];
|
|
71
|
+
/** Every unknown directory, at any depth. */
|
|
72
|
+
readonly directories: readonly string[];
|
|
73
|
+
/**
|
|
74
|
+
* The unknown entries whose parent the watcher already knows: handing these to
|
|
75
|
+
* the watcher is enough, because it descends into a directory by itself.
|
|
76
|
+
*/
|
|
77
|
+
readonly roots: readonly string[];
|
|
78
|
+
/** Directories that could not be listed (vanished, EACCES); the rest was still scanned. */
|
|
79
|
+
readonly failures: ReadonlyArray<{
|
|
80
|
+
readonly directory: string;
|
|
81
|
+
readonly error: unknown;
|
|
82
|
+
}>;
|
|
83
|
+
}
|
|
84
|
+
export interface FindUnwatchedInput {
|
|
85
|
+
/** chokidar's `getWatched()`: absolute directory -> names of its children, files and directories alike. */
|
|
86
|
+
readonly watched: Readonly<Record<string, readonly string[]>>;
|
|
87
|
+
readonly projectRoot: string;
|
|
88
|
+
readonly listDirectory: (directory: string) => Promise<readonly ListedEntry[]>;
|
|
89
|
+
/** The same predicate the watcher was constructed with, applied to files and directories. */
|
|
90
|
+
readonly isIgnored: (absPath: string) => boolean;
|
|
91
|
+
/** Checked between directories, so a closed watcher stops walking a large tree. */
|
|
92
|
+
readonly signal?: AbortSignal;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Finds what is on disk but unknown to the watcher (D067). Pure over its inputs:
|
|
96
|
+
* `watched` is a snapshot and `listDirectory` is the only I/O.
|
|
97
|
+
*
|
|
98
|
+
* Only directories at or under `projectRoot` are listed: chokidar records the root
|
|
99
|
+
* itself as a child of the root's parent, and that parent is not ours to scan.
|
|
100
|
+
* A file the watcher learns about between the snapshot and the listing is reported
|
|
101
|
+
* too; that is a redundant notification, which the incremental reindex absorbs.
|
|
102
|
+
* Symbolic links are listed as files and never followed, so a link cycle cannot loop.
|
|
103
|
+
*/
|
|
104
|
+
export declare function findUnwatchedEntries(input: FindUnwatchedInput): Promise<UnwatchedEntries>;
|
|
105
|
+
/** The members of chokidar's `FSWatcher` that `startWatchMode` uses. */
|
|
106
|
+
export interface FsWatcher {
|
|
107
|
+
onFileEvent(event: 'add' | 'change' | 'unlink', listener: (path: string) => void): void;
|
|
108
|
+
onReady(listener: () => void): void;
|
|
109
|
+
onError(listener: (err: unknown) => void): void;
|
|
110
|
+
getWatched(): Record<string, string[]>;
|
|
111
|
+
add(paths: readonly string[]): unknown;
|
|
112
|
+
close(): Promise<void>;
|
|
113
|
+
}
|
|
114
|
+
export interface FsWatcherOptions {
|
|
115
|
+
readonly ignoreInitial: true;
|
|
116
|
+
readonly ignored: (path: string) => boolean;
|
|
117
|
+
}
|
|
118
|
+
export type FsWatcherFactory = (root: string, options: FsWatcherOptions) => FsWatcher;
|
|
63
119
|
export interface WatchHandle {
|
|
64
120
|
close(): Promise<void>;
|
|
65
121
|
}
|
|
@@ -68,8 +124,46 @@ export interface StartWatchModeOptions {
|
|
|
68
124
|
/** One incremental Phase 1 + embed run; supplied by the serve lifecycle. */
|
|
69
125
|
readonly runBatch: (paths: readonly string[]) => Promise<void>;
|
|
70
126
|
readonly onWarn: (message: string) => void;
|
|
127
|
+
/**
|
|
128
|
+
* Called once, after chokidar's initial scan, a settle period of `settleMs`, and
|
|
129
|
+
* one reconciliation pass over the directories chokidar is watching (D067).
|
|
130
|
+
* It means: every file that existed when the pass ran has been accounted for
|
|
131
|
+
* (queued for reindex if chokidar had not seen it), and the OS watch has had
|
|
132
|
+
* `settleMs` to go live. It is never called after `close()`.
|
|
133
|
+
*
|
|
134
|
+
* What it does not mean: chokidar's own `ready` fires before the OS watch is
|
|
135
|
+
* delivering, and a file created in that gap produces no event at all. The
|
|
136
|
+
* reconciliation pass recovers files created up to the moment it ran; a watch
|
|
137
|
+
* that goes live later than `settleMs` after `ready` is still a gap, and nothing
|
|
138
|
+
* here proves liveness. Each directory has its own OS watch, so a sentinel file
|
|
139
|
+
* would prove one directory only.
|
|
140
|
+
*
|
|
141
|
+
* Before D067 this fired straight from chokidar's `ready`, and under CPU load 12
|
|
142
|
+
* of 550 files written immediately after it were lost, against 0 of 400 written
|
|
143
|
+
* 300 ms after it.
|
|
144
|
+
*
|
|
145
|
+
* The watcher starts AFTER `serve` accepts MCP connections, and it is
|
|
146
|
+
* constructed with `ignoreInitial: true` — correct, since the startup ladder
|
|
147
|
+
* has already indexed the tree, but it means a file created before the scan
|
|
148
|
+
* finishes is treated as pre-existing and fires no event at all. Without this
|
|
149
|
+
* callback nothing outside the process could tell that window apart from
|
|
150
|
+
* "watching, nothing has changed" or from "the watcher failed to start": the
|
|
151
|
+
* EMFILE path warns, and the success path was silent, so silence meant three
|
|
152
|
+
* different things (D061).
|
|
153
|
+
*
|
|
154
|
+
* Optional. A caller that does not want the signal still gets a watcher.
|
|
155
|
+
*/
|
|
156
|
+
readonly onReady?: () => void;
|
|
71
157
|
/** Override for tests; production default 500ms. */
|
|
72
158
|
readonly debounceMs?: number;
|
|
159
|
+
/**
|
|
160
|
+
* Wait between chokidar's `ready` and the reconciliation pass; production
|
|
161
|
+
* default 1000 ms. Measured under load (D067): 0 of 400 files lost when written
|
|
162
|
+
* 300 ms after `ready`; 1000 ms is a margin over that, not a measured bound.
|
|
163
|
+
*/
|
|
164
|
+
readonly settleMs?: number;
|
|
165
|
+
/** Test seam; production uses chokidar. */
|
|
166
|
+
readonly watcherFactory?: FsWatcherFactory;
|
|
73
167
|
}
|
|
74
168
|
/**
|
|
75
169
|
* Start `--watch` mode: a chokidar watcher over the project feeding the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../../src/indexer/watcher.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../../src/indexer/watcher.ts"],"names":[],"mappings":"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"}
|
package/dist/indexer/watcher.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readdir } from 'node:fs/promises';
|
|
2
|
+
import { extname, join, relative, resolve } from 'node:path';
|
|
2
3
|
import { watch as chokidarWatch } from 'chokidar';
|
|
3
4
|
import { globToRegex } from './walker.js';
|
|
4
5
|
/**
|
|
@@ -93,6 +94,102 @@ export function shouldWatchPath(filter, absPath) {
|
|
|
93
94
|
return false;
|
|
94
95
|
return !filter.excludeRegexes.some((rx) => rx.test(rel));
|
|
95
96
|
}
|
|
97
|
+
function isInsideRoot(projectRoot, directory) {
|
|
98
|
+
const rel = relative(projectRoot, directory);
|
|
99
|
+
return rel === '' || (!rel.startsWith('..') && !rel.startsWith('/'));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Finds what is on disk but unknown to the watcher (D067). Pure over its inputs:
|
|
103
|
+
* `watched` is a snapshot and `listDirectory` is the only I/O.
|
|
104
|
+
*
|
|
105
|
+
* Only directories at or under `projectRoot` are listed: chokidar records the root
|
|
106
|
+
* itself as a child of the root's parent, and that parent is not ours to scan.
|
|
107
|
+
* A file the watcher learns about between the snapshot and the listing is reported
|
|
108
|
+
* too; that is a redundant notification, which the incremental reindex absorbs.
|
|
109
|
+
* Symbolic links are listed as files and never followed, so a link cycle cannot loop.
|
|
110
|
+
*/
|
|
111
|
+
export async function findUnwatchedEntries(input) {
|
|
112
|
+
const files = [];
|
|
113
|
+
const directories = [];
|
|
114
|
+
const roots = [];
|
|
115
|
+
const failures = [];
|
|
116
|
+
const list = async (directory) => {
|
|
117
|
+
try {
|
|
118
|
+
return await input.listDirectory(directory);
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
failures.push({ directory, error });
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
// Everything below an unknown directory is unknown too, so it is walked in full.
|
|
126
|
+
const walkUnknown = async (directory) => {
|
|
127
|
+
if (input.signal?.aborted === true)
|
|
128
|
+
return;
|
|
129
|
+
for (const entry of await list(directory)) {
|
|
130
|
+
const abs = join(directory, entry.name);
|
|
131
|
+
if (input.isIgnored(abs))
|
|
132
|
+
continue;
|
|
133
|
+
if (entry.isDirectory) {
|
|
134
|
+
directories.push(abs);
|
|
135
|
+
await walkUnknown(abs);
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
files.push(abs);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
for (const [directory, children] of Object.entries(input.watched)) {
|
|
143
|
+
if (input.signal?.aborted === true)
|
|
144
|
+
break;
|
|
145
|
+
if (!isInsideRoot(input.projectRoot, directory))
|
|
146
|
+
continue;
|
|
147
|
+
const known = new Set(children);
|
|
148
|
+
for (const entry of await list(directory)) {
|
|
149
|
+
if (known.has(entry.name))
|
|
150
|
+
continue;
|
|
151
|
+
const abs = join(directory, entry.name);
|
|
152
|
+
if (input.isIgnored(abs))
|
|
153
|
+
continue;
|
|
154
|
+
roots.push(abs);
|
|
155
|
+
if (entry.isDirectory) {
|
|
156
|
+
directories.push(abs);
|
|
157
|
+
await walkUnknown(abs);
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
files.push(abs);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return { files, directories, roots, failures };
|
|
165
|
+
}
|
|
166
|
+
async function listDirectoryOnDisk(directory) {
|
|
167
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
168
|
+
return entries.map((e) => ({ name: e.name, isDirectory: e.isDirectory() }));
|
|
169
|
+
}
|
|
170
|
+
/** Resolves true after `ms`, or false at once if `signal` aborts first. */
|
|
171
|
+
function delay(ms, signal) {
|
|
172
|
+
return new Promise((resolve) => {
|
|
173
|
+
if (signal.aborted) {
|
|
174
|
+
resolve(false);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
const timer = setTimeout(() => { signal.removeEventListener('abort', onAbort); resolve(true); }, ms);
|
|
178
|
+
const onAbort = () => { clearTimeout(timer); resolve(false); };
|
|
179
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
const defaultWatcherFactory = (root, opts) => {
|
|
183
|
+
const fsw = chokidarWatch(root, opts);
|
|
184
|
+
return {
|
|
185
|
+
onFileEvent: (event, listener) => { fsw.on(event, listener); },
|
|
186
|
+
onReady: (listener) => { fsw.on('ready', listener); },
|
|
187
|
+
onError: (listener) => { fsw.on('error', listener); },
|
|
188
|
+
getWatched: () => fsw.getWatched(),
|
|
189
|
+
add: (paths) => fsw.add([...paths]),
|
|
190
|
+
close: () => fsw.close(),
|
|
191
|
+
};
|
|
192
|
+
};
|
|
96
193
|
/**
|
|
97
194
|
* Start `--watch` mode: a chokidar watcher over the project feeding the
|
|
98
195
|
* debounced scheduler. May throw on construction (e.g. EMFILE) — callers must
|
|
@@ -111,38 +208,94 @@ export function startWatchMode(options) {
|
|
|
111
208
|
onBatch: options.runBatch,
|
|
112
209
|
onWarn: options.onWarn,
|
|
113
210
|
});
|
|
114
|
-
|
|
211
|
+
// Prune ignored subtrees at the directory level so chokidar never descends
|
|
212
|
+
// into node_modules/ or the state dir. Files are re-checked (with the
|
|
213
|
+
// extension allowlist) in shouldWatchPath; `rel + '/'` lets patterns like
|
|
214
|
+
// `**/node_modules/**` match the directory itself, not just its contents.
|
|
215
|
+
// Shared with reconciliation so both agree on what is out of scope.
|
|
216
|
+
const isIgnored = (path) => {
|
|
217
|
+
const abs = resolve(path);
|
|
218
|
+
if (abs === filter.stateDir || abs.startsWith(`${filter.stateDir}/`))
|
|
219
|
+
return true;
|
|
220
|
+
const rel = relative(filter.projectRoot, abs);
|
|
221
|
+
if (rel === '' || rel.startsWith('..'))
|
|
222
|
+
return false;
|
|
223
|
+
return filter.excludeRegexes.some((rx) => rx.test(rel) || rx.test(`${rel}/`));
|
|
224
|
+
};
|
|
225
|
+
const watcher = (options.watcherFactory ?? defaultWatcherFactory)(config.resolved_project_root, {
|
|
115
226
|
// The startup ladder already reindexed — only future changes matter.
|
|
116
227
|
ignoreInitial: true,
|
|
117
|
-
|
|
118
|
-
// into node_modules/ or the state dir. Files are re-checked (with the
|
|
119
|
-
// extension allowlist) in shouldWatchPath; `rel + '/'` lets patterns like
|
|
120
|
-
// `**/node_modules/**` match the directory itself, not just its contents.
|
|
121
|
-
ignored: (path) => {
|
|
122
|
-
const abs = resolve(path);
|
|
123
|
-
if (abs === filter.stateDir || abs.startsWith(`${filter.stateDir}/`))
|
|
124
|
-
return true;
|
|
125
|
-
const rel = relative(filter.projectRoot, abs);
|
|
126
|
-
if (rel === '' || rel.startsWith('..'))
|
|
127
|
-
return false;
|
|
128
|
-
return filter.excludeRegexes.some((rx) => rx.test(rel) || rx.test(`${rel}/`));
|
|
129
|
-
},
|
|
228
|
+
ignored: isIgnored,
|
|
130
229
|
});
|
|
230
|
+
// Aborted by `close()`; cancels the settle wait and the reconciliation walk.
|
|
231
|
+
const closing = new AbortController();
|
|
232
|
+
const reconcile = async () => {
|
|
233
|
+
let found;
|
|
234
|
+
try {
|
|
235
|
+
found = await findUnwatchedEntries({
|
|
236
|
+
watched: watcher.getWatched(),
|
|
237
|
+
projectRoot: filter.projectRoot,
|
|
238
|
+
listDirectory: listDirectoryOnDisk,
|
|
239
|
+
isIgnored,
|
|
240
|
+
signal: closing.signal,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
catch (err) {
|
|
244
|
+
options.onWarn(`[mast] watch: reconciliation failed (continuing): ${String(err)}`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (closing.signal.aborted)
|
|
248
|
+
return;
|
|
249
|
+
for (const failure of found.failures) {
|
|
250
|
+
options.onWarn(`[mast] watch: reconciliation could not list ${failure.directory} (continuing): ${String(failure.error)}`);
|
|
251
|
+
}
|
|
252
|
+
for (const file of found.files) {
|
|
253
|
+
if (shouldWatchPath(filter, file))
|
|
254
|
+
scheduler.notify(file);
|
|
255
|
+
}
|
|
256
|
+
// `add` under `ignoreInitial: true` emits no `add` events (chokidar handler.js:
|
|
257
|
+
// `_addToNodeFs(path, !_internal, ...)` makes this an initial add), so the files
|
|
258
|
+
// notified above are not delivered twice. It is what makes later changes inside
|
|
259
|
+
// a missed directory visible.
|
|
260
|
+
if (found.roots.length > 0)
|
|
261
|
+
watcher.add(found.roots);
|
|
262
|
+
};
|
|
263
|
+
const announceReadiness = async () => {
|
|
264
|
+
if (!(await delay(options.settleMs ?? 1000, closing.signal)))
|
|
265
|
+
return;
|
|
266
|
+
await reconcile();
|
|
267
|
+
if (closing.signal.aborted)
|
|
268
|
+
return;
|
|
269
|
+
options.onReady?.();
|
|
270
|
+
};
|
|
131
271
|
// `unlink` feeds the same incremental run: deleted files are cleaned up by
|
|
132
272
|
// the index run's manifest diff (removeDeletedFiles), not reimplemented here.
|
|
133
|
-
watcher.
|
|
273
|
+
watcher.onFileEvent('add', (path) => { if (shouldWatchPath(filter, resolve(path)))
|
|
134
274
|
scheduler.notify(path); });
|
|
135
|
-
watcher.
|
|
275
|
+
watcher.onFileEvent('change', (path) => { if (shouldWatchPath(filter, resolve(path)))
|
|
136
276
|
scheduler.notify(path); });
|
|
137
|
-
watcher.
|
|
277
|
+
watcher.onFileEvent('unlink', (path) => { if (shouldWatchPath(filter, resolve(path)))
|
|
138
278
|
scheduler.notify(path); });
|
|
139
|
-
|
|
279
|
+
// chokidar's `ready` only says its initial scan finished; the OS watch may not be
|
|
280
|
+
// delivering yet, and a file created in that gap fires no event (D067). So
|
|
281
|
+
// readiness is announced after a settle period and a reconciliation pass, see
|
|
282
|
+
// `onReady`. A construction failure (EMFILE, permissions) throws out of the
|
|
283
|
+
// factory above and never reaches this line, so `onReady` cannot announce a
|
|
284
|
+
// watcher that failed to start. A runtime `error` after a successful scan does
|
|
285
|
+
// not un-fire it: the signal is not "the watcher is healthy".
|
|
286
|
+
watcher.onReady(() => {
|
|
287
|
+
announceReadiness().catch((err) => {
|
|
288
|
+
options.onWarn(`[mast] watch: readiness announcement failed: ${String(err)}`);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
watcher.onError((err) => {
|
|
140
292
|
// Watcher errors (EMFILE, EPERM, …) degrade to no-watch; JIT staleness
|
|
141
293
|
// handling keeps reads correct, so serving continues.
|
|
142
294
|
options.onWarn(`[mast] watch: watcher error (continuing without event): ${String(err)}`);
|
|
143
295
|
});
|
|
144
296
|
return {
|
|
145
297
|
close: async () => {
|
|
298
|
+
closing.abort();
|
|
146
299
|
scheduler.close();
|
|
147
300
|
await watcher.close();
|
|
148
301
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"watcher.js","sourceRoot":"","sources":["../../src/indexer/watcher.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"watcher.js","sourceRoot":"","sources":["../../src/indexer/watcher.ts"],"names":[],"mappings":"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"}
|
package/dist/mcp/context.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Db } from '../graph/db.js';
|
|
2
2
|
import type { ChunkStore } from '../store/sqliteChunkStore.js';
|
|
3
3
|
import type { ResolvedConfig } from '../store/config.js';
|
|
4
|
+
import type { FreshnessProbe } from './freshness-probe.js';
|
|
4
5
|
/**
|
|
5
6
|
* Shared state injected into every MCP tool handler at registration time.
|
|
6
7
|
*
|
|
@@ -19,5 +20,20 @@ export interface AppContext {
|
|
|
19
20
|
readonly config: ResolvedConfig;
|
|
20
21
|
/** Stable identifier for this MCP server session (used in metrics). */
|
|
21
22
|
readonly sessionId: string;
|
|
23
|
+
/**
|
|
24
|
+
* Cached "is the index missing files?" signal, read by `mast_search` to warn
|
|
25
|
+
* that a result set may be incomplete.
|
|
26
|
+
*
|
|
27
|
+
* Optional, and absent is a real configuration rather than an oversight: the
|
|
28
|
+
* probe amortises one project walk across a TTL, which only pays off in a
|
|
29
|
+
* long-lived process. `mast serve` wires one (mcp/server.ts); the one-shot
|
|
30
|
+
* CLI query path (cli/query.ts) deliberately does not, because a single
|
|
31
|
+
* search there would pay the whole walk to answer a question it can answer
|
|
32
|
+
* outright with `mast search --reindex`.
|
|
33
|
+
*
|
|
34
|
+
* Absent, or reporting `null`, means *unknown* — surfaces render that as
|
|
35
|
+
* silence, never as zero.
|
|
36
|
+
*/
|
|
37
|
+
readonly freshness?: FreshnessProbe;
|
|
22
38
|
}
|
|
23
39
|
//# sourceMappingURL=context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/mcp/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/mcp/context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAM3D;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,EAAE,EAAE,EAAE,CAAC;IAChB;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,UAAU,CAAC;IAChC,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,uEAAuE;IACvE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B;;;;;;;;;;;;;OAaG;IACH,QAAQ,CAAC,SAAS,CAAC,EAAE,cAAc,CAAC;CACrC"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cached, non-blocking answer to "is the index missing files?".
|
|
3
|
+
*
|
|
4
|
+
* `mast_search` needs this to warn a caller that a zero-hit result may mean
|
|
5
|
+
* "not indexed" rather than "not present" — the severity-zero failure this
|
|
6
|
+
* package is built around. What it cannot do is pay for it *at scale*, and the
|
|
7
|
+
* cost is worth stating with its corpus attached, because it inverts:
|
|
8
|
+
*
|
|
9
|
+
* - This repository, 213 files walked (medians, measured 2026-09-03):
|
|
10
|
+
* `fusedSearch` 15.3 ms, `measureFreshness` 7.6 ms. The walk is CHEAPER
|
|
11
|
+
* than the search. On a tree this size the probe buys nothing.
|
|
12
|
+
* - A 14k-file monorepo: `measureFreshness` ~183 ms (measured 2026-09-02).
|
|
13
|
+
*
|
|
14
|
+
* That is the whole argument, and it is about scaling, not about a ratio.
|
|
15
|
+
* `measureFreshness` is O(files in the tree) and a search is O(matches), so the
|
|
16
|
+
* walk overtakes the query as the tree grows and then keeps going. The probe
|
|
17
|
+
* exists for the trees where that has already happened.
|
|
18
|
+
*
|
|
19
|
+
* So the probe inverts it. `peekUnindexed` returns the last-known count
|
|
20
|
+
* synchronously and schedules a refresh behind the caller when the value has
|
|
21
|
+
* aged past the TTL. The first search of a session gets `null` — unknown, no
|
|
22
|
+
* warning — and every later one gets an answer that is at most `ttlMs` old.
|
|
23
|
+
* `server.ts` primes it at startup so that window is usually already closed.
|
|
24
|
+
*
|
|
25
|
+
* The cost is amortised, NOT eliminated, and it is worth being exact about
|
|
26
|
+
* which: `measureFreshness` -> `walkProject` globs asynchronously but then
|
|
27
|
+
* `statSync`s every hit in a synchronous loop, so a refresh occupies the event
|
|
28
|
+
* loop for part of its duration. A search that lands mid-refresh can be
|
|
29
|
+
* delayed by that much. One such window per `ttlMs`, rather than one walk per
|
|
30
|
+
* search, is the whole of the improvement — do not read "background" as
|
|
31
|
+
* "free".
|
|
32
|
+
*
|
|
33
|
+
* Deliberately NOT a reindex: no `structure.lock` is taken and nothing is
|
|
34
|
+
* written, so the probe cannot contend with a concurrent `runIndex` or with
|
|
35
|
+
* another reader (E7 measured 35-88.5% JIT failure under reader-vs-reader
|
|
36
|
+
* contention). It reads, it counts, it reports.
|
|
37
|
+
*/
|
|
38
|
+
import type { Db } from '../graph/db.js';
|
|
39
|
+
import type { ResolvedConfig } from '../store/config.js';
|
|
40
|
+
import { type IndexFreshness } from '../indexer/freshness.js';
|
|
41
|
+
/** How long a measurement stays authoritative before the next peek refreshes it. */
|
|
42
|
+
export declare const DEFAULT_FRESHNESS_TTL_MS = 30000;
|
|
43
|
+
export interface FreshnessProbe {
|
|
44
|
+
/**
|
|
45
|
+
* Files on disk that the index has never seen, as of the last completed
|
|
46
|
+
* measurement — or `null` when none has completed yet.
|
|
47
|
+
*
|
|
48
|
+
* Never blocks, never throws, never awaits. May schedule a background
|
|
49
|
+
* refresh as a side effect. `null` means *unknown*, which callers must
|
|
50
|
+
* render as silence rather than as zero.
|
|
51
|
+
*/
|
|
52
|
+
peekUnindexed(): number | null;
|
|
53
|
+
/**
|
|
54
|
+
* Discard the cached value and re-measure on the next peek.
|
|
55
|
+
*
|
|
56
|
+
* Called after this process reindexes: the previous count is not merely old,
|
|
57
|
+
* it is wrong in the direction that produces a false warning about files
|
|
58
|
+
* that were just indexed. A measurement already in flight is *superseded* —
|
|
59
|
+
* it is allowed to finish, and its result is discarded rather than cached,
|
|
60
|
+
* because it was reading the pre-reindex tree.
|
|
61
|
+
*/
|
|
62
|
+
invalidate(): void;
|
|
63
|
+
/** Start a measurement now if none is in flight. Used to prime at startup. */
|
|
64
|
+
refresh(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Resolves when the in-flight measurement settles — success or failure.
|
|
67
|
+
*
|
|
68
|
+
* Settling is not the same as caching a value: a measurement superseded by
|
|
69
|
+
* `invalidate()` settles with its result discarded, leaving `peekUnindexed`
|
|
70
|
+
* at `null` until the next peek starts a clean one.
|
|
71
|
+
*/
|
|
72
|
+
settled(): Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
export interface FreshnessProbeOptions {
|
|
75
|
+
readonly ttlMs?: number;
|
|
76
|
+
/** Injected for tests (§4.4); production reads the real clock. */
|
|
77
|
+
readonly now?: () => number;
|
|
78
|
+
/** Injected for tests (§4.4); production walks the project. */
|
|
79
|
+
readonly measure?: (config: ResolvedConfig, db: Db) => Promise<IndexFreshness>;
|
|
80
|
+
}
|
|
81
|
+
export declare function createFreshnessProbe(config: ResolvedConfig, db: Db, options?: FreshnessProbeOptions): FreshnessProbe;
|
|
82
|
+
//# sourceMappingURL=freshness-probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"freshness-probe.d.ts","sourceRoot":"","sources":["../../src/mcp/freshness-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAEH,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,gBAAgB,CAAC;AACzC,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,EAAoB,KAAK,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAEhF,oFAAoF;AACpF,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAE/C,MAAM,WAAW,cAAc;IAC7B;;;;;;;OAOG;IACH,aAAa,IAAI,MAAM,GAAG,IAAI,CAAC;IAE/B;;;;;;;;OAQG;IACH,UAAU,IAAI,IAAI,CAAC;IAEnB,8EAA8E;IAC9E,OAAO,IAAI,IAAI,CAAC;IAEhB;;;;;;OAMG;IACH,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC1B;AAED,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,kEAAkE;IAClE,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IAC5B,+DAA+D;IAC/D,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,EAAE,EAAE,EAAE,KAAK,OAAO,CAAC,cAAc,CAAC,CAAC;CAChF;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,cAAc,EACtB,EAAE,EAAE,EAAE,EACN,OAAO,GAAE,qBAA0B,GAClC,cAAc,CAuEhB"}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A cached, non-blocking answer to "is the index missing files?".
|
|
3
|
+
*
|
|
4
|
+
* `mast_search` needs this to warn a caller that a zero-hit result may mean
|
|
5
|
+
* "not indexed" rather than "not present" — the severity-zero failure this
|
|
6
|
+
* package is built around. What it cannot do is pay for it *at scale*, and the
|
|
7
|
+
* cost is worth stating with its corpus attached, because it inverts:
|
|
8
|
+
*
|
|
9
|
+
* - This repository, 213 files walked (medians, measured 2026-09-03):
|
|
10
|
+
* `fusedSearch` 15.3 ms, `measureFreshness` 7.6 ms. The walk is CHEAPER
|
|
11
|
+
* than the search. On a tree this size the probe buys nothing.
|
|
12
|
+
* - A 14k-file monorepo: `measureFreshness` ~183 ms (measured 2026-09-02).
|
|
13
|
+
*
|
|
14
|
+
* That is the whole argument, and it is about scaling, not about a ratio.
|
|
15
|
+
* `measureFreshness` is O(files in the tree) and a search is O(matches), so the
|
|
16
|
+
* walk overtakes the query as the tree grows and then keeps going. The probe
|
|
17
|
+
* exists for the trees where that has already happened.
|
|
18
|
+
*
|
|
19
|
+
* So the probe inverts it. `peekUnindexed` returns the last-known count
|
|
20
|
+
* synchronously and schedules a refresh behind the caller when the value has
|
|
21
|
+
* aged past the TTL. The first search of a session gets `null` — unknown, no
|
|
22
|
+
* warning — and every later one gets an answer that is at most `ttlMs` old.
|
|
23
|
+
* `server.ts` primes it at startup so that window is usually already closed.
|
|
24
|
+
*
|
|
25
|
+
* The cost is amortised, NOT eliminated, and it is worth being exact about
|
|
26
|
+
* which: `measureFreshness` -> `walkProject` globs asynchronously but then
|
|
27
|
+
* `statSync`s every hit in a synchronous loop, so a refresh occupies the event
|
|
28
|
+
* loop for part of its duration. A search that lands mid-refresh can be
|
|
29
|
+
* delayed by that much. One such window per `ttlMs`, rather than one walk per
|
|
30
|
+
* search, is the whole of the improvement — do not read "background" as
|
|
31
|
+
* "free".
|
|
32
|
+
*
|
|
33
|
+
* Deliberately NOT a reindex: no `structure.lock` is taken and nothing is
|
|
34
|
+
* written, so the probe cannot contend with a concurrent `runIndex` or with
|
|
35
|
+
* another reader (E7 measured 35-88.5% JIT failure under reader-vs-reader
|
|
36
|
+
* contention). It reads, it counts, it reports.
|
|
37
|
+
*/
|
|
38
|
+
import { measureFreshness } from '../indexer/freshness.js';
|
|
39
|
+
/** How long a measurement stays authoritative before the next peek refreshes it. */
|
|
40
|
+
export const DEFAULT_FRESHNESS_TTL_MS = 30_000;
|
|
41
|
+
export function createFreshnessProbe(config, db, options = {}) {
|
|
42
|
+
const ttlMs = options.ttlMs ?? DEFAULT_FRESHNESS_TTL_MS;
|
|
43
|
+
const now = options.now ?? Date.now;
|
|
44
|
+
const measure = options.measure ?? measureFreshness;
|
|
45
|
+
let unindexed = null;
|
|
46
|
+
/** `null` = never measured or invalidated; otherwise the clock at settlement. */
|
|
47
|
+
let settledAt = null;
|
|
48
|
+
let inFlight = null;
|
|
49
|
+
/**
|
|
50
|
+
* Bumped by `invalidate()`. A measurement that started under an older
|
|
51
|
+
* generation was reading a tree this process has since reindexed, so its
|
|
52
|
+
* count is not merely old — it is wrong in the exact direction that warns
|
|
53
|
+
* about files that now ARE indexed. Gating on the generation is what makes
|
|
54
|
+
* `invalidate()` mean "discard", rather than "discard unless a walk happens
|
|
55
|
+
* to be in flight, in which case adopt its stale answer and stamp it fresh
|
|
56
|
+
* for a full TTL".
|
|
57
|
+
*/
|
|
58
|
+
let generation = 0;
|
|
59
|
+
function refresh() {
|
|
60
|
+
// Still coalesced across generations: a superseded walk is left to finish
|
|
61
|
+
// and its result dropped, rather than disowned and replaced immediately.
|
|
62
|
+
// Disowning would let one walk per `invalidate()` run concurrently, and
|
|
63
|
+
// `measureFreshness` holds the event loop in a synchronous `statSync` loop
|
|
64
|
+
// (see the header) — under a watch-driven edit loop that is worse than the
|
|
65
|
+
// laziness. The cache is left `null` (unknown -> silence, never a wrong
|
|
66
|
+
// warning) and the next peek starts a clean measurement.
|
|
67
|
+
if (inFlight !== null)
|
|
68
|
+
return;
|
|
69
|
+
const startedAt = generation;
|
|
70
|
+
inFlight = measure(config, db)
|
|
71
|
+
.then((result) => {
|
|
72
|
+
if (startedAt !== generation)
|
|
73
|
+
return;
|
|
74
|
+
unindexed = result.unindexed;
|
|
75
|
+
})
|
|
76
|
+
.catch(() => {
|
|
77
|
+
// Swallowed by contract: a failed walk (a directory removed mid-walk, a
|
|
78
|
+
// permission error) must not reject into a search handler or replace a
|
|
79
|
+
// good answer with a wrong one. The last value stands and the stamp
|
|
80
|
+
// below backs the retry off by a full TTL rather than re-walking on
|
|
81
|
+
// every peek.
|
|
82
|
+
})
|
|
83
|
+
.finally(() => {
|
|
84
|
+
inFlight = null;
|
|
85
|
+
// A superseded walk must NOT stamp the cache fresh. Leaving `settledAt`
|
|
86
|
+
// null keeps the value expired, so the next peek re-measures instead of
|
|
87
|
+
// serving `null` for a full TTL.
|
|
88
|
+
if (startedAt !== generation)
|
|
89
|
+
return;
|
|
90
|
+
settledAt = now();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
return {
|
|
94
|
+
peekUnindexed() {
|
|
95
|
+
const isExpired = settledAt === null || now() - settledAt >= ttlMs;
|
|
96
|
+
if (isExpired)
|
|
97
|
+
refresh();
|
|
98
|
+
return unindexed;
|
|
99
|
+
},
|
|
100
|
+
invalidate() {
|
|
101
|
+
unindexed = null;
|
|
102
|
+
settledAt = null;
|
|
103
|
+
generation += 1;
|
|
104
|
+
},
|
|
105
|
+
refresh,
|
|
106
|
+
async settled() {
|
|
107
|
+
await inFlight;
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//# sourceMappingURL=freshness-probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"freshness-probe.js","sourceRoot":"","sources":["../../src/mcp/freshness-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoCG;AAIH,OAAO,EAAE,gBAAgB,EAAuB,MAAM,yBAAyB,CAAC;AAEhF,oFAAoF;AACpF,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC;AA6C/C,MAAM,UAAU,oBAAoB,CAClC,MAAsB,EACtB,EAAM,EACN,UAAiC,EAAE;IAEnC,MAAM,KAAK,GAAG,OAAO,CAAC,KAAK,IAAI,wBAAwB,CAAC;IACxD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,IAAI,CAAC,GAAG,CAAC;IACpC,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,gBAAgB,CAAC;IAEpD,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,iFAAiF;IACjF,IAAI,SAAS,GAAkB,IAAI,CAAC;IACpC,IAAI,QAAQ,GAAyB,IAAI,CAAC;IAC1C;;;;;;;;OAQG;IACH,IAAI,UAAU,GAAG,CAAC,CAAC;IAEnB,SAAS,OAAO;QACd,0EAA0E;QAC1E,yEAAyE;QACzE,wEAAwE;QACxE,2EAA2E;QAC3E,2EAA2E;QAC3E,wEAAwE;QACxE,yDAAyD;QACzD,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO;QAC9B,MAAM,SAAS,GAAG,UAAU,CAAC;QAC7B,QAAQ,GAAG,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC;aAC3B,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE;YACf,IAAI,SAAS,KAAK,UAAU;gBAAE,OAAO;YACrC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAC;QAC/B,CAAC,CAAC;aACD,KAAK,CAAC,GAAG,EAAE;YACV,wEAAwE;YACxE,uEAAuE;YACvE,oEAAoE;YACpE,oEAAoE;YACpE,cAAc;QAChB,CAAC,CAAC;aACD,OAAO,CAAC,GAAG,EAAE;YACZ,QAAQ,GAAG,IAAI,CAAC;YAChB,wEAAwE;YACxE,wEAAwE;YACxE,iCAAiC;YACjC,IAAI,SAAS,KAAK,UAAU;gBAAE,OAAO;YACrC,SAAS,GAAG,GAAG,EAAE,CAAC;QACpB,CAAC,CAAC,CAAC;IACP,CAAC;IAED,OAAO;QACL,aAAa;YACX,MAAM,SAAS,GAAG,SAAS,KAAK,IAAI,IAAI,GAAG,EAAE,GAAG,SAAS,IAAI,KAAK,CAAC;YACnE,IAAI,SAAS;gBAAE,OAAO,EAAE,CAAC;YACzB,OAAO,SAAS,CAAC;QACnB,CAAC;QAED,UAAU;YACR,SAAS,GAAG,IAAI,CAAC;YACjB,SAAS,GAAG,IAAI,CAAC;YACjB,UAAU,IAAI,CAAC,CAAC;QAClB,CAAC;QAED,OAAO;QAEP,KAAK,CAAC,OAAO;YACX,MAAM,QAAQ,CAAC;QACjB,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP `instructions` string sent in the `initialize` result (ADR 017 §6).
|
|
3
|
+
* One paragraph: a client may place it in the model's context verbatim, so every
|
|
4
|
+
* sentence has to earn its tokens.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SERVER_INSTRUCTIONS: string;
|
|
7
|
+
//# sourceMappingURL=instructions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../../src/mcp/instructions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,eAAO,MAAM,mBAAmB,QAI2H,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The MCP `instructions` string sent in the `initialize` result (ADR 017 §6).
|
|
3
|
+
* One paragraph: a client may place it in the model's context verbatim, so every
|
|
4
|
+
* sentence has to earn its tokens.
|
|
5
|
+
*/
|
|
6
|
+
export const SERVER_INSTRUCTIONS = 'mast is an AST-level index of this repository\'s TypeScript, JavaScript and Markdown. ' +
|
|
7
|
+
'To find code — a symbol, its callers, a module\'s exports — call mast_search (or mast_signature, mast_callers, mast_exports) before using grep, glob or reading whole files: it returns ranked declarations, not line matches. ' +
|
|
8
|
+
'Use grep for other languages, non-code files, or an exact regex over text. ' +
|
|
9
|
+
'An empty result is not proof of absence: check index_empty and unindexed_files on the response, and call mast_reindex after creating or renaming files.';
|
|
10
|
+
//# sourceMappingURL=instructions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instructions.js","sourceRoot":"","sources":["../../src/mcp/instructions.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAC9B,wFAAwF;IACxF,iOAAiO;IACjO,6EAA6E;IAC7E,yJAAyJ,CAAC"}
|