@fitlab-ai/agent-infra 0.8.1 → 0.8.2

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/README.md CHANGED
@@ -16,7 +16,7 @@
16
16
  <a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/v/@fitlab-ai/agent-infra" alt="npm version"></a>
17
17
  <a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/dm/@fitlab-ai/agent-infra" alt="npm downloads"></a>
18
18
  <a href="License.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
19
- <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-brightgreen?logo=node.js" alt="Node.js >= 22"></a>
19
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22.9.0-brightgreen?logo=node.js" alt="Node.js >= 22.9.0"></a>
20
20
  <a href="https://github.com/fitlab-ai/agent-infra/releases"><img src="https://img.shields.io/github/v/release/fitlab-ai/agent-infra" alt="GitHub release"></a>
21
21
  <a href="https://codecov.io/gh/fitlab-ai/agent-infra"><img src="https://codecov.io/gh/fitlab-ai/agent-infra/graph/badge.svg" alt="codecov"></a>
22
22
  <a href="CONTRIBUTING.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
package/README.zh-CN.md CHANGED
@@ -16,7 +16,7 @@
16
16
  <a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/v/@fitlab-ai/agent-infra" alt="npm version"></a>
17
17
  <a href="https://www.npmjs.com/package/@fitlab-ai/agent-infra"><img src="https://img.shields.io/npm/dm/@fitlab-ai/agent-infra" alt="npm downloads"></a>
18
18
  <a href="License.txt"><img src="https://img.shields.io/badge/License-MIT-blue.svg" alt="License: MIT"></a>
19
- <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22-brightgreen?logo=node.js" alt="Node.js >= 22"></a>
19
+ <a href="https://nodejs.org/"><img src="https://img.shields.io/badge/Node.js-%3E%3D22.9.0-brightgreen?logo=node.js" alt="Node.js >= 22.9.0"></a>
20
20
  <a href="https://github.com/fitlab-ai/agent-infra/releases"><img src="https://img.shields.io/github/v/release/fitlab-ai/agent-infra" alt="GitHub release"></a>
21
21
  <a href="https://codecov.io/gh/fitlab-ai/agent-infra"><img src="https://codecov.io/gh/fitlab-ai/agent-infra/graph/badge.svg" alt="codecov"></a>
22
22
  <a href="CONTRIBUTING.zh-CN.md"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg" alt="PRs Welcome"></a>
package/bin/cli.ts CHANGED
@@ -2,10 +2,10 @@
2
2
  import { VERSION } from '../lib/version.ts';
3
3
 
4
4
  // Node.js version check
5
- const [major = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
6
- if (major < 22) {
5
+ const [major = 0, minor = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
6
+ if (major < 22 || (major === 22 && minor < 9)) {
7
7
  process.stderr.write(
8
- `agent-infra requires Node.js >= 22 (current: ${process.version})\n`
8
+ `agent-infra requires Node.js >= 22.9.0 (current: ${process.version})\n`
9
9
  );
10
10
  process.exit(1);
11
11
  }
package/dist/bin/cli.js CHANGED
@@ -9,9 +9,9 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
9
9
  };
10
10
  import { VERSION } from "../lib/version.js";
11
11
  // Node.js version check
12
- const [major = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
13
- if (major < 22) {
14
- process.stderr.write(`agent-infra requires Node.js >= 22 (current: ${process.version})\n`);
12
+ const [major = 0, minor = 0] = process.versions.node.split('.').map((part) => parseInt(part, 10));
13
+ if (major < 22 || (major === 22 && minor < 9)) {
14
+ process.stderr.write(`agent-infra requires Node.js >= 22.9.0 (current: ${process.version})\n`);
15
15
  process.exit(1);
16
16
  }
17
17
  const USAGE = `agent-infra ${VERSION} - bootstrap AI collaboration infrastructure
@@ -8,6 +8,7 @@
8
8
  "node22"
9
9
  ],
10
10
  "tools": [
11
+ "agent-infra",
11
12
  "claude-code",
12
13
  "codex",
13
14
  "gemini-cli",
@@ -678,6 +678,38 @@ function resolveHostCatalogPath(value, hostHomeDir) {
678
678
  return null;
679
679
  }
680
680
  }
681
+ const CODEX_DISABLED_FEATURE_FLAGS = ['apps', 'enable_mcp_apps'];
682
+ function removeCodexMcpServers(sandboxParsed) {
683
+ if (!Object.hasOwn(sandboxParsed, 'mcp_servers')) {
684
+ return false;
685
+ }
686
+ delete sandboxParsed.mcp_servers;
687
+ return true;
688
+ }
689
+ function inheritDisabledCodexFeatureFlags(sandboxParsed, hostParsed) {
690
+ if (!isJsonObjectRecord(hostParsed.features)) {
691
+ return false;
692
+ }
693
+ if (Object.hasOwn(sandboxParsed, 'features') && !isJsonObjectRecord(sandboxParsed.features)) {
694
+ return false;
695
+ }
696
+ let sandboxFeatures = sandboxParsed.features;
697
+ let changed = false;
698
+ for (const key of CODEX_DISABLED_FEATURE_FLAGS) {
699
+ if (hostParsed.features[key] !== false) {
700
+ continue;
701
+ }
702
+ if (!sandboxFeatures) {
703
+ sandboxFeatures = {};
704
+ sandboxParsed.features = sandboxFeatures;
705
+ }
706
+ if (sandboxFeatures[key] !== false) {
707
+ sandboxFeatures[key] = false;
708
+ changed = true;
709
+ }
710
+ }
711
+ return changed;
712
+ }
681
713
  export function ensureCodexModelInheritance(toolDir, hostHomeDir, containerCodexDir = '/home/devuser/.codex') {
682
714
  if (!hostHomeDir) {
683
715
  return;
@@ -704,12 +736,13 @@ export function ensureCodexModelInheritance(toolDir, hostHomeDir, containerCodex
704
736
  return;
705
737
  }
706
738
  }
739
+ let changed = removeCodexMcpServers(sandboxParsed);
740
+ changed = inheritDisabledCodexFeatureFlags(sandboxParsed, hostParsed) || changed;
707
741
  const inheritSpecs = [
708
742
  ['model', 'string'],
709
743
  ['model_reasoning_effort', 'string'],
710
744
  ['model_auto_compact_token_limit', 'number']
711
745
  ];
712
- let changed = false;
713
746
  for (const [key, type] of inheritSpecs) {
714
747
  if (Object.hasOwn(sandboxParsed, key)) {
715
748
  continue;
@@ -10,7 +10,7 @@ import { parseCustomTools } from "./tools.js";
10
10
  const DEFAULTS = Object.freeze({
11
11
  engine: null,
12
12
  runtimes: ['node22'],
13
- tools: ['claude-code', 'codex', 'gemini-cli', 'opencode'],
13
+ tools: ['agent-infra', 'claude-code', 'codex', 'gemini-cli', 'opencode'],
14
14
  dockerfile: null,
15
15
  vm: {
16
16
  cpu: null,
@@ -1,3 +1,3 @@
1
- RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
1
+ RUN bash -o pipefail -c 'curl -fsSL --retry 5 --retry-delay 2 --retry-all-errors https://deb.nodesource.com/setup_20.x | bash -' && \
2
2
  apt-get install -y nodejs && \
3
3
  rm -rf /var/lib/apt/lists/*
@@ -1,3 +1,3 @@
1
- RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
1
+ RUN bash -o pipefail -c 'curl -fsSL --retry 5 --retry-delay 2 --retry-all-errors https://deb.nodesource.com/setup_22.x | bash -' && \
2
2
  apt-get install -y nodejs && \
3
3
  rm -rf /var/lib/apt/lists/*
@@ -88,6 +88,15 @@ function createBuiltinTools(home, project) {
88
88
  { hostPath: hostJoin(home, '.gemini', 'settings.json'), sandboxName: 'settings.json' },
89
89
  { hostPath: hostJoin(home, '.gemini', 'google_accounts.json'), sandboxName: 'google_accounts.json' }
90
90
  ]
91
+ },
92
+ 'agent-infra': {
93
+ id: 'agent-infra',
94
+ name: 'agent-infra CLI',
95
+ install: { type: 'npm', cmd: '@fitlab-ai/agent-infra@latest' },
96
+ sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'agent-infra'),
97
+ containerMount: '/home/devuser/.agent-infra-cli',
98
+ versionCmd: 'ai version --raw',
99
+ setupHint: 'Provides the ai and agent-infra CLI commands inside the sandbox.'
91
100
  }
92
101
  };
93
102
  }
@@ -1,4 +1,4 @@
1
- import { createFeishuTransport, normalizeMessage } from "./transport.js";
1
+ import { cardMessage, createFeishuTransport, normalizeMessage } from "./transport.js";
2
2
  // Assemble the feishu adapter. The transport is injectable so unit tests can
3
3
  // drive start/dispatch/reply/sendMessage against a fake transport without the
4
4
  // real SDK; production uses the default SDK-backed transport.
@@ -18,7 +18,9 @@ export function createFeishuAdapter(config, transport = createFeishuTransport(co
18
18
  text: normalized.text,
19
19
  messageId: normalized.messageId,
20
20
  raw: normalized.raw,
21
- reply: (text) => transport.send(normalized.chatId, text)
21
+ reply: async (text) => {
22
+ await transport.send(normalized.chatId, cardMessage(text));
23
+ }
22
24
  };
23
25
  await adapterCtx.dispatch(message);
24
26
  }
@@ -31,7 +33,7 @@ export function createFeishuAdapter(config, transport = createFeishuTransport(co
31
33
  await transport.stop();
32
34
  },
33
35
  async sendMessage(target, text) {
34
- await transport.send(target.chatId, text);
36
+ await transport.send(target.chatId, cardMessage(text));
35
37
  }
36
38
  };
37
39
  }
@@ -1,4 +1,5 @@
1
1
  import * as lark from '@larksuiteoapi/node-sdk';
2
+ import { stripVTControlCharacters } from 'node:util';
2
3
  function asRecord(value) {
3
4
  return typeof value === 'object' && value !== null ? value : {};
4
5
  }
@@ -40,6 +41,23 @@ export function normalizeMessage(event) {
40
41
  function resolveDomain(value) {
41
42
  return value === 'lark' || value === 'Lark' ? lark.Domain.Lark : lark.Domain.Feishu;
42
43
  }
44
+ export function cleanFeishuText(text) {
45
+ return stripVTControlCharacters(text).replace(/\r\n/g, '\n');
46
+ }
47
+ export function cardMessage(text) {
48
+ return { kind: 'interactive', title: 'agent-infra', text: cleanFeishuText(text) };
49
+ }
50
+ export function toFeishuCreateData(chatId, message) {
51
+ return {
52
+ receive_id: chatId,
53
+ msg_type: 'interactive',
54
+ content: JSON.stringify({
55
+ config: { wide_screen_mode: true },
56
+ header: { title: { tag: 'plain_text', content: message.title }, template: 'blue' },
57
+ elements: [{ tag: 'div', text: { tag: 'lark_md', content: message.text } }]
58
+ })
59
+ };
60
+ }
43
61
  // Build the real SDK-backed transport from adapter config. appId/appSecret come
44
62
  // from server config (appSecret only via server.local.json / env). The WSClient
45
63
  // long connection delivers events; the Client REST call sends replies.
@@ -84,10 +102,10 @@ export function createFeishuTransport(config) {
84
102
  // Ignore: the daemon is shutting down regardless.
85
103
  }
86
104
  },
87
- async send(chatId, text) {
105
+ async send(chatId, message) {
88
106
  await client.im.message.create({
89
107
  params: { receive_id_type: 'chat_id' },
90
- data: { receive_id: chatId, msg_type: 'text', content: JSON.stringify({ text }) }
108
+ data: toFeishuCreateData(chatId, message)
91
109
  });
92
110
  }
93
111
  };
@@ -12,10 +12,26 @@ function taskSubcommand(subcommand) {
12
12
  export function commandHelp() {
13
13
  return [
14
14
  `agent-infra ${VERSION}`,
15
- 'Built-ins: /help, /ping, /version',
16
- 'Read: /sandbox ls|show|vm status, /task decisions|log|ls|show|status',
17
- 'Write: /sandbox create|start',
18
- 'Exec: /decide <task-ref> <HD-id> <decision>, /run create-task <description>, /run <skill> <task-ref> ...'
15
+ 'Built-ins:',
16
+ '/help',
17
+ '/ping',
18
+ '/version',
19
+ 'Read:',
20
+ '/sandbox ls',
21
+ '/sandbox show {ref}',
22
+ '/sandbox vm status',
23
+ '/task decisions {ref}',
24
+ '/task log {ref}',
25
+ '/task ls',
26
+ '/task show {ref}',
27
+ '/task status {ref}',
28
+ 'Write:',
29
+ '/sandbox create {ref}',
30
+ '/sandbox start {ref}',
31
+ 'Exec:',
32
+ '/decide {task-ref} {HD-id} {decision}',
33
+ '/run create-task {description}',
34
+ '/run {skill} {task-ref}'
19
35
  ].join('\n');
20
36
  }
21
37
  export function parseCommand(text) {
@@ -7,6 +7,9 @@ import { isPathOwnedByDisabledTUI, resolveEnabledTUIs } from "./builtin-tuis.js"
7
7
  const defaults = JSON.parse(fs.readFileSync(new URL('./defaults.json', import.meta.url), 'utf8'));
8
8
  const CONFIG_DIR = '.agents';
9
9
  const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
10
+ const AGENT_INFRA_SANDBOX_TOOL = 'agent-infra';
11
+ const LEGACY_DEFAULT_SANDBOX_TOOLS = ['claude-code', 'codex', 'gemini-cli', 'opencode'];
12
+ const DEFAULT_SANDBOX_TOOLS = [AGENT_INFRA_SANDBOX_TOOL, ...LEGACY_DEFAULT_SANDBOX_TOOLS];
10
13
  // One-time migration of the legacy project-level PR switch to the three-state
11
14
  // `prFlow` preference. `true` (the old default / "PR flow on") maps to the
12
15
  // strong constraint `required`; `false` maps to `disabled`. A missing or
@@ -25,6 +28,24 @@ function migratePrFlow(config) {
25
28
  }
26
29
  return null;
27
30
  }
31
+ function isLegacyDefaultSandboxTools(value) {
32
+ if (!Array.isArray(value) || value.length !== LEGACY_DEFAULT_SANDBOX_TOOLS.length) {
33
+ return false;
34
+ }
35
+ const tools = new Set(value);
36
+ return LEGACY_DEFAULT_SANDBOX_TOOLS.every((tool) => tools.has(tool));
37
+ }
38
+ function migrateSandboxTools(config) {
39
+ const tools = config.sandbox?.tools;
40
+ if (!isLegacyDefaultSandboxTools(tools)) {
41
+ return false;
42
+ }
43
+ config.sandbox = {
44
+ ...config.sandbox,
45
+ tools: [...DEFAULT_SANDBOX_TOOLS]
46
+ };
47
+ return true;
48
+ }
28
49
  function isPathOwnedByOtherPlatform(relativePath, platformType) {
29
50
  const top = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '').split('/')[0] ?? '';
30
51
  if (!top.startsWith('.'))
@@ -153,6 +174,7 @@ async function cmdUpdate() {
153
174
  const taskAdded = !config.task;
154
175
  const labelsAdded = !config.labels;
155
176
  const prFlowMigrated = migratePrFlow(config);
177
+ const sandboxToolsMigrated = !sandboxAdded && migrateSandboxTools(config);
156
178
  let configChanged = changed;
157
179
  if (platformAdded) {
158
180
  config.platform = structuredClone(defaults.platform);
@@ -173,6 +195,9 @@ async function cmdUpdate() {
173
195
  if (prFlowMigrated) {
174
196
  configChanged = true;
175
197
  }
198
+ if (sandboxToolsMigrated) {
199
+ configChanged = true;
200
+ }
176
201
  if (configChanged) {
177
202
  console.log('');
178
203
  if (hasNewEntries) {
@@ -200,6 +225,9 @@ async function cmdUpdate() {
200
225
  if (prFlowMigrated) {
201
226
  info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
202
227
  }
228
+ if (sandboxToolsMigrated) {
229
+ info(`Migrated default sandbox.tools to include ${AGENT_INFRA_SANDBOX_TOOL} in ${CONFIG_PATH}.`);
230
+ }
203
231
  }
204
232
  else {
205
233
  info(`File registry changed in ${CONFIG_PATH}.`);
@@ -219,6 +247,9 @@ async function cmdUpdate() {
219
247
  if (hasNewEntries && prFlowMigrated) {
220
248
  info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
221
249
  }
250
+ if (hasNewEntries && sandboxToolsMigrated) {
251
+ info(`Migrated default sandbox.tools to include ${AGENT_INFRA_SANDBOX_TOOL} in ${CONFIG_PATH}.`);
252
+ }
222
253
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
223
254
  ok(`Updated ${CONFIG_PATH}`);
224
255
  }
package/lib/defaults.json CHANGED
@@ -8,6 +8,7 @@
8
8
  "node22"
9
9
  ],
10
10
  "tools": [
11
+ "agent-infra",
11
12
  "claude-code",
12
13
  "codex",
13
14
  "gemini-cli",
@@ -915,6 +915,42 @@ function resolveHostCatalogPath(value: unknown, hostHomeDir: string): string | n
915
915
  }
916
916
  }
917
917
 
918
+ const CODEX_DISABLED_FEATURE_FLAGS = ['apps', 'enable_mcp_apps'] as const;
919
+
920
+ function removeCodexMcpServers(sandboxParsed: JsonObject): boolean {
921
+ if (!Object.hasOwn(sandboxParsed, 'mcp_servers')) {
922
+ return false;
923
+ }
924
+ delete sandboxParsed.mcp_servers;
925
+ return true;
926
+ }
927
+
928
+ function inheritDisabledCodexFeatureFlags(sandboxParsed: JsonObject, hostParsed: JsonObject): boolean {
929
+ if (!isJsonObjectRecord(hostParsed.features)) {
930
+ return false;
931
+ }
932
+ if (Object.hasOwn(sandboxParsed, 'features') && !isJsonObjectRecord(sandboxParsed.features)) {
933
+ return false;
934
+ }
935
+
936
+ let sandboxFeatures = sandboxParsed.features as JsonObject | undefined;
937
+ let changed = false;
938
+ for (const key of CODEX_DISABLED_FEATURE_FLAGS) {
939
+ if (hostParsed.features[key] !== false) {
940
+ continue;
941
+ }
942
+ if (!sandboxFeatures) {
943
+ sandboxFeatures = {};
944
+ sandboxParsed.features = sandboxFeatures;
945
+ }
946
+ if (sandboxFeatures[key] !== false) {
947
+ sandboxFeatures[key] = false;
948
+ changed = true;
949
+ }
950
+ }
951
+ return changed;
952
+ }
953
+
918
954
  export function ensureCodexModelInheritance(
919
955
  toolDir: string,
920
956
  hostHomeDir?: string,
@@ -947,13 +983,15 @@ export function ensureCodexModelInheritance(
947
983
  }
948
984
  }
949
985
 
986
+ let changed = removeCodexMcpServers(sandboxParsed);
987
+ changed = inheritDisabledCodexFeatureFlags(sandboxParsed, hostParsed) || changed;
988
+
950
989
  const inheritSpecs: Array<readonly [string, 'string' | 'number']> = [
951
990
  ['model', 'string'],
952
991
  ['model_reasoning_effort', 'string'],
953
992
  ['model_auto_compact_token_limit', 'number']
954
993
  ];
955
994
 
956
- let changed = false;
957
995
  for (const [key, type] of inheritSpecs) {
958
996
  if (Object.hasOwn(sandboxParsed, key)) {
959
997
  continue;
@@ -12,7 +12,7 @@ import type { SandboxTool } from './tools.ts';
12
12
  const DEFAULTS = Object.freeze({
13
13
  engine: null,
14
14
  runtimes: ['node22'],
15
- tools: ['claude-code', 'codex', 'gemini-cli', 'opencode'],
15
+ tools: ['agent-infra', 'claude-code', 'codex', 'gemini-cli', 'opencode'],
16
16
  dockerfile: null,
17
17
  vm: {
18
18
  cpu: null,
@@ -1,3 +1,3 @@
1
- RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
1
+ RUN bash -o pipefail -c 'curl -fsSL --retry 5 --retry-delay 2 --retry-all-errors https://deb.nodesource.com/setup_20.x | bash -' && \
2
2
  apt-get install -y nodejs && \
3
3
  rm -rf /var/lib/apt/lists/*
@@ -1,3 +1,3 @@
1
- RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - && \
1
+ RUN bash -o pipefail -c 'curl -fsSL --retry 5 --retry-delay 2 --retry-all-errors https://deb.nodesource.com/setup_22.x | bash -' && \
2
2
  apt-get install -y nodejs && \
3
3
  rm -rf /var/lib/apt/lists/*
@@ -124,6 +124,15 @@ function createBuiltinTools(home: string, project: string): Record<string, Sandb
124
124
  { hostPath: hostJoin(home, '.gemini', 'settings.json'), sandboxName: 'settings.json' },
125
125
  { hostPath: hostJoin(home, '.gemini', 'google_accounts.json'), sandboxName: 'google_accounts.json' }
126
126
  ]
127
+ },
128
+ 'agent-infra': {
129
+ id: 'agent-infra',
130
+ name: 'agent-infra CLI',
131
+ install: { type: 'npm', cmd: '@fitlab-ai/agent-infra@latest' },
132
+ sandboxBase: hostJoin(home, '.agent-infra', 'sandboxes', 'agent-infra'),
133
+ containerMount: '/home/devuser/.agent-infra-cli',
134
+ versionCmd: 'ai version --raw',
135
+ setupHint: 'Provides the ai and agent-infra CLI commands inside the sandbox.'
127
136
  }
128
137
  };
129
138
  }
@@ -1,5 +1,5 @@
1
1
  import type { Adapter, AdapterCtx, AdapterFactory, InboundMessage } from '../_contract.ts';
2
- import { createFeishuTransport, normalizeMessage } from './transport.ts';
2
+ import { cardMessage, createFeishuTransport, normalizeMessage } from './transport.ts';
3
3
  import type { FeishuTransport } from './transport.ts';
4
4
 
5
5
  // Assemble the feishu adapter. The transport is injectable so unit tests can
@@ -25,7 +25,9 @@ export function createFeishuAdapter(
25
25
  text: normalized.text,
26
26
  messageId: normalized.messageId,
27
27
  raw: normalized.raw,
28
- reply: (text) => transport.send(normalized.chatId, text)
28
+ reply: async (text) => {
29
+ await transport.send(normalized.chatId, cardMessage(text));
30
+ }
29
31
  };
30
32
  await adapterCtx.dispatch(message);
31
33
  } catch (error) {
@@ -37,7 +39,7 @@ export function createFeishuAdapter(
37
39
  await transport.stop();
38
40
  },
39
41
  async sendMessage(target, text) {
40
- await transport.send(target.chatId, text);
42
+ await transport.send(target.chatId, cardMessage(text));
41
43
  }
42
44
  };
43
45
  }
@@ -1,4 +1,5 @@
1
1
  import * as lark from '@larksuiteoapi/node-sdk';
2
+ import { stripVTControlCharacters } from 'node:util';
2
3
 
3
4
  // Transport layer for the feishu adapter. All @larksuiteoapi/node-sdk surface is
4
5
  // confined here so the adapter body (index.ts) depends only on this narrow
@@ -10,7 +11,15 @@ export type FeishuTransport = {
10
11
  // each inbound im.message.receive_v1.
11
12
  start: (onMessage: (raw: unknown) => Promise<void>) => Promise<void>;
12
13
  stop: () => Promise<void>;
13
- send: (chatId: string, text: string) => Promise<void>;
14
+ send: (chatId: string, message: FeishuOutgoingMessage) => Promise<void>;
15
+ };
16
+
17
+ export type FeishuOutgoingMessage = { kind: 'interactive'; title: string; text: string };
18
+
19
+ type FeishuCreateData = {
20
+ receive_id: string;
21
+ msg_type: 'interactive';
22
+ content: string;
14
23
  };
15
24
 
16
25
  export type NormalizedMessage = {
@@ -69,6 +78,26 @@ function resolveDomain(value: unknown): number {
69
78
  return value === 'lark' || value === 'Lark' ? lark.Domain.Lark : lark.Domain.Feishu;
70
79
  }
71
80
 
81
+ export function cleanFeishuText(text: string): string {
82
+ return stripVTControlCharacters(text).replace(/\r\n/g, '\n');
83
+ }
84
+
85
+ export function cardMessage(text: string): FeishuOutgoingMessage {
86
+ return { kind: 'interactive', title: 'agent-infra', text: cleanFeishuText(text) };
87
+ }
88
+
89
+ export function toFeishuCreateData(chatId: string, message: FeishuOutgoingMessage): FeishuCreateData {
90
+ return {
91
+ receive_id: chatId,
92
+ msg_type: 'interactive',
93
+ content: JSON.stringify({
94
+ config: { wide_screen_mode: true },
95
+ header: { title: { tag: 'plain_text', content: message.title }, template: 'blue' },
96
+ elements: [{ tag: 'div', text: { tag: 'lark_md', content: message.text } }]
97
+ })
98
+ };
99
+ }
100
+
72
101
  // Build the real SDK-backed transport from adapter config. appId/appSecret come
73
102
  // from server config (appSecret only via server.local.json / env). The WSClient
74
103
  // long connection delivers events; the Client REST call sends replies.
@@ -118,10 +147,10 @@ export function createFeishuTransport(config: Record<string, unknown>): FeishuTr
118
147
  // Ignore: the daemon is shutting down regardless.
119
148
  }
120
149
  },
121
- async send(chatId, text) {
150
+ async send(chatId, message) {
122
151
  await client.im.message.create({
123
152
  params: { receive_id_type: 'chat_id' },
124
- data: { receive_id: chatId, msg_type: 'text', content: JSON.stringify({ text }) }
153
+ data: toFeishuCreateData(chatId, message)
125
154
  });
126
155
  }
127
156
  };
@@ -23,10 +23,26 @@ function taskSubcommand(subcommand: string): string {
23
23
  export function commandHelp(): string {
24
24
  return [
25
25
  `agent-infra ${VERSION}`,
26
- 'Built-ins: /help, /ping, /version',
27
- 'Read: /sandbox ls|show|vm status, /task decisions|log|ls|show|status',
28
- 'Write: /sandbox create|start',
29
- 'Exec: /decide <task-ref> <HD-id> <decision>, /run create-task <description>, /run <skill> <task-ref> ...'
26
+ 'Built-ins:',
27
+ '/help',
28
+ '/ping',
29
+ '/version',
30
+ 'Read:',
31
+ '/sandbox ls',
32
+ '/sandbox show {ref}',
33
+ '/sandbox vm status',
34
+ '/task decisions {ref}',
35
+ '/task log {ref}',
36
+ '/task ls',
37
+ '/task show {ref}',
38
+ '/task status {ref}',
39
+ 'Write:',
40
+ '/sandbox create {ref}',
41
+ '/sandbox start {ref}',
42
+ 'Exec:',
43
+ '/decide {task-ref} {HD-id} {decision}',
44
+ '/run create-task {description}',
45
+ '/run {skill} {task-ref}'
30
46
  ].join('\n');
31
47
  }
32
48
 
package/lib/update.ts CHANGED
@@ -40,6 +40,9 @@ const defaults = JSON.parse(
40
40
 
41
41
  const CONFIG_DIR = '.agents';
42
42
  const CONFIG_PATH = path.join(CONFIG_DIR, '.airc.json');
43
+ const AGENT_INFRA_SANDBOX_TOOL = 'agent-infra';
44
+ const LEGACY_DEFAULT_SANDBOX_TOOLS = ['claude-code', 'codex', 'gemini-cli', 'opencode'];
45
+ const DEFAULT_SANDBOX_TOOLS = [AGENT_INFRA_SANDBOX_TOOL, ...LEGACY_DEFAULT_SANDBOX_TOOLS];
43
46
 
44
47
  // One-time migration of the legacy project-level PR switch to the three-state
45
48
  // `prFlow` preference. `true` (the old default / "PR flow on") maps to the
@@ -60,6 +63,26 @@ function migratePrFlow(config: UpdateConfig): 'required' | 'disabled' | null {
60
63
  return null;
61
64
  }
62
65
 
66
+ function isLegacyDefaultSandboxTools(value: unknown): value is string[] {
67
+ if (!Array.isArray(value) || value.length !== LEGACY_DEFAULT_SANDBOX_TOOLS.length) {
68
+ return false;
69
+ }
70
+ const tools = new Set(value);
71
+ return LEGACY_DEFAULT_SANDBOX_TOOLS.every((tool) => tools.has(tool));
72
+ }
73
+
74
+ function migrateSandboxTools(config: UpdateConfig): boolean {
75
+ const tools = config.sandbox?.tools;
76
+ if (!isLegacyDefaultSandboxTools(tools)) {
77
+ return false;
78
+ }
79
+ config.sandbox = {
80
+ ...config.sandbox,
81
+ tools: [...DEFAULT_SANDBOX_TOOLS]
82
+ };
83
+ return true;
84
+ }
85
+
63
86
  function isPathOwnedByOtherPlatform(relativePath: string, platformType: string): boolean {
64
87
  const top = String(relativePath || '').replace(/\\/g, '/').replace(/^\.\//, '').split('/')[0] ?? '';
65
88
  if (!top.startsWith('.')) return false;
@@ -215,6 +238,7 @@ async function cmdUpdate(): Promise<void> {
215
238
  const taskAdded = !config.task;
216
239
  const labelsAdded = !config.labels;
217
240
  const prFlowMigrated = migratePrFlow(config);
241
+ const sandboxToolsMigrated = !sandboxAdded && migrateSandboxTools(config);
218
242
  let configChanged = changed;
219
243
 
220
244
  if (platformAdded) {
@@ -241,6 +265,10 @@ async function cmdUpdate(): Promise<void> {
241
265
  configChanged = true;
242
266
  }
243
267
 
268
+ if (sandboxToolsMigrated) {
269
+ configChanged = true;
270
+ }
271
+
244
272
  if (configChanged) {
245
273
  console.log('');
246
274
  if (hasNewEntries) {
@@ -267,6 +295,9 @@ async function cmdUpdate(): Promise<void> {
267
295
  if (prFlowMigrated) {
268
296
  info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
269
297
  }
298
+ if (sandboxToolsMigrated) {
299
+ info(`Migrated default sandbox.tools to include ${AGENT_INFRA_SANDBOX_TOOL} in ${CONFIG_PATH}.`);
300
+ }
270
301
  } else {
271
302
  info(`File registry changed in ${CONFIG_PATH}.`);
272
303
  }
@@ -285,6 +316,9 @@ async function cmdUpdate(): Promise<void> {
285
316
  if (hasNewEntries && prFlowMigrated) {
286
317
  info(`Migrated legacy requiresPullRequest to prFlow="${prFlowMigrated}" in ${CONFIG_PATH}.`);
287
318
  }
319
+ if (hasNewEntries && sandboxToolsMigrated) {
320
+ info(`Migrated default sandbox.tools to include ${AGENT_INFRA_SANDBOX_TOOL} in ${CONFIG_PATH}.`);
321
+ }
288
322
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + '\n', 'utf8');
289
323
  ok(`Updated ${CONFIG_PATH}`);
290
324
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitlab-ai/agent-infra",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Bootstrap tool for AI multi-tool collaboration infrastructure — works with Claude Code, Codex, Gemini CLI, and OpenCode",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  "templates/"
30
30
  ],
31
31
  "engines": {
32
- "node": ">=22"
32
+ "node": ">=22.9.0"
33
33
  },
34
34
  "keywords": [
35
35
  "ai",
@@ -260,7 +260,7 @@ Supported `invoke` placeholders:
260
260
  | Placeholder | Replaced with | Example |
261
261
  |-------------|---------------|---------|
262
262
  | `${skillName}` | The skill command name, such as `review-code` or `commit`. | `<your-cli> ${skillName}` -> `<your-cli> review-code` |
263
- | `${projectName}` | The `.airc.json` `project` value. Use this for namespaced commands. | `/${projectName}:${skillName}` -> `/agent-infra:review-code` |
263
+ | `${projectName}` | The `.airc.json` `project` value. Use this for namespaced commands. | `/${projectName}:${skillName}` -> `/your-project:review-code` |
264
264
 
265
265
  Non-namespaced custom TUI:
266
266
 
@@ -280,7 +280,7 @@ Namespaced custom TUI:
280
280
 
281
281
  ```json
282
282
  {
283
- "project": "agent-infra",
283
+ "project": "your-project",
284
284
  "customTUIs": [
285
285
  {
286
286
  "name": "<your-tui-name>",
@@ -295,7 +295,7 @@ Namespaced custom TUI:
295
295
 
296
296
  ## Sandbox Custom Tools
297
297
 
298
- `customTUIs` (above) generates slash-command files but does not change the sandbox image. To install a non-npm TUI (pip / cargo / curl-based / pre-built binary) into the sandbox image and live-mount its credentials, declare it under `sandbox.customTools` in `.agents/.airc.json`. Built-in tools (`claude-code`, `codex`, `opencode`, `gemini-cli`) keep working unchanged.
298
+ `customTUIs` generates slash-command files but does not change the sandbox image. To install a non-npm CLI or tool (pip / cargo / curl-based / pre-built binary) into the sandbox image and live-mount its credentials, declare it under `sandbox.customTools` in `.agents/.airc.json`. Built-in sandbox tools (`claude-code`, `codex`, `opencode`, `gemini-cli`, `agent-infra`) keep working unchanged; `agent-infra` only provides the in-sandbox `ai` / `agent-infra` CLI and is not part of `tuis` or `customTUIs`.
299
299
 
300
300
  ### Required fields
301
301
 
@@ -260,7 +260,7 @@ args: "<task-id>" # 可选
260
260
  | 占位符 | 替换为 | 示例 |
261
261
  |--------|--------|------|
262
262
  | `${skillName}` | skill 命令名,例如 `review-code` 或 `commit`。 | `<your-cli> ${skillName}` -> `<your-cli> review-code` |
263
- | `${projectName}` | `.airc.json` 中的 `project` 值,适用于带命名空间的命令。 | `/${projectName}:${skillName}` -> `/agent-infra:review-code` |
263
+ | `${projectName}` | `.airc.json` 中的 `project` 值,适用于带命名空间的命令。 | `/${projectName}:${skillName}` -> `/your-project:review-code` |
264
264
 
265
265
  不带命名空间的自定义 TUI:
266
266
 
@@ -280,7 +280,7 @@ args: "<task-id>" # 可选
280
280
 
281
281
  ```json
282
282
  {
283
- "project": "agent-infra",
283
+ "project": "your-project",
284
284
  "customTUIs": [
285
285
  {
286
286
  "name": "<your-tui-name>",
@@ -295,7 +295,7 @@ args: "<task-id>" # 可选
295
295
 
296
296
  ## 沙箱自定义工具(Sandbox Custom Tools)
297
297
 
298
- 上文 `customTUIs` 只负责生成 slash-command 文件,**不影响沙箱镜像**。如果要把一个非 npm 分发的 TUI(pip / cargo / curl 脚本 / 裸二进制)装进沙箱镜像、并 live-mount 它的凭证目录,需要在 `.agents/.airc.json` 的 `sandbox.customTools` 中声明。内建的四个工具(`claude-code` / `codex` / `opencode` / `gemini-cli`)行为保持不变。
298
+ `customTUIs` 只负责生成 slash-command 文件,**不影响沙箱镜像**。如果要把一个非 npm 分发的 CLI 或工具(pip / cargo / curl 脚本 / 裸二进制)装进沙箱镜像、并 live-mount 它的凭证目录,需要在 `.agents/.airc.json` 的 `sandbox.customTools` 中声明。内建 sandbox 工具(`claude-code` / `codex` / `opencode` / `gemini-cli` / `agent-infra`)行为保持不变;其中 `agent-infra` 只提供沙箱内 `ai` / `agent-infra` CLI,不属于 `tuis` 或 `customTUIs` 配置。
299
299
 
300
300
  ### 必填字段
301
301
 
@@ -29,6 +29,7 @@ const DEFAULTS = {
29
29
  "node22"
30
30
  ],
31
31
  "tools": [
32
+ "agent-infra",
32
33
  "claude-code",
33
34
  "codex",
34
35
  "gemini-cli",
@@ -86,6 +87,9 @@ const DEFAULTS = {
86
87
  };
87
88
 
88
89
  const PACKAGE_NAME = '@fitlab-ai/agent-infra';
90
+ const AGENT_INFRA_SANDBOX_TOOL = 'agent-infra';
91
+ const LEGACY_DEFAULT_SANDBOX_TOOLS = ['claude-code', 'codex', 'gemini-cli', 'opencode'];
92
+ const DEFAULT_SANDBOX_TOOLS = [AGENT_INFRA_SANDBOX_TOOL, ...LEGACY_DEFAULT_SANDBOX_TOOLS];
89
93
  // Add a new identifier here only after shipping matching .{platform}. template variants.
90
94
  const KNOWN_PLATFORMS = new Set(['github']);
91
95
  const KNOWN_LANGUAGES = new Set(['en', 'zh-CN']);
@@ -123,6 +127,26 @@ function isPathOwnedByDisabledTUI(rel, enabledSet) {
123
127
  return false;
124
128
  }
125
129
 
130
+ function isLegacyDefaultSandboxTools(value) {
131
+ if (!Array.isArray(value) || value.length !== LEGACY_DEFAULT_SANDBOX_TOOLS.length) {
132
+ return false;
133
+ }
134
+ const tools = new Set(value);
135
+ return LEGACY_DEFAULT_SANDBOX_TOOLS.every(tool => tools.has(tool));
136
+ }
137
+
138
+ function migrateSandboxTools(cfg) {
139
+ const tools = cfg.sandbox?.tools;
140
+ if (!isLegacyDefaultSandboxTools(tools)) {
141
+ return false;
142
+ }
143
+ cfg.sandbox = {
144
+ ...cfg.sandbox,
145
+ tools: [...DEFAULT_SANDBOX_TOOLS]
146
+ };
147
+ return true;
148
+ }
149
+
126
150
  function norm(p) { return p.replace(/\\/g, '/'); }
127
151
 
128
152
  function normDir(p) {
@@ -1296,6 +1320,7 @@ function syncTemplates(projectRoot, templateRootOverride) {
1296
1320
  ) > 0;
1297
1321
 
1298
1322
  const prevVersion = cfg.templateVersion;
1323
+ const sandboxToolsMigrated = migrateSandboxTools(cfg);
1299
1324
 
1300
1325
  cfg.files.managed = managed;
1301
1326
  cfg.files.merged = merged;
@@ -1303,7 +1328,7 @@ function syncTemplates(projectRoot, templateRootOverride) {
1303
1328
  cfg.templateVersion = version;
1304
1329
  delete cfg.templateSource;
1305
1330
 
1306
- report.configUpdated = hasChanges || prevVersion !== version || hadTemplateSource;
1331
+ report.configUpdated = hasChanges || prevVersion !== version || hadTemplateSource || sandboxToolsMigrated;
1307
1332
 
1308
1333
  fs.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2) + '\n', 'utf8');
1309
1334