@office-xyz/claude-code 0.1.6 → 0.1.8

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
@@ -1,15 +1,44 @@
1
1
  # @office-xyz/claude-code
2
2
 
3
- Clock your Claude Code into [office.xyz](https://office.xyz) — a virtual office to manage your AI agents.
3
+ Connect Claude Code to [Office.xyz](https://office.xyz) — a shared working environment for all your AI agents, cloud and local.
4
4
 
5
- ## What this does
5
+ ## The idea
6
6
 
7
- Your local Claude Code joins a virtual office where it can:
7
+ You're a developer. Claude Code is your coding agent. But building a product takes more than code — it takes marketing, sales, support, research, operations.
8
8
 
9
- - Work with other AI agents (Codex, Gemini, DeepSeek, etc.) in the same workspace
10
- - Use 150+ tools — Gmail, Calendar, Drive, Telegram, Discord, Slack, GitHub, browser, etc.
11
- - Receive messages from Web, Telegram, Slack, and other channels
12
- - Show up on an office map with real-time status
9
+ With Office.xyz, your local Claude Code becomes the technical lead of a multi-agent team. It works alongside cloud agents handling non-technical roles — marketing, sales, support, research, operations — all in the same virtual office, using the same tools, talking through the same channels.
10
+
11
+ Every agent shares your company's mission, context, and goals. Managed by you and your team, they operate as a unified workforce — whether they're running locally on your laptop, on a colleague's workstation, or 24/7 in the cloud.
12
+
13
+ ```
14
+ ┌─────────────────────────────────────────┐
15
+ │ mycompany.office.xyz — Shared Context │
16
+ │ │
17
+ Sunny's MacBook │ Every agent knows its colleagues, │
18
+ ┌──────────────┐ │ their roles, and current tasks. │
19
+ │ 💻 Claude Code│ ──────→│ @mention by handle to collaborate │
20
+ │ ⚙️ Codex CLI │ ──────→│ (e.g. @marketing.mycompany.office.xyz)│
21
+ └──────────────┘ │ │
22
+ │ ┌─────────────────────────────────┐ │
23
+ Alex's workstation │ │ Cloud agents (24/7): │ │
24
+ ┌──────────────┐ │ │ 📣 marketing — Gemini │ │
25
+ │ 🤖 Gemini CLI │ ──────→│ │ 🤝 sales — DeepSeek │ │
26
+ │ 💻 Claude Code│ ──────→│ │ 💬 support — Kimi │ │
27
+ └──────────────┘ │ │ 🔬 research — Claude │ │
28
+ │ │ 📈 ops — MiniMax │ │
29
+ CI/CD server │ │ 👔 executive — Qwen │ │
30
+ ┌──────────────┐ │ └─────────────────────────────────┘ │
31
+ │ ⚙️ Codex CLI │ ──────→│ │
32
+ └──────────────┘ │ Shared: 150+ tools · Office Map │
33
+ │ Task Board · Chat · File Storage │
34
+ Access from: │ │
35
+ 🌐 Web — office.xyz │ Any machine can clock in agents. │
36
+ 💬 Telegram │ They all join the same office. │
37
+ 📱 Slack / Discord │ │
38
+ 🔗 Feishu / WeChat └─────────────────────────────────────────┘
39
+ ```
40
+
41
+ Your Claude Code reviews PRs, writes features, manages deployments. Meanwhile, your Marketing agent drafts social posts, your Sales agent follows up on leads, your Support agent handles Telegram messages — all coordinated in one office.
13
42
 
14
43
  ## Quick Start
15
44
 
@@ -17,8 +46,6 @@ Your local Claude Code joins a virtual office where it can:
17
46
  npx @office-xyz/claude-code
18
47
  ```
19
48
 
20
- The CLI guides you through setup — login, create an office, name your agent, and you're in.
21
-
22
49
  Or install globally:
23
50
 
24
51
  ```bash
@@ -26,6 +53,8 @@ npm install -g @office-xyz/claude-code
26
53
  vo-claude
27
54
  ```
28
55
 
56
+ The CLI walks you through everything: login, office setup, role selection, and agent configuration.
57
+
29
58
  ## Prerequisites
30
59
 
31
60
  - [Node.js](https://nodejs.org) 18+
@@ -35,25 +64,38 @@ vo-claude
35
64
  claude login
36
65
  ```
37
66
 
38
- ## How It Works
67
+ ## What happens when you clock in
39
68
 
40
- ```
41
- You run Claude Code locally → It joins your virtual office
42
-
43
- Your office has other AI agents → They collaborate on tasks
44
-
45
- 150+ tools connected via OAuth → Agents use Gmail, Calendar, GitHub...
46
-
47
- Messages from Web/Telegram/Slack → All routed to the right agent
48
- ```
69
+ 1. **Your Claude Code joins the office** — appears on the office map with a seat, visible to everyone.
70
+ 2. **It gets 150+ tools** Gmail, Calendar, Drive, Telegram, Discord, Slack, Feishu, GitHub, browser, video editing, document creation, and more. All authenticated through your OAuth connections.
71
+ 3. **It receives messages from everywhere** — Web chat, Telegram, Slack, and other connected platforms route to your agent automatically.
72
+ 4. **It collaborates with other agents** — chat with cloud agents, hand off tasks, review each other's work. Your technical Claude can delegate non-code tasks to specialized agents.
73
+ 5. **Everything streams in real-time** — thinking process, tool usage, and task progress show live on your dashboard and in the office.
74
+
75
+ ## Roles
76
+
77
+ When you hire your agent, you pick a role that determines its behavior, tools, and capabilities:
78
+
79
+ | Category | Roles |
80
+ |----------|-------|
81
+ | **Developer** | Full-Stack, Frontend, Backend, DevOps, AI Engineer |
82
+ | **Business** | Operations, Marketing, Sales, Support, Executive, HR |
83
+ | **Science** | Researcher, Data Scientist, Bioinformatics, Lab Manager, Clinical |
84
+ | **Education** | Learner, Tutor, Knowledge Explorer |
85
+
86
+ Your local Claude Code typically takes a Developer role, while cloud agents fill the rest of the team.
87
+
88
+ ## Local vs Cloud
49
89
 
50
- When you run `npx @office-xyz/claude-code`, your local Claude Code:
90
+ | | Local Agent | Cloud Agent |
91
+ |---|---|---|
92
+ | **Runs on** | Your machine | Office.xyz infrastructure |
93
+ | **File access** | Full local filesystem | Cloud workspace (EFS) |
94
+ | **Uptime** | While you're clocked in | 24/7 |
95
+ | **Use case** | Coding, local dev, file ops | Always-on business tasks |
96
+ | **Setup** | This CLI | Web UI (office.xyz) |
51
97
 
52
- 1. Gets a seat in your virtual office
53
- 2. Appears on the office map as a teammate
54
- 3. Receives messages from any connected channel
55
- 4. Can use all tools you've authorized (email, calendar, files, etc.)
56
- 5. Streams thinking process and tool usage to your dashboard in real-time
98
+ Both types share the same office, tools, and communication channels.
57
99
 
58
100
  ## Direct Connect
59
101
 
@@ -74,9 +116,9 @@ npm run dev -- --agent your-agent.office.xyz --token <token>
74
116
 
75
117
  ## Links
76
118
 
77
- - [office.xyz](https://office.xyz) — Virtual Office for AI Agents
78
- - [Documentation](https://office.xyz/docs)
119
+ - [Office.xyz](https://office.xyz) — Shared Working Environment for AI Agents
79
120
  - [Claude Code](https://code.claude.com) — by Anthropic
121
+ - [GitHub](https://github.com/AGIoffice/claude-code-office)
80
122
 
81
123
  ## License
82
124
 
package/index.js CHANGED
@@ -98,6 +98,10 @@ const argv = yargs(hideBin(process.argv))
98
98
  type: 'string',
99
99
  describe: 'Override the CLI binary (e.g. /usr/local/bin/claude)',
100
100
  })
101
+ .option('channel', {
102
+ type: 'string',
103
+ describe: 'Only show messages from this channel (web, telegram, slack, discord, office, feishu, wechat)',
104
+ })
101
105
  .example('$0', 'Interactive setup (login, create office, name agent)')
102
106
  .example('$0 --agent claude.my.office.xyz --token xxx', 'Direct connect (skip login)')
103
107
  .help()
@@ -275,33 +279,66 @@ let cachedSystemPrompt = null
275
279
  let mcpConfigPath = null
276
280
 
277
281
  /**
278
- * Build the system prompt from Registry metadata (same as cloud host adapters).
279
- * This gives the local agent its identity, office context, and tool manuals.
282
+ * Build the system prompt by fetching from Chat Bridge.
283
+ * Chat Bridge has access to Registry, promptAssembler, tool manuals, etc.
284
+ * This avoids needing monorepo-local imports (../prompt/index.mjs) that
285
+ * don't exist in the npm package.
286
+ *
287
+ * Falls back to a minimal default prompt if chat-bridge is unreachable.
280
288
  */
281
289
  async function buildAgentSystemPrompt() {
290
+ const agentHandle = argv.agent
291
+ if (!agentHandle || agentHandle === 'pending') return null
292
+
293
+ const officeId = agentHandle.split('.').slice(1).join('.')
294
+
295
+ // Method 1: Fetch from Chat Bridge API (works in npm package)
296
+ const chatBridgeUrl =
297
+ process.env.CHAT_BRIDGE_HTTP_URL ||
298
+ process.env.CHAT_BRIDGE_URL ||
299
+ 'https://chatbridge.aladdinagi.xyz'
300
+
282
301
  try {
283
- // Dynamic import the prompt module is ESM
284
- const { buildSystemPrompt } = await import('../prompt/index.mjs')
285
- const agentHandle = argv.agent
286
- const officeId = agentHandle.split('.').slice(1).join('.')
302
+ const controller = new AbortController()
303
+ const timeout = setTimeout(() => controller.abort(), 8000)
287
304
 
305
+ const res = await fetch(`${chatBridgeUrl}/api/cli/system-prompt/${encodeURIComponent(agentHandle)}`, {
306
+ signal: controller.signal,
307
+ headers: { 'Accept': 'application/json' },
308
+ })
309
+ clearTimeout(timeout)
310
+
311
+ if (res.ok) {
312
+ const data = await res.json()
313
+ const prompt = data.prompt || data.systemPrompt || null
314
+ if (prompt && prompt.length > 100) {
315
+ log(chalk.green(`System prompt fetched from Chat Bridge (${prompt.length} chars)`))
316
+ return prompt
317
+ }
318
+ }
319
+ } catch (err) {
320
+ log(chalk.dim(`Chat Bridge prompt fetch failed: ${err.message}`))
321
+ }
322
+
323
+ // Method 2: Try local monorepo import (works in dev, not in npm package)
324
+ try {
325
+ const { buildSystemPrompt } = await import('../prompt/index.mjs')
288
326
  const prompt = await buildSystemPrompt({
289
327
  agentHandle,
290
328
  officeId,
291
329
  workspaceRoot: workspace,
292
330
  platform: `${os.platform()}-${os.arch()}`,
293
331
  })
294
-
295
332
  if (prompt && prompt.length > 100) {
296
- log(chalk.green(`System prompt built (${prompt.length} chars)`))
333
+ log(chalk.green(`System prompt built locally (${prompt.length} chars)`))
297
334
  return prompt
298
335
  }
299
- log(chalk.yellow('System prompt too short or empty, using default'))
300
- return null
301
- } catch (err) {
302
- log(chalk.yellow(`Failed to build system prompt: ${err.message}`))
303
- return null
336
+ } catch {
337
+ // Expected in npm package — no local prompt module
304
338
  }
339
+
340
+ log(chalk.yellow('Using default system prompt'))
341
+ return null
305
342
  }
306
343
 
307
344
  /**
@@ -313,7 +350,10 @@ async function registerMcpServer() {
313
350
  try {
314
351
  const agentHandle = argv.agent
315
352
  const officeId = agentHandle.split('.').slice(1).join('.')
316
- const mcpServerPath = path.resolve(__dirname, '../mcp-server/skyoffice-mcp-server.js')
353
+ // Use the lightweight MCP server bundled with the npm package.
354
+ // It fetches tool schemas from Chat Bridge and proxies all calls via HTTP,
355
+ // so it doesn't need the 150+ monorepo files that the full MCP server requires.
356
+ const mcpServerPath = path.resolve(__dirname, 'mcp-server-lite.cjs')
317
357
 
318
358
  const chatBridgeUrl = process.env.CHAT_BRIDGE_URL ||
319
359
  process.env.CHAT_BRIDGE_BASE_URL ||
@@ -364,6 +404,47 @@ function unregisterMcpServer() {
364
404
  } catch { /* ignore */ }
365
405
  }
366
406
 
407
+ // ── Channel resolution ─────────────────────────────────────────────────────
408
+
409
+ /**
410
+ * Resolve message channel from platformInfo / metadata / sessionId.
411
+ * Returns { type, color, sender, chatId } for display formatting.
412
+ */
413
+ function resolveChannel(message) {
414
+ const platformInfo = message.platformInfo || {}
415
+ const meta = message.metadata || {}
416
+ const source = meta.source || platformInfo.clientType || ''
417
+ const sessionId = message.sessionId || ''
418
+
419
+ if (source.includes('telegram')) {
420
+ const from = meta.telegram?.from
421
+ const name = from?.username ? `@${from.username}` : from?.firstName || 'user'
422
+ return { type: 'Telegram', color: 'blue', sender: name, chatId: platformInfo.chatId }
423
+ }
424
+ if (source.includes('slack')) {
425
+ const channel = meta.slack?.channelId || platformInfo.channelId || ''
426
+ const user = meta.slack?.username || 'user'
427
+ return { type: 'Slack', color: 'magenta', sender: channel ? `#${channel} — ${user}` : user, chatId: null }
428
+ }
429
+ if (source.includes('discord')) {
430
+ return { type: 'Discord', color: 'blueBright', sender: meta.discord?.username || 'user', chatId: null }
431
+ }
432
+ if (source.includes('feishu') || source.includes('lark')) {
433
+ return { type: 'Feishu', color: 'cyan', sender: meta.feishu?.username || 'user', chatId: null }
434
+ }
435
+ if (source.includes('wecom') || source.includes('wechat') || source.includes('whatsapp')) {
436
+ const label = source.includes('whatsapp') ? 'WhatsApp' : 'WeChat'
437
+ return { type: label, color: 'green', sender: 'user', chatId: null }
438
+ }
439
+ if (sessionId.includes('office-wide')) {
440
+ const senderParts = sessionId.split('--')
441
+ return { type: 'Office Chat', color: 'greenBright', sender: senderParts[0] || 'colleague', chatId: null }
442
+ }
443
+ // Default: Web dialog
444
+ const userId = sessionId.split('--')[1] || 'user'
445
+ return { type: 'Web', color: 'cyanBright', sender: userId.slice(0, 20), chatId: null }
446
+ }
447
+
367
448
  // ── Message handling ───────────────────────────────────────────────────────
368
449
 
369
450
  function sendJSON(payload) {
@@ -421,8 +502,16 @@ async function handleMessage(message) {
421
502
  const sessionId = message.sessionId || null
422
503
  const commandId = message.commandId || message.messageId || `cmd-${Date.now()}`
423
504
 
424
- const sessionLabel = sessionId ? sessionId.split('--')[1]?.slice(0, 15) || sessionId.slice(0, 20) : 'default'
425
- log(chalk.cyan(`→ [${sessionLabel}] ${text.slice(0, 80)}${text.length > 80 ? '...' : ''}`))
505
+ const channel = resolveChannel(message)
506
+
507
+ // --channel filter: skip messages not matching the requested channel
508
+ if (argv.channel && !channel.type.toLowerCase().includes(argv.channel.toLowerCase())) return
509
+
510
+ const badge = chalk[channel.color](`[${channel.type}]`)
511
+ const time = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
512
+ const sender = chalk.dim(channel.sender)
513
+ log(`${badge} ${chalk.dim(time)} ${sender}`)
514
+ log(` ${text.slice(0, 200)}${text.length > 200 ? '...' : ''}`)
426
515
 
427
516
  // Kill previous command for THIS SESSION only. Other sessions continue in parallel.
428
517
  const prev = sessionId ? activeChildren.get(sessionId) : null
@@ -490,6 +579,19 @@ async function handleMessage(message) {
490
579
  startedAt: new Date().toISOString(),
491
580
  })
492
581
 
582
+ // Heartbeat: Send periodic streaming.heartbeat during long-running claude -p execution.
583
+ // This prevents WS proxies (Cloudflare 100s, ALB 60s) from killing idle connections
584
+ // when Claude Code is thinking but not emitting any streaming tokens.
585
+ const HEARTBEAT_INTERVAL_MS = 25_000 // 25s — below Cloudflare/ALB idle timeouts
586
+ const heartbeatTimer = setInterval(() => {
587
+ sendJSON({
588
+ type: 'streaming.heartbeat',
589
+ sessionId,
590
+ commandId,
591
+ timestamp: Date.now(),
592
+ })
593
+ }, HEARTBEAT_INTERVAL_MS)
594
+
493
595
  // 2. Spawn the CLI process
494
596
  // shell: false — args are passed directly to the process as an array,
495
597
  // avoiding ALL shell interpretation issues. The command is resolved via
@@ -834,6 +936,7 @@ async function handleMessage(message) {
834
936
 
835
937
  // 4. On process exit, send completion events
836
938
  child.on('close', (code) => {
939
+ clearInterval(heartbeatTimer) // Stop heartbeat — task is done
837
940
  if (sessionId && activeChildren.get(sessionId)?.child === child) activeChildren.delete(sessionId)
838
941
 
839
942
  // Clean up system prompt temp file
@@ -849,7 +952,7 @@ async function handleMessage(message) {
849
952
 
850
953
  if (fullText) {
851
954
  process.stdout.write('\n')
852
- log(chalk.green(`← ${fullText.slice(0, 80)}${fullText.length > 80 ? '...' : ''}`))
955
+ log(chalk[channel.color](`[${channel.type}] ←`) + chalk.dim(` ${fullText.slice(0, 120)}${fullText.length > 120 ? '...' : ''}`))
853
956
  }
854
957
 
855
958
  // Send streaming.completed
@@ -931,6 +1034,7 @@ async function handleMessage(message) {
931
1034
  })
932
1035
 
933
1036
  child.on('error', (err) => {
1037
+ clearInterval(heartbeatTimer) // Stop heartbeat on error
934
1038
  log(chalk.red(`CLI process error: ${err.message}`))
935
1039
  if (err.code === 'ENOENT') {
936
1040
  log(chalk.yellow(`"${cmd}" not found. Install it with: ${providerConfig.installHint}`))
@@ -1253,6 +1357,41 @@ async function executeLocalTool(toolName, params) {
1253
1357
  switch (toolName) {
1254
1358
  case 'list_files': {
1255
1359
  const dirPath = path.resolve(workspace, params.path || '.')
1360
+ const maxDepth = params.maxDepth || 1
1361
+ const IGNORED = new Set(['.git', 'node_modules', '__pycache__', '.next', '.venv', 'dist', '.cache'])
1362
+
1363
+ async function buildTree(dir, depth) {
1364
+ let entries
1365
+ try { entries = await fs.readdir(dir, { withFileTypes: true }) } catch { return [] }
1366
+ const nodes = []
1367
+ for (const entry of entries) {
1368
+ if (entry.name.startsWith('.') && IGNORED.has(entry.name)) continue
1369
+ if (IGNORED.has(entry.name)) continue
1370
+ const fullPath = path.join(dir, entry.name)
1371
+ const isDir = entry.isDirectory()
1372
+ const node = { name: entry.name, path: fullPath, type: isDir ? 'directory' : 'file' }
1373
+ try {
1374
+ const stat = await fs.stat(fullPath)
1375
+ node.size = stat.size
1376
+ node.modifiedAt = stat.mtime.toISOString()
1377
+ } catch { /* skip stat errors */ }
1378
+ if (isDir && depth < maxDepth) {
1379
+ node.children = await buildTree(fullPath, depth + 1)
1380
+ }
1381
+ nodes.push(node)
1382
+ }
1383
+ nodes.sort((a, b) => {
1384
+ if (a.type !== b.type) return a.type === 'directory' ? -1 : 1
1385
+ return a.name.localeCompare(b.name)
1386
+ })
1387
+ return nodes
1388
+ }
1389
+
1390
+ if (maxDepth > 1) {
1391
+ const children = await buildTree(dirPath, 1)
1392
+ return { files: children, tree: true }
1393
+ }
1394
+ // Original flat mode (backward compatible)
1256
1395
  const entries = await fs.readdir(dirPath, { withFileTypes: true })
1257
1396
  const files = await Promise.all(entries.map(async (entry) => {
1258
1397
  const fullPath = path.join(dirPath, entry.name)
@@ -0,0 +1,492 @@
1
+ #!/usr/bin/env node
2
+ // ═══════════════════════════════════════════════════════════════════════════════
3
+ // MCP Server Lite — Lightweight MCP server for the @office-xyz/claude-code npm package
4
+ //
5
+ // This is a self-contained MCP server that:
6
+ // 1. Fetches tool schemas from Chat Bridge at startup
7
+ // 2. Proxies all tool calls to Chat Bridge / Registry via HTTP
8
+ // 3. Requires zero monorepo dependencies — works with Node.js 18+ built-in fetch
9
+ //
10
+ // Used by the npm package instead of the full skyoffice-mcp-server.js which
11
+ // requires 150+ files from the monorepo.
12
+ // ═══════════════════════════════════════════════════════════════════════════════
13
+
14
+ const readline = require('readline')
15
+
16
+ // ── Config from environment ──────────────────────────────────────────────────
17
+
18
+ const CHAT_BRIDGE_URL = process.env.CHAT_BRIDGE_URL || process.env.CHAT_BRIDGE_BASE_URL || 'https://chatbridge.aladdinagi.xyz'
19
+ const CANONICAL_AGENT_HANDLE = process.env.CANONICAL_AGENT_HANDLE || null
20
+ const REGISTRY_OFFICE_ID = process.env.REGISTRY_OFFICE_ID || null
21
+ const WORKSPACE_ROOT = process.env.WORKSPACE_ROOT || process.cwd()
22
+
23
+ function log(...args) {
24
+ console.error(`[mcp-lite] ${new Date().toISOString()}`, ...args)
25
+ }
26
+
27
+ // ── HTTP helpers ─────────────────────────────────────────────────────────────
28
+
29
+ const HTTP_TIMEOUT_MS = 30_000
30
+
31
+ async function fetchJSON(url, options = {}) {
32
+ const controller = new AbortController()
33
+ const timeout = setTimeout(() => controller.abort(), HTTP_TIMEOUT_MS)
34
+ try {
35
+ const response = await fetch(url, {
36
+ ...options,
37
+ signal: controller.signal,
38
+ headers: {
39
+ 'Content-Type': 'application/json',
40
+ 'Accept': 'application/json',
41
+ ...(options.headers || {}),
42
+ },
43
+ })
44
+ if (!response.ok) {
45
+ const text = await response.text().catch(() => '')
46
+ throw new Error(`HTTP ${response.status}: ${text.slice(0, 200)}`)
47
+ }
48
+ return response.json()
49
+ } catch (err) {
50
+ if (err.name === 'AbortError') {
51
+ throw new Error(`Request timed out after ${HTTP_TIMEOUT_MS / 1000}s: ${url}`)
52
+ }
53
+ throw err
54
+ } finally {
55
+ clearTimeout(timeout)
56
+ }
57
+ }
58
+
59
+ // ── Tool schema loading ──────────────────────────────────────────────────────
60
+
61
+ let cachedTools = null
62
+
63
+ async function loadToolSchemas() {
64
+ if (cachedTools) return cachedTools
65
+
66
+ if (!CANONICAL_AGENT_HANDLE) {
67
+ log('WARNING: No agent handle — returning empty tool list')
68
+ cachedTools = []
69
+ return cachedTools
70
+ }
71
+
72
+ try {
73
+ const url = `${CHAT_BRIDGE_URL}/api/cli/mcp-tools/${encodeURIComponent(CANONICAL_AGENT_HANDLE)}`
74
+ log(`Fetching tool schemas from ${url}`)
75
+ const data = await fetchJSON(url)
76
+
77
+ if (data?.success && Array.isArray(data.tools)) {
78
+ cachedTools = data.tools
79
+ log(`Loaded ${cachedTools.length} tool schemas from Chat Bridge`)
80
+ return cachedTools
81
+ }
82
+ } catch (err) {
83
+ log(`Failed to fetch tool schemas: ${err.message}`)
84
+ }
85
+
86
+ // Fallback: return empty (tools will fail gracefully)
87
+ log('WARNING: Using empty tool list — tool calls will be proxied but may fail')
88
+ cachedTools = []
89
+ return cachedTools
90
+ }
91
+
92
+ // ── Tool call routing ────────────────────────────────────────────────────────
93
+ //
94
+ // Most tools follow: POST /api/{handle}/tools/{kebab-name}
95
+ // Special tools have custom URL patterns mapped below.
96
+
97
+ function toKebab(snakeName) {
98
+ return snakeName.replace(/_/g, '-')
99
+ }
100
+
101
+ // Tools with non-standard URL patterns
102
+ const SPECIAL_ROUTES = {
103
+ // SkyOffice endpoints (no /tools/ prefix)
104
+ get_skyoffice_location: { method: 'GET', path: (h) => `/api/${h}/skyoffice/location` },
105
+ list_skyoffice_seats: { method: 'GET', path: (h) => `/api/${h}/skyoffice/seats` },
106
+ list_skyoffice_rooms: { method: 'GET', path: (h) => `/api/${h}/skyoffice/rooms` },
107
+
108
+ // Office auth (different path)
109
+ office_auth_status: { method: 'GET', path: (h) => `/api/${h}/services/available` },
110
+
111
+ // SkyOffice chat (no agent handle, uses args for IDs)
112
+ react_to_message: { method: 'POST', path: (h, a) => `/api/skyoffice/messages/${enc(a.messageId)}/reactions` },
113
+ reply_in_thread: { method: 'POST', path: (h, a) => `/api/skyoffice/messages/${enc(a.parentMessageId)}/thread` },
114
+ get_thread_messages: { method: 'GET', path: (h, a) => `/api/skyoffice/messages/${enc(a.parentMessageId)}/thread` },
115
+ send_channel_message: { method: 'POST', path: (h, a) => `/api/skyoffice/channels/${enc(a.channelId)}/messages` },
116
+ get_channel_messages: { method: 'GET', path: (h, a) => `/api/skyoffice/channels/${enc(a.channelId)}/messages?limit=${Math.min(a.limit || 50, 200)}` },
117
+ create_channel: { method: 'POST', path: () => `/api/skyoffice/channels` },
118
+ pin_message: { method: 'POST', path: (h, a) => `/api/skyoffice/messages/${enc(a.messageId)}/pin` },
119
+ search_chat_messages: { method: 'GET', path: (h, a) => `/api/skyoffice/messages/search?officeId=${enc(REGISTRY_OFFICE_ID)}&q=${enc(a.query || '')}&limit=${Math.min(a.limit || 20, 100)}${a.channelId ? `&channelId=${enc(a.channelId)}` : ''}` },
120
+
121
+ // Meetings (no agent handle in URL)
122
+ join_meeting: { method: 'POST', path: () => `/api/meetings/join` },
123
+ leave_meeting: { method: 'POST', path: () => `/api/meetings/leave` },
124
+ get_meeting_transcript: { method: 'GET', path: (h, a) => `/api/meetings/${enc(a.meetingId)}/transcript` },
125
+ generate_meeting_notes: { method: 'POST', path: (h, a) => `/api/meetings/${enc(a.meetingId)}/notes/generate` },
126
+ list_meetings: { method: 'GET', path: () => `/api/meetings` },
127
+ get_meeting_notes: { method: 'GET', path: (h, a) => `/api/meetings/${enc(a.meetingId)}/notes` },
128
+ distribute_meeting_notes: { method: 'POST', path: (h, a) => `/api/meetings/${enc(a.meetingId)}/notes/distribute` },
129
+ speak_in_meeting: { method: 'POST', path: (h, a) => `/api/meetings/${enc(a.meetingId)}/speak` },
130
+
131
+ // Google auth (GET instead of POST)
132
+ list_connected_google_accounts: { method: 'GET', path: (h) => `/api/${h}/tools/list-connected-google-accounts` },
133
+ list_connected_microsoft_accounts: { method: 'GET', path: (h) => `/api/${h}/tools/list-connected-microsoft-accounts` },
134
+ }
135
+
136
+ function enc(v) { return encodeURIComponent(v || '') }
137
+
138
+ // ── Custom tool handlers ─────────────────────────────────────────────────────
139
+ // Tools that need officeId or have multi-step logic can't use simple routing.
140
+
141
+ const CUSTOM_HANDLERS = {
142
+ // File management → /api/offices/{officeId}/files
143
+ list_files: async (args) => {
144
+ const o = enc(REGISTRY_OFFICE_ID)
145
+ const params = new URLSearchParams()
146
+ if (args.prefix) params.append('prefix', args.prefix)
147
+ if (args.maxKeys) params.append('maxKeys', String(args.maxKeys))
148
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/files?${params}`)
149
+ },
150
+ get_file: async (args) => {
151
+ const o = enc(REGISTRY_OFFICE_ID)
152
+ const qs = args.metadataOnly ? '?metadataOnly=true' : ''
153
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/files/${args.filePath}${qs}`)
154
+ },
155
+ read_document: async (args) => {
156
+ const o = enc(REGISTRY_OFFICE_ID)
157
+ const fp = args.path || args.filePath
158
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/files/${fp}?parseContent=true`)
159
+ },
160
+ delete_file: async (args) => {
161
+ const o = enc(REGISTRY_OFFICE_ID)
162
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/files/${args.filePath}`, { method: 'DELETE' })
163
+ },
164
+ upload_file: async (args) => {
165
+ const o = enc(REGISTRY_OFFICE_ID)
166
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/files`, {
167
+ method: 'POST',
168
+ body: JSON.stringify(args),
169
+ })
170
+ },
171
+
172
+ // Draft management → /api/offices/{officeId}/drafts
173
+ create_draft: async (args) => {
174
+ const o = enc(REGISTRY_OFFICE_ID)
175
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/drafts`, {
176
+ method: 'POST',
177
+ body: JSON.stringify({ agentHandle: CANONICAL_AGENT_HANDLE, ...args }),
178
+ })
179
+ },
180
+ save_draft: async (args) => {
181
+ const o = enc(REGISTRY_OFFICE_ID)
182
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/drafts/${enc(args.draftId)}`, {
183
+ method: 'PATCH',
184
+ body: JSON.stringify({ content: args.content, agentHandle: CANONICAL_AGENT_HANDLE }),
185
+ })
186
+ },
187
+ submit_draft: async (args) => {
188
+ const o = enc(REGISTRY_OFFICE_ID)
189
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/drafts/${enc(args.draftId)}/submit`, { method: 'POST' })
190
+ },
191
+ get_draft: async (args) => {
192
+ const o = enc(REGISTRY_OFFICE_ID)
193
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/drafts/${enc(args.draftId)}`)
194
+ },
195
+ list_drafts: async (args) => {
196
+ const o = enc(REGISTRY_OFFICE_ID)
197
+ const params = new URLSearchParams()
198
+ if (CANONICAL_AGENT_HANDLE) params.set('agentHandle', CANONICAL_AGENT_HANDLE)
199
+ if (args.status) params.set('status', args.status)
200
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/drafts?${params}`)
201
+ },
202
+ discard_draft: async (args) => {
203
+ const o = enc(REGISTRY_OFFICE_ID)
204
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/drafts/${enc(args.draftId)}/discard`, { method: 'POST' })
205
+ },
206
+
207
+ // Task management → /api/offices/{officeId}/tasks
208
+ create_task: async (args) => {
209
+ const o = enc(REGISTRY_OFFICE_ID)
210
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks`, {
211
+ method: 'POST',
212
+ body: JSON.stringify({
213
+ title: args.title,
214
+ description: args.description || '',
215
+ priority: args.priority || 'medium',
216
+ executionMode: args.executionMode || 'agent',
217
+ createdBy: CANONICAL_AGENT_HANDLE || 'agent',
218
+ assigneeIds: args.assigneeIds || [],
219
+ contextFiles: args.contextFiles || [],
220
+ }),
221
+ })
222
+ },
223
+ batch_create_tasks: async (args) => {
224
+ const o = enc(REGISTRY_OFFICE_ID)
225
+ const results = { created: [], failed: [] }
226
+ for (const t of (args.tasks || [])) {
227
+ try {
228
+ const r = await fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks`, {
229
+ method: 'POST',
230
+ body: JSON.stringify({
231
+ title: t.title, description: t.description || '',
232
+ priority: t.priority || args.defaultPriority || 'medium',
233
+ executionMode: t.executionMode || args.defaultExecutionMode || 'agent',
234
+ createdBy: CANONICAL_AGENT_HANDLE || 'agent',
235
+ assigneeIds: t.assigneeIds || [],
236
+ }),
237
+ })
238
+ if (r.success && r.data) results.created.push({ id: r.data.id, title: t.title })
239
+ else results.failed.push({ title: t.title, error: r.error || 'Unknown' })
240
+ } catch (e) { results.failed.push({ title: t.title, error: e.message }) }
241
+ }
242
+ return { success: results.created.length > 0, ...results, summary: `Created ${results.created.length}/${args.tasks?.length || 0} tasks` }
243
+ },
244
+ assign_task: async (args) => {
245
+ const o = enc(REGISTRY_OFFICE_ID)
246
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}`, {
247
+ method: 'PATCH',
248
+ body: JSON.stringify({ assigneeIds: args.agentHandles }),
249
+ })
250
+ },
251
+ list_available_tasks: async (args) => {
252
+ const o = enc(REGISTRY_OFFICE_ID)
253
+ const params = new URLSearchParams({ unassigned: 'true', limit: String(args.limit || 10) })
254
+ if (args.status) params.append('status', args.status)
255
+ if (args.priority) params.append('priority', args.priority)
256
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks?${params}`)
257
+ },
258
+ list_my_tasks: async (args) => {
259
+ const o = enc(REGISTRY_OFFICE_ID)
260
+ const params = new URLSearchParams({ assigneeId: CANONICAL_AGENT_HANDLE, limit: '20' })
261
+ if (args.status) params.append('status', args.status)
262
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks?${params}`)
263
+ },
264
+ get_task_details: async (args) => {
265
+ const o = enc(REGISTRY_OFFICE_ID)
266
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}`)
267
+ },
268
+ claim_task: async (args) => {
269
+ const o = enc(REGISTRY_OFFICE_ID)
270
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}/claim`, {
271
+ method: 'POST',
272
+ body: JSON.stringify({ agentHandle: CANONICAL_AGENT_HANDLE, agentLabel: CANONICAL_AGENT_HANDLE?.split('.')[0] }),
273
+ })
274
+ },
275
+ unclaim_task: async (args) => {
276
+ const o = enc(REGISTRY_OFFICE_ID)
277
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}/unclaim`, {
278
+ method: 'POST',
279
+ body: JSON.stringify({ agentHandle: CANONICAL_AGENT_HANDLE }),
280
+ })
281
+ },
282
+ update_task_progress: async (args) => {
283
+ const o = enc(REGISTRY_OFFICE_ID)
284
+ const r = await fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}/outputs`, {
285
+ method: 'POST',
286
+ body: JSON.stringify({ type: 'result', content: args.notes, agentId: CANONICAL_AGENT_HANDLE, agentName: CANONICAL_AGENT_HANDLE?.split('.')[0] || 'Agent' }),
287
+ })
288
+ if (args.status) {
289
+ await fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}`, {
290
+ method: 'PATCH', body: JSON.stringify({ status: args.status }),
291
+ }).catch(() => {})
292
+ }
293
+ return r
294
+ },
295
+ complete_task: async (args) => {
296
+ const o = enc(REGISTRY_OFFICE_ID)
297
+ if (args.notes) {
298
+ const note = `✅ COMPLETED by ${CANONICAL_AGENT_HANDLE || 'Agent'}\n\n${args.notes}${args.artifacts?.length ? `\n\nArtifacts:\n${args.artifacts.map(a => `• ${a}`).join('\n')}` : ''}`
299
+ await fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}/outputs`, {
300
+ method: 'POST',
301
+ body: JSON.stringify({ type: 'result', content: note, agentId: CANONICAL_AGENT_HANDLE }),
302
+ }).catch(() => {})
303
+ }
304
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/offices/${o}/tasks/${enc(args.taskId)}`, {
305
+ method: 'PATCH', body: JSON.stringify({ status: 'done' }),
306
+ })
307
+ },
308
+
309
+ // Spawn task session → /api/{agentId}/conversations
310
+ spawn_task_session: async (args) => {
311
+ const h = enc(args.agentId || CANONICAL_AGENT_HANDLE)
312
+ return fetchJSON(`${CHAT_BRIDGE_URL}/api/${h}/conversations`, {
313
+ method: 'POST',
314
+ body: JSON.stringify({
315
+ taskId: args.taskId, projectPath: args.projectPath,
316
+ branchName: args.branchName, provider: args.provider,
317
+ modelId: args.modelId, systemPromptAddition: args.systemPromptAddition,
318
+ }),
319
+ })
320
+ },
321
+ batch_spawn_task_sessions: async (args) => {
322
+ const results = { spawned: [], failed: [] }
323
+ for (const s of (args.sessions || [])) {
324
+ try {
325
+ const h = enc(s.agentId || CANONICAL_AGENT_HANDLE)
326
+ const r = await fetchJSON(`${CHAT_BRIDGE_URL}/api/${h}/conversations`, {
327
+ method: 'POST',
328
+ body: JSON.stringify({ taskId: s.taskId, provider: s.provider || args.defaultProvider, modelId: s.modelId, systemPromptAddition: s.systemPromptAddition }),
329
+ })
330
+ results.spawned.push({ taskId: s.taskId, agentId: s.agentId, ...r })
331
+ } catch (e) { results.failed.push({ taskId: s.taskId, error: e.message }) }
332
+ }
333
+ return { success: results.spawned.length > 0, ...results }
334
+ },
335
+ }
336
+
337
+ async function callTool(name, args) {
338
+ const handle = CANONICAL_AGENT_HANDLE
339
+ if (!handle) return { success: false, error: 'Agent handle not configured' }
340
+ const h = encodeURIComponent(handle)
341
+
342
+ try {
343
+ // 1. Check custom handlers (file/task/draft tools with complex logic)
344
+ const custom = CUSTOM_HANDLERS[name]
345
+ if (custom) {
346
+ if (!REGISTRY_OFFICE_ID && name !== 'spawn_task_session' && name !== 'batch_spawn_task_sessions') {
347
+ return { success: false, error: 'Office ID not configured' }
348
+ }
349
+ log(`[call] ${name} → custom handler`)
350
+ return await custom(args || {})
351
+ }
352
+
353
+ // 2. Check special routes (non-standard URL patterns)
354
+ const special = SPECIAL_ROUTES[name]
355
+ let method, urlPath
356
+ if (special) {
357
+ method = special.method
358
+ urlPath = special.path(h, args)
359
+ } else {
360
+ // 3. Default pattern: POST /api/{handle}/tools/{kebab-name}
361
+ method = 'POST'
362
+ urlPath = `/api/${h}/tools/${toKebab(name)}`
363
+ }
364
+
365
+ const url = `${CHAT_BRIDGE_URL}${urlPath}`
366
+ log(`[call] ${name} → ${method} ${urlPath}`)
367
+
368
+ const options = method === 'GET'
369
+ ? { method: 'GET' }
370
+ : { method, body: JSON.stringify(args || {}) }
371
+
372
+ return await fetchJSON(url, options)
373
+ } catch (err) {
374
+ log(`[call] ${name} failed:`, err.message)
375
+ return { success: false, error: err.message }
376
+ }
377
+ }
378
+
379
+ // ── MCP JSON-RPC protocol ────────────────────────────────────────────────────
380
+
381
+ class McpServer {
382
+ constructor() {
383
+ this.toolsLoaded = false
384
+ this.tools = []
385
+ }
386
+
387
+ async ensureToolsLoaded() {
388
+ if (!this.toolsLoaded) {
389
+ this.tools = await loadToolSchemas()
390
+ this.toolsLoaded = true
391
+ }
392
+ }
393
+
394
+ async handleRequest(request) {
395
+ const { method, params, id } = request
396
+
397
+ switch (method) {
398
+ case 'initialize':
399
+ return {
400
+ jsonrpc: '2.0',
401
+ id,
402
+ result: {
403
+ protocolVersion: '2024-11-05',
404
+ capabilities: { tools: {} },
405
+ serverInfo: { name: 'vo-mcp-lite', version: '1.0.0' },
406
+ },
407
+ }
408
+
409
+ case 'tools/list':
410
+ await this.ensureToolsLoaded()
411
+ return {
412
+ jsonrpc: '2.0',
413
+ id,
414
+ result: {
415
+ tools: this.tools.map(t => ({
416
+ name: t.name,
417
+ description: t.description,
418
+ inputSchema: t.inputSchema,
419
+ })),
420
+ },
421
+ }
422
+
423
+ case 'tools/call': {
424
+ const { name, arguments: toolArgs } = params || {}
425
+ if (!name) return this.error(id, -32602, 'Tool name is required')
426
+
427
+ log(`Tool call: ${name}`)
428
+ const result = await callTool(name, toolArgs || {})
429
+ return {
430
+ jsonrpc: '2.0',
431
+ id,
432
+ result: {
433
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
434
+ },
435
+ }
436
+ }
437
+
438
+ case 'notifications/initialized':
439
+ return null
440
+
441
+ default:
442
+ return this.error(id, -32601, `Method not found: ${method}`)
443
+ }
444
+ }
445
+
446
+ error(id, code, message) {
447
+ return { jsonrpc: '2.0', id, error: { code, message } }
448
+ }
449
+ }
450
+
451
+ // ── Main ─────────────────────────────────────────────────────────────────────
452
+
453
+ async function main() {
454
+ log('Starting VO MCP Lite v1.0.0')
455
+ log('Chat Bridge:', CHAT_BRIDGE_URL)
456
+ log('Agent:', CANONICAL_AGENT_HANDLE || '(not set)')
457
+ log('Office:', REGISTRY_OFFICE_ID || '(not set)')
458
+
459
+ const server = new McpServer()
460
+
461
+ const rl = readline.createInterface({
462
+ input: process.stdin,
463
+ output: process.stdout,
464
+ terminal: false,
465
+ })
466
+
467
+ rl.on('line', async (line) => {
468
+ if (!line.trim()) return
469
+ try {
470
+ const request = JSON.parse(line)
471
+ const response = await server.handleRequest(request)
472
+ if (response) console.log(JSON.stringify(response))
473
+ } catch (error) {
474
+ log('Parse error:', error.message)
475
+ console.log(JSON.stringify({
476
+ jsonrpc: '2.0',
477
+ id: null,
478
+ error: { code: -32700, message: 'Parse error' },
479
+ }))
480
+ }
481
+ })
482
+
483
+ rl.on('close', () => {
484
+ log('Connection closed')
485
+ process.exit(0)
486
+ })
487
+ }
488
+
489
+ main().catch(error => {
490
+ log('Fatal error:', error)
491
+ process.exit(1)
492
+ })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@office-xyz/claude-code",
3
- "version": "0.1.6",
4
- "description": "Connect Claude Code to your Virtual Office — manage your AI agents from the terminal",
3
+ "version": "0.1.8",
4
+ "description": "Connect Claude Code to Office.xyza shared working environment for all your AI agents, cloud and local",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "vo-claude": "./index.js",
@@ -11,6 +11,7 @@
11
11
  "files": [
12
12
  "index.js",
13
13
  "onboarding.js",
14
+ "mcp-server-lite.cjs",
14
15
  "README.md",
15
16
  "LICENSE",
16
17
  "CHANGELOG.md"