machine-bridge-mcp 0.16.2 → 0.17.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/CHANGELOG.md +40 -0
- package/CONTRIBUTING.md +14 -0
- package/README.md +11 -1
- package/SECURITY.md +9 -1
- package/browser-extension/manifest.json +2 -2
- package/browser-extension/service-worker.js +52 -29
- package/docs/ARCHITECTURE.md +4 -2
- package/docs/CLIENTS.md +2 -0
- package/docs/ENGINEERING.md +3 -1
- package/docs/GETTING_STARTED.md +400 -0
- package/docs/MULTI_ACCOUNT.md +406 -0
- package/docs/PROJECT_STANDARDS.md +143 -0
- package/docs/RELEASING.md +12 -0
- package/docs/TESTING.md +7 -5
- package/docs/TOOL_REFERENCE.md +2836 -0
- package/package.json +13 -4
- package/scripts/commit-message-check.mjs +63 -0
- package/scripts/coverage-check.mjs +9 -1
- package/scripts/generate-policy-reference.mjs +1 -4
- package/scripts/generate-tool-reference.mjs +87 -0
- package/scripts/generate-worker-types.mjs +19 -0
- package/scripts/markdown.mjs +9 -0
- package/src/local/agent-context.mjs +8 -15
- package/src/local/cli.mjs +1 -107
- package/src/local/daemon-process.mjs +16 -4
- package/src/local/default-instructions.mjs +2 -45
- package/src/local/git-service.mjs +4 -9
- package/src/local/managed-job-plan.mjs +2 -7
- package/src/local/managed-jobs.mjs +2 -7
- package/src/local/numbers.mjs +9 -0
- package/src/local/process-execution.mjs +4 -9
- package/src/local/process-sessions.mjs +5 -10
- package/src/local/project-metadata.mjs +50 -0
- package/src/local/project-package.mjs +2 -45
- package/src/local/records.mjs +5 -0
- package/src/local/runtime.mjs +5 -12
- package/src/local/state-inventory.mjs +139 -0
- package/src/local/workspace-file-service.mjs +6 -12
- package/src/worker/index.ts +33 -17
- package/tsconfig.json +1 -1
- package/src/worker/worker-configuration.d.ts +0 -14716
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "machine-bridge-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.1",
|
|
4
4
|
"description": "Cross-client MCP bridge for local agent context, structured browser and application automation, files, Git, processes, resources, and durable jobs over stdio or OAuth relay.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -40,10 +40,10 @@
|
|
|
40
40
|
"version": "node scripts/sync-worker-version.mjs && git add package.json package-lock.json src/worker/index.ts browser-extension/manifest.json",
|
|
41
41
|
"prepack": "npm run version:check && npm run privacy:check && npm run release-impact:check",
|
|
42
42
|
"prepublishOnly": "npm run check && npm run version:check && npm run release:check",
|
|
43
|
-
"worker:types": "
|
|
43
|
+
"worker:types": "node scripts/generate-worker-types.mjs",
|
|
44
44
|
"typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit",
|
|
45
45
|
"syntax": "node scripts/syntax-check.mjs",
|
|
46
|
-
"check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run release-state:test && npm run release-ci:test && npm run network-retry:test && npm run relay:test && npm run secure-file:test && npm run architecture:test && npm run policy:test && npm run policy-docs:check && npm run runtime-infrastructure:test && npm run runtime-handlers:test && npm run cli-entrypoint:test && npm run lifecycle:test && npm run logging-structure:test && npm run coverage:test && npm run worker-runtime-infrastructure:test && npm run typecheck && npm run syntax && npm run process-lock:test && npm run service-lifecycle:test && npm run service-platform:test && npm run catalog:test && npm run agent-context:test && npm run capability-ranking:test && npm run browser-command:test && npm run browser-devtools-input:test && npm run browser-service-worker:test && npm run browser-page-automation:test && npm run browser-bridge:test && npm run app-automation:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
|
|
46
|
+
"check": "npm run privacy:check && npm run privacy:test && npm run release-impact:check && npm run release-impact:test && npm run release-state:test && npm run release-ci:test && npm run network-retry:test && npm run relay:test && npm run secure-file:test && npm run architecture:test && npm run markdown:test && npm run project-metadata:test && npm run numbers:test && npm run records:test && npm run state-inventory:test && npm run commit-message:test && npm run policy:test && npm run policy-docs:check && npm run tool-docs:check && npm run runtime-infrastructure:test && npm run runtime-handlers:test && npm run cli-entrypoint:test && npm run lifecycle:test && npm run logging-structure:test && npm run coverage:test && npm run worker-runtime-infrastructure:test && npm run typecheck && npm run syntax && npm run process-lock:test && npm run service-lifecycle:test && npm run service-platform:test && npm run catalog:test && npm run agent-context:test && npm run capability-ranking:test && npm run browser-command:test && npm run browser-devtools-input:test && npm run browser-service-worker:test && npm run browser-page-automation:test && npm run browser-bridge:test && npm run app-automation:test && npm run self-test && npm run atomic-fs:test && npm run package:test && npm run ssh-key:test && npm run full-access:test && npm run managed-jobs:test && npm run stdio:integration-test && npm run worker:integration-test",
|
|
47
47
|
"worker:dry-run": "wrangler deploy --dry-run",
|
|
48
48
|
"worker:integration-test": "node tests/worker-integration-test.mjs",
|
|
49
49
|
"release:check": "node scripts/github-release.mjs --check",
|
|
@@ -66,6 +66,11 @@
|
|
|
66
66
|
"install:test": "node tests/install-smoke-test.mjs",
|
|
67
67
|
"secure-file:test": "node tests/secure-file-test.mjs",
|
|
68
68
|
"architecture:test": "node tests/architecture-test.mjs",
|
|
69
|
+
"markdown:test": "node tests/markdown-test.mjs",
|
|
70
|
+
"project-metadata:test": "node tests/project-metadata-test.mjs",
|
|
71
|
+
"numbers:test": "node tests/numbers-test.mjs",
|
|
72
|
+
"records:test": "node tests/records-test.mjs",
|
|
73
|
+
"state-inventory:test": "node tests/state-inventory-test.mjs",
|
|
69
74
|
"release-state:test": "node tests/release-state-test.mjs",
|
|
70
75
|
"release-ci:test": "node tests/release-ci-test.mjs",
|
|
71
76
|
"agent-context:test": "node tests/agent-context-test.mjs",
|
|
@@ -89,7 +94,11 @@
|
|
|
89
94
|
"capability-ranking:test": "node tests/capability-ranking-test.mjs",
|
|
90
95
|
"lifecycle:test": "node tests/lifecycle-test.mjs",
|
|
91
96
|
"runtime-handlers:test": "node tests/runtime-handler-matrix-test.mjs",
|
|
92
|
-
"cli-entrypoint:test": "node tests/cli-entrypoint-test.mjs"
|
|
97
|
+
"cli-entrypoint:test": "node tests/cli-entrypoint-test.mjs",
|
|
98
|
+
"commit-message:check": "node scripts/commit-message-check.mjs",
|
|
99
|
+
"commit-message:test": "node tests/commit-message-test.mjs",
|
|
100
|
+
"tool-docs:generate": "node scripts/generate-tool-reference.mjs",
|
|
101
|
+
"tool-docs:check": "node scripts/generate-tool-reference.mjs --check"
|
|
93
102
|
},
|
|
94
103
|
"dependencies": {
|
|
95
104
|
"https-proxy-agent": "9.1.0",
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
|
|
4
|
+
const allowedTypes = Object.freeze([
|
|
5
|
+
"feat",
|
|
6
|
+
"fix",
|
|
7
|
+
"docs",
|
|
8
|
+
"refactor",
|
|
9
|
+
"perf",
|
|
10
|
+
"test",
|
|
11
|
+
"build",
|
|
12
|
+
"ci",
|
|
13
|
+
"chore",
|
|
14
|
+
"security",
|
|
15
|
+
"release",
|
|
16
|
+
"revert",
|
|
17
|
+
]);
|
|
18
|
+
const subjectPattern = new RegExp(`^(?:${allowedTypes.join("|")})(?:\\([a-z0-9][a-z0-9._/-]*\\))?!?: [^\\s].*$`);
|
|
19
|
+
const titles = readTitles(process.argv.slice(2));
|
|
20
|
+
const failures = [];
|
|
21
|
+
|
|
22
|
+
for (const title of titles) {
|
|
23
|
+
const normalized = String(title).trim();
|
|
24
|
+
if (!normalized) {
|
|
25
|
+
failures.push("commit or pull-request title is empty");
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (normalized.length > 120) failures.push(`title exceeds 120 characters: ${normalized}`);
|
|
29
|
+
if (!subjectPattern.test(normalized)) {
|
|
30
|
+
failures.push(`invalid Conventional Commit title: ${normalized}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (failures.length) {
|
|
35
|
+
console.error([
|
|
36
|
+
...failures,
|
|
37
|
+
"Expected: <type>[optional scope][optional !]: <imperative description>",
|
|
38
|
+
`Allowed types: ${allowedTypes.join(", ")}`,
|
|
39
|
+
].join("\n"));
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
} else {
|
|
42
|
+
console.log(`commit message check ok (${titles.length} title${titles.length === 1 ? "" : "s"})`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function readTitles(args) {
|
|
46
|
+
const titleIndex = args.indexOf("--title");
|
|
47
|
+
if (titleIndex !== -1) {
|
|
48
|
+
const title = args[titleIndex + 1];
|
|
49
|
+
if (title === undefined) throw new Error("--title requires a value");
|
|
50
|
+
return [title];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const rangeIndex = args.indexOf("--range");
|
|
54
|
+
if (rangeIndex !== -1) {
|
|
55
|
+
const range = args[rangeIndex + 1];
|
|
56
|
+
if (!range) throw new Error("--range requires a Git revision range");
|
|
57
|
+
return execFileSync("git", ["log", "--format=%s", range], { encoding: "utf8" })
|
|
58
|
+
.split(/\r?\n/)
|
|
59
|
+
.filter(Boolean);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return [execFileSync("git", ["log", "-1", "--format=%s"], { encoding: "utf8" }).trim()];
|
|
63
|
+
}
|
|
@@ -14,6 +14,10 @@ const tests = [
|
|
|
14
14
|
"tests/runtime-handler-matrix-test.mjs",
|
|
15
15
|
"tests/cli-entrypoint-test.mjs",
|
|
16
16
|
"tests/local-self-test.mjs",
|
|
17
|
+
"tests/numbers-test.mjs",
|
|
18
|
+
"tests/records-test.mjs",
|
|
19
|
+
"tests/project-metadata-test.mjs",
|
|
20
|
+
"tests/state-inventory-test.mjs",
|
|
17
21
|
];
|
|
18
22
|
|
|
19
23
|
try {
|
|
@@ -41,9 +45,13 @@ try {
|
|
|
41
45
|
"src/local/process-tracker.mjs": [65, 35],
|
|
42
46
|
"src/local/log.mjs": [60, 40],
|
|
43
47
|
"src/local/runtime.mjs": [60, 35],
|
|
44
|
-
"src/local/cli.mjs": [40,
|
|
48
|
+
"src/local/cli.mjs": [40, 10],
|
|
45
49
|
"src/local/cli-options.mjs": [65, 35],
|
|
46
50
|
"src/local/cli-policy.mjs": [70, 35],
|
|
51
|
+
"src/local/numbers.mjs": [100, 100],
|
|
52
|
+
"src/local/records.mjs": [100, 100],
|
|
53
|
+
"src/local/project-metadata.mjs": [85, 55],
|
|
54
|
+
"src/local/state-inventory.mjs": [80, 50],
|
|
47
55
|
"src/worker/pending-calls.ts": [85, null],
|
|
48
56
|
"src/worker/policy.ts": [90, null],
|
|
49
57
|
"src/worker/errors.ts": [100, null],
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, writeFile } from "node:fs/promises";
|
|
2
2
|
import process from "node:process";
|
|
3
|
+
import { normalizeLineEndings } from "./markdown.mjs";
|
|
3
4
|
|
|
4
5
|
const root = new URL("../", import.meta.url);
|
|
5
6
|
const contract = JSON.parse(await readFile(new URL("src/shared/policy-contract.json", root), "utf8"));
|
|
@@ -89,7 +90,3 @@ function display(value) {
|
|
|
89
90
|
if (typeof value === "boolean") return value ? "yes" : "no";
|
|
90
91
|
return String(value);
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
-
function normalizeLineEndings(value) {
|
|
94
|
-
return String(value).replace(/\r\n/g, "\n");
|
|
95
|
-
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import process from "node:process";
|
|
3
|
+
import { escapeMarkdownTableCell, normalizeLineEndings } from "./markdown.mjs";
|
|
4
|
+
|
|
5
|
+
const root = new URL("../", import.meta.url);
|
|
6
|
+
const catalogUrl = new URL("src/shared/tool-catalog.json", root);
|
|
7
|
+
const targetUrl = new URL("docs/TOOL_REFERENCE.md", root);
|
|
8
|
+
const catalog = JSON.parse(await readFile(catalogUrl, "utf8"));
|
|
9
|
+
const generated = renderToolReference(catalog);
|
|
10
|
+
|
|
11
|
+
if (process.argv.includes("--check")) {
|
|
12
|
+
const current = await readFile(targetUrl, "utf8").catch(() => "");
|
|
13
|
+
if (normalizeLineEndings(current) !== generated) {
|
|
14
|
+
console.error("docs/TOOL_REFERENCE.md is stale; run npm run tool-docs:generate");
|
|
15
|
+
process.exitCode = 1;
|
|
16
|
+
} else {
|
|
17
|
+
console.log(`tool reference is up to date (${catalog.length} tools)`);
|
|
18
|
+
}
|
|
19
|
+
} else {
|
|
20
|
+
await writeFile(targetUrl, generated, "utf8");
|
|
21
|
+
console.log(`generated docs/TOOL_REFERENCE.md (${catalog.length} tools)`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function renderToolReference(tools) {
|
|
25
|
+
const lines = [
|
|
26
|
+
"# MCP tool reference",
|
|
27
|
+
"",
|
|
28
|
+
"> Generated from `src/shared/tool-catalog.json`. Do not edit this file manually.",
|
|
29
|
+
"",
|
|
30
|
+
"This catalog is the authoritative public tool contract for names, availability classes, behavior annotations, and JSON input schemas. Runtime authorization is additionally determined by the active policy described in [POLICY_REFERENCE.md](POLICY_REFERENCE.md).",
|
|
31
|
+
"",
|
|
32
|
+
`Tool count: **${tools.length}**.`,
|
|
33
|
+
"",
|
|
34
|
+
"## Index",
|
|
35
|
+
"",
|
|
36
|
+
"| Tool | Availability | Read only | Destructive | Idempotent | Open world |",
|
|
37
|
+
"|---|---|---:|---:|---:|---:|",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
for (const tool of tools) {
|
|
41
|
+
const annotations = tool.annotations || {};
|
|
42
|
+
lines.push(`| [\`${escapeMarkdownTableCell(tool.name)}\`](#${anchor(tool.name)}) | \`${escapeMarkdownTableCell(tool.availability)}\` | ${yesNo(annotations.readOnlyHint)} | ${yesNo(annotations.destructiveHint)} | ${yesNo(annotations.idempotentHint)} | ${yesNo(annotations.openWorldHint)} |`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const tool of tools) {
|
|
46
|
+
const annotations = tool.annotations || {};
|
|
47
|
+
lines.push(
|
|
48
|
+
"",
|
|
49
|
+
`## \`${tool.name}\``,
|
|
50
|
+
"",
|
|
51
|
+
`**${tool.title}**`,
|
|
52
|
+
"",
|
|
53
|
+
tool.description,
|
|
54
|
+
"",
|
|
55
|
+
"| Contract field | Value |",
|
|
56
|
+
"|---|---|",
|
|
57
|
+
`| Availability | \`${tool.availability}\` |`,
|
|
58
|
+
`| Read-only hint | ${yesNo(annotations.readOnlyHint)} |`,
|
|
59
|
+
`| Destructive hint | ${yesNo(annotations.destructiveHint)} |`,
|
|
60
|
+
`| Idempotent hint | ${yesNo(annotations.idempotentHint)} |`,
|
|
61
|
+
`| Open-world hint | ${yesNo(annotations.openWorldHint)} |`,
|
|
62
|
+
"",
|
|
63
|
+
"### Input schema",
|
|
64
|
+
"",
|
|
65
|
+
"```json",
|
|
66
|
+
JSON.stringify(tool.inputSchema || { type: "object", additionalProperties: false }, null, 2),
|
|
67
|
+
"```",
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
lines.push(
|
|
72
|
+
"",
|
|
73
|
+
"## Change discipline",
|
|
74
|
+
"",
|
|
75
|
+
"A catalog change must update its runtime handler and protocol tests in the same pull request. CI verifies catalog uniqueness, local/Worker use of the shared catalog, handler parity, generated policy documentation, and this generated reference.",
|
|
76
|
+
"",
|
|
77
|
+
);
|
|
78
|
+
return `${lines.join("\n").trimEnd()}\n`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function anchor(name) {
|
|
82
|
+
return name.toLowerCase().replace(/[^a-z0-9_-]+/g, "-");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function yesNo(value) {
|
|
86
|
+
return value === true ? "yes" : "no";
|
|
87
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import process from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
|
+
const target = resolve(root, ".wrangler", "worker-configuration.d.ts");
|
|
9
|
+
const wrangler = resolve(root, "node_modules", "wrangler", "bin", "wrangler.js");
|
|
10
|
+
|
|
11
|
+
mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
|
|
12
|
+
const result = spawnSync(process.execPath, [wrangler, "types", target], {
|
|
13
|
+
cwd: root,
|
|
14
|
+
env: process.env,
|
|
15
|
+
stdio: "inherit",
|
|
16
|
+
windowsHide: true,
|
|
17
|
+
});
|
|
18
|
+
if (result.error) throw result.error;
|
|
19
|
+
if (result.status !== 0) process.exit(result.status ?? 1);
|
|
@@ -5,6 +5,8 @@ import { lstat, open, opendir, realpath, stat } from "node:fs/promises";
|
|
|
5
5
|
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
6
6
|
import { createBuiltinInstruction, discoverAutomaticProjectInstruction } from "./default-instructions.mjs";
|
|
7
7
|
import { automaticPackageCommands, readProjectPackageMetadata } from "./project-package.mjs";
|
|
8
|
+
import { clampInteger } from "./numbers.mjs";
|
|
9
|
+
import { isPlainRecord } from "./records.mjs";
|
|
8
10
|
|
|
9
11
|
const CONFIG_RELATIVE_PATH = join(".machine-bridge", "agent.json");
|
|
10
12
|
const GLOBAL_CONFIG_RELATIVE_PATH = join(".config", "machine-bridge-mcp", "agent.json");
|
|
@@ -42,7 +44,7 @@ export class AgentContextManager {
|
|
|
42
44
|
async agentContext(args = {}, context = {}) {
|
|
43
45
|
const state = await this.discoverState(args.path || ".", context);
|
|
44
46
|
const includeContent = args.include_instruction_content !== false;
|
|
45
|
-
const skillLimit =
|
|
47
|
+
const skillLimit = clampInteger(args.max_skills, 100, 1, MAX_SKILL_RESULTS);
|
|
46
48
|
const discoveredSkills = await this.discoverSkills(state, { maxResults: MAX_SKILL_RESULTS }, context);
|
|
47
49
|
const contextSkills = contextSkillSummaries(discoveredSkills.skills, this.displayPath, skillLimit, MAX_CONTEXT_SKILL_SUMMARY_CHARS);
|
|
48
50
|
const result = {
|
|
@@ -111,7 +113,7 @@ export class AgentContextManager {
|
|
|
111
113
|
if (task.length > 20_000) throw new Error("task exceeds 20000 characters");
|
|
112
114
|
const state = await this.discoverState(args.path || ".", context);
|
|
113
115
|
const discovered = await this.discoverSkills(state, { maxResults: MAX_SKILL_RESULTS }, context);
|
|
114
|
-
const maxSkills =
|
|
116
|
+
const maxSkills = clampInteger(args.max_skills, 10, 1, 50);
|
|
115
117
|
const skillMatches = discovered.skills
|
|
116
118
|
.map((skill) => ({ skill, score: relevanceScore(task, `${skill.name} ${skill.description}`, skill.name) }))
|
|
117
119
|
.filter((item) => item.score > 0)
|
|
@@ -169,7 +171,7 @@ export class AgentContextManager {
|
|
|
169
171
|
const state = await this.discoverState(args.path || ".", context);
|
|
170
172
|
const result = await this.discoverSkills(state, {
|
|
171
173
|
query: typeof args.query === "string" ? args.query : "",
|
|
172
|
-
maxResults:
|
|
174
|
+
maxResults: clampInteger(args.max_results, 100, 1, MAX_SKILL_RESULTS),
|
|
173
175
|
}, context);
|
|
174
176
|
return {
|
|
175
177
|
target: this.displayPath(state.target),
|
|
@@ -190,7 +192,7 @@ export class AgentContextManager {
|
|
|
190
192
|
if (matches.length > 1) throw new Error(`local skill name is ambiguous; use its id: ${requested}`);
|
|
191
193
|
const skill = matches[0];
|
|
192
194
|
const content = await readRegularUtf8(skill.entrypoint, MAX_SKILL_ENTRY_BYTES, "skill entrypoint");
|
|
193
|
-
const inventory = await listSkillFiles(skill.directory,
|
|
195
|
+
const inventory = await listSkillFiles(skill.directory, clampInteger(args.max_files, 200, 1, MAX_SKILL_FILES), context, this.throwIfCancelled);
|
|
194
196
|
return {
|
|
195
197
|
skill: publicSkill(skill, this.displayPath),
|
|
196
198
|
instructions: content.text,
|
|
@@ -381,7 +383,7 @@ export class AgentContextManager {
|
|
|
381
383
|
|
|
382
384
|
async discoverSkills(state, options = {}, context = {}) {
|
|
383
385
|
const query = String(options.query || "").trim().toLowerCase();
|
|
384
|
-
const maxResults =
|
|
386
|
+
const maxResults = clampInteger(options.maxResults, 100, 1, MAX_SKILL_RESULTS);
|
|
385
387
|
const skills = [];
|
|
386
388
|
const warnings = [];
|
|
387
389
|
const seenEntrypoints = new Set();
|
|
@@ -581,7 +583,7 @@ function normalizeCommand(value, name, configPath) {
|
|
|
581
583
|
const description = typeof value.description === "string" ? value.description.trim() : "";
|
|
582
584
|
if (!description || description.length > 1000) throw new Error(`registered command '${name}' requires a description of at most 1000 characters: ${configPath}`);
|
|
583
585
|
const cwd = value.cwd === undefined ? "." : requiredString(value.cwd, `commands.${name}.cwd`);
|
|
584
|
-
const timeoutSeconds =
|
|
586
|
+
const timeoutSeconds = clampInteger(value.timeout_seconds, 120, 1, 600);
|
|
585
587
|
if (value.timeout_seconds !== undefined && timeoutSeconds !== value.timeout_seconds) {
|
|
586
588
|
throw new Error(`registered command '${name}' timeout_seconds must be an integer from 1 to 600: ${configPath}`);
|
|
587
589
|
}
|
|
@@ -877,9 +879,6 @@ function isContainedPath(root, target) {
|
|
|
877
879
|
return rel === "" || (!rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel));
|
|
878
880
|
}
|
|
879
881
|
|
|
880
|
-
function isPlainRecord(value) {
|
|
881
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
882
|
-
}
|
|
883
882
|
|
|
884
883
|
function boundedMessage(error) {
|
|
885
884
|
return String(error?.message || error || "invalid local skill").replace(/[\r\n]+/g, " ").slice(0, 1000);
|
|
@@ -897,12 +896,6 @@ function unquoteScalar(value) {
|
|
|
897
896
|
return value;
|
|
898
897
|
}
|
|
899
898
|
|
|
900
|
-
function clampInt(value, fallback, min, max) {
|
|
901
|
-
if (value === undefined || value === null || value === "") return fallback;
|
|
902
|
-
const parsed = Number(value);
|
|
903
|
-
if (!Number.isInteger(parsed)) return fallback;
|
|
904
|
-
return Math.min(Math.max(parsed, min), max);
|
|
905
|
-
}
|
|
906
899
|
|
|
907
900
|
function sha256(value) {
|
|
908
901
|
return createHash("sha256").update(String(value)).digest("hex");
|
package/src/local/cli.mjs
CHANGED
|
@@ -13,12 +13,11 @@ import { createLocalAdminCommands } from "./cli-local-admin.mjs";
|
|
|
13
13
|
export { resolvePolicy } from "./cli-policy.mjs";
|
|
14
14
|
export { parseArgs, validateCommandOptions, validateLoggingOptions, validatePositionals } from "./cli-options.mjs";
|
|
15
15
|
import { classifyOperationalError, createLogger, normalizeLogLevel, sanitizeLogText } from "./log.mjs";
|
|
16
|
-
import { activeManagedJobs } from "./managed-jobs.mjs";
|
|
17
16
|
import { run, runWrangler } from "./shell.mjs";
|
|
18
17
|
import { runFullAccessTest } from "./full-access-test.mjs";
|
|
19
|
-
import { readBoundedRegularFileSync } from "./secure-file.mjs";
|
|
20
18
|
import { inspectProcessInstance } from "./process-identity.mjs";
|
|
21
19
|
import { stopAndRemoveAutostart } from "./service-lifecycle.mjs";
|
|
20
|
+
import { activeStateJobs, activeStateLocks, knownProfileStates, knownWorkerNames } from "./state-inventory.mjs";
|
|
22
21
|
import { createExclusiveFileSync } from "./exclusive-file.mjs";
|
|
23
22
|
import {
|
|
24
23
|
acquireMaintenanceLock,
|
|
@@ -908,37 +907,6 @@ async function deleteKnownWorkers(stateRoot) {
|
|
|
908
907
|
}
|
|
909
908
|
}
|
|
910
909
|
|
|
911
|
-
export function knownWorkerNames(stateRoot) {
|
|
912
|
-
const profiles = resolve(expandHome(stateRoot), "profiles");
|
|
913
|
-
if (!existsSync(profiles)) return [];
|
|
914
|
-
const names = new Set();
|
|
915
|
-
for (const entry of readdirSync(profiles, { withFileTypes: true })) {
|
|
916
|
-
if (!entry.isDirectory() || !/^[a-f0-9]{24}$/.test(entry.name)) continue;
|
|
917
|
-
const profileDir = resolve(profiles, entry.name);
|
|
918
|
-
const stateFile = resolve(profileDir, "state.json");
|
|
919
|
-
if (!existsSync(stateFile)) {
|
|
920
|
-
const evidence = readdirSync(profileDir).some((name) => /^state\.json\.corrupt-/.test(name) || name === "daemon.lock");
|
|
921
|
-
if (evidence) throw new Error(`cannot determine deployed Worker from profile ${entry.name}; local state was kept for inspection`);
|
|
922
|
-
continue;
|
|
923
|
-
}
|
|
924
|
-
let state;
|
|
925
|
-
try {
|
|
926
|
-
state = JSON.parse(readBoundedRegularFileSync(stateFile, 2 * 1024 * 1024).toString("utf8"));
|
|
927
|
-
} catch {
|
|
928
|
-
throw new Error(`cannot determine deployed Worker from profile ${entry.name}; local state was kept for inspection`);
|
|
929
|
-
}
|
|
930
|
-
if (!state || typeof state !== "object" || Array.isArray(state)) {
|
|
931
|
-
throw new Error(`cannot determine deployed Worker from profile ${entry.name}; local state was kept for inspection`);
|
|
932
|
-
}
|
|
933
|
-
const name = String(state?.worker?.name || "");
|
|
934
|
-
if (name && !/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(name)) {
|
|
935
|
-
throw new Error(`profile ${entry.name} contains an invalid Worker name; local state was kept for inspection`);
|
|
936
|
-
}
|
|
937
|
-
if (name) names.add(name);
|
|
938
|
-
}
|
|
939
|
-
return [...names];
|
|
940
|
-
}
|
|
941
|
-
|
|
942
910
|
async function removeAutostartBestEffort(stateRoot) {
|
|
943
911
|
const logger = structuredLogger(false);
|
|
944
912
|
try {
|
|
@@ -963,80 +931,6 @@ async function removeAutostartBestEffort(stateRoot) {
|
|
|
963
931
|
}
|
|
964
932
|
}
|
|
965
933
|
|
|
966
|
-
export function knownProfileStates(stateRoot) {
|
|
967
|
-
const canonicalStateRoot = resolve(expandHome(stateRoot));
|
|
968
|
-
const profiles = resolve(canonicalStateRoot, "profiles");
|
|
969
|
-
if (!existsSync(profiles)) return [];
|
|
970
|
-
const states = [];
|
|
971
|
-
const seen = new Set();
|
|
972
|
-
for (const entry of readdirSync(profiles, { withFileTypes: true })) {
|
|
973
|
-
if (!entry.isDirectory() || !/^[a-f0-9]{24}$/.test(entry.name)) continue;
|
|
974
|
-
const profileDir = resolve(profiles, entry.name);
|
|
975
|
-
const statePath = resolve(profileDir, "state.json");
|
|
976
|
-
const candidates = [];
|
|
977
|
-
if (existsSync(statePath)) {
|
|
978
|
-
try {
|
|
979
|
-
const value = JSON.parse(readBoundedRegularFileSync(statePath, 2 * 1024 * 1024).toString("utf8"));
|
|
980
|
-
if (typeof value?.workspace?.path === "string") candidates.push(value.workspace.path);
|
|
981
|
-
} catch {}
|
|
982
|
-
}
|
|
983
|
-
const daemonLock = resolve(profileDir, "daemon.lock");
|
|
984
|
-
const daemonOwner = readDaemonLockOwner(daemonLock);
|
|
985
|
-
if (existsSync(daemonLock) && !daemonOwner) {
|
|
986
|
-
throw new Error(`cannot inspect daemon lock for profile ${entry.name}; service definitions and state were kept`);
|
|
987
|
-
}
|
|
988
|
-
if (typeof daemonOwner?.workspace === "string") candidates.push(daemonOwner.workspace);
|
|
989
|
-
for (const candidate of candidates) {
|
|
990
|
-
try {
|
|
991
|
-
const workspace = resolveWorkspace(candidate);
|
|
992
|
-
if (seen.has(workspace)) break;
|
|
993
|
-
states.push({
|
|
994
|
-
schemaVersion: 5,
|
|
995
|
-
workspace: { path: workspace, hash: entry.name },
|
|
996
|
-
paths: { stateRoot: canonicalStateRoot, profileDir, statePath },
|
|
997
|
-
});
|
|
998
|
-
seen.add(workspace);
|
|
999
|
-
break;
|
|
1000
|
-
} catch {}
|
|
1001
|
-
}
|
|
1002
|
-
}
|
|
1003
|
-
return states;
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
function activeStateJobs(stateRoot) {
|
|
1007
|
-
const profiles = resolve(expandHome(stateRoot), "profiles");
|
|
1008
|
-
if (!existsSync(profiles)) return [];
|
|
1009
|
-
const active = [];
|
|
1010
|
-
for (const profile of readdirSync(profiles, { withFileTypes: true })) {
|
|
1011
|
-
if (!profile.isDirectory()) continue;
|
|
1012
|
-
for (const job of activeManagedJobs(resolve(profiles, profile.name, "jobs"))) {
|
|
1013
|
-
active.push({ profile: profile.name, ...job });
|
|
1014
|
-
}
|
|
1015
|
-
}
|
|
1016
|
-
return active;
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
function activeStateLocks(stateRoot) {
|
|
1020
|
-
const profiles = resolve(expandHome(stateRoot), "profiles");
|
|
1021
|
-
if (!existsSync(profiles)) return [];
|
|
1022
|
-
const active = [];
|
|
1023
|
-
for (const profile of readdirSync(profiles, { withFileTypes: true })) {
|
|
1024
|
-
if (!profile.isDirectory()) continue;
|
|
1025
|
-
for (const [kind, name] of [["daemon", "daemon.lock"], ["startup", "startup.lock"]]) {
|
|
1026
|
-
const lockPath = resolve(profiles, profile.name, name);
|
|
1027
|
-
if (!existsSync(lockPath)) continue;
|
|
1028
|
-
const owner = readDaemonLockOwner(lockPath);
|
|
1029
|
-
if (!owner) {
|
|
1030
|
-
active.push({ kind, pid: null, path: lockPath, reason: "invalid_or_unreadable_lock" });
|
|
1031
|
-
continue;
|
|
1032
|
-
}
|
|
1033
|
-
const identity = inspectProcessInstance(owner, { maxAgeMs: kind === "startup" ? 2 * 60 * 60 * 1000 : Number.POSITIVE_INFINITY });
|
|
1034
|
-
if (identity.current || (identity.alive && !identity.reclaimable)) active.push({ kind, pid: owner.pid, path: lockPath, reason: identity.reason });
|
|
1035
|
-
}
|
|
1036
|
-
}
|
|
1037
|
-
return active;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
934
|
function structuredLogger(quiet) {
|
|
1041
935
|
return createLogger({ quiet, component: "service" });
|
|
1042
936
|
}
|
|
@@ -13,13 +13,15 @@ const DEFAULT_TAKEOVER_POLL_MS = 100;
|
|
|
13
13
|
|
|
14
14
|
export async function acquireDaemonLockWithTakeover(state, options = {}) {
|
|
15
15
|
const ownerMetadata = options.ownerMetadata || {};
|
|
16
|
+
const timeoutMs = boundedPositiveInt(options.timeoutMs, DEFAULT_TAKEOVER_TIMEOUT_MS);
|
|
17
|
+
const pollMs = boundedPositiveInt(options.pollMs, DEFAULT_TAKEOVER_POLL_MS);
|
|
16
18
|
let lock = acquireDaemonLock(state, ownerMetadata);
|
|
17
19
|
if (lock.acquired || !options.takeOverServiceOwner) return lock;
|
|
18
20
|
|
|
19
21
|
const stopped = await stopWorkspaceServiceDaemon(state, {
|
|
20
22
|
owner: lock.owner,
|
|
21
|
-
timeoutMs
|
|
22
|
-
pollMs
|
|
23
|
+
timeoutMs,
|
|
24
|
+
pollMs,
|
|
23
25
|
logger: options.logger,
|
|
24
26
|
reason: "foreground startup",
|
|
25
27
|
});
|
|
@@ -29,8 +31,18 @@ export async function acquireDaemonLockWithTakeover(state, options = {}) {
|
|
|
29
31
|
throw new Error(`background daemon did not release the workspace within ${Math.ceil(stopped.timeout_ms / 1000)} seconds (${pid}); run \`machine-mcp service stop\`, verify \`machine-mcp service status\`, and retry`);
|
|
30
32
|
}
|
|
31
33
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
+
// A terminated daemon can release its token immediately before the filesystem
|
|
35
|
+
// makes the lock removal visible to this process. Retry only this handoff.
|
|
36
|
+
const handoffDeadline = Date.now() + Math.min(timeoutMs, 1_000);
|
|
37
|
+
do {
|
|
38
|
+
lock = acquireDaemonLock(state, ownerMetadata);
|
|
39
|
+
if (lock.acquired) {
|
|
40
|
+
options.logger?.info?.("background daemon stopped; foreground startup is taking over the workspace");
|
|
41
|
+
return lock;
|
|
42
|
+
}
|
|
43
|
+
if (lock.owner?.pid && isPidAlive(lock.owner.pid)) return lock;
|
|
44
|
+
await sleep(Math.min(pollMs, Math.max(1, handoffDeadline - Date.now())));
|
|
45
|
+
} while (Date.now() < handoffDeadline);
|
|
34
46
|
return lock;
|
|
35
47
|
}
|
|
36
48
|
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import {
|
|
3
|
-
import { lstat, open, opendir } from "node:fs/promises";
|
|
2
|
+
import { lstat, opendir } from "node:fs/promises";
|
|
4
3
|
import { join, relative, resolve, sep } from "node:path";
|
|
5
4
|
import { packageScriptDisplayCommand, readProjectPackageMetadata, safeVersionValue } from "./project-package.mjs";
|
|
5
|
+
import { isRegularNonSymlink, readOptionalRegularUtf8, safeSingleLine, skippableMetadataError } from "./project-metadata.mjs";
|
|
6
6
|
|
|
7
7
|
const MAX_PROJECT_CONTEXT_BYTES = 16 * 1024;
|
|
8
8
|
const MAX_SCRIPT_NAMES = 24;
|
|
@@ -208,36 +208,6 @@ async function listWorkflowFiles(root, throwIfCancelled) {
|
|
|
208
208
|
return files.sort((left, right) => left.localeCompare(right));
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
async function isRegularNonSymlink(filePath) {
|
|
212
|
-
const info = await lstat(filePath).catch((error) => skippableMetadataError(error) ? null : Promise.reject(error));
|
|
213
|
-
return Boolean(info && !info.isSymbolicLink() && info.isFile());
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
async function readOptionalRegularUtf8(filePath, maxBytes) {
|
|
217
|
-
const info = await lstat(filePath).catch((error) => skippableMetadataError(error) ? null : Promise.reject(error));
|
|
218
|
-
if (!info || info.isSymbolicLink() || !info.isFile() || info.size > maxBytes) return null;
|
|
219
|
-
const handle = await open(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0)).catch((error) => skippableMetadataError(error) ? null : Promise.reject(error));
|
|
220
|
-
if (!handle) return null;
|
|
221
|
-
try {
|
|
222
|
-
const current = await handle.stat();
|
|
223
|
-
if (!current.isFile() || current.size > maxBytes) return null;
|
|
224
|
-
const buffer = Buffer.alloc(current.size);
|
|
225
|
-
let offset = 0;
|
|
226
|
-
while (offset < buffer.length) {
|
|
227
|
-
const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
|
|
228
|
-
if (!bytesRead) break;
|
|
229
|
-
offset += bytesRead;
|
|
230
|
-
}
|
|
231
|
-
try {
|
|
232
|
-
return new TextDecoder("utf-8", { fatal: true }).decode(buffer.subarray(0, offset));
|
|
233
|
-
} catch {
|
|
234
|
-
return null;
|
|
235
|
-
}
|
|
236
|
-
} finally {
|
|
237
|
-
await handle.close();
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
211
|
function virtualInstruction(source, content, precedence, scope) {
|
|
242
212
|
return {
|
|
243
213
|
scope,
|
|
@@ -249,19 +219,6 @@ function virtualInstruction(source, content, precedence, scope) {
|
|
|
249
219
|
};
|
|
250
220
|
}
|
|
251
221
|
|
|
252
|
-
function skippableMetadataError(error) {
|
|
253
|
-
return ["ENOENT", "ENOTDIR", "EACCES", "EPERM", "ELOOP", "EBUSY"].includes(error?.code);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function safeSingleLine(value, maxLength) {
|
|
257
|
-
if (typeof value !== "string") return "";
|
|
258
|
-
return value.replace(/[\r\n\t]+/g, " ").replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
222
|
function escapeInlineCode(value) {
|
|
262
223
|
return String(value).replaceAll("`", "'");
|
|
263
224
|
}
|
|
264
|
-
|
|
265
|
-
function isPlainRecord(value) {
|
|
266
|
-
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
267
|
-
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { stat } from "node:fs/promises";
|
|
2
2
|
import { dirname, isAbsolute, relative, sep } from "node:path";
|
|
3
3
|
import { BridgeError } from "./errors.mjs";
|
|
4
|
+
import { clampInteger } from "./numbers.mjs";
|
|
4
5
|
|
|
5
6
|
export class GitService {
|
|
6
7
|
constructor({ resolveExistingPath, displayPath, runProcess, maximumBytes }) {
|
|
@@ -20,7 +21,7 @@ export class GitService {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
async diff(args = {}, context = {}) {
|
|
23
|
-
const maxBytes =
|
|
24
|
+
const maxBytes = clampInteger(args.max_bytes, 1024 * 1024, 1, this.maximumBytes);
|
|
24
25
|
const git = await this.context(args.path || ".", context);
|
|
25
26
|
if (!git.ok) return { ...git.result, path: this.displayPath(git.target) };
|
|
26
27
|
const commandArgs = ["-c", "core.fsmonitor=false", "-c", "diff.external=", "-C", git.root, "diff", "--no-ext-diff", "--no-textconv"];
|
|
@@ -33,7 +34,7 @@ export class GitService {
|
|
|
33
34
|
async log(args = {}, context = {}) {
|
|
34
35
|
const git = await this.context(args.path || ".", context);
|
|
35
36
|
if (!git.ok) return { ...git.result, path: this.displayPath(git.target) };
|
|
36
|
-
const maxCount =
|
|
37
|
+
const maxCount = clampInteger(args.max_count, 20, 1, 100);
|
|
37
38
|
const format = "%H%x1f%h%x1f%aI%x1f%an%x1f%ae%x1f%s%x1e";
|
|
38
39
|
const commandArgs = ["-c", "core.fsmonitor=false", "-C", git.root, "log", `--max-count=${maxCount}`, `--format=${format}`];
|
|
39
40
|
if (git.pathspec) commandArgs.push("--", git.pathspec);
|
|
@@ -51,7 +52,7 @@ export class GitService {
|
|
|
51
52
|
const git = await this.context(args.path || ".", context);
|
|
52
53
|
if (!git.ok) return { ...git.result, path: this.displayPath(git.target) };
|
|
53
54
|
const revision = validateRevision(args.revision || "HEAD");
|
|
54
|
-
const maxBytes =
|
|
55
|
+
const maxBytes = clampInteger(args.max_bytes, 1024 * 1024, 1, this.maximumBytes);
|
|
55
56
|
const commandArgs = ["-c", "core.fsmonitor=false", "-c", "diff.external=", "-C", git.root, "show", "--no-ext-diff", "--no-textconv", "--decorate=no", revision];
|
|
56
57
|
if (git.pathspec) commandArgs.push("--", git.pathspec);
|
|
57
58
|
const result = await this.runProcess("git", commandArgs, 60_000, true, maxBytes, context);
|
|
@@ -80,9 +81,3 @@ function validateRevision(value) {
|
|
|
80
81
|
}
|
|
81
82
|
return revision;
|
|
82
83
|
}
|
|
83
|
-
|
|
84
|
-
function clampInt(value, fallback, minimum, maximum) {
|
|
85
|
-
const parsed = Number.parseInt(String(value ?? ""), 10);
|
|
86
|
-
const number = Number.isFinite(parsed) ? parsed : fallback;
|
|
87
|
-
return Math.min(Math.max(number, minimum), maximum);
|
|
88
|
-
}
|