codelocal 1.5.0-beta.1
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 +27 -0
- package/dist/approval-memory.js +105 -0
- package/dist/audit.js +34 -0
- package/dist/chat-approval.js +77 -0
- package/dist/cli-saas.js +311 -0
- package/dist/cli.js +344 -0
- package/dist/client-entry-v2.js +22 -0
- package/dist/client-v2.js +910 -0
- package/dist/cloud-client-sync.js +6 -0
- package/dist/context-engine.js +295 -0
- package/dist/editing-engine.js +205 -0
- package/dist/identity.js +30 -0
- package/dist/log.js +235 -0
- package/dist/lsp.js +288 -0
- package/dist/mcp-cloud-sync.js +3 -0
- package/dist/mcp-hub.js +508 -0
- package/dist/native-watcher.js +148 -0
- package/dist/process-manager.js +261 -0
- package/dist/protocol.js +52 -0
- package/dist/runtime-daemon.js +162 -0
- package/dist/security-policy.js +293 -0
- package/dist/semantic-router.js +378 -0
- package/dist/semantic.js +263 -0
- package/dist/state.js +110 -0
- package/dist/terminal-history.js +102 -0
- package/dist/verification.js +66 -0
- package/dist/workspace-index.js +457 -0
- package/dist/workspace-registry.js +86 -0
- package/package.json +31 -0
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import { isSensitivePath } from "./security-policy.js";
|
|
6
|
+
const SKIP_DIRS = new Set([
|
|
7
|
+
".git", "node_modules", ".next", "dist", "build", "target", ".venv", "venv", "coverage",
|
|
8
|
+
".cache", ".turbo", ".dart_tool", ".idea", ".gradle", "Pods", "DerivedData",
|
|
9
|
+
]);
|
|
10
|
+
const MANIFEST_RE = /(^|\/)(package\.json|pyproject\.toml|requirements[^/]*\.txt|Cargo\.toml|go\.mod|pom\.xml|build\.gradle(?:\.kts)?|[^/]+\.csproj|CMakeLists\.txt|composer\.json|pubspec\.yaml|Package\.swift|Podfile)$/i;
|
|
11
|
+
const CONFIG_RE = /(^|\/)(tsconfig[^/]*\.json|jsconfig[^/]*\.json|analysis_options\.yaml|\.eslintrc(?:\.[^/]+)?|eslint\.config\.[^/]+|vite\.config\.[^/]+|next\.config\.[^/]+)$/i;
|
|
12
|
+
const TEST_RE = /(^|\/)(__tests__|test|tests|spec|specs)(\/|$)|\.(test|spec)\.[^.]+$/i;
|
|
13
|
+
const LANGUAGE_BY_EXT = {
|
|
14
|
+
".ts": "typescript", ".tsx": "typescript", ".mts": "typescript", ".cts": "typescript",
|
|
15
|
+
".js": "javascript", ".jsx": "javascript", ".mjs": "javascript", ".cjs": "javascript",
|
|
16
|
+
".py": "python", ".rs": "rust", ".go": "go", ".c": "c", ".h": "c", ".cc": "cpp", ".cpp": "cpp", ".cxx": "cpp", ".hpp": "cpp",
|
|
17
|
+
".java": "java", ".kt": "kotlin", ".kts": "kotlin", ".cs": "csharp", ".php": "php", ".rb": "ruby", ".lua": "lua",
|
|
18
|
+
".swift": "swift", ".dart": "dart", ".ex": "elixir", ".exs": "elixir", ".zig": "zig", ".sol": "solidity",
|
|
19
|
+
};
|
|
20
|
+
function normalizeRelative(value) {
|
|
21
|
+
return value.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
22
|
+
}
|
|
23
|
+
function tokenize(value) {
|
|
24
|
+
return [...new Set(value.toLowerCase().split(/[^a-z0-9_$.-]+/).flatMap((part) => part.split(/[._/-]+/)).filter((part) => part.length >= 2))];
|
|
25
|
+
}
|
|
26
|
+
function languageFor(file) {
|
|
27
|
+
return LANGUAGE_BY_EXT[path.extname(file).toLowerCase()] ?? null;
|
|
28
|
+
}
|
|
29
|
+
function kindFor(file, language) {
|
|
30
|
+
if (MANIFEST_RE.test(file))
|
|
31
|
+
return "manifest";
|
|
32
|
+
if (CONFIG_RE.test(file))
|
|
33
|
+
return "config";
|
|
34
|
+
if (TEST_RE.test(file))
|
|
35
|
+
return "test";
|
|
36
|
+
return language ? "source" : "other";
|
|
37
|
+
}
|
|
38
|
+
function parseImports(text, language) {
|
|
39
|
+
const out = new Set();
|
|
40
|
+
const patterns = [];
|
|
41
|
+
if (language === "typescript" || language === "javascript") {
|
|
42
|
+
patterns.push(/\b(?:import|export)\b[^"'`]*?\bfrom\s*["'`]([^"'`]+)["'`]/g, /\brequire\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g, /\bimport\s*\(\s*["'`]([^"'`]+)["'`]\s*\)/g);
|
|
43
|
+
}
|
|
44
|
+
else if (language === "dart") {
|
|
45
|
+
patterns.push(/\b(?:import|export|part)\s+["']([^"']+)["']/g);
|
|
46
|
+
}
|
|
47
|
+
else if (language === "python") {
|
|
48
|
+
patterns.push(/^\s*from\s+([A-Za-z0-9_.]+)\s+import\b/gm, /^\s*import\s+([A-Za-z0-9_.]+)/gm);
|
|
49
|
+
}
|
|
50
|
+
else if (language === "rust") {
|
|
51
|
+
patterns.push(/\buse\s+([A-Za-z0-9_:]+)/g, /\bmod\s+([A-Za-z0-9_]+)/g);
|
|
52
|
+
}
|
|
53
|
+
else if (language === "go") {
|
|
54
|
+
patterns.push(/\bimport\s+(?:\([^)]*?["`]([^"`]+)["`]|["`]([^"`]+)["`])/gs);
|
|
55
|
+
}
|
|
56
|
+
else if (language === "swift") {
|
|
57
|
+
patterns.push(/^\s*import\s+([A-Za-z0-9_.]+)/gm);
|
|
58
|
+
}
|
|
59
|
+
for (const pattern of patterns) {
|
|
60
|
+
pattern.lastIndex = 0;
|
|
61
|
+
let match;
|
|
62
|
+
while ((match = pattern.exec(text))) {
|
|
63
|
+
const value = match.slice(1).find(Boolean);
|
|
64
|
+
if (value)
|
|
65
|
+
out.add(value);
|
|
66
|
+
if (out.size >= 200)
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return [...out];
|
|
71
|
+
}
|
|
72
|
+
function parseSymbols(text, language) {
|
|
73
|
+
const out = new Set();
|
|
74
|
+
const patterns = [
|
|
75
|
+
/\b(?:class|interface|enum|type|struct|trait|mixin|extension|module)\s+([A-Za-z_$][\w$]*)/g,
|
|
76
|
+
/\b(?:function|def|fn|func)\s+([A-Za-z_$][\w$]*)/g,
|
|
77
|
+
/\b(?:const|let|var|final)\s+([A-Za-z_$][\w$]*)\s*(?=[=:])/g,
|
|
78
|
+
];
|
|
79
|
+
if (language === "dart")
|
|
80
|
+
patterns.push(/\b(?:Future<[^>]+>|Future|Widget|void|int|double|String|bool|dynamic|[A-Z][A-Za-z0-9_<>, ?]*)\s+([a-zA-Z_$][\w$]*)\s*\(/g);
|
|
81
|
+
for (const pattern of patterns) {
|
|
82
|
+
pattern.lastIndex = 0;
|
|
83
|
+
let match;
|
|
84
|
+
while ((match = pattern.exec(text))) {
|
|
85
|
+
if (match[1])
|
|
86
|
+
out.add(match[1]);
|
|
87
|
+
if (out.size >= 300)
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return [...out];
|
|
92
|
+
}
|
|
93
|
+
function parsePackageName(file, text) {
|
|
94
|
+
if (file.endsWith("pubspec.yaml")) {
|
|
95
|
+
return /^\s*name\s*:\s*["']?([A-Za-z0-9_-]+)["']?\s*$/m.exec(text)?.[1] ?? null;
|
|
96
|
+
}
|
|
97
|
+
if (file.endsWith("package.json")) {
|
|
98
|
+
try {
|
|
99
|
+
const value = JSON.parse(text)?.name;
|
|
100
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
101
|
+
}
|
|
102
|
+
catch { }
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
function resolveRelativeImport(from, specifier, known) {
|
|
107
|
+
if (!specifier.startsWith("."))
|
|
108
|
+
return null;
|
|
109
|
+
const base = normalizeRelative(path.posix.normalize(path.posix.join(path.posix.dirname(from), specifier)));
|
|
110
|
+
const candidates = [
|
|
111
|
+
base,
|
|
112
|
+
...Object.keys(LANGUAGE_BY_EXT).map((ext) => `${base}${ext}`),
|
|
113
|
+
...Object.keys(LANGUAGE_BY_EXT).map((ext) => `${base}/index${ext}`),
|
|
114
|
+
];
|
|
115
|
+
return candidates.find((candidate) => known.has(candidate)) ?? null;
|
|
116
|
+
}
|
|
117
|
+
export class WorkspaceIntelligenceIndex {
|
|
118
|
+
root;
|
|
119
|
+
maxEntries;
|
|
120
|
+
maxDepth;
|
|
121
|
+
maxReadBytes;
|
|
122
|
+
freshnessMs;
|
|
123
|
+
files = new Map();
|
|
124
|
+
reverseImports = new Map();
|
|
125
|
+
packageRoots = new Map();
|
|
126
|
+
recentChanges = new Map();
|
|
127
|
+
builtAt = 0;
|
|
128
|
+
lastScanAt = 0;
|
|
129
|
+
dirty = true;
|
|
130
|
+
scanPromise = null;
|
|
131
|
+
cacheLoaded = false;
|
|
132
|
+
cacheDir;
|
|
133
|
+
cacheFile;
|
|
134
|
+
constructor(root, maxEntries = Math.max(1000, Number(process.env.CODELOCAL_INDEX_MAX_FILES ?? 12000) || 12000), maxDepth = Math.max(2, Number(process.env.CODELOCAL_INDEX_MAX_DEPTH ?? 8) || 8), maxReadBytes = Math.max(16_384, Number(process.env.CODELOCAL_INDEX_MAX_FILE_BYTES ?? 384 * 1024) || 384 * 1024), freshnessMs = Math.max(500, Number(process.env.CODELOCAL_INDEX_FRESHNESS_MS ?? 1500) || 1500)) {
|
|
135
|
+
this.root = root;
|
|
136
|
+
this.maxEntries = maxEntries;
|
|
137
|
+
this.maxDepth = maxDepth;
|
|
138
|
+
this.maxReadBytes = maxReadBytes;
|
|
139
|
+
this.freshnessMs = freshnessMs;
|
|
140
|
+
this.cacheDir = path.join(os.homedir(), ".codelocal", "indexes");
|
|
141
|
+
const key = createHash("sha256").update(path.resolve(root)).digest("hex").slice(0, 24);
|
|
142
|
+
this.cacheFile = path.join(this.cacheDir, `${key}.json`);
|
|
143
|
+
}
|
|
144
|
+
invalidate(paths) {
|
|
145
|
+
this.dirty = true;
|
|
146
|
+
if (paths?.length)
|
|
147
|
+
for (const file of paths)
|
|
148
|
+
this.noteChange(file);
|
|
149
|
+
}
|
|
150
|
+
noteChange(relativePath) {
|
|
151
|
+
const normalized = normalizeRelative(relativePath);
|
|
152
|
+
if (!normalized || normalized === "." || normalized.startsWith("../"))
|
|
153
|
+
return;
|
|
154
|
+
this.recentChanges.set(normalized, Date.now());
|
|
155
|
+
this.dirty = true;
|
|
156
|
+
}
|
|
157
|
+
async loadCache() {
|
|
158
|
+
if (this.cacheLoaded)
|
|
159
|
+
return;
|
|
160
|
+
this.cacheLoaded = true;
|
|
161
|
+
try {
|
|
162
|
+
const parsed = JSON.parse(await fs.readFile(this.cacheFile, "utf8"));
|
|
163
|
+
if (parsed?.version !== 2 || parsed?.root !== path.resolve(this.root) || !Array.isArray(parsed?.files))
|
|
164
|
+
return;
|
|
165
|
+
const restored = new Map();
|
|
166
|
+
for (const raw of parsed.files) {
|
|
167
|
+
if (!raw || typeof raw.path !== "string" || isSensitivePath(raw.path))
|
|
168
|
+
continue;
|
|
169
|
+
restored.set(raw.path, { ...raw, packageName: typeof raw.packageName === "string" ? raw.packageName : null });
|
|
170
|
+
}
|
|
171
|
+
this.files = restored;
|
|
172
|
+
this.builtAt = Number(parsed.builtAt ?? 0);
|
|
173
|
+
if (Array.isArray(parsed.recentChanges)) {
|
|
174
|
+
for (const item of parsed.recentChanges) {
|
|
175
|
+
if (item && typeof item.file === "string" && Number.isFinite(item.changedAt))
|
|
176
|
+
this.recentChanges.set(item.file, Number(item.changedAt));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
this.rebuildGraphMetadata();
|
|
180
|
+
}
|
|
181
|
+
catch { }
|
|
182
|
+
}
|
|
183
|
+
async persistCache() {
|
|
184
|
+
try {
|
|
185
|
+
await fs.mkdir(this.cacheDir, { recursive: true, mode: 0o700 });
|
|
186
|
+
const payload = JSON.stringify({
|
|
187
|
+
version: 2,
|
|
188
|
+
root: path.resolve(this.root),
|
|
189
|
+
builtAt: this.builtAt,
|
|
190
|
+
files: [...this.files.values()],
|
|
191
|
+
recentChanges: [...this.recentChanges.entries()].map(([file, changedAt]) => ({ file, changedAt })),
|
|
192
|
+
});
|
|
193
|
+
const temp = `${this.cacheFile}.${process.pid}.${randomUUID()}.tmp`;
|
|
194
|
+
await fs.writeFile(temp, payload, { encoding: "utf8", mode: 0o600 });
|
|
195
|
+
await fs.rename(temp, this.cacheFile);
|
|
196
|
+
}
|
|
197
|
+
catch { }
|
|
198
|
+
}
|
|
199
|
+
async discover() {
|
|
200
|
+
const out = [];
|
|
201
|
+
const walk = async (dir, depth) => {
|
|
202
|
+
if (out.length >= this.maxEntries || depth > this.maxDepth)
|
|
203
|
+
return;
|
|
204
|
+
let entries;
|
|
205
|
+
try {
|
|
206
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
for (const entry of entries) {
|
|
212
|
+
if (out.length >= this.maxEntries)
|
|
213
|
+
break;
|
|
214
|
+
if (entry.isSymbolicLink())
|
|
215
|
+
continue;
|
|
216
|
+
if (entry.isDirectory() && SKIP_DIRS.has(entry.name))
|
|
217
|
+
continue;
|
|
218
|
+
const absolute = path.join(dir, entry.name);
|
|
219
|
+
const relative = normalizeRelative(path.relative(this.root, absolute));
|
|
220
|
+
if (!relative || isSensitivePath(relative))
|
|
221
|
+
continue;
|
|
222
|
+
if (entry.isDirectory()) {
|
|
223
|
+
await walk(absolute, depth + 1);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
try {
|
|
227
|
+
const stat = await fs.stat(absolute);
|
|
228
|
+
out.push({ path: relative, size: stat.size, mtimeMs: stat.mtimeMs });
|
|
229
|
+
}
|
|
230
|
+
catch { }
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
await walk(this.root, 0);
|
|
234
|
+
return out;
|
|
235
|
+
}
|
|
236
|
+
async indexOne(meta, previous) {
|
|
237
|
+
const language = languageFor(meta.path);
|
|
238
|
+
const kind = kindFor(meta.path, language);
|
|
239
|
+
const unchanged = !!previous && previous.size === meta.size && previous.mtimeMs === meta.mtimeMs;
|
|
240
|
+
const changedAt = this.recentChanges.get(meta.path) ?? (!unchanged && previous ? Date.now() : previous?.changedAt ?? null);
|
|
241
|
+
if (unchanged && previous)
|
|
242
|
+
return { ...previous, changedAt };
|
|
243
|
+
if (!unchanged && previous)
|
|
244
|
+
this.recentChanges.set(meta.path, changedAt ?? Date.now());
|
|
245
|
+
let text = "";
|
|
246
|
+
if ((language || kind === "manifest" || kind === "config") && meta.size <= this.maxReadBytes) {
|
|
247
|
+
try {
|
|
248
|
+
text = await fs.readFile(path.join(this.root, meta.path), "utf8");
|
|
249
|
+
}
|
|
250
|
+
catch { }
|
|
251
|
+
}
|
|
252
|
+
const symbols = text ? parseSymbols(text, language) : [];
|
|
253
|
+
const imports = text ? parseImports(text, language) : [];
|
|
254
|
+
const packageName = text && kind === "manifest" ? parsePackageName(meta.path, text) : null;
|
|
255
|
+
const tokens = [...new Set([...tokenize(meta.path), ...symbols.flatMap(tokenize), ...imports.flatMap(tokenize), ...(packageName ? tokenize(packageName) : [])])].slice(0, 800);
|
|
256
|
+
return { ...meta, language, kind, imports, symbols, tokens, packageName, changedAt };
|
|
257
|
+
}
|
|
258
|
+
async scan() {
|
|
259
|
+
await this.loadCache();
|
|
260
|
+
const discovered = await this.discover();
|
|
261
|
+
const next = new Map();
|
|
262
|
+
for (const meta of discovered) {
|
|
263
|
+
next.set(meta.path, await this.indexOne(meta, this.files.get(meta.path)));
|
|
264
|
+
}
|
|
265
|
+
for (const oldPath of this.files.keys()) {
|
|
266
|
+
if (!next.has(oldPath))
|
|
267
|
+
this.recentChanges.set(oldPath, Date.now());
|
|
268
|
+
}
|
|
269
|
+
this.files = next;
|
|
270
|
+
this.rebuildGraphMetadata();
|
|
271
|
+
this.builtAt = Date.now();
|
|
272
|
+
this.lastScanAt = this.builtAt;
|
|
273
|
+
this.dirty = false;
|
|
274
|
+
const cutoff = Date.now() - 10 * 60_000;
|
|
275
|
+
for (const [file, at] of this.recentChanges)
|
|
276
|
+
if (at < cutoff)
|
|
277
|
+
this.recentChanges.delete(file);
|
|
278
|
+
await this.persistCache();
|
|
279
|
+
}
|
|
280
|
+
rebuildGraphMetadata() {
|
|
281
|
+
this.reverseImports.clear();
|
|
282
|
+
this.packageRoots.clear();
|
|
283
|
+
for (const record of this.files.values()) {
|
|
284
|
+
if (!record.packageName)
|
|
285
|
+
continue;
|
|
286
|
+
if (record.path.endsWith("pubspec.yaml") || record.path.endsWith("package.json")) {
|
|
287
|
+
this.packageRoots.set(record.packageName, path.posix.dirname(record.path) === "." ? "" : path.posix.dirname(record.path));
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
const known = new Set(this.files.keys());
|
|
291
|
+
for (const record of this.files.values()) {
|
|
292
|
+
for (const specifier of record.imports) {
|
|
293
|
+
const resolved = this.resolveImport(record.path, specifier, known);
|
|
294
|
+
if (!resolved)
|
|
295
|
+
continue;
|
|
296
|
+
const set = this.reverseImports.get(resolved) ?? new Set();
|
|
297
|
+
set.add(record.path);
|
|
298
|
+
this.reverseImports.set(resolved, set);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
resolveImport(from, specifier, known = new Set(this.files.keys())) {
|
|
303
|
+
const relative = resolveRelativeImport(from, specifier, known);
|
|
304
|
+
if (relative)
|
|
305
|
+
return relative;
|
|
306
|
+
const dartPackage = /^package:([^/]+)\/(.+)$/.exec(specifier);
|
|
307
|
+
if (dartPackage) {
|
|
308
|
+
const packageRoot = this.packageRoots.get(dartPackage[1]);
|
|
309
|
+
if (packageRoot !== undefined) {
|
|
310
|
+
const candidate = normalizeRelative(path.posix.join(packageRoot, "lib", dartPackage[2]));
|
|
311
|
+
if (known.has(candidate))
|
|
312
|
+
return candidate;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return null;
|
|
316
|
+
}
|
|
317
|
+
async ensureFresh(force = false) {
|
|
318
|
+
await this.loadCache();
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
if (!force && !this.dirty && now - this.lastScanAt < this.freshnessMs)
|
|
321
|
+
return this.summary();
|
|
322
|
+
if (!force && this.dirty && this.lastScanAt > 0 && now - this.lastScanAt < Math.min(this.freshnessMs, 750))
|
|
323
|
+
return this.summary();
|
|
324
|
+
if (!this.scanPromise) {
|
|
325
|
+
this.scanPromise = this.scan().finally(() => { this.scanPromise = null; });
|
|
326
|
+
}
|
|
327
|
+
await this.scanPromise;
|
|
328
|
+
return this.summary();
|
|
329
|
+
}
|
|
330
|
+
allFiles() {
|
|
331
|
+
return [...this.files.values()];
|
|
332
|
+
}
|
|
333
|
+
summary() {
|
|
334
|
+
const files = [...this.files.values()];
|
|
335
|
+
return {
|
|
336
|
+
builtAt: this.builtAt,
|
|
337
|
+
dirty: this.dirty,
|
|
338
|
+
files: files.length,
|
|
339
|
+
sourceFiles: files.filter((file) => file.kind === "source" || file.kind === "test").length,
|
|
340
|
+
manifests: files.filter((file) => file.kind === "manifest").length,
|
|
341
|
+
packages: [...this.packageRoots.entries()].slice(0, 100).map(([name, root]) => ({ name, root: root || "." })),
|
|
342
|
+
languages: [...new Set(files.map((file) => file.language).filter(Boolean))],
|
|
343
|
+
recentChanges: [...this.recentChanges.entries()].sort((a, b) => b[1] - a[1]).slice(0, 30).map(([file, changedAt]) => ({ file, changedAt })),
|
|
344
|
+
persistentCache: this.cacheFile,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
neighbors(paths, limit = 100) {
|
|
348
|
+
const out = new Set();
|
|
349
|
+
const known = new Set(this.files.keys());
|
|
350
|
+
for (const input of paths) {
|
|
351
|
+
const record = this.files.get(input);
|
|
352
|
+
if (record) {
|
|
353
|
+
for (const specifier of record.imports) {
|
|
354
|
+
const resolved = this.resolveImport(record.path, specifier, known);
|
|
355
|
+
if (resolved)
|
|
356
|
+
out.add(resolved);
|
|
357
|
+
if (out.size >= limit)
|
|
358
|
+
return [...out];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
for (const importer of this.reverseImports.get(input) ?? []) {
|
|
362
|
+
out.add(importer);
|
|
363
|
+
if (out.size >= limit)
|
|
364
|
+
return [...out];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return [...out];
|
|
368
|
+
}
|
|
369
|
+
rank(taskHint, limit = 40) {
|
|
370
|
+
const terms = tokenize(taskHint).filter((term) => term.length >= 2).slice(0, 16);
|
|
371
|
+
const wantsTests = terms.some((term) => ["test", "tests", "spec", "bug", "fix", "regression"].includes(term));
|
|
372
|
+
const now = Date.now();
|
|
373
|
+
const ranked = [];
|
|
374
|
+
for (const file of this.files.values()) {
|
|
375
|
+
let score = 0;
|
|
376
|
+
const reasons = [];
|
|
377
|
+
const lowerPath = file.path.toLowerCase();
|
|
378
|
+
const basename = path.posix.basename(lowerPath);
|
|
379
|
+
const symbolLower = file.symbols.map((value) => value.toLowerCase());
|
|
380
|
+
const importLower = file.imports.map((value) => value.toLowerCase());
|
|
381
|
+
for (const term of terms) {
|
|
382
|
+
if (basename.includes(term)) {
|
|
383
|
+
score += 18;
|
|
384
|
+
reasons.push(`filename:${term}`);
|
|
385
|
+
}
|
|
386
|
+
else if (lowerPath.includes(term)) {
|
|
387
|
+
score += 10;
|
|
388
|
+
reasons.push(`path:${term}`);
|
|
389
|
+
}
|
|
390
|
+
if (symbolLower.some((value) => value === term)) {
|
|
391
|
+
score += 24;
|
|
392
|
+
reasons.push(`symbol=${term}`);
|
|
393
|
+
}
|
|
394
|
+
else if (symbolLower.some((value) => value.includes(term))) {
|
|
395
|
+
score += 14;
|
|
396
|
+
reasons.push(`symbol:${term}`);
|
|
397
|
+
}
|
|
398
|
+
if (importLower.some((value) => value.includes(term))) {
|
|
399
|
+
score += 7;
|
|
400
|
+
reasons.push(`import:${term}`);
|
|
401
|
+
}
|
|
402
|
+
if (file.packageName?.toLowerCase().includes(term)) {
|
|
403
|
+
score += 8;
|
|
404
|
+
reasons.push(`package:${term}`);
|
|
405
|
+
}
|
|
406
|
+
if (file.tokens.includes(term))
|
|
407
|
+
score += 3;
|
|
408
|
+
}
|
|
409
|
+
if (file.kind === "manifest" || file.kind === "config")
|
|
410
|
+
score += terms.length ? 1 : 4;
|
|
411
|
+
if (file.kind === "test")
|
|
412
|
+
score += wantsTests ? 8 : -2;
|
|
413
|
+
if (/(^|\/)(main|index|app|server)\.[^.]+$/i.test(file.path))
|
|
414
|
+
score += 3;
|
|
415
|
+
if (file.changedAt) {
|
|
416
|
+
const age = now - file.changedAt;
|
|
417
|
+
if (age < 5 * 60_000) {
|
|
418
|
+
score += 14;
|
|
419
|
+
reasons.push("recently-changed");
|
|
420
|
+
}
|
|
421
|
+
else if (age < 30 * 60_000) {
|
|
422
|
+
score += 7;
|
|
423
|
+
reasons.push("recent-change");
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (!terms.length && (file.kind === "source" || file.kind === "manifest"))
|
|
427
|
+
score += 1;
|
|
428
|
+
if (score > 0)
|
|
429
|
+
ranked.push({ ...file, score, reasons: [...new Set(reasons)].slice(0, 8) });
|
|
430
|
+
}
|
|
431
|
+
ranked.sort((a, b) => b.score - a.score || b.mtimeMs - a.mtimeMs || a.path.localeCompare(b.path));
|
|
432
|
+
const seeds = ranked.slice(0, Math.min(12, ranked.length)).map((item) => item.path);
|
|
433
|
+
const neighbors = new Set(this.neighbors(seeds, 120));
|
|
434
|
+
for (const item of ranked) {
|
|
435
|
+
if (neighbors.has(item.path)) {
|
|
436
|
+
item.score += 6;
|
|
437
|
+
item.reasons = [...new Set([...item.reasons, "graph-neighbor"])];
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
ranked.sort((a, b) => b.score - a.score || b.mtimeMs - a.mtimeMs || a.path.localeCompare(b.path));
|
|
441
|
+
return ranked.slice(0, limit);
|
|
442
|
+
}
|
|
443
|
+
graph(limit = 3000) {
|
|
444
|
+
const known = new Set(this.files.keys());
|
|
445
|
+
const edges = [];
|
|
446
|
+
for (const record of this.files.values()) {
|
|
447
|
+
for (const specifier of record.imports) {
|
|
448
|
+
const resolved = this.resolveImport(record.path, specifier, known);
|
|
449
|
+
if (resolved)
|
|
450
|
+
edges.push({ from: record.path, to: resolved, specifier });
|
|
451
|
+
if (edges.length >= limit)
|
|
452
|
+
return edges;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return edges;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { promises as fs } from "node:fs";
|
|
5
|
+
import { DEFAULT_STATE_DIR, readJsonFile, writeJsonAtomic } from "./state.js";
|
|
6
|
+
export function workspaceIdForPath(project) {
|
|
7
|
+
const normalized = path.resolve(project);
|
|
8
|
+
const slug = path.basename(normalized).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 48) || "workspace";
|
|
9
|
+
const digest = createHash("sha256").update(normalized).digest("hex").slice(0, 10);
|
|
10
|
+
return `${slug}-${digest}`;
|
|
11
|
+
}
|
|
12
|
+
function unsafeBroadGrant(project) {
|
|
13
|
+
const root = path.parse(project).root;
|
|
14
|
+
const home = path.resolve(os.homedir());
|
|
15
|
+
return project === root || project === home;
|
|
16
|
+
}
|
|
17
|
+
export class WorkspaceRegistry {
|
|
18
|
+
file;
|
|
19
|
+
constructor(file = path.join(DEFAULT_STATE_DIR, "workspaces.json")) {
|
|
20
|
+
this.file = file;
|
|
21
|
+
}
|
|
22
|
+
async read() {
|
|
23
|
+
const value = await readJsonFile(this.file, { version: 1, workspaces: [] });
|
|
24
|
+
return { version: 1, workspaces: Array.isArray(value.workspaces) ? value.workspaces : [] };
|
|
25
|
+
}
|
|
26
|
+
async write(workspaces) {
|
|
27
|
+
const unique = new Map(workspaces.map((workspace) => [workspace.workspaceId, workspace]));
|
|
28
|
+
await writeJsonAtomic(this.file, { version: 1, workspaces: [...unique.values()].sort((a, b) => a.workspaceName.localeCompare(b.workspaceName)) });
|
|
29
|
+
}
|
|
30
|
+
async list() {
|
|
31
|
+
const data = await this.read();
|
|
32
|
+
const valid = [];
|
|
33
|
+
for (const workspace of data.workspaces) {
|
|
34
|
+
const exists = await fs.stat(workspace.localPath).then((stat) => stat.isDirectory()).catch(() => false);
|
|
35
|
+
if (exists)
|
|
36
|
+
valid.push(workspace);
|
|
37
|
+
}
|
|
38
|
+
return valid;
|
|
39
|
+
}
|
|
40
|
+
async grant(projectArg, workspaceName) {
|
|
41
|
+
const project = await fs.realpath(path.resolve(projectArg));
|
|
42
|
+
const stat = await fs.stat(project);
|
|
43
|
+
if (!stat.isDirectory())
|
|
44
|
+
throw new Error("Only directories can be granted as CodeLocal workspaces.");
|
|
45
|
+
if (unsafeBroadGrant(project))
|
|
46
|
+
throw new Error("Refusing to grant the filesystem root or entire home directory. Grant a specific project folder instead.");
|
|
47
|
+
if (/(^|[\\/])\.(ssh|aws|gnupg|gcloud|azure)([\\/]|$)/i.test(project))
|
|
48
|
+
throw new Error("Credential/config directories cannot be granted as CodeLocal workspaces.");
|
|
49
|
+
const existing = await this.read();
|
|
50
|
+
const workspaceId = workspaceIdForPath(project);
|
|
51
|
+
const previous = existing.workspaces.find((item) => item.workspaceId === workspaceId);
|
|
52
|
+
const entry = {
|
|
53
|
+
workspaceId,
|
|
54
|
+
workspaceName: (workspaceName?.trim() || path.basename(project)).slice(0, 120),
|
|
55
|
+
localPath: project,
|
|
56
|
+
grantedAt: previous?.grantedAt ?? Date.now(),
|
|
57
|
+
lastActivatedAt: previous?.lastActivatedAt,
|
|
58
|
+
};
|
|
59
|
+
await this.write([...existing.workspaces.filter((item) => item.workspaceId !== workspaceId), entry]);
|
|
60
|
+
return entry;
|
|
61
|
+
}
|
|
62
|
+
async revoke(identifier) {
|
|
63
|
+
const data = await this.read();
|
|
64
|
+
let resolvedPath = null;
|
|
65
|
+
try {
|
|
66
|
+
resolvedPath = await fs.realpath(path.resolve(identifier));
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
const next = data.workspaces.filter((workspace) => workspace.workspaceId !== identifier && workspace.localPath !== resolvedPath);
|
|
70
|
+
if (next.length === data.workspaces.length)
|
|
71
|
+
return false;
|
|
72
|
+
await this.write(next);
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
async get(workspaceId) {
|
|
76
|
+
return (await this.list()).find((workspace) => workspace.workspaceId === workspaceId) ?? null;
|
|
77
|
+
}
|
|
78
|
+
async markActivated(workspaceId) {
|
|
79
|
+
const data = await this.read();
|
|
80
|
+
const workspace = data.workspaces.find((item) => item.workspaceId === workspaceId);
|
|
81
|
+
if (!workspace)
|
|
82
|
+
return;
|
|
83
|
+
workspace.lastActivatedAt = Date.now();
|
|
84
|
+
await this.write(data.workspaces);
|
|
85
|
+
}
|
|
86
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codelocal",
|
|
3
|
+
"version": "1.5.0-beta.1",
|
|
4
|
+
"description": "CodeLocal local code intelligence and execution runtime for ChatGPT.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"codelocal": "dist/cli-saas.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/*.js",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"publishConfig": {
|
|
15
|
+
"access": "public"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@modelcontextprotocol/sdk": "^1.18.2",
|
|
19
|
+
"@parcel/watcher": "^2.6.0",
|
|
20
|
+
"chokidar": "^4.0.3",
|
|
21
|
+
"ignore": "^7.0.5",
|
|
22
|
+
"typescript": "^5.9.2",
|
|
23
|
+
"ws": "^8.18.3"
|
|
24
|
+
},
|
|
25
|
+
"optionalDependencies": {
|
|
26
|
+
"node-pty": "^1.0.0"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=20"
|
|
30
|
+
}
|
|
31
|
+
}
|