@codeursenior/pek 0.0.0-stage → 0.1.8
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/LICENSE +21 -0
- package/README.md +128 -2
- package/dist/browser/3rdpartylicenses.txt +534 -0
- package/dist/browser/client-logos/chatgpt.webp +0 -0
- package/dist/browser/client-logos/claude.png +0 -0
- package/dist/browser/client-logos/copilot.svg +26 -0
- package/dist/browser/client-logos/cursor.png +0 -0
- package/dist/browser/favicon.svg +1 -0
- package/dist/browser/index.html +12 -0
- package/dist/browser/main-73Q4GOTD.js +8 -0
- package/dist/browser/mountain-context.jpg +0 -0
- package/dist/browser/styles-2QCIL5GS.css +1 -0
- package/dist/browser/third-party-licenses.txt +643 -0
- package/dist/cli.js +87 -0
- package/dist/contracts.js +1 -0
- package/dist/http/assets.js +42 -0
- package/dist/http/errors.js +7 -0
- package/dist/http/files.js +72 -0
- package/dist/http/security.js +16 -0
- package/dist/optimization-checks.js +57 -0
- package/dist/scan/assets.js +171 -0
- package/dist/scan/collect.js +107 -0
- package/dist/scan/graph.js +71 -0
- package/dist/scan/identity.js +61 -0
- package/dist/scan/mcp.js +111 -0
- package/dist/scan/metadata.js +67 -0
- package/dist/scan/references.js +46 -0
- package/dist/scan/types.js +1 -0
- package/dist/scan/values.js +36 -0
- package/dist/scanner.js +68 -0
- package/dist/server.js +127 -0
- package/dist/token-estimator.js +159 -0
- package/docs/architecture.md +47 -0
- package/docs/token-optimizer.md +56 -0
- package/package.json +89 -4
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { inside } from "./values.js";
|
|
4
|
+
/** Read only bounded regular files inside the selected project. */
|
|
5
|
+
async function read(root, name, limit) {
|
|
6
|
+
try {
|
|
7
|
+
const real = await fs.realpath(path.join(root, ".agents", name));
|
|
8
|
+
if (!inside(root, real))
|
|
9
|
+
return;
|
|
10
|
+
const stat = await fs.stat(real);
|
|
11
|
+
if (!stat.isFile() || stat.size > limit)
|
|
12
|
+
return;
|
|
13
|
+
const file = await fs.open(real, "r");
|
|
14
|
+
try {
|
|
15
|
+
const opened = await file.stat();
|
|
16
|
+
if (!opened.isFile() || opened.size > limit)
|
|
17
|
+
return;
|
|
18
|
+
const buffer = Buffer.alloc(limit + 1);
|
|
19
|
+
let size = 0;
|
|
20
|
+
while (size < buffer.length) {
|
|
21
|
+
const { bytesRead } = await file.read(buffer, size, buffer.length - size);
|
|
22
|
+
if (!bytesRead)
|
|
23
|
+
break;
|
|
24
|
+
size += bytesRead;
|
|
25
|
+
}
|
|
26
|
+
return size <= limit ? buffer.subarray(0, size) : undefined;
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
await file.close();
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// An optional identity must never prevent opening the workspace.
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function discoverIdentity(root) {
|
|
38
|
+
const identity = {};
|
|
39
|
+
const text = await read(root, "NAME.md", 4096);
|
|
40
|
+
const name = text
|
|
41
|
+
?.toString("utf8")
|
|
42
|
+
.trim()
|
|
43
|
+
.split(/\r?\n|\r/)[0]
|
|
44
|
+
?.replace(/^#{1,6}(?:\s+|$)/, "")
|
|
45
|
+
.trim();
|
|
46
|
+
if (name && [...name].length <= 100 && !/[\p{Cc}\p{Cf}\uFFFD]/u.test(name))
|
|
47
|
+
identity.name = name;
|
|
48
|
+
for (const [filename, mime, signature] of [
|
|
49
|
+
["AVATAR.png", "image/png", "89504e470d0a1a0a"],
|
|
50
|
+
["AVATAR.jpeg", "image/jpeg", "ffd8ff"],
|
|
51
|
+
]) {
|
|
52
|
+
const bytes = await read(root, filename, 2 * 1024 * 1024);
|
|
53
|
+
if (bytes
|
|
54
|
+
?.subarray(0, signature.length / 2)
|
|
55
|
+
.equals(Buffer.from(signature, "hex"))) {
|
|
56
|
+
identity.avatar = `data:${mime};base64,${bytes.toString("base64")}`;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return identity.name || identity.avatar ? identity : undefined;
|
|
61
|
+
}
|
package/dist/scan/mcp.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { parse as jsonc } from "jsonc-parser";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { parse as toml } from "smol-toml";
|
|
5
|
+
import { errorCode, id, inside, record, slash } from "./values.js";
|
|
6
|
+
export async function discoverMcp(projectConfigs, env) {
|
|
7
|
+
const { root, home, codexHome, includeUser, allowed, display, warnings } = env;
|
|
8
|
+
const mcp = [];
|
|
9
|
+
async function readConfig(file, format = "json") {
|
|
10
|
+
try {
|
|
11
|
+
const real = await fs.realpath(file);
|
|
12
|
+
if (![...allowed, path.join(home, ".claude.json")].some((base) => inside(base, real)))
|
|
13
|
+
return {};
|
|
14
|
+
if ((await fs.stat(real)).size > 2 * 1024 * 1024) {
|
|
15
|
+
warnings.push(`Skipped oversized config: ${path.basename(file)}`);
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
const text = await fs.readFile(real, "utf8");
|
|
19
|
+
if (format === "toml")
|
|
20
|
+
return record(toml(text));
|
|
21
|
+
const errors = [];
|
|
22
|
+
const result = jsonc(text, errors, { allowTrailingComma: true });
|
|
23
|
+
if (errors.length)
|
|
24
|
+
throw new Error("Invalid JSON");
|
|
25
|
+
return record(result);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (!["ENOENT", "ENOTDIR"].includes(errorCode(error) ?? ""))
|
|
29
|
+
warnings.push(`Could not parse ${display(file, inside(root, file) ? "project" : "user")}.`);
|
|
30
|
+
return {};
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function addServers(servers, file, scope, clients, origin) {
|
|
34
|
+
if (!servers || typeof servers !== "object" || Array.isArray(servers))
|
|
35
|
+
return;
|
|
36
|
+
for (const [name, raw] of Object.entries(record(servers))) {
|
|
37
|
+
if (!raw || typeof raw !== "object")
|
|
38
|
+
continue;
|
|
39
|
+
const config = record(raw);
|
|
40
|
+
const disabled = config.enabled === false || config.disabled === true;
|
|
41
|
+
// Whitelist public fields. Never expose arguments, env, headers, URLs or credentials.
|
|
42
|
+
mcp.push({
|
|
43
|
+
id: id(`${file}:${scope}:${clients.join(",")}:${name}:${origin || ""}`),
|
|
44
|
+
name,
|
|
45
|
+
scope,
|
|
46
|
+
path: display(file, inside(root, file) ? "project" : "user"),
|
|
47
|
+
clients,
|
|
48
|
+
transport: config.url
|
|
49
|
+
? config.type === "sse"
|
|
50
|
+
? "SSE"
|
|
51
|
+
: "HTTP"
|
|
52
|
+
: config.command
|
|
53
|
+
? "stdio"
|
|
54
|
+
: "Unknown",
|
|
55
|
+
status: disabled ? "Disabled" : "Not verified",
|
|
56
|
+
configured: !disabled,
|
|
57
|
+
origin: origin || "File configuration",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const bases = [[root, "project"]];
|
|
62
|
+
if (includeUser)
|
|
63
|
+
bases.push([home, "user"]);
|
|
64
|
+
const locations = [
|
|
65
|
+
[".cursor/mcp.json", ["cursor"], "json"],
|
|
66
|
+
[".mcp.json", ["claude", "copilot"], "json"],
|
|
67
|
+
[".codex/config.toml", ["codex"], "toml"],
|
|
68
|
+
[".vscode/mcp.json", ["copilot"], "json"],
|
|
69
|
+
[".copilot/mcp-config.json", ["copilot"], "json"],
|
|
70
|
+
];
|
|
71
|
+
for (const [base, scope] of bases) {
|
|
72
|
+
for (const [rel, clients, format] of locations) {
|
|
73
|
+
if (scope === "user" &&
|
|
74
|
+
(rel === ".mcp.json" || rel.startsWith(".vscode")))
|
|
75
|
+
continue;
|
|
76
|
+
if (scope === "project" && rel.startsWith(".copilot"))
|
|
77
|
+
continue;
|
|
78
|
+
const file = scope === "user" && clients.includes("codex")
|
|
79
|
+
? path.join(codexHome, "config.toml")
|
|
80
|
+
: path.join(base, rel);
|
|
81
|
+
const config = await readConfig(file, format);
|
|
82
|
+
addServers(config?.mcpServers || config?.mcp_servers || config?.servers, file, scope, clients);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
for (const file of projectConfigs) {
|
|
86
|
+
const relative = slash(path.relative(root, file));
|
|
87
|
+
if ([
|
|
88
|
+
".mcp.json",
|
|
89
|
+
".cursor/mcp.json",
|
|
90
|
+
".codex/config.toml",
|
|
91
|
+
".vscode/mcp.json",
|
|
92
|
+
].includes(relative))
|
|
93
|
+
continue;
|
|
94
|
+
const clients = relative.endsWith(".cursor/mcp.json")
|
|
95
|
+
? ["cursor"]
|
|
96
|
+
: relative.endsWith(".codex/config.toml")
|
|
97
|
+
? ["codex"]
|
|
98
|
+
: relative.endsWith(".vscode/mcp.json")
|
|
99
|
+
? ["copilot"]
|
|
100
|
+
: ["claude", "copilot"];
|
|
101
|
+
const config = await readConfig(file, clients.includes("codex") ? "toml" : "json");
|
|
102
|
+
addServers(config?.mcpServers || config?.mcp_servers || config?.servers, file, "project", clients, "Nested project configuration");
|
|
103
|
+
}
|
|
104
|
+
if (includeUser) {
|
|
105
|
+
const file = path.join(home, ".claude.json");
|
|
106
|
+
const config = await readConfig(file);
|
|
107
|
+
addServers(config?.mcpServers, file, "user", ["claude"]);
|
|
108
|
+
addServers(record(record(config.projects)[root]).mcpServers, file, "project", ["claude"], "Project-local configuration in user settings");
|
|
109
|
+
}
|
|
110
|
+
return mcp.sort((a, b) => a.name.localeCompare(b.name));
|
|
111
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { parse as yaml } from "yaml";
|
|
3
|
+
import { inside, MARKDOWN, record, required, slash } from "./values.js";
|
|
4
|
+
export const ruleClient = (file) => {
|
|
5
|
+
if (path.basename(file).toLowerCase() === ".cursorrules")
|
|
6
|
+
return "cursor";
|
|
7
|
+
if (!MARKDOWN.test(file))
|
|
8
|
+
return null;
|
|
9
|
+
const normalized = slash(file).toLowerCase();
|
|
10
|
+
if (/(^|\/)\.cursor\/rules\//.test(normalized))
|
|
11
|
+
return "cursor";
|
|
12
|
+
if (/(^|\/)\.claude\/rules\//.test(normalized))
|
|
13
|
+
return "claude";
|
|
14
|
+
if (/(^|\/)(?:\.github|\.copilot)\/instructions\/.+\.instructions\.md$/.test(normalized))
|
|
15
|
+
return "copilot";
|
|
16
|
+
return null;
|
|
17
|
+
};
|
|
18
|
+
export const toolsForSkill = (p) => {
|
|
19
|
+
const result = [];
|
|
20
|
+
if (/(^|\/)\.(agents|cursor|claude|codex)\/skills\//.test(p))
|
|
21
|
+
result.push("cursor");
|
|
22
|
+
if (/(^|\/)\.claude\/skills\//.test(p))
|
|
23
|
+
result.push("claude");
|
|
24
|
+
if (/(^|\/)\.(agents|codex)\/skills\//.test(p))
|
|
25
|
+
result.push("codex");
|
|
26
|
+
if (/(^|\/)\.(github|agents|claude|copilot)\/skills\//.test(p))
|
|
27
|
+
result.push("copilot");
|
|
28
|
+
return result;
|
|
29
|
+
};
|
|
30
|
+
export const clientsForContextEntry = (alias, scope, root, codexHome) => {
|
|
31
|
+
const name = path.basename(alias).toLowerCase();
|
|
32
|
+
if (name === "copilot-instructions.md")
|
|
33
|
+
return /[\\/](?:\.github|\.copilot)[\\/]copilot-instructions\.md$/i.test(alias)
|
|
34
|
+
? ["copilot"]
|
|
35
|
+
: [];
|
|
36
|
+
if (name === "agents.override.md")
|
|
37
|
+
return ["codex"];
|
|
38
|
+
if (name === "agents.md") {
|
|
39
|
+
if (scope === "user")
|
|
40
|
+
return inside(codexHome, alias) ? ["codex"] : ["cursor"];
|
|
41
|
+
return ["cursor", "codex", "copilot"];
|
|
42
|
+
}
|
|
43
|
+
if (name === "claude.local.md")
|
|
44
|
+
return ["claude"];
|
|
45
|
+
if (name === "claude.md")
|
|
46
|
+
return scope === "project" && path.dirname(alias) === root
|
|
47
|
+
? ["cursor", "claude"]
|
|
48
|
+
: ["claude"];
|
|
49
|
+
return [];
|
|
50
|
+
};
|
|
51
|
+
export function frontmatter(text) {
|
|
52
|
+
const match = text.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
|
|
53
|
+
if (!match)
|
|
54
|
+
return {};
|
|
55
|
+
const result = yaml(required(match[1]), { maxAliasCount: 20 });
|
|
56
|
+
return record(result);
|
|
57
|
+
}
|
|
58
|
+
export function links(text) {
|
|
59
|
+
const result = [];
|
|
60
|
+
for (const m of text.matchAll(/\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g))
|
|
61
|
+
result.push({ target: required(m[1]).split("#")[0] ?? "", wiki: true });
|
|
62
|
+
for (const m of text.matchAll(/\]\(\s*(?:<([^>]+)>|([^\s)]+))(?:\s+['"][^)]*)?\)/g))
|
|
63
|
+
result.push({ target: required(m[1] || m[2]), wiki: false });
|
|
64
|
+
for (const m of text.matchAll(/(?:`|@)((?:\.{0,2}\/|~\/)?[\w.\-/À-ž]+\.(?:md|mdc))(?=`|\s|$|[,;)])/g))
|
|
65
|
+
result.push({ target: required(m[1]), wiki: false });
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { required, slash } from "./values.js";
|
|
3
|
+
/** Shared matching semantics for the context graph and token inventory. */
|
|
4
|
+
export function referenceResolver(entries, env) {
|
|
5
|
+
const { root, home } = env;
|
|
6
|
+
const byAlias = new Map();
|
|
7
|
+
const byName = new Map();
|
|
8
|
+
for (const entry of entries) {
|
|
9
|
+
for (const alias of entry.aliases)
|
|
10
|
+
byAlias.set(alias, entry);
|
|
11
|
+
const name = path
|
|
12
|
+
.basename(entry.file)
|
|
13
|
+
.replace(/\.(md|mdc)$/i, "")
|
|
14
|
+
.toLowerCase();
|
|
15
|
+
if (!byName.has(name))
|
|
16
|
+
byName.set(name, []);
|
|
17
|
+
required(byName.get(name)).push(entry);
|
|
18
|
+
}
|
|
19
|
+
return (entry, link) => {
|
|
20
|
+
let target;
|
|
21
|
+
try {
|
|
22
|
+
target = decodeURIComponent(link.target.split("#")[0] ?? "");
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
if (!target || /^(https?:|mailto:|data:|file:|javascript:)/i.test(target))
|
|
28
|
+
return undefined;
|
|
29
|
+
let found;
|
|
30
|
+
const absolute = target.startsWith("~/")
|
|
31
|
+
? path.join(home, target.slice(2))
|
|
32
|
+
: path.resolve(path.dirname(entry.file), target);
|
|
33
|
+
found = byAlias.get(absolute) || byAlias.get(`${absolute}.md`);
|
|
34
|
+
if (!found && !link.wiki)
|
|
35
|
+
found = byAlias.get(path.resolve(root, target));
|
|
36
|
+
if (!found && link.wiki) {
|
|
37
|
+
const candidates = (byName.get(path.basename(target).replace(/\.md$/i, "").toLowerCase()) ||
|
|
38
|
+
[]).filter((e) => e.scope === entry.scope &&
|
|
39
|
+
(!target.includes("/") ||
|
|
40
|
+
e.aliases.some((a) => slash(a).endsWith(`${target.replace(/\.md$/i, "")}.md`))));
|
|
41
|
+
if (candidates.length === 1)
|
|
42
|
+
found = candidates[0];
|
|
43
|
+
}
|
|
44
|
+
return found;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export const MAX_FILES = 20000;
|
|
4
|
+
export const MAX_BYTES = 512 * 1024;
|
|
5
|
+
export const MAX_DEPTH = 35;
|
|
6
|
+
export const INSTRUCTION = /^(AGENTS(?:\.override)?|CLAUDE(?:\.local)?|GEMINI|copilot-instructions)\.md$|^\.cursorrules$/i;
|
|
7
|
+
export const MARKDOWN = /\.(md|mdc)$/i;
|
|
8
|
+
export const id = (value) => createHash("sha256").update(value).digest("hex").slice(0, 16);
|
|
9
|
+
export const slash = (value) => value.split(path.sep).join("/");
|
|
10
|
+
export const inside = (base, file) => {
|
|
11
|
+
const relative = path.relative(base, file);
|
|
12
|
+
return (relative === "" ||
|
|
13
|
+
(!relative.startsWith(`..${path.sep}`) &&
|
|
14
|
+
relative !== ".." &&
|
|
15
|
+
!path.isAbsolute(relative)));
|
|
16
|
+
};
|
|
17
|
+
export const lineCount = (text) => text.length === 0
|
|
18
|
+
? 0
|
|
19
|
+
: text.split(/\r\n|\r|\n/).length - Number(/(?:\r\n|\r|\n)$/.test(text));
|
|
20
|
+
export const truth = (value) => value === true || /^(true|yes|on|1)$/i.test(String(value));
|
|
21
|
+
export const falsehood = (value) => value === false || /^(false|no|off|0)$/i.test(String(value));
|
|
22
|
+
export function record(value) {
|
|
23
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
24
|
+
? value
|
|
25
|
+
: {};
|
|
26
|
+
}
|
|
27
|
+
export function errorCode(error) {
|
|
28
|
+
const code = record(error).code;
|
|
29
|
+
return typeof code === "string" ? code : undefined;
|
|
30
|
+
}
|
|
31
|
+
/** Indexes asserted here come from internal maps/arrays populated in the same scan. */
|
|
32
|
+
export function required(value) {
|
|
33
|
+
if (value === undefined || value === null)
|
|
34
|
+
throw new Error("Missing indexed scan value");
|
|
35
|
+
return value;
|
|
36
|
+
}
|
package/dist/scanner.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { discoverAssets } from "./scan/assets.js";
|
|
5
|
+
import { collectFiles } from "./scan/collect.js";
|
|
6
|
+
import { buildGraph } from "./scan/graph.js";
|
|
7
|
+
import { discoverIdentity } from "./scan/identity.js";
|
|
8
|
+
import { discoverMcp } from "./scan/mcp.js";
|
|
9
|
+
import { inside, slash } from "./scan/values.js";
|
|
10
|
+
import { contextInventory } from "./token-estimator.js";
|
|
11
|
+
/** Inventory only: never executes instructions, hooks, skills or MCP commands. */
|
|
12
|
+
export async function scanProject(project, options = {}) {
|
|
13
|
+
const home = options.home ?? os.homedir();
|
|
14
|
+
const includeUser = options.includeUser ?? true;
|
|
15
|
+
const codexHome = options.codexHome ?? process.env.CODEX_HOME ?? path.join(home, ".codex");
|
|
16
|
+
const root = await fs.realpath(path.resolve(project));
|
|
17
|
+
if (!(await fs.stat(root)).isDirectory())
|
|
18
|
+
throw new Error("Choose a folder to scan.");
|
|
19
|
+
const allowed = [
|
|
20
|
+
root,
|
|
21
|
+
...(includeUser
|
|
22
|
+
? [
|
|
23
|
+
path.join(home, ".agents"),
|
|
24
|
+
path.join(home, ".claude"),
|
|
25
|
+
path.join(home, ".cursor"),
|
|
26
|
+
path.join(home, ".copilot"),
|
|
27
|
+
codexHome,
|
|
28
|
+
]
|
|
29
|
+
: []),
|
|
30
|
+
];
|
|
31
|
+
const env = {
|
|
32
|
+
root,
|
|
33
|
+
home,
|
|
34
|
+
codexHome,
|
|
35
|
+
includeUser,
|
|
36
|
+
allowed,
|
|
37
|
+
warnings: [],
|
|
38
|
+
display(file, scope) {
|
|
39
|
+
if (scope === "user" && process.platform === "win32")
|
|
40
|
+
return file;
|
|
41
|
+
return scope === "user" && inside(home, file)
|
|
42
|
+
? "~/" + slash(path.relative(home, file))
|
|
43
|
+
: slash(path.relative(root, file)) || path.basename(root);
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
const { entries, projectConfigs, inspected, capped } = await collectFiles(env);
|
|
47
|
+
const { context, rules, skills, content, editable, addContext } = await discoverAssets(entries, env);
|
|
48
|
+
const edges = buildGraph(entries, context, addContext, env);
|
|
49
|
+
const mcp = await discoverMcp(projectConfigs, env);
|
|
50
|
+
const data = {
|
|
51
|
+
project: { name: path.basename(root), path: root },
|
|
52
|
+
agent: await discoverIdentity(root),
|
|
53
|
+
scannedAt: new Date().toISOString(),
|
|
54
|
+
nodes: [...context.values()],
|
|
55
|
+
rules: [...rules.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
56
|
+
edges,
|
|
57
|
+
skills: [...skills.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
58
|
+
mcp,
|
|
59
|
+
warnings: [...new Set(env.warnings)],
|
|
60
|
+
limits: { includeUser, scannedFiles: inspected, truncated: capped },
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
data,
|
|
64
|
+
tokenInventory: contextInventory(entries, data, env),
|
|
65
|
+
content,
|
|
66
|
+
editable,
|
|
67
|
+
};
|
|
68
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import http from "node:http";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { browserAssets } from "./http/assets.js";
|
|
6
|
+
import { HttpError } from "./http/errors.js";
|
|
7
|
+
import { FileOperations } from "./http/files.js";
|
|
8
|
+
import { authenticated, localRequest, securityHeaders, } from "./http/security.js";
|
|
9
|
+
import { scanProject } from "./scanner.js";
|
|
10
|
+
import { optimizationChecks } from "./optimization-checks.js";
|
|
11
|
+
import { estimateTokens } from "./token-estimator.js";
|
|
12
|
+
export async function startServer({ root, port = 0, ...options }) {
|
|
13
|
+
let snapshot = await scanProject(root, options);
|
|
14
|
+
let refresh;
|
|
15
|
+
let checking;
|
|
16
|
+
const files = new FileOperations();
|
|
17
|
+
const token = randomBytes(24).toString("hex");
|
|
18
|
+
const assets = await browserAssets(fileURLToPath(new URL("../dist/browser/", import.meta.url)));
|
|
19
|
+
const server = http.createServer(async (req, res) => {
|
|
20
|
+
const address = server.address();
|
|
21
|
+
if (!address || typeof address === "string") {
|
|
22
|
+
res.end();
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const host = `127.0.0.1:${address.port}`;
|
|
26
|
+
const nonce = randomBytes(24).toString("base64");
|
|
27
|
+
securityHeaders(res, nonce);
|
|
28
|
+
const send = (status, body, type = "application/json; charset=utf-8") => {
|
|
29
|
+
res.writeHead(status, { "Content-Type": type });
|
|
30
|
+
res.end(typeof body === "string" || Buffer.isBuffer(body)
|
|
31
|
+
? body
|
|
32
|
+
: JSON.stringify(body));
|
|
33
|
+
};
|
|
34
|
+
if (!localRequest(req, host))
|
|
35
|
+
return send(403, { error: "Local requests only." });
|
|
36
|
+
let url;
|
|
37
|
+
try {
|
|
38
|
+
url = new URL(req.url ?? "/", `http://${host}`);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return send(400, { error: "Invalid URL." });
|
|
42
|
+
}
|
|
43
|
+
if (url.pathname.startsWith("/api/")) {
|
|
44
|
+
if (!authenticated(req, token))
|
|
45
|
+
return send(401, {
|
|
46
|
+
error: "Open the URL printed in your terminal to access this session.",
|
|
47
|
+
});
|
|
48
|
+
try {
|
|
49
|
+
if (url.pathname === "/api/file") {
|
|
50
|
+
if (!["GET", "PUT", "DELETE"].includes(req.method ?? ""))
|
|
51
|
+
return send(405, { error: "Method not allowed." });
|
|
52
|
+
const target = snapshot.editable.get(url.searchParams.get("id") ?? "");
|
|
53
|
+
if (!target)
|
|
54
|
+
return send(404, { error: "Editable file not found." });
|
|
55
|
+
return send(200, await files.handle(req, target));
|
|
56
|
+
}
|
|
57
|
+
if (req.method !== "GET")
|
|
58
|
+
return send(405, { error: "Method not allowed." });
|
|
59
|
+
if (url.pathname === "/api/optimization-checks") {
|
|
60
|
+
checking ??= optimizationChecks(snapshot.data.project.path).finally(() => {
|
|
61
|
+
checking = undefined;
|
|
62
|
+
});
|
|
63
|
+
return send(200, await checking);
|
|
64
|
+
}
|
|
65
|
+
if (url.pathname === "/api/token-estimate")
|
|
66
|
+
return send(200, estimateTokens(snapshot, url.searchParams.get("client"), url.searchParams.get("model")));
|
|
67
|
+
if (url.pathname === "/api/scan") {
|
|
68
|
+
if (url.searchParams.has("refresh")) {
|
|
69
|
+
refresh ??= scanProject(root, options)
|
|
70
|
+
.then((result) => {
|
|
71
|
+
snapshot = result;
|
|
72
|
+
})
|
|
73
|
+
.finally(() => {
|
|
74
|
+
refresh = undefined;
|
|
75
|
+
});
|
|
76
|
+
await refresh;
|
|
77
|
+
}
|
|
78
|
+
return send(200, snapshot.data);
|
|
79
|
+
}
|
|
80
|
+
if (url.pathname === "/api/content") {
|
|
81
|
+
const text = snapshot.content.get(url.searchParams.get("id") ?? "");
|
|
82
|
+
return text === undefined
|
|
83
|
+
? send(404, { error: "Context file not found." })
|
|
84
|
+
: send(200, { text });
|
|
85
|
+
}
|
|
86
|
+
return send(404, { error: "Not found." });
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return send(error instanceof HttpError ? error.status : 500, {
|
|
90
|
+
error: error instanceof HttpError
|
|
91
|
+
? error.message
|
|
92
|
+
: "File operation failed. Check permissions and try again.",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (req.method !== "GET")
|
|
97
|
+
return send(405, { error: "Method not allowed." });
|
|
98
|
+
const asset = assets.get(url.pathname);
|
|
99
|
+
if (!asset)
|
|
100
|
+
return send(url.pathname === "/" ? 500 : 404, {
|
|
101
|
+
error: url.pathname === "/"
|
|
102
|
+
? "UI assets missing. Run npm run build."
|
|
103
|
+
: "Not found.",
|
|
104
|
+
});
|
|
105
|
+
try {
|
|
106
|
+
const bytes = await fs.readFile(asset.file);
|
|
107
|
+
send(200, url.pathname === "/"
|
|
108
|
+
? bytes.toString("utf8").replaceAll("__CSP_NONCE__", nonce)
|
|
109
|
+
: bytes, asset.type);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
send(500, { error: "UI assets missing. Run npm run build." });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
await new Promise((resolve, reject) => {
|
|
116
|
+
server.once("error", reject);
|
|
117
|
+
server.listen(port, "127.0.0.1", resolve);
|
|
118
|
+
});
|
|
119
|
+
const address = server.address();
|
|
120
|
+
if (!address || typeof address === "string")
|
|
121
|
+
throw new Error("Server did not bind to a local port.");
|
|
122
|
+
return {
|
|
123
|
+
server,
|
|
124
|
+
token,
|
|
125
|
+
url: `http://127.0.0.1:${address.port}/#token=${token}`,
|
|
126
|
+
};
|
|
127
|
+
}
|