agy-mcp-bridge 0.1.31 → 0.1.32

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,7 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { isQuotaError, isStrongQuotaError } from './quota.js';
4
- import { startProcess, terminateOwned } from '../antigravity/process.js';
4
+ import { captureOutput } from './probe.js';
5
5
  const CAPABILITIES = ['code', 'review', 'test', 'research', 'long-run'];
6
6
  const REQUIRED_FLAGS = ['--json', '--sandbox', '--cd', '--output-last-message', '--ephemeral', '--skip-git-repo-check'];
7
7
  const MAX_RESULT_BYTES = 1024 * 1024;
@@ -86,20 +86,6 @@ export function codexAdapter(command, options = {}) {
86
86
  export function codexAgent(command, status, options = {}) {
87
87
  return { adapter: codexAdapter(command, options), status, enabled: true, prefixArgs: command.prefixArgs };
88
88
  }
89
- async function capture(command, args, cwd, extraEnvPassthrough) {
90
- return new Promise((resolve, reject) => {
91
- const child = startProcess(command.executable, [...command.prefixArgs, ...args], cwd, { extraEnvPassthrough });
92
- let text = '';
93
- let exceeded = false;
94
- const timer = setTimeout(() => { exceeded = true; void terminateOwned(child).catch(reject); }, 10000);
95
- const append = (chunk) => { if (text.length < 32768)
96
- text += chunk.toString().slice(0, 32768 - text.length); };
97
- child.stdout.on('data', append);
98
- child.stderr.on('data', append);
99
- child.once('error', error => { clearTimeout(timer); reject(error); });
100
- child.once('close', code => { clearTimeout(timer); exceeded ? reject(new Error('Codex inspection timed out')) : resolve({ code, text }); });
101
- });
102
- }
103
89
  /** Non-interactive `codex exec` support is the only contract this adapter relies on. */
104
90
  export async function detectCodex(command, options = {}) {
105
91
  const cwd = options.cwd ?? process.cwd();
@@ -108,8 +94,8 @@ export async function detectCodex(command, options = {}) {
108
94
  id: 'codex', label: 'Codex', available, version, executable: command.source, capabilities: CAPABILITIES, warnings
109
95
  });
110
96
  try {
111
- const version = await capture(command, ['--version'], cwd, extra);
112
- const help = await capture(command, ['exec', '--help'], cwd, extra);
97
+ const version = await captureOutput('Codex', command, ['--version'], cwd, extra);
98
+ const help = await captureOutput('Codex', command, ['exec', '--help'], cwd, extra);
113
99
  const versionText = version.text.trim().split(/\r?\n/)[0] ?? '';
114
100
  const parsed = /codex-cli\s+([^\s]+)/i.exec(versionText)?.[1] ?? (versionText || null);
115
101
  const missing = REQUIRED_FLAGS.filter(flag => !new RegExp(`(?:^|\\s)${flag.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(?=\\s|,|$)`).test(help.text));
@@ -1,4 +1,4 @@
1
- import { startProcess, terminateOwned } from '../antigravity/process.js';
1
+ import { captureOutput } from './probe.js';
2
2
  import { isQuotaError } from './quota.js';
3
3
  export const CAPABILITIES = ['code', 'review', 'test', 'research', 'long-run'];
4
4
  // Verified against opencode v2.2.5: `opencode run` reads the message from stdin and, with `--format json`, writes one JSON event per line.
@@ -120,20 +120,6 @@ export function opencodeAdapter(command, options = {}) {
120
120
  export function opencodeAgent(command, status, options = {}) {
121
121
  return { adapter: opencodeAdapter(command, options), status, enabled: true, prefixArgs: command.prefixArgs };
122
122
  }
123
- function capture(command, args, cwd, extraEnvPassthrough) {
124
- return new Promise((resolve, reject) => {
125
- const child = startProcess(command.executable, [...command.prefixArgs, ...args], cwd, { extraEnvPassthrough });
126
- let text = '';
127
- let exceeded = false;
128
- const timer = setTimeout(() => { exceeded = true; void terminateOwned(child).catch(reject); }, 10000);
129
- const append = (chunk) => { if (text.length < 32768)
130
- text += chunk.toString().slice(0, 32768 - text.length); };
131
- child.stdout.on('data', append);
132
- child.stderr.on('data', append);
133
- child.once('error', error => { clearTimeout(timer); reject(error); });
134
- child.once('close', code => { clearTimeout(timer); exceeded ? reject(new Error('OpenCode inspection timed out')) : resolve({ code, text }); });
135
- });
136
- }
137
123
  /** Non-interactive `opencode run --format json` is the only contract this adapter relies on. */
138
124
  export async function detectOpenCode(command, opts = {}) {
139
125
  const cwd = opts.cwd ?? process.cwd();
@@ -142,8 +128,8 @@ export async function detectOpenCode(command, opts = {}) {
142
128
  id: 'opencode', label: 'OpenCode', available, version, executable: command.executable, capabilities: CAPABILITIES, warnings
143
129
  });
144
130
  try {
145
- const version = await capture(command, ['--version'], cwd, extra);
146
- const help = await capture(command, ['run', '--help'], cwd, extra);
131
+ const version = await captureOutput('OpenCode', command, ['--version'], cwd, extra);
132
+ const help = await captureOutput('OpenCode', command, ['run', '--help'], cwd, extra);
147
133
  const parsed = version.text.trim().split(/\r?\n/)[0]?.replace(/^opencode\s+/i, '').replace(/^v/i, '') || null;
148
134
  if (version.code !== 0)
149
135
  return status(false, null, ['opencode --version failed']);
@@ -0,0 +1,20 @@
1
+ import { startProcess, terminateOwned } from '../antigravity/process.js';
2
+ /**
3
+ * Runs an agent CLI once to read its output (`--version`, `<subcommand> --help`) while it is being inspected. Bounded in time (10 s) and in
4
+ * size (32 KiB of combined stdout and stderr), and the process is killed with its tree when the time runs out. `label` names the CLI in the error.
5
+ */
6
+ export function captureOutput(label, command, args, cwd, extraEnvPassthrough) {
7
+ return new Promise((resolve, reject) => {
8
+ const child = startProcess(command.executable, [...command.prefixArgs, ...args], cwd, { extraEnvPassthrough });
9
+ let text = '';
10
+ let exceeded = false;
11
+ const timer = setTimeout(() => { exceeded = true; void terminateOwned(child).catch(reject); }, 10000);
12
+ const append = (chunk) => { if (text.length < 32768)
13
+ text += chunk.toString().slice(0, 32768 - text.length); };
14
+ child.stdout.on('data', append);
15
+ child.stderr.on('data', append);
16
+ child.once('error', error => { clearTimeout(timer); reject(error); });
17
+ child.once('close', code => { clearTimeout(timer); exceeded ? reject(new Error(`${label} inspection timed out`)) : resolve({ code, text }); });
18
+ });
19
+ }
20
+ //# sourceMappingURL=probe.js.map
@@ -54,23 +54,30 @@ export async function createAgentRegistry(config, antigravity) {
54
54
  askForApproval: settings.codex_ask_for_approval
55
55
  }));
56
56
  const opencodeEnabled = enabled.includes('opencode');
57
- let opencodeStatus = {
57
+ const autoApproveWarning = (id) => `Agent "${id}" is configured to auto-approve permissions (opencode_auto_approve): tasks run with opencode --auto`;
58
+ // The OpenCode CLI is probed when the `opencode` agent is enabled, and also when an enabled opencode profile relies on it:
59
+ // OpenCode is opt-in, so a profile declared without `enabled` must not end up unavailable because the base agent is off.
60
+ const profileUsesOpencode = Object.entries(settings.profiles ?? {}).some(([id, profile]) => profile.adapter === 'opencode' && !profile.executable && (enabled.includes(id) || !settings.enabled));
61
+ let opencodeProbe = {
58
62
  id: 'opencode', label: 'OpenCode', available: false, version: null, executable: null,
59
63
  capabilities: CAPABILITIES, warnings: ['Disabled in the bridge config']
60
64
  };
61
65
  let opencodeCommand = { executable: 'opencode', prefixArgs: [], source: 'opencode' };
62
- if (opencodeEnabled) {
66
+ if (opencodeEnabled || profileUsesOpencode) {
63
67
  try {
64
68
  opencodeCommand = await resolveCommand(settings.opencode_executable ?? 'opencode');
65
- opencodeStatus = await detectOpenCode(opencodeCommand, { cwd: process.cwd(), extraEnvPassthrough: config.extra_env_passthrough });
69
+ opencodeProbe = await detectOpenCode(opencodeCommand, { cwd: process.cwd(), extraEnvPassthrough: config.extra_env_passthrough });
66
70
  }
67
71
  catch (error) {
68
- opencodeStatus = { ...opencodeStatus, warnings: [`OpenCode CLI not found: ${error instanceof Error ? error.message : 'unknown error'}`] };
72
+ opencodeProbe = { ...opencodeProbe, warnings: [`OpenCode CLI not found: ${error instanceof Error ? error.message : 'unknown error'}`] };
69
73
  }
70
74
  }
71
- const opencodeAutoWarning = 'Agent "opencode" is configured to auto-approve permissions (opencode_auto_approve): tasks run with opencode --auto';
75
+ // The base agent is only available when it was asked for; the probe above may exist just for a profile.
76
+ let opencodeStatus = opencodeEnabled
77
+ ? opencodeProbe
78
+ : { ...opencodeProbe, available: false, warnings: ['Disabled in the bridge config'] };
72
79
  if (opencodeEnabled && settings.opencode_auto_approve)
73
- opencodeStatus = { ...opencodeStatus, warnings: [...opencodeStatus.warnings, opencodeAutoWarning] };
80
+ opencodeStatus = { ...opencodeStatus, warnings: [...opencodeStatus.warnings, autoApproveWarning('opencode')] };
74
81
  agents.push(opencodeAgent(opencodeCommand, opencodeStatus, {
75
82
  model: settings.opencode_model,
76
83
  autoApprove: settings.opencode_auto_approve
@@ -79,7 +86,7 @@ export async function createAgentRegistry(config, antigravity) {
79
86
  for (const [profileId, profile] of Object.entries(settings.profiles)) {
80
87
  const isProfileEnabled = enabled.includes(profileId) || (!settings.enabled && true);
81
88
  const adapterType = profile.adapter;
82
- const label = profile.label ?? (adapterType === 'codex' ? `Codex (${profileId})` : `Antigravity (${profileId})`);
89
+ const label = profile.label ?? `${{ codex: 'Codex', antigravity: 'Antigravity', opencode: 'OpenCode' }[adapterType]} (${profileId})`;
83
90
  const profileWarnings = [];
84
91
  if (!isProfileEnabled)
85
92
  profileWarnings.push('Disabled in the bridge config');
@@ -148,8 +155,8 @@ export async function createAgentRegistry(config, antigravity) {
148
155
  else if (adapterType === 'opencode') {
149
156
  let cmd = opencodeCommand.executable;
150
157
  let prefixArgs = opencodeCommand.prefixArgs;
151
- let available = opencodeStatus.available;
152
- let version = opencodeStatus.version;
158
+ let available = opencodeProbe.available;
159
+ let version = opencodeProbe.version;
153
160
  if (profile.executable) {
154
161
  try {
155
162
  const resolved = await resolveCommand(profile.executable);
@@ -170,7 +177,7 @@ export async function createAgentRegistry(config, antigravity) {
170
177
  version: version || null, executable: cmd || null,
171
178
  capabilities: CAPABILITIES,
172
179
  warnings: isProfileEnabled
173
- ? [...opencodeStatus.warnings.filter(w => !w.includes('Disabled') && w !== opencodeAutoWarning), ...profileWarnings, ...(settings.opencode_auto_approve ? [opencodeAutoWarning.replace('"opencode"', `"${profileId}"`)] : [])]
180
+ ? [...(profile.executable ? [] : opencodeProbe.warnings.filter(w => !w.includes('Disabled'))), ...profileWarnings, ...(settings.opencode_auto_approve ? [autoApproveWarning(profileId)] : [])]
174
181
  : profileWarnings
175
182
  };
176
183
  agents.push({
package/dist/src/cli.js CHANGED
@@ -10,7 +10,7 @@ import { addSessionWorkspaces } from './config.js';
10
10
  import { recoverExecutionLock } from './tasks/execution-lock.js';
11
11
  import { renderSetup } from './setup.js';
12
12
  import { DEFAULT_TOOL_TIMEOUT_SECONDS, codexConfigPath, planCodexConfig, readCodexConfig, writeCodexConfig } from './codex-config.js';
13
- import { freebuffConfigPath, planFreebuffConfig, readFreebuffConfig, writeFreebuffConfig } from './freebuff-config.js';
13
+ import { freebuffConfigPath, isTransientInstall, planFreebuffConfig, readFreebuffConfig, writeFreebuffConfig } from './freebuff-config.js';
14
14
  import { readWait } from './wait-setting.js';
15
15
  import { configuredPort } from './http.js';
16
16
  import { offerSkillInstall, offerSkillUpdate } from './onboarding.js';
@@ -217,6 +217,8 @@ async function setup(args) {
217
217
  return;
218
218
  }
219
219
  const clients = target === 'both' ? ['codex', 'claude'] : [target];
220
+ if (target === 'freebuff' && isTransientInstall(paths.packageRoot))
221
+ console.error(`Note: this copy of ${BIN} runs from a temporary npx cache, so the stdio command printed below stops working once npm clears it. Install it globally (npm install -g ${pkg.name}) and print it again before using it.`);
220
222
  console.log(renderSetup({ clients, mcpUrl, shells: shell, bin: BIN, waitSeconds: readWait(paths.stateDirectory) ?? undefined, stdio: stdioCommand() }));
221
223
  }
222
224
  /** The command a stdio client must run for THIS installation: the interpreter and the packaged stdio entry, both absolute. */
@@ -225,6 +227,11 @@ export function stdioCommand() {
225
227
  }
226
228
  /** Shows exactly what would change in Freebuff's .agents/mcp.json and writes it only after a yes (or --yes). */
227
229
  async function applyFreebuffSetup(project, yes) {
230
+ if (isTransientInstall(paths.packageRoot)) {
231
+ console.error(`Not changed: this copy of ${BIN} runs from a temporary npx cache (${paths.packageRoot}), which npm can delete, so Freebuff's config would point at a path that stops working. Install it first: npm install -g ${pkg.name}, then run ${BIN} setup freebuff --apply again.`);
232
+ process.exitCode = 1;
233
+ return;
234
+ }
228
235
  if (project !== undefined) {
229
236
  const info = await stat(project).catch(() => null);
230
237
  if (!info?.isDirectory())
@@ -10,6 +10,11 @@ import path from 'node:path';
10
10
  * diff stays readable. The caller shows the plan and asks before anything is written, and a backup is made first.
11
11
  */
12
12
  export const SERVER_KEY = 'antigravity';
13
+ /**
14
+ * `npx` unpacks a package into a cache directory (`.../_npx/<hash>/node_modules/...`) that npm may delete at any time. An
15
+ * absolute path into it must not be written into a client's persistent config: it works today and breaks silently later.
16
+ */
17
+ export const isTransientInstall = (packageRoot) => /[\\/]_npx[\\/]/.test(packageRoot);
13
18
  /** `~/.agents/mcp.json`, or `<project>/.agents/mcp.json`: Freebuff reads the file nearest to where it runs. */
14
19
  export const freebuffConfigPath = (options = {}) => path.join(options.project ?? options.home ?? os.homedir(), '.agents', 'mcp.json');
15
20
  /** The indent the file already uses, so re-serializing it does not reformat the servers someone else wrote. */
@@ -5,6 +5,24 @@
5
5
 
6
6
  Không thay đổi hành vi hiện tại của antigravity và codex. OpenCode nằm trong **Phase 3** theo `docs/MULTI-AGENT-PLAN.md`.
7
7
 
8
+ ## Đã làm khác thiết kế ban đầu (đọc mục này trước các mục 1–3)
9
+
10
+ Các mục 1–3 bên dưới là bản thiết kế viết trước khi có code, giữ lại để biết lý do. Nơi nào mâu thuẫn với bảng này thì **bảng này đúng** (đã đối chiếu với `opencode` v2.2.5 và với code):
11
+
12
+ | Thiết kế ban đầu | Thực tế |
13
+ |---|---|
14
+ | `opencode` vào `DEFAULT_ENABLED` | **Mặc định tắt**; chỉ chạy khi `agents.enabled` liệt kê. Profile OpenCode khai báo mà bỏ `agents.enabled` vẫn được kiểm tra CLI và báo lý do nếu không dùng được |
15
+ | `opencode run <prompt>` qua argv | `opencode run --standalone --format json`, prompt qua **stdin** (argv bị giới hạn độ dài, và prompt bắt đầu bằng `-` bị đọc thành cờ) |
16
+ | không nhắc service nền | **`--standalone` bắt buộc**: mặc định `opencode run` giao việc cho service nền dùng chung của OpenCode Desktop, hủy task không dừng agent và shell của agent nằm ngoài cây process mà bridge dọn (đo ở `docs/HANDOFF.md` mục 7) |
17
+ | `OPENCODE_HOME` tách tài khoản | Biến này **không có tác dụng**; dùng `XDG_DATA_HOME` / `XDG_CONFIG_HOME` trong `profile.env` |
18
+ | model `gpt-5` | Dạng `provider/model` (xem `opencode models`), ví dụ `opencode/big-pickle` |
19
+ | `parseResult` đọc JSON đơn hoặc text thường | Đọc luồng JSONL: sự kiện `text` cuối là câu trả lời, `step_finish` cho lượt và token, `error` là thất bại |
20
+ | `autonomy: "full"` có thể thêm `--auto` | **Không bao giờ**: `autonomy` do client chọn và chỉ là chỉ dẫn. `--auto` chỉ bật bằng `agents.opencode_auto_approve` trong config của chủ máy, và hiện trong cảnh báo của `agents_list` |
21
+ | không có giới hạn log | `compactLine` bỏ `step_start` và rút `tool_use` về tên tool và trạng thái, để luồng JSON không vượt giới hạn output |
22
+ | quyền mặc định không bàn tới | OpenCode **chạy lệnh shell và sửa file mà không hỏi** kể cả không có `--auto` (đo trên v2.2.5): mô hình quyền của nó lỏng hơn mặc định của `agy` và Codex `workspace-write`. Bật nó trong `agents.enabled` là chấp nhận điều đó |
23
+
24
+ Mục 3 ("Câu hỏi cần trả lời") đã được trả lời bằng bảng trên; không còn câu hỏi mở.
25
+
8
26
  ---
9
27
 
10
28
  ## 1. Khai báo kỹ thuật
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agy-mcp-bridge",
3
- "version": "0.1.31",
3
+ "version": "0.1.32",
4
4
  "description": "Local loopback HTTP MCP bridge that delegates bounded coding tasks to multiple agent CLIs (Antigravity, Codex) with optional parallel git worktrees",
5
5
  "license": "MIT",
6
6
  "keywords": [