machine-bridge-mcp 0.9.0 → 0.11.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.
@@ -1,6 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { constants as fsConstants } from "node:fs";
2
3
  import { lstat, open, opendir, realpath, stat } from "node:fs/promises";
3
4
  import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
5
+ import { createBuiltinInstruction, discoverAutomaticProjectInstruction } from "./default-instructions.mjs";
4
6
 
5
7
  const CONFIG_RELATIVE_PATH = join(".machine-bridge", "agent.json");
6
8
  const GLOBAL_CONFIG_RELATIVE_PATH = join(".config", "machine-bridge-mcp", "agent.json");
@@ -21,7 +23,8 @@ const MAX_COMMANDS = 128;
21
23
  const MAX_COMMAND_ARGV = 128;
22
24
  const MAX_COMMAND_ARGUMENT_BYTES = 256 * 1024;
23
25
  const COMMAND_NAME_PATTERN = /^[a-z][a-z0-9._-]{0,63}$/;
24
- const CONFIG_KEYS = new Set(["version", "instruction_files", "instruction_max_bytes", "skill_roots", "commands"]);
26
+ 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", "及", "和", "的", "了", "在", "用", "使用", "进行", "根据"]);
27
+ const CONFIG_KEYS = new Set(["version", "builtin_instructions", "automatic_project_context", "model_instructions_file", "instruction_files", "instruction_max_bytes", "skill_roots", "commands"]);
25
28
  const COMMAND_KEYS = new Set(["description", "argv", "cwd", "timeout_seconds", "allow_extra_args"]);
26
29
 
27
30
  export class AgentContextManager {
@@ -44,8 +47,16 @@ export class AgentContextManager {
44
47
  const result = {
45
48
  target: this.displayPath(state.target),
46
49
  scope_root: this.displayPath(state.scopeRoot),
47
- precedence: "global guidance first, then scope root to target directory; each directory contributes the first non-empty instruction_files candidate; later directories have higher precedence",
50
+ precedence: "built-in defaults, automatic project facts, user-global guidance, then scope root to target directory; explicit files loaded later have higher precedence",
48
51
  config_files: state.configFiles.map((file) => this.displayPath(file)),
52
+ builtin_instructions: publicVirtualInstruction(state.builtinInstructions, includeContent),
53
+ automatic_project_context: publicVirtualInstruction(state.automaticProjectContext, includeContent),
54
+ model_instructions_file: state.modelInstructions ? {
55
+ path: this.displayPath(state.modelInstructions.path),
56
+ bytes: state.modelInstructions.bytes,
57
+ sha256: state.modelInstructions.sha256,
58
+ ...(includeContent ? { content: state.modelInstructions.content } : {}),
59
+ } : null,
49
60
  instruction_files: state.instructions.map((item) => ({
50
61
  scope: item.scope,
51
62
  path: this.displayPath(item.path),
@@ -60,15 +71,95 @@ export class AgentContextManager {
60
71
  instructions_truncated: state.instructionsTruncated,
61
72
  commands: publicCommands(state.commands, this.displayPath),
62
73
  guidance: [
74
+ "Apply built-in instructions and automatic project context as lower-precedence defaults; explicit global/project instruction files loaded later take precedence.",
63
75
  "Treat instruction_files as authoritative workspace guidance in the returned precedence order.",
64
76
  "Load a relevant skill with load_local_skill before following its workflow; SKILL.md is an instruction bundle, not executable code by itself.",
65
77
  "Prefer run_local_command for registered repeatable commands. Use run_process or exec_command only when no registered command fits and policy permits it.",
66
78
  ],
67
79
  };
68
- if (includeContent) result.effective_instructions = renderEffectiveInstructions(state.instructions, this.displayPath);
80
+ if (includeContent) result.effective_instructions = renderEffectiveInstructions(effectiveInstructionItems(state), this.displayPath);
69
81
  return result;
70
82
  }
71
83
 
84
+
85
+ async sessionBootstrap(args = {}, context = {}) {
86
+ const state = await this.discoverState(args.path || ".", context);
87
+ const fingerprint = capabilityFingerprint(state, []);
88
+ return {
89
+ target: this.displayPath(state.target),
90
+ instructions: renderEffectiveInstructions(effectiveInstructionItems(state), this.displayPath),
91
+ builtin_instructions: publicVirtualInstruction(state.builtinInstructions, false),
92
+ automatic_project_context: publicVirtualInstruction(state.automaticProjectContext, false),
93
+ model_instructions_file: state.modelInstructions ? this.displayPath(state.modelInstructions.path) : null,
94
+ capability_refresh: {
95
+ strategy: "resolve_task_capabilities-rescans-on-every-call",
96
+ instruction_and_command_fingerprint: fingerprint,
97
+ skills_scanned: false,
98
+ generated_at: new Date().toISOString(),
99
+ },
100
+ guidance: [
101
+ "Built-in working agreements and bounded automatic project facts are present by default unless disabled in the user-global agent config.",
102
+ "Call resolve_task_capabilities with the current user task before substantive local work or at the start of a reused-host conversation.",
103
+ "The resolver returns the effective instructions again and rescans skill and command metadata on every call; the runtime supplements application and browser capability metadata.",
104
+ ],
105
+ };
106
+ }
107
+
108
+ async resolveTaskCapabilities(args = {}, context = {}) {
109
+ const task = requiredString(args.task, "task");
110
+ if (task.length > 20_000) throw new Error("task exceeds 20000 characters");
111
+ const state = await this.discoverState(args.path || ".", context);
112
+ const discovered = await this.discoverSkills(state, { maxResults: MAX_SKILL_RESULTS }, context);
113
+ const maxSkills = clampInt(args.max_skills, 10, 1, 50);
114
+ const skillMatches = discovered.skills
115
+ .map((skill) => ({ skill, score: relevanceScore(task, `${skill.name} ${skill.description}`) }))
116
+ .filter((item) => item.score > 0)
117
+ .sort((a, b) => b.score - a.score || a.skill.name.localeCompare(b.skill.name))
118
+ .slice(0, maxSkills);
119
+ const commandMatches = [...state.commands.values()]
120
+ .map((command) => ({ command, score: relevanceScore(task, `${command.name} ${command.description} ${command.argv.join(" ")}`) }))
121
+ .filter((item) => item.score > 0)
122
+ .sort((a, b) => b.score - a.score || a.command.name.localeCompare(b.command.name))
123
+ .slice(0, 20);
124
+ const selected = skillMatches[0]?.score >= 3 ? skillMatches[0].skill : null;
125
+ let selectedSkill = null;
126
+ if (selected && args.include_selected_skill !== false) {
127
+ const content = await readRegularUtf8(selected.entrypoint, MAX_SKILL_ENTRY_BYTES, "skill entrypoint");
128
+ selectedSkill = { ...publicSkill(selected, this.displayPath), instructions: content.text };
129
+ }
130
+ const recommendedTools = recommendTools(task, commandMatches.length > 0, skillMatches.length > 0);
131
+ const refresh = capabilityFingerprint(state, discovered.skills);
132
+ return {
133
+ task,
134
+ target: this.displayPath(state.target),
135
+ effective_instructions: renderEffectiveInstructions(effectiveInstructionItems(state), this.displayPath),
136
+ builtin_instructions: publicVirtualInstruction(state.builtinInstructions, false),
137
+ automatic_project_context: publicVirtualInstruction(state.automaticProjectContext, false),
138
+ model_instructions_file: state.modelInstructions ? this.displayPath(state.modelInstructions.path) : null,
139
+ instruction_files: state.instructions.map((item) => ({ path: this.displayPath(item.path), scope: item.scope, bytes: item.bytes, sha256: item.sha256, precedence: item.precedence })),
140
+ instructions_truncated: state.instructionsTruncated,
141
+ refresh: { strategy: "rescan-on-every-call", fingerprint: refresh, generated_at: new Date().toISOString() },
142
+ selected_skill: selectedSkill,
143
+ skill_matches: skillMatches.map(({ skill, score }) => ({ ...publicSkill(skill, this.displayPath), score })),
144
+ command_matches: commandMatches.map(({ command, score }) => ({ ...publicCommands(new Map([[command.name, command]]), this.displayPath)[0], score })),
145
+ recommended_tools: recommendedTools,
146
+ host_semantics: "Machine Bridge can discover, rank, and load capabilities automatically. The MCP host remains responsible for deciding whether to call the recommended tools.",
147
+ warnings: publicSkillWarnings(discovered.warnings, this.displayPath),
148
+ truncated: discovered.truncated,
149
+ };
150
+ }
151
+
152
+ async collectModelInstructions(state, context) {
153
+ this.throwIfCancelled(context);
154
+ const path = state.modelInstructionsFile;
155
+ const info = await lstat(path).catch((error) => error?.code === "ENOENT" ? null : Promise.reject(error));
156
+ if (!info) throw new Error(`model_instructions_file does not exist: ${path}`);
157
+ if (info.isSymbolicLink() || !info.isFile()) throw new Error(`model_instructions_file must be a regular non-symbolic-link file: ${path}`);
158
+ const content = await readRegularUtf8(path, MAX_INSTRUCTION_FILE_BYTES, "model instructions file");
159
+ if (!content.text.trim()) throw new Error(`model_instructions_file is empty: ${path}`);
160
+ state.modelInstructions = { scope: "model", path, bytes: content.bytes, sha256: sha256(content.text), content: content.text, precedence: 0 };
161
+ }
162
+
72
163
  async listLocalSkills(args = {}, context = {}) {
73
164
  const state = await this.discoverState(args.path || ".", context);
74
165
  const result = await this.discoverSkills(state, {
@@ -150,31 +241,69 @@ export class AgentContextManager {
150
241
  instructionMaxBytes: DEFAULT_INSTRUCTION_MAX_BYTES,
151
242
  skillRoots: defaultSkillRoots(directories, this.home, this.policy.unrestrictedPaths === true),
152
243
  commands: new Map(),
244
+ builtinInstructionsEnabled: true,
245
+ automaticProjectContextEnabled: true,
246
+ builtinInstructions: null,
247
+ automaticProjectContext: null,
248
+ modelInstructionsFile: "",
249
+ modelInstructions: null,
153
250
  configFiles: [],
154
251
  instructions: [],
155
252
  instructionBytes: 0,
156
253
  instructionsTruncated: false,
157
254
  };
158
255
 
159
- if (this.policy.unrestrictedPaths === true && this.home) {
256
+ if (this.home) {
160
257
  const globalConfig = join(this.home, GLOBAL_CONFIG_RELATIVE_PATH);
161
258
  const config = await readOptionalConfig(globalConfig, this.home, false);
162
- if (config) this.applyConfig(state, config, globalConfig, this.home);
163
- if (this.codexHome) await this.collectDirectoryInstruction(state, this.codexHome, context, "global");
259
+ if (config) {
260
+ if (this.policy.unrestrictedPaths === true) this.applyConfig(state, config, globalConfig, this.home, { global: true });
261
+ else {
262
+ state.configFiles.push(globalConfig);
263
+ if (config.builtinInstructions !== null) state.builtinInstructionsEnabled = config.builtinInstructions;
264
+ if (config.automaticProjectContext !== null) state.automaticProjectContextEnabled = config.automaticProjectContext;
265
+ if (config.modelInstructionsFile) state.modelInstructionsFile = resolveConfiguredPath(config.modelInstructionsFile, this.home, this.home, this.workspace, true);
266
+ }
267
+ }
268
+ }
269
+
270
+ state.builtinInstructions = createBuiltinInstruction(state.builtinInstructionsEnabled);
271
+ state.automaticProjectContext = await discoverAutomaticProjectInstruction({
272
+ scopeRoot,
273
+ targetDir,
274
+ enabled: state.automaticProjectContextEnabled,
275
+ throwIfCancelled: () => this.throwIfCancelled(context),
276
+ });
277
+
278
+ if (this.home) {
279
+ if (state.modelInstructionsFile) await this.collectModelInstructions(state, context);
280
+ if (this.policy.unrestrictedPaths === true && this.codexHome) await this.collectDirectoryInstruction(state, this.codexHome, context, "global");
164
281
  }
165
282
 
166
283
  for (const directory of directories) {
167
284
  this.throwIfCancelled(context);
168
285
  const configPath = join(directory, CONFIG_RELATIVE_PATH);
169
286
  const config = await readOptionalConfig(configPath, directory, true);
170
- if (config) this.applyConfig(state, config, configPath, directory);
287
+ if (config) this.applyConfig(state, config, configPath, directory, { global: false });
171
288
  await this.collectDirectoryInstruction(state, directory, context, "project");
172
289
  }
173
290
  return state;
174
291
  }
175
292
 
176
- applyConfig(state, config, configPath, baseDir) {
293
+ applyConfig(state, config, configPath, baseDir, { global = false } = {}) {
177
294
  state.configFiles.push(configPath);
295
+ if (config.builtinInstructions !== null) {
296
+ if (!global) throw new Error(`builtin_instructions is only allowed in the global agent config: ${configPath}`);
297
+ state.builtinInstructionsEnabled = config.builtinInstructions;
298
+ }
299
+ if (config.automaticProjectContext !== null) {
300
+ if (!global) throw new Error(`automatic_project_context is only allowed in the global agent config: ${configPath}`);
301
+ state.automaticProjectContextEnabled = config.automaticProjectContext;
302
+ }
303
+ if (config.modelInstructionsFile) {
304
+ if (!global) throw new Error(`model_instructions_file is only allowed in the global agent config: ${configPath}`);
305
+ state.modelInstructionsFile = resolveConfiguredPath(config.modelInstructionsFile, baseDir, this.home, this.workspace, true);
306
+ }
178
307
  if (config.instructionFiles) state.instructionFiles = [...config.instructionFiles];
179
308
  if (config.instructionMaxBytes !== null) {
180
309
  state.instructionMaxBytes = config.instructionMaxBytes;
@@ -385,7 +514,26 @@ function normalizeConfig(value, configPath) {
385
514
  if (!isPlainRecord(value)) throw new Error(`agent config must be a JSON object: ${configPath}`);
386
515
  for (const key of Object.keys(value)) if (!CONFIG_KEYS.has(key)) throw new Error(`unknown agent config field '${key}': ${configPath}`);
387
516
  if (value.version !== 1) throw new Error(`agent config version must be 1: ${configPath}`);
388
- const result = { instructionFiles: null, instructionMaxBytes: null, skillRoots: null, commands: new Map() };
517
+ const result = {
518
+ builtinInstructions: null,
519
+ automaticProjectContext: null,
520
+ modelInstructionsFile: null,
521
+ instructionFiles: null,
522
+ instructionMaxBytes: null,
523
+ skillRoots: null,
524
+ commands: new Map(),
525
+ };
526
+ if (value.builtin_instructions !== undefined) {
527
+ if (typeof value.builtin_instructions !== "boolean") throw new Error(`builtin_instructions must be boolean: ${configPath}`);
528
+ result.builtinInstructions = value.builtin_instructions;
529
+ }
530
+ if (value.automatic_project_context !== undefined) {
531
+ if (typeof value.automatic_project_context !== "boolean") throw new Error(`automatic_project_context must be boolean: ${configPath}`);
532
+ result.automaticProjectContext = value.automatic_project_context;
533
+ }
534
+ if (value.model_instructions_file !== undefined) {
535
+ result.modelInstructionsFile = requiredString(value.model_instructions_file, "model_instructions_file");
536
+ }
389
537
  if (value.instruction_files !== undefined) {
390
538
  result.instructionFiles = validateInstructionFiles(value.instruction_files, configPath);
391
539
  }
@@ -556,6 +704,67 @@ async function listSkillFiles(root, maxFiles, context, throwIfCancelled) {
556
704
  return { files, truncated };
557
705
  }
558
706
 
707
+ function capabilityFingerprint(state, skills) {
708
+ return sha256(JSON.stringify({
709
+ configs: state.configFiles,
710
+ instructions: [
711
+ state.builtinInstructions?.sha256 || "",
712
+ state.automaticProjectContext?.sha256 || "",
713
+ state.modelInstructions?.sha256 || "",
714
+ ...state.instructions.map((item) => item.sha256),
715
+ ],
716
+ skills: skills.map((skill) => [skill.id, skill.sha256]),
717
+ commands: [...state.commands.values()].map((command) => [command.name, command.argv]),
718
+ }));
719
+ }
720
+
721
+ function relevanceScore(task, candidate) {
722
+ const taskTokens = tokenize(task);
723
+ const candidateTokens = tokenize(candidate);
724
+ if (!taskTokens.size || !candidateTokens.size) return 0;
725
+ let score = 0;
726
+ for (const token of taskTokens) {
727
+ if (candidateTokens.has(token)) score += token.length >= 6 ? 2 : 1;
728
+ }
729
+ const taskLower = task.toLowerCase();
730
+ const candidateLower = candidate.toLowerCase();
731
+ if (candidateLower.includes(taskLower) || taskLower.includes(candidateLower)) score += 4;
732
+ return score;
733
+ }
734
+
735
+ function tokenize(value) {
736
+ const text = String(value || "").toLowerCase();
737
+ const tokens = new Set();
738
+ for (const raw of text.match(/[a-z0-9_][a-z0-9_.-]{1,}/g) || []) {
739
+ const token = raw.replace(/^[.-]+|[.-]+$/g, "");
740
+ if (token.length >= 2 && !TOKEN_STOP_WORDS.has(token)) tokens.add(token);
741
+ }
742
+ for (const sequence of text.match(/[\p{Script=Han}]{1,}/gu) || []) {
743
+ if (!TOKEN_STOP_WORDS.has(sequence)) tokens.add(sequence);
744
+ const minimumSize = sequence.length === 1 ? 1 : 2;
745
+ for (let size = minimumSize; size <= Math.min(3, sequence.length); size += 1) {
746
+ for (let index = 0; index + size <= sequence.length; index += 1) {
747
+ const token = sequence.slice(index, index + size);
748
+ if (!TOKEN_STOP_WORDS.has(token)) tokens.add(token);
749
+ }
750
+ }
751
+ }
752
+ return tokens;
753
+ }
754
+
755
+ function recommendTools(task, hasCommands, hasSkills) {
756
+ const lower = task.toLowerCase();
757
+ const tools = ["agent_context"];
758
+ if (hasSkills) tools.push("load_local_skill");
759
+ if (hasCommands) tools.push("run_local_command");
760
+ if (/browser|chrome|edge|brave|网页|浏览器|表单|网站/.test(lower)) tools.push("browser_status", "browser_list_tabs", "browser_inspect_page", "browser_action", "browser_fill_form");
761
+ if (/app|application|gui|window|应用|软件|窗口|界面/.test(lower)) tools.push("list_local_applications", "inspect_local_application", "operate_local_application");
762
+ if (/git|commit|branch|diff|仓库|提交|分支/.test(lower)) tools.push("git_status", "git_diff");
763
+ if (/test|build|lint|command|terminal|测试|构建|命令|终端/.test(lower)) tools.push(hasCommands ? "run_local_command" : "run_process");
764
+ if (/file|code|source|edit|write|文件|代码|源码|修改|写入/.test(lower)) tools.push("read_file", "search_text", "edit_file", "apply_patch");
765
+ return [...new Set(tools)];
766
+ }
767
+
559
768
  function publicSkill(skill, displayPath) {
560
769
  return {
561
770
  id: skill.id,
@@ -609,12 +818,36 @@ function publicCommands(commands, displayPath) {
609
818
  }));
610
819
  }
611
820
 
821
+ function effectiveInstructionItems(state) {
822
+ return [
823
+ ...(state.builtinInstructions ? [state.builtinInstructions] : []),
824
+ ...(state.automaticProjectContext ? [state.automaticProjectContext] : []),
825
+ ...(state.modelInstructions ? [state.modelInstructions] : []),
826
+ ...state.instructions,
827
+ ];
828
+ }
829
+
830
+ function publicVirtualInstruction(item, includeContent) {
831
+ if (!item) return null;
832
+ return {
833
+ source: item.source,
834
+ scope: item.scope,
835
+ bytes: item.bytes,
836
+ sha256: item.sha256,
837
+ precedence: item.precedence,
838
+ ...(includeContent ? { content: item.content } : {}),
839
+ };
840
+ }
841
+
612
842
  function renderEffectiveInstructions(instructions, displayPath) {
613
- return instructions.map((item) => [
614
- `--- BEGIN ${displayPath(item.path)} (precedence ${item.precedence}) ---`,
615
- item.content,
616
- `--- END ${displayPath(item.path)} ---`,
617
- ].join("\n")).join("\n\n");
843
+ return instructions.map((item) => {
844
+ const source = item.source || displayPath(item.path);
845
+ return [
846
+ `--- BEGIN ${source} (precedence ${item.precedence}) ---`,
847
+ item.content,
848
+ `--- END ${source} ---`,
849
+ ].join("\n");
850
+ }).join("\n\n");
618
851
  }
619
852
 
620
853
  async function readOptionalRegularUtf8(filePath, maxBytes, label) {
@@ -626,7 +859,7 @@ async function readOptionalRegularUtf8(filePath, maxBytes, label) {
626
859
  }
627
860
 
628
861
  async function readRegularUtf8(filePath, maxBytes, label) {
629
- const handle = await open(filePath, "r");
862
+ const handle = await open(filePath, fsConstants.O_RDONLY | (fsConstants.O_NOFOLLOW || 0));
630
863
  try {
631
864
  const info = await handle.stat();
632
865
  if (!info.isFile()) throw new Error(`${label} is not a regular file: ${filePath}`);