@office-xyz/claude-code 0.1.7 → 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/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()
@@ -346,7 +350,10 @@ async function registerMcpServer() {
346
350
  try {
347
351
  const agentHandle = argv.agent
348
352
  const officeId = agentHandle.split('.').slice(1).join('.')
349
- 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')
350
357
 
351
358
  const chatBridgeUrl = process.env.CHAT_BRIDGE_URL ||
352
359
  process.env.CHAT_BRIDGE_BASE_URL ||
@@ -397,6 +404,47 @@ function unregisterMcpServer() {
397
404
  } catch { /* ignore */ }
398
405
  }
399
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
+
400
448
  // ── Message handling ───────────────────────────────────────────────────────
401
449
 
402
450
  function sendJSON(payload) {
@@ -454,8 +502,16 @@ async function handleMessage(message) {
454
502
  const sessionId = message.sessionId || null
455
503
  const commandId = message.commandId || message.messageId || `cmd-${Date.now()}`
456
504
 
457
- const sessionLabel = sessionId ? sessionId.split('--')[1]?.slice(0, 15) || sessionId.slice(0, 20) : 'default'
458
- 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 ? '...' : ''}`)
459
515
 
460
516
  // Kill previous command for THIS SESSION only. Other sessions continue in parallel.
461
517
  const prev = sessionId ? activeChildren.get(sessionId) : null
@@ -523,6 +579,19 @@ async function handleMessage(message) {
523
579
  startedAt: new Date().toISOString(),
524
580
  })
525
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
+
526
595
  // 2. Spawn the CLI process
527
596
  // shell: false — args are passed directly to the process as an array,
528
597
  // avoiding ALL shell interpretation issues. The command is resolved via
@@ -867,6 +936,7 @@ async function handleMessage(message) {
867
936
 
868
937
  // 4. On process exit, send completion events
869
938
  child.on('close', (code) => {
939
+ clearInterval(heartbeatTimer) // Stop heartbeat — task is done
870
940
  if (sessionId && activeChildren.get(sessionId)?.child === child) activeChildren.delete(sessionId)
871
941
 
872
942
  // Clean up system prompt temp file
@@ -882,7 +952,7 @@ async function handleMessage(message) {
882
952
 
883
953
  if (fullText) {
884
954
  process.stdout.write('\n')
885
- 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 ? '...' : ''}`))
886
956
  }
887
957
 
888
958
  // Send streaming.completed
@@ -964,6 +1034,7 @@ async function handleMessage(message) {
964
1034
  })
965
1035
 
966
1036
  child.on('error', (err) => {
1037
+ clearInterval(heartbeatTimer) // Stop heartbeat on error
967
1038
  log(chalk.red(`CLI process error: ${err.message}`))
968
1039
  if (err.code === 'ENOENT') {
969
1040
  log(chalk.yellow(`"${cmd}" not found. Install it with: ${providerConfig.installHint}`))
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@office-xyz/claude-code",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Connect Claude Code to Office.xyz — a shared working environment for all your AI agents, cloud and local",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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"