vault-go 0.18.0 → 0.19.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/README.md +117 -5
- package/dist/capabilities.d.ts +33 -0
- package/dist/capabilities.js +31 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +37 -8
- package/dist/cloud.d.ts +16 -0
- package/dist/cloud.js +22 -12
- package/dist/context-engines.js +14 -4
- package/dist/hook-install.js +1 -2
- package/dist/hook-queue.d.ts +19 -5
- package/dist/hook-queue.js +127 -53
- package/dist/hooks.d.ts +1 -0
- package/dist/hooks.js +34 -14
- package/dist/index.js +5 -1
- package/dist/local-install.js +37 -28
- package/dist/local-service.d.ts +1 -0
- package/dist/local-service.js +2 -2
- package/dist/local-worker.js +3 -1
- package/dist/mcp-result.d.ts +20 -0
- package/dist/mcp-result.js +21 -0
- package/dist/modes.d.ts +17 -0
- package/dist/modes.js +131 -0
- package/dist/parity-tools.d.ts +3 -0
- package/dist/parity-tools.js +82 -0
- package/dist/server.js +21 -32
- package/dist/smart-code.d.ts +80 -0
- package/dist/smart-code.js +453 -0
- package/dist/update-control.d.ts +24 -0
- package/dist/update-control.js +191 -0
- package/dist/update-runner.d.ts +1 -0
- package/dist/update-runner.js +16 -0
- package/dist/updater.d.ts +34 -0
- package/dist/updater.js +404 -0
- package/package.json +19 -2
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { Lang, parse } from '@ast-grep/napi';
|
|
2
|
+
import { readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
|
|
3
|
+
import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
4
|
+
export const SUPPORTED_LANGUAGES = [
|
|
5
|
+
{ language: 'JavaScript', extensions: ['.js', '.jsx', '.mjs', '.cjs'] },
|
|
6
|
+
{ language: 'TypeScript', extensions: ['.ts', '.mts', '.cts'] },
|
|
7
|
+
{ language: 'Tsx', extensions: ['.tsx'] },
|
|
8
|
+
{ language: 'Html', extensions: ['.html', '.htm'] },
|
|
9
|
+
{ language: 'Css', extensions: ['.css'] },
|
|
10
|
+
];
|
|
11
|
+
const LANGUAGE_BY_EXTENSION = new Map([
|
|
12
|
+
['.js', { name: 'JavaScript', parser: Lang.JavaScript }],
|
|
13
|
+
['.jsx', { name: 'JavaScript', parser: Lang.JavaScript }],
|
|
14
|
+
['.mjs', { name: 'JavaScript', parser: Lang.JavaScript }],
|
|
15
|
+
['.cjs', { name: 'JavaScript', parser: Lang.JavaScript }],
|
|
16
|
+
['.ts', { name: 'TypeScript', parser: Lang.TypeScript }],
|
|
17
|
+
['.mts', { name: 'TypeScript', parser: Lang.TypeScript }],
|
|
18
|
+
['.cts', { name: 'TypeScript', parser: Lang.TypeScript }],
|
|
19
|
+
['.tsx', { name: 'Tsx', parser: Lang.Tsx }],
|
|
20
|
+
['.html', { name: 'Html', parser: Lang.Html }],
|
|
21
|
+
['.htm', { name: 'Html', parser: Lang.Html }],
|
|
22
|
+
['.css', { name: 'Css', parser: Lang.Css }],
|
|
23
|
+
]);
|
|
24
|
+
export const SMART_CODE_LIMITS = Object.freeze({
|
|
25
|
+
maxFileBytes: 1_048_576,
|
|
26
|
+
maxOutlineSymbols: 1_000,
|
|
27
|
+
maxSearchFiles: 500,
|
|
28
|
+
maxSearchBytes: 8_000_000,
|
|
29
|
+
maxSearchResults: 100,
|
|
30
|
+
maxDirectoryEntries: 10_000,
|
|
31
|
+
maxTraversalDepth: 20,
|
|
32
|
+
maxMatchTextChars: 4_000,
|
|
33
|
+
maxSearchOutputChars: 200_000,
|
|
34
|
+
maxUnfoldChars: 100_000,
|
|
35
|
+
maxSymbolNameChars: 512,
|
|
36
|
+
maxSignatureChars: 1_000,
|
|
37
|
+
});
|
|
38
|
+
const DENIED_DIRECTORIES = new Set(['.git', 'node_modules', 'build', 'dist', 'coverage', '.next', 'target', 'out']);
|
|
39
|
+
const TRUNCATION_SUFFIX = '\n… [truncated]';
|
|
40
|
+
function languageFor(filePath) {
|
|
41
|
+
const language = LANGUAGE_BY_EXTENSION.get(extname(filePath).toLowerCase());
|
|
42
|
+
if (!language) {
|
|
43
|
+
const extensions = [...LANGUAGE_BY_EXTENSION.keys()].join(', ');
|
|
44
|
+
throw new Error(`Unsupported language for "${filePath}". Supported extensions: ${extensions}`);
|
|
45
|
+
}
|
|
46
|
+
return language;
|
|
47
|
+
}
|
|
48
|
+
function isWithin(root, candidate) {
|
|
49
|
+
const pathFromRoot = relative(root, candidate);
|
|
50
|
+
return pathFromRoot === '' || (!pathFromRoot.startsWith(`..${sep}`) && pathFromRoot !== '..' && !isAbsolute(pathFromRoot));
|
|
51
|
+
}
|
|
52
|
+
function relativeOutputPath(root, candidate) {
|
|
53
|
+
const path = relative(root, candidate).split(sep).join('/');
|
|
54
|
+
return path || '.';
|
|
55
|
+
}
|
|
56
|
+
function isSensitiveFile(name) {
|
|
57
|
+
const lower = name.toLowerCase();
|
|
58
|
+
return lower === '.env'
|
|
59
|
+
|| lower.startsWith('.env.')
|
|
60
|
+
|| ['id_rsa', 'id_dsa', 'id_ecdsa', 'id_ed25519', 'credentials.json', 'secrets.json'].includes(lower)
|
|
61
|
+
|| ['.pem', '.key', '.p12', '.pfx', '.kdbx'].some((extension) => lower.endsWith(extension));
|
|
62
|
+
}
|
|
63
|
+
function deniedReason(root, candidate) {
|
|
64
|
+
if (!isWithin(root, candidate))
|
|
65
|
+
return 'escapes workspace';
|
|
66
|
+
const parts = relative(root, candidate).split(sep).filter(Boolean);
|
|
67
|
+
if (parts.some((part) => DENIED_DIRECTORIES.has(part.toLowerCase())))
|
|
68
|
+
return 'is in a denied directory';
|
|
69
|
+
const name = parts.at(-1);
|
|
70
|
+
if (name && isSensitiveFile(name))
|
|
71
|
+
return 'is a sensitive file';
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
function assertAllowedPath(root, candidate, displayPath) {
|
|
75
|
+
const reason = deniedReason(root, candidate);
|
|
76
|
+
if (!reason)
|
|
77
|
+
return;
|
|
78
|
+
if (reason === 'escapes workspace')
|
|
79
|
+
throw new Error(`Path escapes workspace: ${displayPath}`);
|
|
80
|
+
if (reason === 'is a sensitive file')
|
|
81
|
+
throw new Error(`Path is a sensitive file: ${displayPath}`);
|
|
82
|
+
throw new Error(`Path is denied: ${displayPath}`);
|
|
83
|
+
}
|
|
84
|
+
function resolveFile(filePath, workspace) {
|
|
85
|
+
const root = realpathSync(workspace);
|
|
86
|
+
if (!statSync(root).isDirectory())
|
|
87
|
+
throw new Error(`Workspace is not a directory: ${workspace}`);
|
|
88
|
+
const requested = isAbsolute(filePath) ? filePath : resolve(root, filePath);
|
|
89
|
+
assertAllowedPath(root, requested, filePath);
|
|
90
|
+
const file = realpathSync(requested);
|
|
91
|
+
assertAllowedPath(root, file, filePath);
|
|
92
|
+
const stats = statSync(file);
|
|
93
|
+
if (!stats.isFile())
|
|
94
|
+
throw new Error(`Path is not a file: ${filePath}`);
|
|
95
|
+
if (stats.size > SMART_CODE_LIMITS.maxFileBytes) {
|
|
96
|
+
throw new Error(`File exceeds ${SMART_CODE_LIMITS.maxFileBytes} byte limit: ${filePath}`);
|
|
97
|
+
}
|
|
98
|
+
return { file, root, relativePath: relativeOutputPath(root, file) };
|
|
99
|
+
}
|
|
100
|
+
function sourceLocation(node) {
|
|
101
|
+
const range = node.range();
|
|
102
|
+
return {
|
|
103
|
+
startLine: range.start.line + 1,
|
|
104
|
+
startColumn: range.start.column + 1,
|
|
105
|
+
endLine: range.end.line + 1,
|
|
106
|
+
endColumn: range.end.column + 1,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const IDENTIFIER_KINDS = new Set(['identifier', 'type_identifier', 'property_identifier', 'private_property_identifier']);
|
|
110
|
+
function directIdentifier(node) {
|
|
111
|
+
const identifier = node.namedChildren().find((child) => IDENTIFIER_KINDS.has(String(child.kind())));
|
|
112
|
+
return identifier?.text() ?? null;
|
|
113
|
+
}
|
|
114
|
+
function symbolDescriptor(node) {
|
|
115
|
+
const syntaxKind = String(node.kind());
|
|
116
|
+
const fixedKinds = {
|
|
117
|
+
class_declaration: 'class',
|
|
118
|
+
abstract_class_declaration: 'class',
|
|
119
|
+
function_declaration: 'function',
|
|
120
|
+
generator_function_declaration: 'function',
|
|
121
|
+
method_definition: 'method',
|
|
122
|
+
method_signature: 'method',
|
|
123
|
+
interface_declaration: 'interface',
|
|
124
|
+
type_alias_declaration: 'type',
|
|
125
|
+
enum_declaration: 'enum',
|
|
126
|
+
internal_module: 'namespace',
|
|
127
|
+
};
|
|
128
|
+
const fixedKind = fixedKinds[syntaxKind];
|
|
129
|
+
if (fixedKind) {
|
|
130
|
+
const name = directIdentifier(node);
|
|
131
|
+
return name ? { name, kind: fixedKind } : null;
|
|
132
|
+
}
|
|
133
|
+
if (syntaxKind === 'variable_declarator' || syntaxKind === 'public_field_definition') {
|
|
134
|
+
const children = node.namedChildren();
|
|
135
|
+
const value = children.at(-1);
|
|
136
|
+
if (!value || !['arrow_function', 'function_expression', 'generator_function'].includes(String(value.kind())))
|
|
137
|
+
return null;
|
|
138
|
+
const name = directIdentifier(node);
|
|
139
|
+
return name ? { name, kind: syntaxKind === 'variable_declarator' ? 'function' : 'field' } : null;
|
|
140
|
+
}
|
|
141
|
+
if (syntaxKind === 'element') {
|
|
142
|
+
const startTag = node.namedChildren().find((child) => String(child.kind()) === 'start_tag');
|
|
143
|
+
const tagName = startTag?.namedChildren().find((child) => String(child.kind()) === 'tag_name');
|
|
144
|
+
return tagName ? { name: tagName.text(), kind: 'element' } : null;
|
|
145
|
+
}
|
|
146
|
+
if (syntaxKind === 'rule_set') {
|
|
147
|
+
const selectors = node.namedChildren().find((child) => String(child.kind()) === 'selectors');
|
|
148
|
+
return selectors ? { name: selectors.text(), kind: 'rule' } : null;
|
|
149
|
+
}
|
|
150
|
+
if (syntaxKind === 'media_statement') {
|
|
151
|
+
const query = node.namedChildren().find((child) => String(child.kind()) === 'feature_query');
|
|
152
|
+
return { name: query ? `@media ${query.text()}` : '@media', kind: 'media' };
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const DIRECT_BODY_KINDS = new Set(['statement_block', 'class_body', 'interface_body', 'enum_body', 'block']);
|
|
157
|
+
function functionValueBody(node) {
|
|
158
|
+
if (!['variable_declarator', 'public_field_definition'].includes(String(node.kind())))
|
|
159
|
+
return null;
|
|
160
|
+
const value = node.namedChildren().at(-1);
|
|
161
|
+
if (!value || !['arrow_function', 'function_expression', 'generator_function'].includes(String(value.kind())))
|
|
162
|
+
return null;
|
|
163
|
+
const candidate = value.namedChildren().at(-1);
|
|
164
|
+
if (!candidate || ['formal_parameters', 'type_annotation', 'type_parameters', 'identifier'].includes(String(candidate.kind())))
|
|
165
|
+
return null;
|
|
166
|
+
return candidate;
|
|
167
|
+
}
|
|
168
|
+
function signatureFor(node, descriptor) {
|
|
169
|
+
if (String(node.kind()) === 'element') {
|
|
170
|
+
return node.namedChildren().find((child) => String(child.kind()) === 'start_tag')?.text() ?? descriptor.name;
|
|
171
|
+
}
|
|
172
|
+
const body = node.namedChildren().find((child) => DIRECT_BODY_KINDS.has(String(child.kind()))) ?? functionValueBody(node);
|
|
173
|
+
if (!body)
|
|
174
|
+
return node.text().trim();
|
|
175
|
+
const nodeText = node.text();
|
|
176
|
+
const bodyText = body.text();
|
|
177
|
+
const bodyOffset = nodeText.lastIndexOf(bodyText);
|
|
178
|
+
if (bodyOffset < 0)
|
|
179
|
+
return nodeText.trim();
|
|
180
|
+
const trailingText = nodeText.slice(bodyOffset + bodyText.length).trim();
|
|
181
|
+
if (trailingText !== '' && trailingText !== ';')
|
|
182
|
+
return nodeText.trim();
|
|
183
|
+
return nodeText.slice(0, bodyOffset).trimEnd();
|
|
184
|
+
}
|
|
185
|
+
function extractSymbols(root) {
|
|
186
|
+
let count = 0;
|
|
187
|
+
let truncated = false;
|
|
188
|
+
const visit = (node) => {
|
|
189
|
+
const descriptor = symbolDescriptor(node);
|
|
190
|
+
if (descriptor) {
|
|
191
|
+
if (count >= SMART_CODE_LIMITS.maxOutlineSymbols) {
|
|
192
|
+
truncated = true;
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
count += 1;
|
|
196
|
+
const children = node.namedChildren().flatMap(visit);
|
|
197
|
+
let name = descriptor.name;
|
|
198
|
+
if (name.length > SMART_CODE_LIMITS.maxSymbolNameChars) {
|
|
199
|
+
name = `${name.slice(0, SMART_CODE_LIMITS.maxSymbolNameChars - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}`;
|
|
200
|
+
truncated = true;
|
|
201
|
+
}
|
|
202
|
+
const signature = truncateText(signatureFor(node, descriptor), SMART_CODE_LIMITS.maxSignatureChars);
|
|
203
|
+
if (signature.truncated)
|
|
204
|
+
truncated = true;
|
|
205
|
+
return [{
|
|
206
|
+
...descriptor,
|
|
207
|
+
name,
|
|
208
|
+
syntaxKind: String(node.kind()),
|
|
209
|
+
signature: signature.text,
|
|
210
|
+
location: sourceLocation(node),
|
|
211
|
+
children,
|
|
212
|
+
}];
|
|
213
|
+
}
|
|
214
|
+
return node.namedChildren().flatMap(visit);
|
|
215
|
+
};
|
|
216
|
+
return { symbols: visit(root), truncated };
|
|
217
|
+
}
|
|
218
|
+
export function smartOutline(filePath, workspace = process.cwd()) {
|
|
219
|
+
const resolved = resolveFile(filePath, workspace);
|
|
220
|
+
const language = languageFor(resolved.file);
|
|
221
|
+
const source = readFileSync(resolved.file, 'utf8');
|
|
222
|
+
const outline = extractSymbols(parse(language.parser, source).root());
|
|
223
|
+
return {
|
|
224
|
+
path: resolved.relativePath,
|
|
225
|
+
language: language.name,
|
|
226
|
+
symbols: outline.symbols,
|
|
227
|
+
truncated: outline.truncated,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
function findSymbol(root, symbolName) {
|
|
231
|
+
const descriptor = symbolDescriptor(root);
|
|
232
|
+
if (descriptor?.name === symbolName)
|
|
233
|
+
return { node: root, descriptor };
|
|
234
|
+
for (const child of root.namedChildren()) {
|
|
235
|
+
const found = findSymbol(child, symbolName);
|
|
236
|
+
if (found)
|
|
237
|
+
return found;
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
function truncateText(text, maximum) {
|
|
242
|
+
if (text.length <= maximum)
|
|
243
|
+
return { text, truncated: false };
|
|
244
|
+
if (maximum <= TRUNCATION_SUFFIX.length) {
|
|
245
|
+
return { text: TRUNCATION_SUFFIX.slice(0, Math.max(0, maximum)), truncated: true };
|
|
246
|
+
}
|
|
247
|
+
return {
|
|
248
|
+
text: `${text.slice(0, maximum - TRUNCATION_SUFFIX.length)}${TRUNCATION_SUFFIX}`,
|
|
249
|
+
truncated: true,
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
export function smartUnfold(filePath, symbolName, workspace = process.cwd()) {
|
|
253
|
+
if (!symbolName || symbolName.length > SMART_CODE_LIMITS.maxSymbolNameChars) {
|
|
254
|
+
throw new Error(`Symbol name must contain 1-${SMART_CODE_LIMITS.maxSymbolNameChars} characters`);
|
|
255
|
+
}
|
|
256
|
+
const resolved = resolveFile(filePath, workspace);
|
|
257
|
+
const language = languageFor(resolved.file);
|
|
258
|
+
const source = readFileSync(resolved.file, 'utf8');
|
|
259
|
+
const found = findSymbol(parse(language.parser, source).root(), symbolName);
|
|
260
|
+
if (!found) {
|
|
261
|
+
return { path: resolved.relativePath, language: language.name, symbol: null, truncated: false };
|
|
262
|
+
}
|
|
263
|
+
const text = truncateText(found.node.text(), SMART_CODE_LIMITS.maxUnfoldChars);
|
|
264
|
+
return {
|
|
265
|
+
path: resolved.relativePath,
|
|
266
|
+
language: language.name,
|
|
267
|
+
symbol: {
|
|
268
|
+
...found.descriptor,
|
|
269
|
+
syntaxKind: String(found.node.kind()),
|
|
270
|
+
location: sourceLocation(found.node),
|
|
271
|
+
text: text.text,
|
|
272
|
+
},
|
|
273
|
+
truncated: text.truncated,
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function globExpression(pattern) {
|
|
277
|
+
if (!pattern || pattern.length > 256 || pattern.includes('\0'))
|
|
278
|
+
throw new Error('File pattern must contain 1-256 safe characters');
|
|
279
|
+
let expression = '^';
|
|
280
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
281
|
+
const character = pattern[index];
|
|
282
|
+
if (character === '*') {
|
|
283
|
+
if (pattern[index + 1] === '*') {
|
|
284
|
+
index += 1;
|
|
285
|
+
if (pattern[index + 1] === '/') {
|
|
286
|
+
index += 1;
|
|
287
|
+
expression += '(?:.*/)?';
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
expression += '.*';
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
expression += '[^/]*';
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
else if (character === '?') {
|
|
298
|
+
expression += '[^/]';
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
expression += /[\\^$.*+?()[\]{}|]/.test(character) ? `\\${character}` : character;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return new RegExp(`${expression}$`);
|
|
305
|
+
}
|
|
306
|
+
function discoverFiles(searchPath, workspace, filePattern) {
|
|
307
|
+
const root = realpathSync(workspace);
|
|
308
|
+
if (!statSync(root).isDirectory())
|
|
309
|
+
throw new Error(`Workspace is not a directory: ${workspace}`);
|
|
310
|
+
const requested = isAbsolute(searchPath) ? searchPath : resolve(root, searchPath);
|
|
311
|
+
assertAllowedPath(root, requested, searchPath);
|
|
312
|
+
const target = realpathSync(requested);
|
|
313
|
+
assertAllowedPath(root, target, searchPath);
|
|
314
|
+
const targetStats = statSync(target);
|
|
315
|
+
const pattern = filePattern ? globExpression(filePattern) : null;
|
|
316
|
+
const patternHasDirectory = filePattern?.includes('/') ?? false;
|
|
317
|
+
const files = [];
|
|
318
|
+
let entryCount = 0;
|
|
319
|
+
let sourceBytes = 0;
|
|
320
|
+
let truncated = false;
|
|
321
|
+
const visitedDirectories = new Set();
|
|
322
|
+
const accepts = (relativePath) => {
|
|
323
|
+
if (!pattern)
|
|
324
|
+
return true;
|
|
325
|
+
return pattern.test(patternHasDirectory ? relativePath : basename(relativePath));
|
|
326
|
+
};
|
|
327
|
+
const addFile = (file, outputPath) => {
|
|
328
|
+
if (!LANGUAGE_BY_EXTENSION.has(extname(file).toLowerCase()) || !accepts(outputPath))
|
|
329
|
+
return;
|
|
330
|
+
const stats = statSync(file);
|
|
331
|
+
if (stats.size > SMART_CODE_LIMITS.maxFileBytes) {
|
|
332
|
+
truncated = true;
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
if (files.length >= SMART_CODE_LIMITS.maxSearchFiles) {
|
|
336
|
+
truncated = true;
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (sourceBytes + stats.size > SMART_CODE_LIMITS.maxSearchBytes) {
|
|
340
|
+
truncated = true;
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
files.push({ file, relativePath: outputPath });
|
|
344
|
+
sourceBytes += stats.size;
|
|
345
|
+
};
|
|
346
|
+
const walk = (directory, depth) => {
|
|
347
|
+
if (depth > SMART_CODE_LIMITS.maxTraversalDepth) {
|
|
348
|
+
truncated = true;
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const realDirectory = realpathSync(directory);
|
|
352
|
+
if (visitedDirectories.has(realDirectory))
|
|
353
|
+
return;
|
|
354
|
+
visitedDirectories.add(realDirectory);
|
|
355
|
+
const entries = readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
entryCount += 1;
|
|
358
|
+
if (entryCount > SMART_CODE_LIMITS.maxDirectoryEntries) {
|
|
359
|
+
truncated = true;
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
if (DENIED_DIRECTORIES.has(entry.name.toLowerCase()) || isSensitiveFile(entry.name))
|
|
363
|
+
continue;
|
|
364
|
+
const lexicalPath = resolve(directory, entry.name);
|
|
365
|
+
let realPath;
|
|
366
|
+
try {
|
|
367
|
+
realPath = realpathSync(lexicalPath);
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (deniedReason(root, realPath))
|
|
373
|
+
continue;
|
|
374
|
+
const stats = statSync(realPath);
|
|
375
|
+
if (stats.isDirectory())
|
|
376
|
+
walk(realPath, depth + 1);
|
|
377
|
+
else if (stats.isFile())
|
|
378
|
+
addFile(realPath, relativeOutputPath(root, realPath));
|
|
379
|
+
}
|
|
380
|
+
};
|
|
381
|
+
if (targetStats.isFile()) {
|
|
382
|
+
languageFor(target);
|
|
383
|
+
if (targetStats.size > SMART_CODE_LIMITS.maxFileBytes) {
|
|
384
|
+
throw new Error(`File exceeds ${SMART_CODE_LIMITS.maxFileBytes} byte limit: ${searchPath}`);
|
|
385
|
+
}
|
|
386
|
+
const outputPath = relativeOutputPath(root, target);
|
|
387
|
+
if (accepts(outputPath))
|
|
388
|
+
files.push({ file: target, relativePath: outputPath });
|
|
389
|
+
}
|
|
390
|
+
else if (targetStats.isDirectory()) {
|
|
391
|
+
walk(target, 0);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
throw new Error(`Path is not a file or directory: ${searchPath}`);
|
|
395
|
+
}
|
|
396
|
+
return { files, displayPath: relativeOutputPath(root, target), truncated };
|
|
397
|
+
}
|
|
398
|
+
export function smartSearch(query, path = '.', maxResults = 20, filePattern, workspace = process.cwd()) {
|
|
399
|
+
if (!query || query.length > 10_000)
|
|
400
|
+
throw new Error('AST query must contain 1-10000 characters');
|
|
401
|
+
if (!Number.isSafeInteger(maxResults) || maxResults < 1)
|
|
402
|
+
throw new Error('maxResults must be a positive integer');
|
|
403
|
+
const limit = Math.min(maxResults, SMART_CODE_LIMITS.maxSearchResults);
|
|
404
|
+
const discovered = discoverFiles(path, workspace, filePattern);
|
|
405
|
+
const matches = [];
|
|
406
|
+
let outputChars = 0;
|
|
407
|
+
let scannedFiles = 0;
|
|
408
|
+
let truncated = discovered.truncated || maxResults > SMART_CODE_LIMITS.maxSearchResults;
|
|
409
|
+
let acceptedByAnyParser = discovered.files.length === 0;
|
|
410
|
+
for (const [fileIndex, file] of discovered.files.entries()) {
|
|
411
|
+
const language = languageFor(file.file);
|
|
412
|
+
const source = readFileSync(file.file, 'utf8');
|
|
413
|
+
const root = parse(language.parser, source).root();
|
|
414
|
+
scannedFiles += 1;
|
|
415
|
+
let fileMatches;
|
|
416
|
+
try {
|
|
417
|
+
fileMatches = root.findAll(query);
|
|
418
|
+
acceptedByAnyParser = true;
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
for (const [matchIndex, node] of fileMatches.entries()) {
|
|
424
|
+
if (matches.length >= limit || outputChars >= SMART_CODE_LIMITS.maxSearchOutputChars) {
|
|
425
|
+
truncated = true;
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
const perMatch = truncateText(node.text(), SMART_CODE_LIMITS.maxMatchTextChars);
|
|
429
|
+
const remaining = SMART_CODE_LIMITS.maxSearchOutputChars - outputChars;
|
|
430
|
+
const bounded = truncateText(perMatch.text, remaining);
|
|
431
|
+
matches.push({
|
|
432
|
+
path: file.relativePath,
|
|
433
|
+
language: language.name,
|
|
434
|
+
syntaxKind: String(node.kind()),
|
|
435
|
+
location: sourceLocation(node),
|
|
436
|
+
text: bounded.text,
|
|
437
|
+
});
|
|
438
|
+
outputChars += bounded.text.length;
|
|
439
|
+
if (perMatch.truncated || bounded.truncated)
|
|
440
|
+
truncated = true;
|
|
441
|
+
if (matchIndex < fileMatches.length - 1 && matches.length >= limit)
|
|
442
|
+
truncated = true;
|
|
443
|
+
}
|
|
444
|
+
if (matches.length >= limit || outputChars >= SMART_CODE_LIMITS.maxSearchOutputChars) {
|
|
445
|
+
if (fileIndex < discovered.files.length - 1)
|
|
446
|
+
truncated = true;
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
if (!acceptedByAnyParser)
|
|
451
|
+
throw new Error('AST query is invalid for every supported file in the search path');
|
|
452
|
+
return { query, path: discovered.displayPath, matches, scannedFiles, truncated };
|
|
453
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type LocalHealth } from './local-install.js';
|
|
2
|
+
import { type UpdateState } from './updater.js';
|
|
3
|
+
export declare function activatePreparedUpdate(home: string, state: UpdateState, operations?: {
|
|
4
|
+
stop: (home: string) => Promise<boolean>;
|
|
5
|
+
start: (home: string) => Promise<LocalHealth>;
|
|
6
|
+
}, options?: {
|
|
7
|
+
automatic?: boolean;
|
|
8
|
+
}): Promise<LocalHealth>;
|
|
9
|
+
export declare function launchPreparedUpdate(home: string, state: UpdateState, options?: {
|
|
10
|
+
automatic?: boolean;
|
|
11
|
+
}): Promise<void>;
|
|
12
|
+
/** Checks start after boot and run while idle. Active model calls are never interrupted. */
|
|
13
|
+
export interface AutomaticUpdateOperations {
|
|
14
|
+
install?: (home: string) => Promise<UpdateState>;
|
|
15
|
+
launch?: (home: string, state: UpdateState, options?: {
|
|
16
|
+
automatic?: boolean;
|
|
17
|
+
}) => Promise<void>;
|
|
18
|
+
status?: (home: string) => UpdateState;
|
|
19
|
+
queueStatus?: (home: string) => {
|
|
20
|
+
pending: number;
|
|
21
|
+
};
|
|
22
|
+
schedule?: (callback: () => Promise<void>, delay: number, repeat: boolean) => () => void;
|
|
23
|
+
}
|
|
24
|
+
export declare function scheduleAutomaticUpdates(isIdle: () => boolean, home?: string, operations?: AutomaticUpdateOperations): () => void;
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { existsSync, openSync, closeSync, readFileSync, realpathSync, renameSync, rmSync, lstatSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join, relative, isAbsolute } from 'node:path';
|
|
5
|
+
import { stageLocalRuntime, startLocal, stopLocal } from './local-install.js';
|
|
6
|
+
import { installUpdate, updateStatus } from './updater.js';
|
|
7
|
+
import { vaultHome } from './config.js';
|
|
8
|
+
import { generationQueueStatus } from './hook-queue.js';
|
|
9
|
+
function preparedPackage(home, state) {
|
|
10
|
+
if (state.status !== 'ready' || !state.packageRoot || !state.latest)
|
|
11
|
+
throw new Error('No validated update is ready.');
|
|
12
|
+
const root = realpathSync(state.packageRoot), releases = realpathSync(join(home, 'releases'));
|
|
13
|
+
const within = relative(releases, root);
|
|
14
|
+
if (!within || within.startsWith('..') || isAbsolute(within))
|
|
15
|
+
throw new Error('Invalid update location.');
|
|
16
|
+
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
|
|
17
|
+
if (pkg.name !== 'vault-go' || pkg.version !== state.latest)
|
|
18
|
+
throw new Error('Invalid prepared update.');
|
|
19
|
+
return root;
|
|
20
|
+
}
|
|
21
|
+
export async function activatePreparedUpdate(home, state, operations = { stop: stopLocal, start: startLocal }, options = {}) {
|
|
22
|
+
const release = await acquireActivationLock(home);
|
|
23
|
+
try {
|
|
24
|
+
if (options.automatic && !updateStatus(home).enabled)
|
|
25
|
+
throw new Error('Automatic updates are disabled.');
|
|
26
|
+
const source = preparedPackage(home, state);
|
|
27
|
+
const active = runtimeVersion(home);
|
|
28
|
+
if (active === state.latest) {
|
|
29
|
+
const health = await operations.start(home);
|
|
30
|
+
if (health.version !== state.latest)
|
|
31
|
+
throw new Error('Active worker version mismatch.');
|
|
32
|
+
return health;
|
|
33
|
+
}
|
|
34
|
+
if (active && compareVersions(active, state.latest) > 0)
|
|
35
|
+
throw new Error('Refusing to activate an older runtime.');
|
|
36
|
+
return await replaceRuntime(home, state, source, operations);
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
release();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function runtimeVersion(home) {
|
|
43
|
+
try {
|
|
44
|
+
const version = JSON.parse(readFileSync(join(home, 'local-runtime', 'package.json'), 'utf8')).version;
|
|
45
|
+
return typeof version === 'string' && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(version) ? version : undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function compareVersions(a, b) {
|
|
52
|
+
const left = a.split('.').map(Number), right = b.split('.').map(Number);
|
|
53
|
+
for (let index = 0; index < 3; index++) {
|
|
54
|
+
if (left[index] !== right[index])
|
|
55
|
+
return left[index] > right[index] ? 1 : -1;
|
|
56
|
+
}
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
function lockOwner(path) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return {};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function acquireActivationLock(home) {
|
|
68
|
+
const path = join(home, 'activation.lock'), token = randomUUID(), started = Date.now();
|
|
69
|
+
for (;;) {
|
|
70
|
+
let descriptor;
|
|
71
|
+
try {
|
|
72
|
+
descriptor = openSync(path, 'wx', 0o600);
|
|
73
|
+
writeFileSync(descriptor, JSON.stringify({ pid: process.pid, token }));
|
|
74
|
+
closeSync(descriptor);
|
|
75
|
+
return () => { if (lockOwner(path).token === token)
|
|
76
|
+
unlinkSync(path); };
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (descriptor !== undefined)
|
|
80
|
+
closeSync(descriptor);
|
|
81
|
+
if (error.code !== 'EEXIST')
|
|
82
|
+
throw error;
|
|
83
|
+
try {
|
|
84
|
+
const before = lstatSync(path), owner = lockOwner(path);
|
|
85
|
+
let stale = false;
|
|
86
|
+
if (typeof owner.pid === 'number' && Number.isInteger(owner.pid) && owner.pid > 0) {
|
|
87
|
+
try {
|
|
88
|
+
process.kill(owner.pid, 0);
|
|
89
|
+
}
|
|
90
|
+
catch (failure) {
|
|
91
|
+
stale = failure.code === 'ESRCH';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
else
|
|
95
|
+
stale = Date.now() - before.mtimeMs > 600_000;
|
|
96
|
+
if (stale && lstatSync(path).ino === before.ino) {
|
|
97
|
+
unlinkSync(path);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (failure) {
|
|
102
|
+
if (failure.code === 'ENOENT')
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (Date.now() - started >= 30_000)
|
|
106
|
+
throw new Error('Another update activation is already running.');
|
|
107
|
+
await new Promise(resolve => setTimeout(resolve, 25));
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function replaceRuntime(home, state, source, operations) {
|
|
112
|
+
await operations.stop(home);
|
|
113
|
+
const runtime = join(home, 'local-runtime'), previous = join(home, 'local-runtime.previous');
|
|
114
|
+
let staged = false;
|
|
115
|
+
try {
|
|
116
|
+
stageLocalRuntime(home, join(source, 'dist'));
|
|
117
|
+
staged = true;
|
|
118
|
+
const health = await operations.start(home);
|
|
119
|
+
if (health.version !== state.latest)
|
|
120
|
+
throw new Error('New worker version mismatch.');
|
|
121
|
+
return health;
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
if (staged)
|
|
125
|
+
await operations.stop(home);
|
|
126
|
+
// A failed stage normally leaves the current runtime intact. Do not replace it
|
|
127
|
+
// with an unrelated backup from a previous successful update.
|
|
128
|
+
if (!staged && existsSync(runtime)) {
|
|
129
|
+
await operations.start(home);
|
|
130
|
+
throw new Error('Update staging failed; previous runtime restarted.');
|
|
131
|
+
}
|
|
132
|
+
if (existsSync(previous)) {
|
|
133
|
+
rmSync(runtime, { recursive: true, force: true });
|
|
134
|
+
renameSync(previous, runtime);
|
|
135
|
+
await operations.start(home);
|
|
136
|
+
throw new Error('Update failed; previous runtime restored.');
|
|
137
|
+
}
|
|
138
|
+
throw new Error('Update failed to start.');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
export async function launchPreparedUpdate(home, state, options = {}) {
|
|
142
|
+
if (options.automatic && !updateStatus(home).enabled)
|
|
143
|
+
return;
|
|
144
|
+
const root = preparedPackage(home, state);
|
|
145
|
+
const runner = join(root, 'dist', 'update-runner.js');
|
|
146
|
+
if (!existsSync(runner))
|
|
147
|
+
throw new Error('Update activation runner is missing.');
|
|
148
|
+
const log = openSync(join(home, 'update.log'), 'a', 0o600);
|
|
149
|
+
try {
|
|
150
|
+
const child = spawn(process.execPath, [runner, ...(options.automatic ? ['--automatic'] : [])], { detached: true, shell: false, stdio: ['ignore', log, log], env: { ...process.env, VAULT_GO_HOME: home } });
|
|
151
|
+
await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
|
|
152
|
+
child.unref();
|
|
153
|
+
}
|
|
154
|
+
finally {
|
|
155
|
+
closeSync(log);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
export function scheduleAutomaticUpdates(isIdle, home = vaultHome(), operations = {}) {
|
|
159
|
+
const install = operations.install ?? installUpdate;
|
|
160
|
+
const launch = operations.launch ?? launchPreparedUpdate;
|
|
161
|
+
const status = operations.status ?? updateStatus;
|
|
162
|
+
const queueStatus = operations.queueStatus ?? generationQueueStatus;
|
|
163
|
+
const schedule = operations.schedule ?? ((callback, delay, repeat) => {
|
|
164
|
+
const timer = repeat ? setInterval(() => void callback(), delay) : setTimeout(() => void callback(), delay);
|
|
165
|
+
timer.unref();
|
|
166
|
+
return () => { if (repeat)
|
|
167
|
+
clearInterval(timer);
|
|
168
|
+
else
|
|
169
|
+
clearTimeout(timer); };
|
|
170
|
+
});
|
|
171
|
+
let busy = false, closed = false;
|
|
172
|
+
const tick = async () => {
|
|
173
|
+
if (closed || busy)
|
|
174
|
+
return;
|
|
175
|
+
busy = true;
|
|
176
|
+
try {
|
|
177
|
+
if (!isIdle() || queueStatus(home).pending > 0 || !status(home).enabled)
|
|
178
|
+
return;
|
|
179
|
+
const state = await install(home);
|
|
180
|
+
if (!closed && isIdle() && queueStatus(home).pending === 0 && status(home).enabled && state.status === 'ready' && state.restartRequired)
|
|
181
|
+
await launch(home, state, { automatic: true });
|
|
182
|
+
}
|
|
183
|
+
catch { /* sanitized status is available through the updater; retry on the next tick */ }
|
|
184
|
+
finally {
|
|
185
|
+
busy = false;
|
|
186
|
+
}
|
|
187
|
+
};
|
|
188
|
+
const cancelBoot = schedule(tick, 60_000, false);
|
|
189
|
+
const cancelInterval = schedule(tick, 60 * 60 * 1000, true);
|
|
190
|
+
return () => { closed = true; cancelBoot(); cancelInterval(); };
|
|
191
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { activatePreparedUpdate } from './update-control.js';
|
|
2
|
+
import { updateStatus } from './updater.js';
|
|
3
|
+
import { vaultHome } from './config.js';
|
|
4
|
+
try {
|
|
5
|
+
const home = vaultHome();
|
|
6
|
+
const automatic = process.argv.includes('--automatic');
|
|
7
|
+
const state = updateStatus(home);
|
|
8
|
+
if (!automatic || state.enabled) {
|
|
9
|
+
const health = await activatePreparedUpdate(home, state, undefined, { automatic });
|
|
10
|
+
process.stderr.write(`Vault Go updated to ${health.version}. Reconnect MCP clients to load the new tools.\n`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
process.stderr.write('Vault Go update activation failed. The previous runtime was retained where possible.\n');
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
}
|