machine-bridge-mcp 0.12.1 → 0.13.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "machine-bridge-mcp",
3
- "version": "0.12.1",
3
+ "version": "0.13.0",
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",
@@ -42,7 +42,7 @@
42
42
  "worker:types": "wrangler types src/worker/worker-configuration.d.ts",
43
43
  "typecheck": "npm run worker:types && tsc -p tsconfig.json --noEmit",
44
44
  "syntax": "node scripts/syntax-check.mjs",
45
- "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 network-retry:test && npm run relay:test && npm run secure-file:test && npm run architecture:test && npm run typecheck && npm run syntax && npm run process-lock:test && npm run service-lifecycle:test && npm run catalog:test && npm run agent-context: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",
45
+ "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 typecheck && npm run syntax && npm run process-lock:test && npm run service-lifecycle:test && npm run catalog:test && npm run agent-context: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
46
  "worker:dry-run": "wrangler deploy --dry-run",
47
47
  "worker:integration-test": "node tests/worker-integration-test.mjs",
48
48
  "release:check": "node scripts/github-release.mjs --check",
@@ -55,6 +55,7 @@
55
55
  "full-access:test": "node tests/full-access-test.mjs",
56
56
  "atomic-fs:test": "node tests/atomic-fs-test.mjs",
57
57
  "privacy:check": "node scripts/privacy-check.mjs",
58
+ "privacy:history": "node scripts/privacy-check.mjs --history",
58
59
  "package:test": "node tests/package-test.mjs",
59
60
  "release-impact:check": "node scripts/release-impact-check.mjs",
60
61
  "release-impact:test": "node tests/release-impact-test.mjs",
@@ -65,6 +66,7 @@
65
66
  "secure-file:test": "node tests/secure-file-test.mjs",
66
67
  "architecture:test": "node tests/architecture-test.mjs",
67
68
  "release-state:test": "node tests/release-state-test.mjs",
69
+ "release-ci:test": "node tests/release-ci-test.mjs",
68
70
  "agent-context:test": "node tests/agent-context-test.mjs",
69
71
  "browser-bridge:test": "node tests/browser-bridge-test.mjs",
70
72
  "app-automation:test": "node tests/app-automation-test.mjs",
@@ -73,6 +75,8 @@
73
75
  "app-automation:live-test": "node tests/app-automation-test.mjs --live-macos"
74
76
  },
75
77
  "dependencies": {
78
+ "https-proxy-agent": "9.1.0",
79
+ "proxy-from-env": "2.1.0",
76
80
  "wrangler": "4.110.0",
77
81
  "ws": "8.21.0"
78
82
  },
@@ -10,6 +10,7 @@ import { tmpdir } from "node:os";
10
10
  import { dirname, join, resolve } from "node:path";
11
11
  import { spawnSync } from "node:child_process";
12
12
  import { runNetworkCommand } from "./network-retry.mjs";
13
+ import { requireSuccessfulCiRun } from "./release-ci.mjs";
13
14
  import { tagSyncError } from "./release-state.mjs";
14
15
  import { fileURLToPath } from "node:url";
15
16
 
@@ -124,6 +125,31 @@ function remoteTagCommit(tag) {
124
125
  return (peeled ?? direct)?.[0] ?? null;
125
126
  }
126
127
 
128
+ function assertSuccessfulCi(head) {
129
+ const text = outputNetwork("gh", [
130
+ "run",
131
+ "list",
132
+ "--workflow",
133
+ ".github/workflows/ci.yml",
134
+ "--commit",
135
+ head,
136
+ "--event",
137
+ "push",
138
+ "--limit",
139
+ "20",
140
+ "--json",
141
+ "databaseId,status,conclusion,headSha,event,createdAt,url",
142
+ ]);
143
+ let runs;
144
+ try { runs = JSON.parse(text); }
145
+ catch { fail("GitHub Actions did not return valid JSON"); }
146
+ let run;
147
+ try { run = requireSuccessfulCiRun(runs, head); }
148
+ catch (error) { fail(String(error?.message || error)); }
149
+ console.log(`GitHub Actions CI succeeded for ${head} (run ${run.databaseId}).`);
150
+ return run;
151
+ }
152
+
127
153
  function releaseInfo(tag) {
128
154
  const result = runNetwork(
129
155
  "gh",
@@ -148,6 +174,7 @@ function assertCoreSync({ requireReleaseAsset }) {
148
174
  if (head !== originMain) {
149
175
  fail(`HEAD ${head} does not match origin/main ${originMain}`);
150
176
  }
177
+ assertSuccessfulCi(head);
151
178
 
152
179
  const localCommit = localTagCommit(tag);
153
180
  const localTagError = tagSyncError({ scope: "local", tag, head, commit: localCommit });
@@ -275,6 +302,7 @@ function publishCurrent() {
275
302
  }
276
303
  runNetwork("git", ["push", "origin", "HEAD:main"]);
277
304
  }
305
+ assertSuccessfulCi(head);
278
306
 
279
307
  const existingLocal = localTagCommit(tag);
280
308
  if (existingLocal && existingLocal !== head) {
@@ -5,6 +5,11 @@ import { fileURLToPath } from "node:url";
5
5
 
6
6
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
7
7
  const selfPath = "scripts/privacy-check.mjs";
8
+ const requestedOptions = new Set(process.argv.slice(2));
9
+ for (const option of requestedOptions) {
10
+ if (option !== "--history") throw new Error(`unknown privacy-check option: ${option}`);
11
+ }
12
+ const scanHistory = requestedOptions.has("--history");
8
13
  const candidates = collectCandidateFiles(root);
9
14
  const denylist = loadDenylist(path.join(root, ".privacy-denylist"));
10
15
  const findings = [];
@@ -47,6 +52,8 @@ for (const relativePath of candidates) {
47
52
  scanDenylist(relativePath, text, denylist, findings);
48
53
  }
49
54
 
55
+ const historySummaryCounts = scanHistory ? scanReachableHistory(root, denylist, findings) : { blobs: 0, commits: 0 };
56
+
50
57
  if (findings.length) {
51
58
  for (const finding of findings.slice(0, 100)) {
52
59
  process.stderr.write(`${redactReportPath(finding.path, denylist)}:${finding.line}: ${finding.rule}\n`);
@@ -56,7 +63,8 @@ if (findings.length) {
56
63
  process.exit(1);
57
64
  }
58
65
 
59
- process.stderr.write(`privacy check ok (${candidates.length} tracked/unignored files; ${denylist.length} local denylist entries)\n`);
66
+ const historySummary = scanHistory ? `; ${historySummaryCounts.blobs} reachable history blobs; ${historySummaryCounts.commits} commit messages` : "";
67
+ process.stderr.write(`privacy check ok (${candidates.length} tracked/unignored files; ${denylist.length} local denylist entries${historySummary})\n`);
60
68
 
61
69
  function collectCandidateFiles(directory) {
62
70
  try {
@@ -87,6 +95,136 @@ function collectCandidateFiles(directory) {
87
95
  }
88
96
  }
89
97
 
98
+ function scanReachableHistory(directory, entries, out) {
99
+ let listing;
100
+ try {
101
+ listing = execFileSync("git", ["-C", directory, "rev-list", "--objects", "--all", "-z"], {
102
+ encoding: "buffer",
103
+ maxBuffer: 64 * 1024 * 1024,
104
+ stdio: ["ignore", "pipe", "pipe"],
105
+ });
106
+ } catch {
107
+ throw new Error("privacy history scan could not enumerate reachable Git objects");
108
+ }
109
+ let records;
110
+ try { records = new TextDecoder("utf-8", { fatal: true }).decode(listing).split("\0").filter(Boolean); }
111
+ catch { throw new Error("privacy history scan encountered a non-UTF-8 Git object path"); }
112
+ const objectPaths = new Map();
113
+ let objectHash = "";
114
+ for (const record of records) {
115
+ if (/^[0-9a-f]{40,64}$/.test(record)) {
116
+ objectHash = record;
117
+ continue;
118
+ }
119
+ if (!record.startsWith("path=") || !objectHash) {
120
+ objectHash = "";
121
+ continue;
122
+ }
123
+ const relativePath = record.slice(5);
124
+ if (relativePath && !relativePath.includes("\0")) {
125
+ if (!objectPaths.has(objectHash)) objectPaths.set(objectHash, new Set());
126
+ objectPaths.get(objectHash).add(relativePath);
127
+ }
128
+ objectHash = "";
129
+ }
130
+ const hashes = [...objectPaths.keys()];
131
+ let metadata;
132
+ try {
133
+ metadata = execFileSync("git", ["-C", directory, "cat-file", "--batch-check=%(objectname) %(objecttype) %(objectsize)"], {
134
+ input: `${hashes.join("\n")}\n`,
135
+ encoding: "utf8",
136
+ maxBuffer: 64 * 1024 * 1024,
137
+ stdio: ["pipe", "pipe", "pipe"],
138
+ }).trim().split(/\r?\n/);
139
+ } catch {
140
+ throw new Error("privacy history scan could not classify reachable Git objects");
141
+ }
142
+ const blobSizes = new Map();
143
+ for (const line of metadata) {
144
+ const match = /^([0-9a-f]{40,64}) blob ([0-9]+)$/.exec(line);
145
+ if (match) blobSizes.set(match[1], Number(match[2]));
146
+ }
147
+ let scanned = 0;
148
+ for (const [hash, paths] of objectPaths) {
149
+ if (!blobSizes.has(hash)) continue;
150
+ const relativePaths = [...paths].sort();
151
+ const reportPath = relativePaths[0] || "unknown";
152
+ const contentPath = relativePaths.find((relativePath) => relativePath !== selfPath) || reportPath;
153
+ for (const relativePath of relativePaths) {
154
+ if (relativePath === ".privacy-denylist") continue;
155
+ const historicalPath = `history/${hash.slice(0, 12)}/${relativePath}`;
156
+ scanDenylistPath(historicalPath, entries, out);
157
+ scanSensitivePath(historicalPath, out);
158
+ }
159
+ if (blobSizes.get(hash) > 5 * 1024 * 1024) {
160
+ out.push({ path: `history/${hash.slice(0, 12)}/${reportPath}`, line: 1, rule: "historical file exceeds privacy scanner size limit and requires manual review" });
161
+ continue;
162
+ }
163
+ let buffer;
164
+ try {
165
+ buffer = execFileSync("git", ["-C", directory, "cat-file", "blob", hash], {
166
+ encoding: "buffer",
167
+ maxBuffer: 6 * 1024 * 1024,
168
+ stdio: ["ignore", "pipe", "pipe"],
169
+ });
170
+ } catch {
171
+ out.push({ path: `history/${hash.slice(0, 12)}/${reportPath}`, line: 1, rule: "historical file content could not be read" });
172
+ continue;
173
+ }
174
+ scanned += 1;
175
+ const historicalPath = `history/${hash.slice(0, 12)}/${contentPath}`;
176
+ if (buffer.includes(0)) {
177
+ out.push({ path: historicalPath, line: 1, rule: "binary file in reachable Git history requires manual review" });
178
+ continue;
179
+ }
180
+ let historicalText;
181
+ try { historicalText = new TextDecoder("utf-8", { fatal: true }).decode(buffer); }
182
+ catch {
183
+ out.push({ path: historicalPath, line: 1, rule: "non-UTF-8 file in reachable Git history requires manual review" });
184
+ continue;
185
+ }
186
+ if (contentPath !== selfPath) scanBuiltIn(historicalPath, historicalText, out);
187
+ if (relativePaths.some((relativePath) => path.basename(relativePath).toLowerCase() === ".npmrc")) scanNpmrc(historicalPath, historicalText, out);
188
+ scanDenylist(historicalPath, historicalText, entries, out);
189
+ }
190
+ const commitCount = scanReachableCommitMessages(directory, entries, out);
191
+ return { blobs: scanned, commits: commitCount };
192
+ }
193
+
194
+ function scanReachableCommitMessages(directory, entries, out) {
195
+ let output;
196
+ try {
197
+ output = execFileSync("git", ["-C", directory, "log", "--all", "--format=%H%x00%B%x00"], {
198
+ encoding: "buffer",
199
+ maxBuffer: 64 * 1024 * 1024,
200
+ stdio: ["ignore", "pipe", "pipe"],
201
+ });
202
+ } catch {
203
+ throw new Error("privacy history scan could not read reachable commit messages");
204
+ }
205
+ let records;
206
+ try { records = new TextDecoder("utf-8", { fatal: true }).decode(output).split("\0"); }
207
+ catch { throw new Error("privacy history scan encountered a non-UTF-8 commit message"); }
208
+ let count = 0;
209
+ for (let index = 0; index + 1 < records.length; index += 2) {
210
+ const hash = records[index].trim();
211
+ const message = records[index + 1];
212
+ if (!/^[0-9a-f]{40,64}$/.test(hash)) continue;
213
+ const reportPath = `history/commit/${hash.slice(0, 12)}/message`;
214
+ const scannedMessage = removeKnownPublicAutomationTrailers(message);
215
+ scanBuiltIn(reportPath, scannedMessage, out);
216
+ scanDenylist(reportPath, scannedMessage, entries, out);
217
+ count += 1;
218
+ }
219
+ return count;
220
+ }
221
+
222
+ function removeKnownPublicAutomationTrailers(message) {
223
+ return String(message).split(/\r?\n/).filter((line) => {
224
+ return !/^(?:Signed-off-by|Co-authored-by):\s+dependabot\[bot\]\s+<[^>\s]+@github\.com>\s*$/i.test(line);
225
+ }).join("\n");
226
+ }
227
+
90
228
  function redactReportPath(relativePath, entries) {
91
229
  let shown = String(relativePath);
92
230
  for (const entry of entries) {
@@ -112,7 +250,6 @@ function scanBuiltIn(relativePath, text, out) {
112
250
  ["live payment API key", /\b(?:sk|rk|pk)_live_[A-Za-z0-9]{16,}\b/g],
113
251
  ["API secret token", /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/g],
114
252
  ["JWT-like bearer token", /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g],
115
- ["URL with embedded credentials", /https?:\/\/[^\s/@:"'<>]+:[^\s/@"'<>]+@[^\s/"'<>]+/gi],
116
253
  ["absolute macOS/Linux home path", /(?:^|[\s"'=(:,])\/(?:Users|home)\/([^/\s"'<>]+)\//gm],
117
254
  ["absolute Windows home path", /(?:^|[\s"'=(:,])\b[A-Za-z]:\\Users\\([^\\\s"'<>]+)\\/gm],
118
255
  ];
@@ -122,6 +259,11 @@ function scanBuiltIn(relativePath, text, out) {
122
259
  out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule });
123
260
  }
124
261
  }
262
+ const credentialUrl = /https?:\/\/[^\s/@:"'<>]+:[^\s/@"'<>]+@([^\s/"'<>]+)/gi;
263
+ for (const match of text.matchAll(credentialUrl)) {
264
+ if (isReservedExampleHost(stripHostPort(match[1]))) continue;
265
+ out.push({ path: relativePath, line: lineNumber(text, match.index || 0), rule: "URL with embedded credentials" });
266
+ }
125
267
  const emailLike = /\b[A-Z0-9._%+-]+@([A-Z0-9.-]+\.[A-Z]{2,})\b/gi;
126
268
  for (const match of text.matchAll(emailLike)) {
127
269
  if (isReservedExampleHost(match[1]) || String(match[1]).toLowerCase().endsWith(".noreply.github.com")) continue;
@@ -215,6 +357,12 @@ function hasNearbySshContext(text, offset) {
215
357
  return /\b(?:ssh|scp|sftp)\b/i.test(text.slice(start, end));
216
358
  }
217
359
 
360
+ function stripHostPort(host) {
361
+ const value = String(host || "");
362
+ if (value.startsWith("[")) return value.slice(1, value.indexOf("]") > 0 ? value.indexOf("]") : undefined);
363
+ return value.replace(/:[0-9]+$/, "");
364
+ }
365
+
218
366
  function isReservedExampleHost(host) {
219
367
  const value = String(host || "").toLowerCase().replace(/\.$/, "");
220
368
  return value === "localhost"
@@ -0,0 +1,18 @@
1
+ export function requireSuccessfulCiRun(runs, head) {
2
+ if (!Array.isArray(runs)) throw new Error("GitHub Actions response is not an array");
3
+ if (!/^[0-9a-f]{40,64}$/i.test(String(head || ""))) throw new Error("release commit SHA is invalid");
4
+ const matching = runs
5
+ .filter((run) => run && run.headSha === head && run.event === "push")
6
+ .sort((left, right) => String(right.createdAt || "").localeCompare(String(left.createdAt || "")));
7
+ const run = matching[0];
8
+ if (!run) {
9
+ throw new Error(`no push-triggered CI run exists for release commit ${head}; wait for GitHub Actions to register the run and retry`);
10
+ }
11
+ if (run.status !== "completed") {
12
+ throw new Error(`CI run ${run.databaseId || "unknown"} for release commit ${head} is ${run.status || "unknown"}; wait for completion and retry`);
13
+ }
14
+ if (run.conclusion !== "success") {
15
+ throw new Error(`CI run ${run.databaseId || "unknown"} for release commit ${head} concluded ${run.conclusion || "unknown"}; fix or rerun CI before release`);
16
+ }
17
+ return run;
18
+ }
@@ -3,6 +3,7 @@ import { constants as fsConstants } from "node:fs";
3
3
  import { lstat, open, opendir, realpath, stat } from "node:fs/promises";
4
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
5
  import { createBuiltinInstruction, discoverAutomaticProjectInstruction } from "./default-instructions.mjs";
6
+ import { automaticPackageCommands, readProjectPackageMetadata } from "./project-package.mjs";
6
7
 
7
8
  const CONFIG_RELATIVE_PATH = join(".machine-bridge", "agent.json");
8
9
  const GLOBAL_CONFIG_RELATIVE_PATH = join(".config", "machine-bridge-mcp", "agent.json");
@@ -24,6 +25,35 @@ const MAX_COMMAND_ARGV = 128;
24
25
  const MAX_COMMAND_ARGUMENT_BYTES = 256 * 1024;
25
26
  const COMMAND_NAME_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/;
26
27
  const TOKEN_STOP_WORDS = new Set(["the", "and", "for", "with", "from", "this", "that", "use", "using", "into", "of", "to", "a", "an", "in", "on", "is", "are", "or", "及", "和", "的", "了", "在", "用", "使用", "进行", "根据"]);
28
+ const ENGLISH_TOKEN_CANONICAL = new Map([
29
+ ["created", "create"], ["creating", "create"], ["creation", "create"], ["creator", "create"],
30
+ ["improved", "improve"], ["improving", "improve"], ["improvement", "improve"],
31
+ ["installed", "install"], ["installing", "install"], ["installation", "install"], ["installer", "install"],
32
+ ["searched", "search"], ["searching", "search"], ["finder", "find"],
33
+ ["documentation", "docs"], ["document", "docs"], ["documents", "docs"],
34
+ ["validated", "verify"], ["validate", "verify"], ["validation", "verify"], ["verification", "verify"],
35
+ ["factcheck", "verify"], ["fact-check", "verify"], ["testing", "test"], ["tests", "test"],
36
+ ["building", "build"], ["built", "build"], ["deployment", "deploy"], ["deployed", "deploy"],
37
+ ]);
38
+ const HAN_TOKEN_ALIASES = Object.freeze([
39
+ [/创建|新建|编写/u, ["create", "creator"]],
40
+ [/改进|优化|更新|维护/u, ["improve", "update"]],
41
+ [/技能/u, ["skill"]],
42
+ [/安装/u, ["install", "installer"]],
43
+ [/部署/u, ["deploy", "deployment"]],
44
+ [/查找|搜索|检索/u, ["search", "find"]],
45
+ [/最新|当前/u, ["latest", "current"]],
46
+ [/官方/u, ["official"]],
47
+ [/文档|资料/u, ["docs", "documentation"]],
48
+ [/事实核查|核查|验证|校验/u, ["verify", "factcheck"]],
49
+ [/测试/u, ["test"]],
50
+ [/前端/u, ["frontend"]],
51
+ [/设计/u, ["design"]],
52
+ [/邮件|邮箱/u, ["email"]],
53
+ [/浏览器|网页|网站/u, ["browser", "web"]],
54
+ [/性能/u, ["performance"]],
55
+ [/安全|漏洞/u, ["security", "audit"]],
56
+ ]);
27
57
  const CONFIG_KEYS = new Set(["version", "builtin_instructions", "automatic_project_context", "model_instructions_file", "instruction_files", "instruction_max_bytes", "skill_roots", "commands"]);
28
58
  const COMMAND_KEYS = new Set(["description", "argv", "cwd", "timeout_seconds", "allow_extra_args"]);
29
59
 
@@ -117,7 +147,7 @@ export class AgentContextManager {
117
147
  .sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name))
118
148
  .slice(0, maxSkills);
119
149
  const commandMatches = [...state.commands.values()]
120
- .map((command) => ({ command, score: relevanceScore(task, `${command.name} ${command.description} ${command.argv.join(" ")}`, command.name) }))
150
+ .map((command) => ({ command, score: relevanceScore(task, commandMatchText(command), command.name) }))
121
151
  .filter((item) => item.score > 0)
122
152
  .sort((a, b) => b.score - a.score || a.command.name.localeCompare(b.command.name))
123
153
  .slice(0, 20);
@@ -127,7 +157,11 @@ export class AgentContextManager {
127
157
  const content = await readRegularUtf8(selected.entrypoint, MAX_SKILL_ENTRY_BYTES, "skill entrypoint");
128
158
  selectedSkill = { ...publicSkill(selected, this.displayPath), instructions: content.text };
129
159
  }
130
- const recommendedTools = recommendTools(task, commandMatches.length > 0, skillMatches.length > 0);
160
+ const recommendedTools = recommendTools(task, {
161
+ commandsAvailable: state.commands.size > 0,
162
+ commandRelevant: (commandMatches[0]?.score || 0) >= 3,
163
+ skillRelevant: Boolean(selected),
164
+ });
131
165
  const refresh = capabilityFingerprint(state, discovered.skills);
132
166
  return {
133
167
  task,
@@ -239,7 +273,7 @@ export class AgentContextManager {
239
273
  scopeRoot,
240
274
  instructionFiles: [...DEFAULT_INSTRUCTION_FILES],
241
275
  instructionMaxBytes: DEFAULT_INSTRUCTION_MAX_BYTES,
242
- skillRoots: defaultSkillRoots(directories, this.home, this.policy.unrestrictedPaths === true),
276
+ skillRoots: defaultSkillRoots(directories, this.home, this.codexHome, this.policy.unrestrictedPaths === true),
243
277
  commands: new Map(),
244
278
  builtinInstructionsEnabled: true,
245
279
  automaticProjectContextEnabled: true,
@@ -253,6 +287,9 @@ export class AgentContextManager {
253
287
  instructionsTruncated: false,
254
288
  };
255
289
 
290
+ const packageMetadata = await readProjectPackageMetadata(scopeRoot, () => this.throwIfCancelled(context));
291
+ state.commands = automaticPackageCommands(packageMetadata, scopeRoot);
292
+
256
293
  if (this.home) {
257
294
  const globalConfig = join(this.home, GLOBAL_CONFIG_RELATIVE_PATH);
258
295
  const config = await readOptionalConfig(globalConfig, this.home, false);
@@ -488,9 +525,14 @@ function directoriesBetween(root, leaf) {
488
525
  return result.reverse();
489
526
  }
490
527
 
491
- function defaultSkillRoots(directories, home, unrestricted) {
492
- const roots = [...directories].reverse().map((directory) => resolve(directory, ".agents", "skills"));
528
+ function defaultSkillRoots(directories, home, codexHome, unrestricted) {
529
+ const roots = [];
530
+ for (const directory of [...directories].reverse()) {
531
+ roots.push(resolve(directory, ".agents", "skills"));
532
+ roots.push(resolve(directory, ".codex", "skills"));
533
+ }
493
534
  if (unrestricted && home) roots.push(resolve(home, ".agents", "skills"));
535
+ if (unrestricted && codexHome) roots.push(resolve(codexHome, "skills"));
494
536
  if (unrestricted && process.platform !== "win32") roots.push(resolve("/etc/codex/skills"));
495
537
  return [...new Set(roots)];
496
538
  }
@@ -718,13 +760,19 @@ function capabilityFingerprint(state, skills) {
718
760
  }));
719
761
  }
720
762
 
763
+ function commandMatchText(command) {
764
+ return `${command.name} ${command.description} ${command.argv.join(" ")} ${command.searchTerms || ""}`;
765
+ }
766
+
721
767
  function relevanceScore(task, candidate, identity = "") {
722
768
  const taskTokens = tokenize(task);
723
769
  const candidateTokens = tokenize(candidate);
724
770
  if (!taskTokens.size || !candidateTokens.size) return 0;
771
+ const identityTokens = tokenize(identity);
725
772
  let score = 0;
726
773
  for (const token of taskTokens) {
727
774
  if (candidateTokens.has(token)) score += token.length >= 6 ? 2 : 1;
775
+ if (identityTokens.has(token)) score += token.length >= 6 ? 4 : 3;
728
776
  }
729
777
  const taskComparable = comparableText(task);
730
778
  const candidateComparable = comparableText(candidate);
@@ -747,33 +795,40 @@ function tokenize(value) {
747
795
  const tokens = new Set();
748
796
  for (const raw of text.match(/[a-z0-9_][a-z0-9_.-]{1,}/g) || []) {
749
797
  const token = raw.replace(/^[.-]+|[.-]+$/g, "");
750
- if (token.length >= 2 && !TOKEN_STOP_WORDS.has(token)) tokens.add(token);
751
- for (const part of token.split(/[._-]+/)) {
752
- if (part.length >= 2 && !TOKEN_STOP_WORDS.has(part)) tokens.add(part);
753
- }
798
+ addToken(tokens, token);
799
+ for (const part of token.split(/[._-]+/)) addToken(tokens, part);
754
800
  }
755
801
  for (const sequence of text.match(/[\p{Script=Han}]{1,}/gu) || []) {
756
- if (!TOKEN_STOP_WORDS.has(sequence)) tokens.add(sequence);
802
+ addToken(tokens, sequence);
757
803
  const minimumSize = sequence.length === 1 ? 1 : 2;
758
804
  for (let size = minimumSize; size <= Math.min(3, sequence.length); size += 1) {
759
- for (let index = 0; index + size <= sequence.length; index += 1) {
760
- const token = sequence.slice(index, index + size);
761
- if (!TOKEN_STOP_WORDS.has(token)) tokens.add(token);
762
- }
805
+ for (let index = 0; index + size <= sequence.length; index += 1) addToken(tokens, sequence.slice(index, index + size));
763
806
  }
764
807
  }
808
+ for (const [pattern, aliases] of HAN_TOKEN_ALIASES) {
809
+ if (!pattern.test(text)) continue;
810
+ for (const alias of aliases) addToken(tokens, alias);
811
+ }
765
812
  return tokens;
766
813
  }
767
814
 
768
- function recommendTools(task, hasCommands, hasSkills) {
815
+ function addToken(tokens, raw) {
816
+ const token = String(raw || "").replace(/^[.-]+|[.-]+$/g, "");
817
+ if (token.length < 2 || TOKEN_STOP_WORDS.has(token)) return;
818
+ tokens.add(token);
819
+ const canonical = ENGLISH_TOKEN_CANONICAL.get(token);
820
+ if (canonical) tokens.add(canonical);
821
+ }
822
+
823
+ function recommendTools(task, { commandsAvailable, commandRelevant, skillRelevant }) {
769
824
  const lower = task.toLowerCase();
770
825
  const tools = ["agent_context"];
771
- if (hasSkills) tools.push("load_local_skill");
772
- if (hasCommands) tools.push("run_local_command");
826
+ if (skillRelevant) tools.push("load_local_skill");
827
+ if (commandRelevant) tools.push("run_local_command");
773
828
  if (/browser|chrome|edge|brave|网页|浏览器|表单|网站/.test(lower)) tools.push("browser_status", "browser_list_tabs", "browser_inspect_page", "browser_action", "browser_fill_form");
774
829
  if (/app|application|gui|window|应用|软件|窗口|界面/.test(lower)) tools.push("list_local_applications", "inspect_local_application", "operate_local_application");
775
830
  if (/git|commit|branch|diff|仓库|提交|分支/.test(lower)) tools.push("git_status", "git_diff");
776
- if (/test|build|lint|command|terminal|测试|构建|命令|终端/.test(lower)) tools.push(hasCommands ? "run_local_command" : "run_process");
831
+ if (/test|build|lint|command|terminal|测试|构建|命令|终端/.test(lower)) tools.push(commandsAvailable ? "run_local_command" : "run_process");
777
832
  if (/file|code|source|edit|write|文件|代码|源码|修改|写入/.test(lower)) tools.push("read_file", "search_text", "edit_file", "apply_patch");
778
833
  return [...new Set(tools)];
779
834
  }
@@ -828,6 +883,8 @@ function publicCommands(commands, displayPath) {
828
883
  timeout_seconds: command.timeoutSeconds,
829
884
  allow_extra_args: command.allowExtraArgs,
830
885
  source: displayPath(command.source),
886
+ source_type: command.sourceType || "agent-config",
887
+ ...(command.script ? { package_script: command.script } : {}),
831
888
  }));
832
889
  }
833
890
 
@@ -9,9 +9,10 @@ const MAX_UI_ELEMENTS = 500;
9
9
  const MAX_UI_DEPTH = 12;
10
10
  const MAX_TEXT_CHARS = 4000;
11
11
  const APP_NAME_PATTERN = /^[^\0\r\n]{1,300}$/;
12
+ const DEFAULT_APPLICATION_CACHE_MS = 30_000;
12
13
 
13
14
  export class AppAutomationManager {
14
- constructor({ policy, displayPath, runProcess, readResourceText, throwIfCancelled = () => {}, platform = process.platform, home = homedir(), applicationRoots = null }) {
15
+ constructor({ policy, displayPath, runProcess, readResourceText, throwIfCancelled = () => {}, platform = process.platform, home = homedir(), applicationRoots = null, applicationCacheMs = DEFAULT_APPLICATION_CACHE_MS, now = () => Date.now() }) {
15
16
  this.policy = policy || {};
16
17
  this.displayPath = displayPath;
17
18
  this.runProcess = runProcess;
@@ -20,6 +21,9 @@ export class AppAutomationManager {
20
21
  this.platform = platform;
21
22
  this.home = home;
22
23
  this.applicationRoots = applicationRoots;
24
+ this.applicationCacheMs = Math.max(0, Number(applicationCacheMs) || 0);
25
+ this.now = now;
26
+ this.applicationCache = null;
23
27
  }
24
28
 
25
29
  capabilities() {
@@ -57,6 +61,16 @@ export class AppAutomationManager {
57
61
  }
58
62
 
59
63
  async discoverApplications(context = {}) {
64
+ const now = this.now();
65
+ if (this.applicationCache && now - this.applicationCache.createdAt < this.applicationCacheMs) {
66
+ return this.applicationCache.result;
67
+ }
68
+ const result = await this.scanApplications(context);
69
+ this.applicationCache = { createdAt: this.now(), result };
70
+ return result;
71
+ }
72
+
73
+ async scanApplications(context = {}) {
60
74
  if (this.platform === "darwin") {
61
75
  return listMacApplications(this.applicationRoots || ["/Applications", "/System/Applications", join(this.home, "Applications")], context, this.throwIfCancelled);
62
76
  }
@@ -5,8 +5,11 @@ const WAIT_BUFFER = new Int32Array(new SharedArrayBuffer(4));
5
5
 
6
6
  export function replaceFileSync(source, target, options = {}) {
7
7
  const rename = typeof options.rename === "function" ? options.rename : renameSync;
8
- const attempts = clampInteger(options.attempts, 8, 1, 32);
8
+ const sleep = typeof options.sleep === "function" ? options.sleep : sleepSync;
9
+ const random = typeof options.random === "function" ? options.random : Math.random;
10
+ const attempts = clampInteger(options.attempts, 16, 1, 64);
9
11
  const baseDelayMs = clampInteger(options.baseDelayMs, 15, 0, 1000);
12
+ const maxDelayMs = Math.max(baseDelayMs, clampInteger(options.maxDelayMs, 250, 0, 2000));
10
13
  let lastError;
11
14
  for (let attempt = 1; attempt <= attempts; attempt += 1) {
12
15
  try {
@@ -15,7 +18,7 @@ export function replaceFileSync(source, target, options = {}) {
15
18
  } catch (error) {
16
19
  lastError = error;
17
20
  if (!isTransientReplaceError(error) || attempt === attempts) throw error;
18
- sleepSync(Math.min(baseDelayMs * attempt, 250));
21
+ sleep(retryDelayMs(attempt, baseDelayMs, maxDelayMs, random));
19
22
  }
20
23
  }
21
24
  throw lastError || new Error("atomic file replacement failed");
@@ -25,6 +28,15 @@ export function isTransientReplaceError(error) {
25
28
  return TRANSIENT_REPLACE_ERRORS.has(String(error?.code || ""));
26
29
  }
27
30
 
31
+ function retryDelayMs(attempt, baseDelayMs, maxDelayMs, random) {
32
+ if (baseDelayMs <= 0 || maxDelayMs <= 0) return 0;
33
+ const exponential = Math.min(baseDelayMs * (2 ** Math.min(attempt - 1, 5)), maxDelayMs);
34
+ const randomValue = Number(random());
35
+ const normalizedRandom = Number.isFinite(randomValue) ? Math.min(Math.max(randomValue, 0), 1) : 0;
36
+ const jitter = Math.floor(exponential * 0.5 * normalizedRandom);
37
+ return Math.min(exponential + jitter, maxDelayMs);
38
+ }
39
+
28
40
  function sleepSync(milliseconds) {
29
41
  if (milliseconds <= 0) return;
30
42
  Atomics.wait(WAIT_BUFFER, 0, 0, milliseconds);
@@ -355,7 +355,9 @@ export class BrowserBridgeManager {
355
355
  if (settled) return;
356
356
  settled = true;
357
357
  clearTimeout(timer);
358
- if (!ok) try { ws.close(); } catch {}
358
+ if (!ok) {
359
+ try { ws.terminate(); } catch { try { ws.close(); } catch {} }
360
+ }
359
361
  resolvePromise(ok);
360
362
  };
361
363
  ws.on("message", (data) => {
@@ -0,0 +1,56 @@
1
+ import { createHmac, randomBytes } from "node:crypto";
2
+
3
+ export class CapabilityObserver {
4
+ constructor(now = () => Date.now(), taskFingerprintKey = randomBytes(32)) {
5
+ this.now = now;
6
+ this.taskFingerprintKey = Buffer.from(taskFingerprintKey);
7
+ this.bootstrapCount = 0;
8
+ this.resolutionCount = 0;
9
+ this.lastBootstrap = null;
10
+ this.lastResolution = null;
11
+ }
12
+
13
+ recordBootstrap(result) {
14
+ this.bootstrapCount += 1;
15
+ this.lastBootstrap = {
16
+ observed_at: timestamp(this.now()),
17
+ target: result?.target || ".",
18
+ instruction_bytes: Buffer.byteLength(String(result?.instructions || "")),
19
+ builtin_loaded: Boolean(result?.builtin_instructions),
20
+ automatic_project_context_loaded: Boolean(result?.automatic_project_context),
21
+ global_model_instructions_loaded: Boolean(result?.model_instructions_file),
22
+ refresh_fingerprint: String(result?.capability_refresh?.instruction_and_command_fingerprint || ""),
23
+ };
24
+ }
25
+
26
+ recordResolution(task, result) {
27
+ this.resolutionCount += 1;
28
+ this.lastResolution = {
29
+ observed_at: timestamp(this.now()),
30
+ task_fingerprint: createHmac("sha256", this.taskFingerprintKey).update(String(task || "")).digest("hex"),
31
+ target: result?.target || ".",
32
+ selected_skill: result?.selected_skill?.name || null,
33
+ matched_skills: Array.isArray(result?.skill_matches) ? result.skill_matches.length : 0,
34
+ matched_commands: Array.isArray(result?.command_matches) ? result.command_matches.length : 0,
35
+ matched_applications: Array.isArray(result?.application_matches) ? result.application_matches.length : 0,
36
+ recommended_tools: Array.isArray(result?.recommended_tools) ? [...result.recommended_tools] : [],
37
+ refresh_fingerprint: String(result?.refresh?.fingerprint || ""),
38
+ };
39
+ }
40
+
41
+ snapshot() {
42
+ return {
43
+ bootstrap_observed: this.bootstrapCount > 0,
44
+ bootstrap_count: this.bootstrapCount,
45
+ task_resolution_observed: this.resolutionCount > 0,
46
+ task_resolution_count: this.resolutionCount,
47
+ last_bootstrap: this.lastBootstrap ? structuredClone(this.lastBootstrap) : null,
48
+ last_task_resolution: this.lastResolution ? structuredClone(this.lastResolution) : null,
49
+ enforcement_boundary: "The server can discover, rank, load, and report capabilities; the MCP host decides whether to call the resolver or recommended tools.",
50
+ };
51
+ }
52
+ }
53
+
54
+ function timestamp(milliseconds) {
55
+ return new Date(Number(milliseconds)).toISOString();
56
+ }