openteam 0.7.2 → 1.0.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/bin/openteam.js CHANGED
@@ -7,30 +7,25 @@
7
7
  import { createRequire } from 'module';
8
8
  import { program } from 'commander';
9
9
  import {
10
- cmdStart, cmdAttach, cmdList, cmdStop,
11
- cmdInspect, cmdDashboard, cmdAgentAttach,
10
+ cmdStart, cmdList, cmdStop,
11
+ cmdInspect, cmdDashboard,
12
12
  } from '../src/interfaces/cli.js';
13
13
 
14
14
  const require = createRequire(import.meta.url);
15
15
  const { version } = require('../package.json');
16
16
 
17
- program.name('openteam').description('Team management for OpenCode').version(version);
17
+ program.name('openteam').description('Agent team collaboration framework').version(version);
18
18
 
19
19
  program
20
20
  .command('start [team]')
21
21
  .description('启动团队(创建 tmux/zellij session)')
22
22
  .option('-d, --detach', '后台运行')
23
23
  .option('--dir <directory>', '项目目录')
24
+ .option('--cli <type>', 'CLI 类型(claude-code / opencode)', 'claude-code')
24
25
  .option('--tmux', '强制使用 tmux')
25
26
  .option('--zellij', '强制使用 zellij')
26
27
  .action(cmdStart);
27
28
 
28
- program
29
- .command('attach [team] [agent]', { hidden: true })
30
- .description('附加到 agent 会话')
31
- .option('--dir <directory>', '项目目录')
32
- .action(cmdAttach);
33
-
34
29
  program
35
30
  .command('list')
36
31
  .alias('ls')
@@ -45,7 +40,7 @@ program
45
40
 
46
41
  program
47
42
  .command('inspect <team>')
48
- .description('查看团队详细状态与会话有效性')
43
+ .description('查看团队详细状态与 agent 在线情况')
49
44
  .option('--dir <directory>', '项目目录')
50
45
  .action(cmdInspect);
51
46
 
@@ -55,21 +50,23 @@ program
55
50
  .option('--dir <directory>', '项目目录')
56
51
  .action(cmdDashboard);
57
52
 
58
- // 内部命令(不在帮助中显示,由 layout / daemon 自动调用)
59
- program
60
- .command('agent-attach <team> <agent>', { hidden: true })
61
- .description('等待 agent 会话就绪后 attach')
62
- .option('--dir <directory>', '项目目录')
63
- .action(cmdAgentAttach);
64
-
53
+ // 内部命令(不在帮助中显示)
65
54
  program
66
55
  .command('daemon <team>', { hidden: true })
67
- .option('--port <port>', 'serve 端口', parseInt)
56
+ .option('--port <port>', 'server 端口', parseInt)
68
57
  .option('--dir <directory>', '项目目录')
69
58
  .option('--mux <type>', '复用器类型', 'tmux')
59
+ .option('--cli <type>', 'CLI 类型', 'claude-code')
70
60
  .action(async (teamName, options) => {
71
61
  const { runDaemon } = await import('../src/interfaces/daemon/index.js');
72
62
  await runDaemon(teamName, options.dir || process.cwd(), options);
73
63
  });
74
64
 
65
+ program
66
+ .command('wrapper', { hidden: true })
67
+ .description('Wrapper 进程(由 daemon pane 自动调用)')
68
+ .action(async () => {
69
+ await import('../src/wrapper/index.js');
70
+ });
71
+
75
72
  program.parse();
package/package.json CHANGED
@@ -1,9 +1,8 @@
1
1
  {
2
2
  "name": "openteam",
3
- "version": "0.7.2",
4
- "description": "Agent-centric team collaboration for OpenCode",
3
+ "version": "1.0.0",
4
+ "description": "Agent-centric team collaboration framework",
5
5
  "type": "module",
6
- "main": "./src/index.js",
7
6
  "bin": {
8
7
  "openteam": "./bin/openteam.js"
9
8
  },
@@ -17,10 +16,10 @@
17
16
  "test": "node test/smoke.js"
18
17
  },
19
18
  "keywords": [
20
- "opencode",
21
19
  "agent",
22
20
  "team",
23
- "collaboration"
21
+ "collaboration",
22
+ "mcp"
24
23
  ],
25
24
  "author": "goodfish",
26
25
  "license": "MIT",
@@ -29,8 +28,10 @@
29
28
  "url": "git+https://github.com/opencode-dev/openteam.git"
30
29
  },
31
30
  "dependencies": {
32
- "@opencode-ai/plugin": "^1.2.18",
31
+ "@modelcontextprotocol/sdk": "^1.27.1",
33
32
  "blessed": "^0.1.81",
34
- "commander": "^12.0.0"
33
+ "commander": "^12.0.0",
34
+ "node-pty": "^1.1.0",
35
+ "zod": "^4.3.6"
35
36
  }
36
37
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * CLI 适配器基类 + 工厂函数
3
+ *
4
+ * 每个适配器封装特定 CLI 工具的启动参数、MCP 配置方式等差异。
5
+ * adapters/ 是基础层 — 只提供 CLI 命令构建,不含业务逻辑。
6
+ */
7
+
8
+ /**
9
+ * CLI 适配器基类
10
+ */
11
+ export class BaseAdapter {
12
+ /**
13
+ * CLI 二进制名称
14
+ * @returns {string}
15
+ */
16
+ get binary() {
17
+ throw new Error('subclass must implement binary getter');
18
+ }
19
+
20
+ /**
21
+ * 构建 CLI 启动命令参数(数组形式,第一个元素是二进制名)
22
+ * @param {object} params
23
+ * @param {string} params.agent - agent 名称
24
+ * @param {string} params.systemPrompt - 追加的系统提示词
25
+ * @param {string} params.mcpConfigPath - MCP 配置文件路径
26
+ * @param {string} params.cwd - 工作目录
27
+ * @returns {string[]} 命令数组,如 ['claude', '--agent', 'pm', ...]
28
+ */
29
+ buildLaunchArgs({ agent, systemPrompt, mcpConfigPath, cwd, extraArgs = [] }) {
30
+ throw new Error('subclass must implement buildLaunchArgs');
31
+ }
32
+
33
+ /**
34
+ * 构建 MCP 配置对象
35
+ * 返回可写入 JSON 文件的配置,让 CLI 知道如何连接 openteam MCP server。
36
+ * @param {object} params
37
+ * @param {string} params.serverUrl - openteam server URL
38
+ * @param {string} params.agent - agent 名称
39
+ * @returns {object} MCP 配置 JSON
40
+ */
41
+ buildMcpConfig({ serverUrl, agent }) {
42
+ throw new Error('subclass must implement buildMcpConfig');
43
+ }
44
+
45
+ /**
46
+ * MCP 配置文件路径(每个 agent 独立,避免多 agent 覆盖冲突)
47
+ * @param {string} projectDir - 项目目录
48
+ * @param {string} agent - agent 名称
49
+ * @returns {string} MCP 配置文件的绝对路径
50
+ */
51
+ getMcpConfigPath(projectDir, agent) {
52
+ throw new Error('subclass must implement getMcpConfigPath');
53
+ }
54
+ }
55
+
56
+ /**
57
+ * 适配器工厂
58
+ * @param {string} cliType - CLI 类型标识('claude-code' | 'opencode')
59
+ * @returns {BaseAdapter}
60
+ */
61
+ export async function createAdapter(cliType) {
62
+ // 动态 import 避免启动时加载所有适配器
63
+ const adapters = {
64
+ 'claude-code': () => import('./claude-code.js').then(m => new m.ClaudeCodeAdapter()),
65
+ 'opencode': () => import('./opencode.js').then(m => new m.OpenCodeAdapter()),
66
+ };
67
+
68
+ const factory = adapters[cliType];
69
+ if (!factory) {
70
+ throw new Error(`Unknown CLI type: "${cliType}". Available: ${Object.keys(adapters).join(', ')}`);
71
+ }
72
+ return factory();
73
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Claude Code CLI 适配器
3
+ *
4
+ * claude code CLI 关键 flags:
5
+ * --agent <name> agent 模式
6
+ * --append-system-prompt <text> 追加系统提示词
7
+ * --mcp-config <path> 加载 MCP 配置文件
8
+ * --strict-mcp-config 只用 --mcp-config 指定的 MCP server
9
+ *
10
+ * MCP 配置格式(.mcp.json 风格):
11
+ * { "openteam": { "type": "http", "url": "http://host:port/mcp?agent=xxx" } }
12
+ */
13
+
14
+ import os from 'os';
15
+ import path from 'path';
16
+ import { BaseAdapter } from './base.js';
17
+
18
+ export class ClaudeCodeAdapter extends BaseAdapter {
19
+ get binary() { return 'claude'; }
20
+
21
+ buildLaunchArgs({ agent, systemPrompt, mcpConfigPath, cwd, extraArgs = [] }) {
22
+ const args = [this.binary];
23
+ if (agent) args.push('--agent', agent);
24
+ if (systemPrompt) args.push('--append-system-prompt', systemPrompt);
25
+ if (mcpConfigPath) args.push('--mcp-config', mcpConfigPath);
26
+ args.push(...extraArgs);
27
+ return args;
28
+ }
29
+
30
+ buildMcpConfig({ serverUrl, agent }) {
31
+ // claude code --mcp-config 格式:需要 mcpServers 包裹
32
+ return {
33
+ mcpServers: {
34
+ openteam: {
35
+ type: 'http',
36
+ url: `${serverUrl}/mcp?agent=${encodeURIComponent(agent)}`,
37
+ },
38
+ },
39
+ };
40
+ }
41
+
42
+ getMcpConfigPath(projectDir, agent) {
43
+ // 用临时目录避免污染项目的 .mcp.json(可能有用户自己的 MCP 配置)
44
+ // 每个 agent 独立文件,避免多 agent 覆盖冲突
45
+ // wrapper 通过 --mcp-config 显式传入这个路径
46
+ return path.join(os.tmpdir(), `openteam-mcp-${agent}-${encodeURIComponent(path.basename(projectDir))}.json`);
47
+ }
48
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * OpenCode CLI 适配器
3
+ *
4
+ * TODO: opencode 的 CLI flags 和 MCP 配置方式待确认。
5
+ * 当前实现基于推测,联调时根据实际文档调整。
6
+ */
7
+
8
+ import os from 'os';
9
+ import path from 'path';
10
+ import { BaseAdapter } from './base.js';
11
+
12
+ export class OpenCodeAdapter extends BaseAdapter {
13
+ get binary() { return 'opencode'; }
14
+
15
+ buildLaunchArgs({ agent, systemPrompt, mcpConfigPath, cwd, extraArgs = [] }) {
16
+ const args = [this.binary];
17
+ // TODO: opencode 的 system prompt 和 agent 参数待确认
18
+ if (systemPrompt) args.push('--prompt', systemPrompt);
19
+ args.push(...extraArgs);
20
+ return args;
21
+ }
22
+
23
+ buildMcpConfig({ serverUrl, agent }) {
24
+ // TODO: opencode 的 MCP 配置格式待确认
25
+ return {
26
+ openteam: {
27
+ type: 'http',
28
+ url: `${serverUrl}/mcp?agent=${encodeURIComponent(agent)}`,
29
+ },
30
+ };
31
+ }
32
+
33
+ getMcpConfigPath(projectDir, agent) {
34
+ // TODO: opencode 的 MCP 配置路径待确认
35
+ return path.join(os.tmpdir(), `openteam-mcp-opencode-${agent}-${encodeURIComponent(path.basename(projectDir))}.json`);
36
+ }
37
+ }
@@ -1,19 +1,29 @@
1
1
  /**
2
2
  * 任务看板 — 创建/完成/依赖检查/自动通知
3
+ *
4
+ * 通知通过 hub.deliver() 投递到消息队列,由 wrapper 轮询取走注入 CLI。
3
5
  */
4
6
 
5
7
  import { loadTasks, saveTasks } from '../foundation/tasks.js';
6
- import { deliverMessage } from './messaging.js';
7
8
  import { isAgentInTeam, getTeamLeader } from '../foundation/config.js';
8
9
  import { createLogger } from '../foundation/logger.js';
9
10
 
10
11
  const log = createLogger('taskboard');
11
12
 
12
13
  /**
13
- * 创建任务(仅 leader 可调,权限校验由 tools.js 负责)
14
+ * 创建任务(仅 leader 可调,权限校验由 mcp.js 负责)
15
+ * @param {object} params
16
+ * @param {string} params.teamName
17
+ * @param {string} params.projectDir
18
+ * @param {MessageHub} params.hub - 消息中枢
19
+ * @param {string} params.title
20
+ * @param {string} params.description
21
+ * @param {string} params.assignee
22
+ * @param {number[]} params.dependsOn
23
+ * @param {string} params.trace
14
24
  * @returns {{ ok: true, task: object, triggered: string[] } | { ok: false, error: string }}
15
25
  */
16
- export async function createTask({ teamName, projectDir, serveUrl, title, description, assignee, dependsOn = [], trace }) {
26
+ export async function createTask({ teamName, projectDir, hub, title, description, assignee, dependsOn = [], trace }) {
17
27
  // 1. 校验 assignee 在团队中
18
28
  if (!isAgentInTeam(teamName, assignee)) {
19
29
  return { ok: false, error: `团队中没有 "${assignee}"` };
@@ -45,16 +55,23 @@ export async function createTask({ teamName, projectDir, serveUrl, title, descri
45
55
  log.info('task.created', { trace, id: task.id, title, assignee, dependsOn });
46
56
 
47
57
  // 4. 检查依赖是否已满足,满足则通知
48
- const triggered = await notifyIfReady(task, data.tasks, teamName, projectDir, serveUrl, trace);
58
+ const triggered = notifyIfReady(task, data.tasks, hub, trace);
49
59
 
50
60
  return { ok: true, task, triggered };
51
61
  }
52
62
 
53
63
  /**
54
64
  * 完成任务
65
+ * @param {object} params
66
+ * @param {string} params.teamName
67
+ * @param {string} params.projectDir
68
+ * @param {MessageHub} params.hub - 消息中枢
69
+ * @param {string} params.agentName
70
+ * @param {number} params.taskId
71
+ * @param {string} params.trace
55
72
  * @returns {{ ok: true, task: object, triggered: string[] } | { ok: false, error: string }}
56
73
  */
57
- export async function completeTask({ teamName, projectDir, serveUrl, agentName, taskId, trace }) {
74
+ export async function completeTask({ teamName, projectDir, hub, agentName, taskId, trace }) {
58
75
  const data = loadTasks(teamName, projectDir);
59
76
  const task = data.tasks.find(t => t.id === taskId);
60
77
 
@@ -74,7 +91,7 @@ export async function completeTask({ teamName, projectDir, serveUrl, agentName,
74
91
  for (const t of data.tasks) {
75
92
  if (t.status !== 'pending') continue;
76
93
  if (t.dependsOn.length === 0) continue;
77
- if (!t.dependsOn.includes(taskId)) continue; // 不依赖刚完成的任务,跳过
94
+ if (!t.dependsOn.includes(taskId)) continue;
78
95
 
79
96
  const allDone = t.dependsOn.every(depId => {
80
97
  const dep = data.tasks.find(d => d.id === depId);
@@ -82,7 +99,7 @@ export async function completeTask({ teamName, projectDir, serveUrl, agentName,
82
99
  });
83
100
 
84
101
  if (allDone) {
85
- const result = await notifyAssignee(t, teamName, projectDir, serveUrl, trace);
102
+ const result = notifyAssignee(t, hub, trace);
86
103
  triggered.push(`#${t.id} ${t.title} → ${t.assignee} (${result})`);
87
104
  }
88
105
  }
@@ -92,7 +109,7 @@ export async function completeTask({ teamName, projectDir, serveUrl, agentName,
92
109
  if (leader && leader !== agentName) {
93
110
  const leaderMsg = `[task #${taskId} done] ${task.title} — ${agentName} 已完成`;
94
111
  try {
95
- await deliverMessage({ to: leader, message: leaderMsg, teamName, projectDir, serveUrl, trace });
112
+ hub.deliver({ from: agentName, to: leader, message: leaderMsg });
96
113
  } catch (err) {
97
114
  log.warn('notifyLeader failed', { trace, taskId, leader, error: err.message });
98
115
  }
@@ -114,10 +131,10 @@ export function listTasks(teamName, projectDir) {
114
131
  /**
115
132
  * 检查任务依赖是否满足,满足则通知 assignee
116
133
  */
117
- async function notifyIfReady(task, allTasks, teamName, projectDir, serveUrl, trace) {
134
+ function notifyIfReady(task, allTasks, hub, trace) {
118
135
  if (task.dependsOn.length === 0) {
119
136
  // 无依赖,立即通知
120
- const result = await notifyAssignee(task, teamName, projectDir, serveUrl, trace);
137
+ const result = notifyAssignee(task, hub, trace);
121
138
  return [`#${task.id} ${task.title} → ${task.assignee} (${result})`];
122
139
  }
123
140
 
@@ -127,7 +144,7 @@ async function notifyIfReady(task, allTasks, teamName, projectDir, serveUrl, tra
127
144
  });
128
145
 
129
146
  if (allDone) {
130
- const result = await notifyAssignee(task, teamName, projectDir, serveUrl, trace);
147
+ const result = notifyAssignee(task, hub, trace);
131
148
  return [`#${task.id} ${task.title} → ${task.assignee} (${result})`];
132
149
  }
133
150
 
@@ -137,13 +154,14 @@ async function notifyIfReady(task, allTasks, teamName, projectDir, serveUrl, tra
137
154
  /**
138
155
  * 发送任务就绪通知(通知失败不影响任务操作本身)
139
156
  */
140
- async function notifyAssignee(task, teamName, projectDir, serveUrl, trace) {
157
+ function notifyAssignee(task, hub, trace) {
141
158
  const message = task.description
142
159
  ? `[task #${task.id}] ${task.title}:${task.description}`
143
160
  : `[task #${task.id}] ${task.title}`;
144
161
 
145
162
  try {
146
- return await deliverMessage({ to: task.assignee, message, teamName, projectDir, serveUrl, trace });
163
+ const result = hub.deliver({ from: 'system', to: task.assignee, message });
164
+ return `${task.assignee}: ${result.online ? 'delivered' : 'queued'}`;
147
165
  } catch (err) {
148
166
  log.warn('notifyAssignee failed', { trace, taskId: task.id, assignee: task.assignee, error: err.message });
149
167
  return `${task.assignee}: 通知失败 (${err.message})`;
@@ -71,16 +71,17 @@ export function isAgentInTeam(teamName, agentName) {
71
71
 
72
72
  /**
73
73
  * List all teams
74
+ * 扫描 ~/.openteam/teams/ 下含 team.json 的目录
74
75
  */
75
76
  export function listTeams() {
76
- if (!fs.existsSync(PATHS.AGENTS_DIR)) return [];
77
+ if (!fs.existsSync(PATHS.TEAMS_DIR)) return [];
77
78
 
78
- const entries = fs.readdirSync(PATHS.AGENTS_DIR, { withFileTypes: true });
79
+ const entries = fs.readdirSync(PATHS.TEAMS_DIR, { withFileTypes: true });
79
80
  const teams = [];
80
81
 
81
82
  for (const entry of entries) {
82
83
  if (!entry.isDirectory()) continue;
83
- const teamConfigPath = path.join(PATHS.AGENTS_DIR, entry.name, FILES.TEAM_CONFIG);
84
+ const teamConfigPath = path.join(PATHS.TEAMS_DIR, entry.name, FILES.TEAM_CONFIG);
84
85
  if (fs.existsSync(teamConfigPath)) {
85
86
  teams.push(entry.name);
86
87
  }
@@ -5,17 +5,17 @@ import crypto from 'crypto';
5
5
  const homeDir = os.homedir();
6
6
 
7
7
  export const PATHS = {
8
- OPENCODE_DIR: path.join(homeDir, '.opencode'),
9
- AGENTS_DIR: path.join(homeDir, '.opencode/agents'),
10
8
  OPENTEAM_DIR: path.join(homeDir, '.openteam'),
11
9
  SETTINGS: path.join(homeDir, '.openteam', 'settings.json'),
10
+ AGENTS_DEFS_DIR: path.join(homeDir, '.openteam', 'agents'), // agent 定义(所有团队共享)
11
+ SKILLS_DIR: path.join(homeDir, '.openteam', 'skills'), // skill 定义
12
+ TEAMS_DIR: path.join(homeDir, '.openteam', 'teams'), // 团队配置 + 运行时状态
12
13
  };
13
14
 
14
15
  export const FILES = {
15
16
  TEAM_CONFIG: 'team.json',
16
17
  STATE: '.state.json',
17
- RUNTIME: '.runtime.json',
18
- ACTIVE_SESSIONS: '.active-sessions.json',
18
+ RUNTIME: '.runtime.json', // 迁移兼容用
19
19
  TASKS: '.tasks.json',
20
20
  };
21
21
 
@@ -26,7 +26,7 @@ export const DEFAULTS = {
26
26
  };
27
27
 
28
28
  export function getTeamDir(teamName) {
29
- return path.join(PATHS.AGENTS_DIR, teamName);
29
+ return path.join(PATHS.TEAMS_DIR, teamName);
30
30
  }
31
31
 
32
32
  /**
@@ -38,10 +38,10 @@ export function projectDirHash(projectDir) {
38
38
 
39
39
  /**
40
40
  * 获取团队的项目级状态目录(纯计算,不创建)
41
- * 路径: ~/.opencode/agents/<teamName>/<hash>/
41
+ * 路径: ~/.openteam/teams/<teamName>/<hash>/
42
42
  */
43
43
  export function getTeamStateDir(teamName, projectDir) {
44
- return path.join(PATHS.AGENTS_DIR, teamName, projectDirHash(projectDir));
44
+ return path.join(PATHS.TEAMS_DIR, teamName, projectDirHash(projectDir));
45
45
  }
46
46
 
47
47
  /**