@office-xyz/claude-code 0.1.7 → 0.1.9
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 +184 -32
- package/mcp-server-lite.cjs +492 -0
- package/onboarding.js +91 -26
- package/package.json +2 -1
package/index.js
CHANGED
|
@@ -37,6 +37,13 @@ const TOOL_NAME_MAP = {
|
|
|
37
37
|
execute_command: 'Bash', read_file: 'Read', write_file: 'Write', edit_file: 'Edit',
|
|
38
38
|
search: 'Grep', run_command: 'Bash', code_edit: 'FileEdit', code_search: 'Grep',
|
|
39
39
|
terminal: 'Bash', file_operation: 'FileEdit',
|
|
40
|
+
// Claude server-side tools (content_block type → display name)
|
|
41
|
+
server_tool_use: 'ServerTool', web_search_tool_result: 'WebSearch',
|
|
42
|
+
web_fetch_tool_result: 'WebFetch', code_execution_tool_result: 'CodeExecution',
|
|
43
|
+
mcp_tool_use: 'Mcp', mcp_tool_result: 'Mcp',
|
|
44
|
+
bash_code_execution_tool_result: 'Bash',
|
|
45
|
+
text_editor_code_execution_tool_result: 'Edit',
|
|
46
|
+
tool_search_tool_result: 'ToolSearch', container_upload: 'Upload',
|
|
40
47
|
// General aliases
|
|
41
48
|
bash: 'Bash', grep: 'Grep', glob: 'Glob', ls: 'LS',
|
|
42
49
|
}
|
|
@@ -98,6 +105,10 @@ const argv = yargs(hideBin(process.argv))
|
|
|
98
105
|
type: 'string',
|
|
99
106
|
describe: 'Override the CLI binary (e.g. /usr/local/bin/claude)',
|
|
100
107
|
})
|
|
108
|
+
.option('channel', {
|
|
109
|
+
type: 'string',
|
|
110
|
+
describe: 'Only show messages from this channel (web, telegram, slack, discord, office, feishu, wechat)',
|
|
111
|
+
})
|
|
101
112
|
.example('$0', 'Interactive setup (login, create office, name agent)')
|
|
102
113
|
.example('$0 --agent claude.my.office.xyz --token xxx', 'Direct connect (skip login)')
|
|
103
114
|
.help()
|
|
@@ -253,16 +264,28 @@ const model = argv.model || providerConfig.defaultModel
|
|
|
253
264
|
|
|
254
265
|
// ── Session tracking ───────────────────────────────────────────────────────
|
|
255
266
|
// Map VO sessionId → Claude session_id for conversation continuity.
|
|
256
|
-
//
|
|
267
|
+
// Persisted to disk so sessions survive clock-out / clock-in cycles.
|
|
268
|
+
// --resume and --append-system-prompt coexist fine, so resumed sessions
|
|
269
|
+
// still pick up fresh system prompts and MCP tools (registered globally).
|
|
257
270
|
const SESSION_MAP_FILE = path.join(os.tmpdir(), `vo-sessions-${(argv.agent || 'pending').replace(/\./g, '-')}.json`)
|
|
258
271
|
const sessionMap = new Map()
|
|
259
272
|
|
|
260
|
-
//
|
|
261
|
-
// ignore new --append-system-prompt and MCP tools.
|
|
273
|
+
// Load persisted sessions from previous clock-in (if any)
|
|
262
274
|
try {
|
|
263
|
-
require('fs').
|
|
264
|
-
|
|
265
|
-
|
|
275
|
+
const raw = require('fs').readFileSync(SESSION_MAP_FILE, 'utf-8')
|
|
276
|
+
const parsed = JSON.parse(raw)
|
|
277
|
+
// Support both formats: Array of entries [[k,v], ...] and Object {k: v}
|
|
278
|
+
const entries = Array.isArray(parsed) ? parsed : Object.entries(parsed)
|
|
279
|
+
for (const [k, v] of entries) sessionMap.set(k, v)
|
|
280
|
+
log(chalk.dim(`Restored ${sessionMap.size} session mapping(s) from previous clock-in`))
|
|
281
|
+
} catch { /* no file or invalid — start fresh */ }
|
|
282
|
+
|
|
283
|
+
/** Persist session map to disk (fire-and-forget) */
|
|
284
|
+
function persistSessionMap() {
|
|
285
|
+
try {
|
|
286
|
+
require('fs').writeFileSync(SESSION_MAP_FILE, JSON.stringify([...sessionMap]), 'utf-8')
|
|
287
|
+
} catch { /* best-effort */ }
|
|
288
|
+
}
|
|
266
289
|
|
|
267
290
|
// Track active command processes PER SESSION for concurrent conversation support.
|
|
268
291
|
// Key: sessionId, Value: { child, commandId }. Different clients (web, Telegram)
|
|
@@ -346,7 +369,10 @@ async function registerMcpServer() {
|
|
|
346
369
|
try {
|
|
347
370
|
const agentHandle = argv.agent
|
|
348
371
|
const officeId = agentHandle.split('.').slice(1).join('.')
|
|
349
|
-
|
|
372
|
+
// Use the lightweight MCP server bundled with the npm package.
|
|
373
|
+
// It fetches tool schemas from Chat Bridge and proxies all calls via HTTP,
|
|
374
|
+
// so it doesn't need the 150+ monorepo files that the full MCP server requires.
|
|
375
|
+
const mcpServerPath = path.resolve(__dirname, 'mcp-server-lite.cjs')
|
|
350
376
|
|
|
351
377
|
const chatBridgeUrl = process.env.CHAT_BRIDGE_URL ||
|
|
352
378
|
process.env.CHAT_BRIDGE_BASE_URL ||
|
|
@@ -397,6 +423,47 @@ function unregisterMcpServer() {
|
|
|
397
423
|
} catch { /* ignore */ }
|
|
398
424
|
}
|
|
399
425
|
|
|
426
|
+
// ── Channel resolution ─────────────────────────────────────────────────────
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Resolve message channel from platformInfo / metadata / sessionId.
|
|
430
|
+
* Returns { type, color, sender, chatId } for display formatting.
|
|
431
|
+
*/
|
|
432
|
+
function resolveChannel(message) {
|
|
433
|
+
const platformInfo = message.platformInfo || {}
|
|
434
|
+
const meta = message.metadata || {}
|
|
435
|
+
const source = meta.source || platformInfo.clientType || ''
|
|
436
|
+
const sessionId = message.sessionId || ''
|
|
437
|
+
|
|
438
|
+
if (source.includes('telegram')) {
|
|
439
|
+
const from = meta.telegram?.from
|
|
440
|
+
const name = from?.username ? `@${from.username}` : from?.firstName || 'user'
|
|
441
|
+
return { type: 'Telegram', color: 'blue', sender: name, chatId: platformInfo.chatId }
|
|
442
|
+
}
|
|
443
|
+
if (source.includes('slack')) {
|
|
444
|
+
const channel = meta.slack?.channelId || platformInfo.channelId || ''
|
|
445
|
+
const user = meta.slack?.username || 'user'
|
|
446
|
+
return { type: 'Slack', color: 'magenta', sender: channel ? `#${channel} — ${user}` : user, chatId: null }
|
|
447
|
+
}
|
|
448
|
+
if (source.includes('discord')) {
|
|
449
|
+
return { type: 'Discord', color: 'blueBright', sender: meta.discord?.username || 'user', chatId: null }
|
|
450
|
+
}
|
|
451
|
+
if (source.includes('feishu') || source.includes('lark')) {
|
|
452
|
+
return { type: 'Feishu', color: 'cyan', sender: meta.feishu?.username || 'user', chatId: null }
|
|
453
|
+
}
|
|
454
|
+
if (source.includes('wecom') || source.includes('wechat') || source.includes('whatsapp')) {
|
|
455
|
+
const label = source.includes('whatsapp') ? 'WhatsApp' : 'WeChat'
|
|
456
|
+
return { type: label, color: 'green', sender: 'user', chatId: null }
|
|
457
|
+
}
|
|
458
|
+
if (sessionId.includes('office-wide')) {
|
|
459
|
+
const senderParts = sessionId.split('--')
|
|
460
|
+
return { type: 'Office Chat', color: 'greenBright', sender: senderParts[0] || 'colleague', chatId: null }
|
|
461
|
+
}
|
|
462
|
+
// Default: Web dialog
|
|
463
|
+
const userId = sessionId.split('--')[1] || 'user'
|
|
464
|
+
return { type: 'Web', color: 'cyanBright', sender: userId.slice(0, 20), chatId: null }
|
|
465
|
+
}
|
|
466
|
+
|
|
400
467
|
// ── Message handling ───────────────────────────────────────────────────────
|
|
401
468
|
|
|
402
469
|
function sendJSON(payload) {
|
|
@@ -454,13 +521,21 @@ async function handleMessage(message) {
|
|
|
454
521
|
const sessionId = message.sessionId || null
|
|
455
522
|
const commandId = message.commandId || message.messageId || `cmd-${Date.now()}`
|
|
456
523
|
|
|
457
|
-
const
|
|
458
|
-
|
|
524
|
+
const channel = resolveChannel(message)
|
|
525
|
+
|
|
526
|
+
// --channel filter: skip messages not matching the requested channel
|
|
527
|
+
if (argv.channel && !channel.type.toLowerCase().includes(argv.channel.toLowerCase())) return
|
|
528
|
+
|
|
529
|
+
const badge = chalk[channel.color](`[${channel.type}]`)
|
|
530
|
+
const time = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' })
|
|
531
|
+
const sender = chalk.dim(channel.sender)
|
|
532
|
+
log(`${badge} ${chalk.dim(time)} ${sender}`)
|
|
533
|
+
log(` ${text.slice(0, 200)}${text.length > 200 ? '...' : ''}`)
|
|
459
534
|
|
|
460
535
|
// Kill previous command for THIS SESSION only. Other sessions continue in parallel.
|
|
461
536
|
const prev = sessionId ? activeChildren.get(sessionId) : null
|
|
462
537
|
if (prev?.child) {
|
|
463
|
-
log(chalk.dim(`[${
|
|
538
|
+
log(chalk.dim(`[${sessionId}] Killing previous command for same session`))
|
|
464
539
|
sendJSON({
|
|
465
540
|
type: 'streaming.aborted',
|
|
466
541
|
sessionId,
|
|
@@ -482,6 +557,9 @@ async function handleMessage(message) {
|
|
|
482
557
|
const claudeSessionId = sessionId ? sessionMap.get(sessionId) : null
|
|
483
558
|
if (claudeSessionId && providerConfig.resumeFlag) {
|
|
484
559
|
args.push(providerConfig.resumeFlag, claudeSessionId)
|
|
560
|
+
log(chalk.green(`[session] Resuming: ${sessionId} → ${claudeSessionId}`))
|
|
561
|
+
} else if (sessionId) {
|
|
562
|
+
log(chalk.yellow(`[session] No resume binding for ${sessionId} (map size: ${sessionMap.size})`))
|
|
485
563
|
}
|
|
486
564
|
// System prompt + platform context injection.
|
|
487
565
|
// shell: false — no escaping needed, args passed directly.
|
|
@@ -523,6 +601,19 @@ async function handleMessage(message) {
|
|
|
523
601
|
startedAt: new Date().toISOString(),
|
|
524
602
|
})
|
|
525
603
|
|
|
604
|
+
// Heartbeat: Send periodic streaming.heartbeat during long-running claude -p execution.
|
|
605
|
+
// This prevents WS proxies (Cloudflare 100s, ALB 60s) from killing idle connections
|
|
606
|
+
// when Claude Code is thinking but not emitting any streaming tokens.
|
|
607
|
+
const HEARTBEAT_INTERVAL_MS = 25_000 // 25s — below Cloudflare/ALB idle timeouts
|
|
608
|
+
const heartbeatTimer = setInterval(() => {
|
|
609
|
+
sendJSON({
|
|
610
|
+
type: 'streaming.heartbeat',
|
|
611
|
+
sessionId,
|
|
612
|
+
commandId,
|
|
613
|
+
timestamp: Date.now(),
|
|
614
|
+
})
|
|
615
|
+
}, HEARTBEAT_INTERVAL_MS)
|
|
616
|
+
|
|
526
617
|
// 2. Spawn the CLI process
|
|
527
618
|
// shell: false — args are passed directly to the process as an array,
|
|
528
619
|
// avoiding ALL shell interpretation issues. The command is resolved via
|
|
@@ -665,6 +756,11 @@ async function handleMessage(message) {
|
|
|
665
756
|
return
|
|
666
757
|
}
|
|
667
758
|
|
|
759
|
+
// Citations delta — ignore silently (citations are embedded in final text)
|
|
760
|
+
if (streamEvent?.type === 'content_block_delta' && streamEvent?.delta?.type === 'citations_delta') {
|
|
761
|
+
return
|
|
762
|
+
}
|
|
763
|
+
|
|
668
764
|
// Thinking deltas
|
|
669
765
|
if (streamEvent?.type === 'content_block_delta' && streamEvent?.delta?.type === 'thinking_delta') {
|
|
670
766
|
const deltaText = streamEvent.delta.thinking || streamEvent.delta.text || ''
|
|
@@ -719,19 +815,42 @@ async function handleMessage(message) {
|
|
|
719
815
|
return
|
|
720
816
|
}
|
|
721
817
|
|
|
722
|
-
|
|
723
|
-
|
|
818
|
+
// All tool-like block types: tool_use (client), server_tool_use (Anthropic server),
|
|
819
|
+
// mcp_tool_use, and result blocks that appear as content_block_start in stream-json.
|
|
820
|
+
// The original Claude CLI sets "tool-input" spinner state for all of these.
|
|
821
|
+
const TOOL_BLOCK_TYPES = new Set([
|
|
822
|
+
'tool_use', 'server_tool_use', 'mcp_tool_use',
|
|
823
|
+
'web_search_tool_result', 'web_fetch_tool_result',
|
|
824
|
+
'code_execution_tool_result', 'bash_code_execution_tool_result',
|
|
825
|
+
'text_editor_code_execution_tool_result', 'tool_search_tool_result',
|
|
826
|
+
'mcp_tool_result', 'container_upload',
|
|
827
|
+
])
|
|
828
|
+
if (TOOL_BLOCK_TYPES.has(block?.type)) {
|
|
829
|
+
// For server_tool_use, the tool name is in block.name (e.g. "web_search")
|
|
830
|
+
// For result blocks, derive name from block type itself
|
|
831
|
+
const rawName = block.name || block.type
|
|
832
|
+
const toolUseId = block.id || block.tool_use_id || `tool-${now}-${Math.random().toString(36).slice(2, 8)}`
|
|
724
833
|
if (blockIndex !== null) {
|
|
725
834
|
activeToolsByIndex.set(blockIndex, {
|
|
726
835
|
toolUseId,
|
|
727
|
-
toolName:
|
|
728
|
-
input: block.input,
|
|
836
|
+
toolName: rawName,
|
|
837
|
+
input: block.input || {},
|
|
729
838
|
})
|
|
730
839
|
}
|
|
731
840
|
emitToolStart({
|
|
732
841
|
toolUseId,
|
|
733
|
-
toolName:
|
|
734
|
-
input: block.input,
|
|
842
|
+
toolName: rawName,
|
|
843
|
+
input: block.input || {},
|
|
844
|
+
timestamp: now,
|
|
845
|
+
})
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
// Compaction events — notify frontend that context is being compressed
|
|
849
|
+
if (block?.type === 'compaction') {
|
|
850
|
+
emitToolStart({
|
|
851
|
+
toolUseId: block.id || `compaction-${now}`,
|
|
852
|
+
toolName: 'Compaction',
|
|
853
|
+
input: {},
|
|
735
854
|
timestamp: now,
|
|
736
855
|
})
|
|
737
856
|
}
|
|
@@ -796,10 +915,11 @@ async function handleMessage(message) {
|
|
|
796
915
|
if (block.type === 'text' && block.text) {
|
|
797
916
|
if (!fullText) fullText = block.text
|
|
798
917
|
}
|
|
799
|
-
// Extract complete
|
|
918
|
+
// Extract complete tool input — the assistant message contains the
|
|
800
919
|
// fully accumulated input (unlike content_block_start which has {}).
|
|
801
|
-
// This
|
|
802
|
-
|
|
920
|
+
// This applies to tool_use, server_tool_use, and mcp_tool_use blocks.
|
|
921
|
+
const isToolBlock = block.type === 'tool_use' || block.type === 'server_tool_use' || block.type === 'mcp_tool_use'
|
|
922
|
+
if (isToolBlock && block.id && block.input) {
|
|
803
923
|
const existing = activeToolsById.get(block.id)
|
|
804
924
|
if (existing && (!existing.input || Object.keys(existing.input).length === 0)) {
|
|
805
925
|
existing.input = block.input
|
|
@@ -852,6 +972,39 @@ async function handleMessage(message) {
|
|
|
852
972
|
}
|
|
853
973
|
return
|
|
854
974
|
}
|
|
975
|
+
|
|
976
|
+
// System events — forward plan mode, hooks, and other system subtypes
|
|
977
|
+
// These are top-level events (not wrapped in stream_event) that the CLI
|
|
978
|
+
// emits in --verbose stream-json mode for UI state changes.
|
|
979
|
+
if (event.type === 'system') {
|
|
980
|
+
const subtype = event.subtype
|
|
981
|
+
// Plan mode transitions — let frontend know agent entered/exited planning
|
|
982
|
+
if (subtype === 'plan_mode' || subtype === 'plan_mode_exit' || subtype === 'plan_mode_reentry') {
|
|
983
|
+
sendJSON({ type: 'system_event', sessionId, commandId, event: { subtype, timestamp: now } })
|
|
984
|
+
return
|
|
985
|
+
}
|
|
986
|
+
// Hook lifecycle — show user that hooks are running
|
|
987
|
+
if (subtype === 'hook_started' || subtype === 'hook_progress' || subtype === 'hook_response') {
|
|
988
|
+
sendJSON({ type: 'system_event', sessionId, commandId, event: { subtype, hookName: event.hook_name, timestamp: now } })
|
|
989
|
+
return
|
|
990
|
+
}
|
|
991
|
+
// Task/agent notifications (subagent spawned, progress, etc.)
|
|
992
|
+
if (subtype === 'task_notification' || subtype === 'task_progress') {
|
|
993
|
+
sendJSON({ type: 'system_event', sessionId, commandId, event: { subtype, message: event.message, timestamp: now } })
|
|
994
|
+
return
|
|
995
|
+
}
|
|
996
|
+
// MCP progress
|
|
997
|
+
if (subtype === 'mcp_message' || subtype === 'mcp_progress') {
|
|
998
|
+
sendJSON({ type: 'system_event', sessionId, commandId, event: { subtype, message: event.message, timestamp: now } })
|
|
999
|
+
return
|
|
1000
|
+
}
|
|
1001
|
+
// Context compaction boundaries
|
|
1002
|
+
if (subtype === 'compact_boundary' || subtype === 'microcompact_boundary') {
|
|
1003
|
+
sendJSON({ type: 'system_event', sessionId, commandId, event: { subtype, timestamp: now } })
|
|
1004
|
+
return
|
|
1005
|
+
}
|
|
1006
|
+
return
|
|
1007
|
+
}
|
|
855
1008
|
} catch {
|
|
856
1009
|
// Not JSON or unrecognized format — ignore
|
|
857
1010
|
}
|
|
@@ -867,6 +1020,7 @@ async function handleMessage(message) {
|
|
|
867
1020
|
|
|
868
1021
|
// 4. On process exit, send completion events
|
|
869
1022
|
child.on('close', (code) => {
|
|
1023
|
+
clearInterval(heartbeatTimer) // Stop heartbeat — task is done
|
|
870
1024
|
if (sessionId && activeChildren.get(sessionId)?.child === child) activeChildren.delete(sessionId)
|
|
871
1025
|
|
|
872
1026
|
// Clean up system prompt temp file
|
|
@@ -877,12 +1031,13 @@ async function handleMessage(message) {
|
|
|
877
1031
|
// Store Claude session_id for conversation continuity
|
|
878
1032
|
if (resultSessionId && sessionId) {
|
|
879
1033
|
sessionMap.set(sessionId, resultSessionId)
|
|
1034
|
+
persistSessionMap()
|
|
880
1035
|
log(chalk.dim(`Session mapped: ${sessionId} → ${resultSessionId}`))
|
|
881
1036
|
}
|
|
882
1037
|
|
|
883
1038
|
if (fullText) {
|
|
884
1039
|
process.stdout.write('\n')
|
|
885
|
-
log(chalk.
|
|
1040
|
+
log(chalk[channel.color](`[${channel.type}] ←`) + chalk.dim(` ${fullText.slice(0, 120)}${fullText.length > 120 ? '...' : ''}`))
|
|
886
1041
|
}
|
|
887
1042
|
|
|
888
1043
|
// Send streaming.completed
|
|
@@ -964,6 +1119,7 @@ async function handleMessage(message) {
|
|
|
964
1119
|
})
|
|
965
1120
|
|
|
966
1121
|
child.on('error', (err) => {
|
|
1122
|
+
clearInterval(heartbeatTimer) // Stop heartbeat on error
|
|
967
1123
|
log(chalk.red(`CLI process error: ${err.message}`))
|
|
968
1124
|
if (err.code === 'ENOENT') {
|
|
969
1125
|
log(chalk.yellow(`"${cmd}" not found. Install it with: ${providerConfig.installHint}`))
|
|
@@ -1061,18 +1217,14 @@ function connect() {
|
|
|
1061
1217
|
if (mcpRegistered) mcpConfigPath = 'registered' // flag to skip re-registration
|
|
1062
1218
|
}
|
|
1063
1219
|
|
|
1064
|
-
// Banner
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
console.log(chalk.dim(` Identity: ${cachedSystemPrompt ? 'loaded' : 'default'}`))
|
|
1073
|
-
console.log(chalk.dim(` Tools: ${mcpRegistered ? 'VO MCP registered ✓' : 'basic only'}`))
|
|
1074
|
-
console.log(chalk.dim(` Press Ctrl+C to clock out`))
|
|
1075
|
-
console.log('')
|
|
1220
|
+
// Banner — use the printClockInBanner from onboarding for consistent look
|
|
1221
|
+
const { printClockInBanner: printBanner } = await import('./onboarding.js')
|
|
1222
|
+
printBanner({
|
|
1223
|
+
agentHandle: argv.agent,
|
|
1224
|
+
model: argv.provider,
|
|
1225
|
+
seat: null,
|
|
1226
|
+
workspace,
|
|
1227
|
+
})
|
|
1076
1228
|
})
|
|
1077
1229
|
|
|
1078
1230
|
ws.on('message', (data) => {
|
|
@@ -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/onboarding.js
CHANGED
|
@@ -125,38 +125,67 @@ export async function checkForUpdate() {
|
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// ── Adam Sprite Art (auto-generated from pixel art sprite sheet) ──────────
|
|
129
|
+
// Half-block rendering: 2x2 pixels → 1 char, truecolor ANSI
|
|
130
|
+
import { ADAM_IDLE_FRAMES, ADAM_SIT_FRAME, ADAM_FRAMES } from './adam-frames.js'
|
|
131
|
+
|
|
128
132
|
// ── Banner ────────────────────────────────────────────────────────────────
|
|
129
133
|
|
|
130
134
|
export function printBanner(subtitle = 'Manage Your AI Agents') {
|
|
135
|
+
const frame = ADAM_IDLE_FRAMES[0]
|
|
136
|
+
|
|
137
|
+
// Text lines aligned to Adam sprite rows (~23 rows)
|
|
138
|
+
// Adam is ~32 chars wide; text appears to the right with padding
|
|
139
|
+
const textLines = [
|
|
140
|
+
'',
|
|
141
|
+
'',
|
|
142
|
+
'',
|
|
143
|
+
'',
|
|
144
|
+
'',
|
|
145
|
+
'',
|
|
146
|
+
'',
|
|
147
|
+
'',
|
|
148
|
+
chalk.bold.white(' Virtual Office'),
|
|
149
|
+
chalk.dim(` ${subtitle}`),
|
|
150
|
+
'',
|
|
151
|
+
chalk.dim(' office.xyz'),
|
|
152
|
+
]
|
|
153
|
+
|
|
131
154
|
console.log('')
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
console.log(chalk.cyan(' │') + chalk.bold.white(' ▓░░▓') + chalk.bold.white(' Virtual Office') + ' ' + chalk.cyan('│'))
|
|
136
|
-
console.log(chalk.cyan(' │') + chalk.bold.white(' ▓▓▓▓') + chalk.dim(` ${subtitle}`) + ' ' + chalk.cyan('│'))
|
|
137
|
-
console.log(chalk.cyan(' │') + chalk.bold.white(' ██') + ' ' + chalk.cyan('│'))
|
|
138
|
-
console.log(chalk.cyan(' │') + chalk.dim(' ▓▓▓▓ office.xyz') + ' ' + chalk.cyan('│'))
|
|
139
|
-
console.log(chalk.cyan(' │') + ' ' + chalk.cyan('│'))
|
|
140
|
-
console.log(chalk.cyan(' └─────────────────────────────────────────┘'))
|
|
155
|
+
for (let i = 0; i < frame.length; i++) {
|
|
156
|
+
console.log(' ' + frame[i] + (textLines[i] || ''))
|
|
157
|
+
}
|
|
141
158
|
console.log('')
|
|
142
159
|
}
|
|
143
160
|
|
|
144
161
|
export function printClockInBanner({ agentHandle, model, seat, workspace }) {
|
|
162
|
+
const frame = ADAM_SIT_FRAME
|
|
163
|
+
|
|
164
|
+
// Right-side info lines aligned to Adam's sitting sprite (~23 rows)
|
|
165
|
+
const infoLines = [
|
|
166
|
+
'',
|
|
167
|
+
'',
|
|
168
|
+
'',
|
|
169
|
+
'',
|
|
170
|
+
'',
|
|
171
|
+
chalk.green.bold(' ✓ Clocked in to Virtual Office'),
|
|
172
|
+
'',
|
|
173
|
+
chalk.dim(' Agent: ') + chalk.bold.white(agentHandle),
|
|
174
|
+
chalk.dim(' Model: ') + chalk.white(model || 'Claude Opus 4.6'),
|
|
175
|
+
seat ? (chalk.dim(' Seat: ') + chalk.white(seat)) : '',
|
|
176
|
+
chalk.dim(' Dir: ') + chalk.white(workspace || process.cwd()),
|
|
177
|
+
'',
|
|
178
|
+
chalk.dim(' Web: ') + chalk.underline.cyan('https://beta.office.xyz'),
|
|
179
|
+
'',
|
|
180
|
+
chalk.dim(' Press ') + chalk.yellow('Ctrl+C') + chalk.dim(' to clock out'),
|
|
181
|
+
]
|
|
182
|
+
|
|
145
183
|
console.log('')
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
console.log(chalk.cyan(' │') + chalk.dim(' Model: ') + chalk.white(model || 'Claude Opus 4.6'))
|
|
151
|
-
if (seat) {
|
|
152
|
-
console.log(chalk.cyan(' │') + chalk.dim(' Seat: ') + chalk.white(seat))
|
|
184
|
+
for (let i = 0; i < Math.max(frame.length, infoLines.length); i++) {
|
|
185
|
+
const sprite = (i < frame.length) ? frame[i] : ''
|
|
186
|
+
const info = (i < infoLines.length) ? infoLines[i] : ''
|
|
187
|
+
console.log(' ' + sprite + info)
|
|
153
188
|
}
|
|
154
|
-
console.log(chalk.cyan(' │') + chalk.dim(' Workspace: ') + chalk.white(workspace || process.cwd()))
|
|
155
|
-
console.log(chalk.cyan(' │') + ' ' + chalk.cyan('│'))
|
|
156
|
-
console.log(chalk.cyan(' │') + chalk.dim(' Web UI: ') + chalk.underline.cyan('https://beta.office.xyz'))
|
|
157
|
-
console.log(chalk.cyan(' │') + ' ' + chalk.cyan('│'))
|
|
158
|
-
console.log(chalk.cyan(' │') + chalk.yellow(' Press Ctrl+C to clock out') + ' ' + chalk.cyan('│'))
|
|
159
|
-
console.log(chalk.cyan(' └─────────────────────────────────────────┘'))
|
|
160
189
|
console.log('')
|
|
161
190
|
}
|
|
162
191
|
|
|
@@ -510,19 +539,54 @@ export async function runOnboarding() {
|
|
|
510
539
|
// 1. Check cached session
|
|
511
540
|
const cached = loadSession()
|
|
512
541
|
|
|
513
|
-
if (cached?.sessionToken && cached?.lastAgent?.handle
|
|
514
|
-
// Quick reconnect path
|
|
542
|
+
if (cached?.sessionToken && cached?.lastAgent?.handle) {
|
|
543
|
+
// Quick reconnect path — always refresh token to avoid stale token rejection
|
|
515
544
|
const spinner = ora('Validating session...').start()
|
|
516
545
|
try {
|
|
517
546
|
const validation = await api('GET', '/api/cli/auth/session', null, cached.sessionToken)
|
|
518
547
|
if (validation.success) {
|
|
548
|
+
spinner.text = 'Refreshing connection...'
|
|
549
|
+
|
|
550
|
+
// Re-hire with same agent name to get a fresh connectionToken.
|
|
551
|
+
// This is idempotent — if agent exists, it just refreshes the token.
|
|
552
|
+
const agentName = cached.lastAgent.handle.split('.')[0]
|
|
553
|
+
const officeId = cached.lastOfficeId || null
|
|
554
|
+
|
|
555
|
+
let freshToken = cached.lastAgent.connectionToken
|
|
556
|
+
let seat = cached.lastAgent.seat
|
|
557
|
+
|
|
558
|
+
if (agentName && officeId) {
|
|
559
|
+
try {
|
|
560
|
+
const result = await api('POST', '/api/cli/office/hire', {
|
|
561
|
+
officeId,
|
|
562
|
+
agentName,
|
|
563
|
+
provider: 'claude-code',
|
|
564
|
+
}, cached.sessionToken)
|
|
565
|
+
freshToken = result.connectionToken || freshToken
|
|
566
|
+
seat = result.seat || seat
|
|
567
|
+
|
|
568
|
+
// Update cached session with fresh token
|
|
569
|
+
saveSession({
|
|
570
|
+
...cached,
|
|
571
|
+
lastAgent: {
|
|
572
|
+
...cached.lastAgent,
|
|
573
|
+
connectionToken: freshToken,
|
|
574
|
+
seat,
|
|
575
|
+
},
|
|
576
|
+
})
|
|
577
|
+
} catch (err) {
|
|
578
|
+
// If refresh fails, try with cached token anyway
|
|
579
|
+
console.log(chalk.dim(` Token refresh failed (${err.message}), using cached token`))
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
519
583
|
spinner.succeed(`Welcome back, ${chalk.bold(cached.email || cached.displayName || 'user')}!`)
|
|
520
584
|
console.log(chalk.dim(` Reconnecting as ${cached.lastAgent.handle}...`))
|
|
521
585
|
console.log(chalk.dim(` Web interface: ${chalk.underline.cyan('https://beta.office.xyz')}`))
|
|
522
586
|
return {
|
|
523
587
|
agent: cached.lastAgent.handle,
|
|
524
|
-
token:
|
|
525
|
-
seat
|
|
588
|
+
token: freshToken,
|
|
589
|
+
seat,
|
|
526
590
|
}
|
|
527
591
|
}
|
|
528
592
|
} catch {
|
|
@@ -548,6 +612,7 @@ export async function runOnboarding() {
|
|
|
548
612
|
sessionToken: session.sessionToken,
|
|
549
613
|
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
550
614
|
lastOffice: domain,
|
|
615
|
+
lastOfficeId: officeId,
|
|
551
616
|
lastAgent: {
|
|
552
617
|
handle: hired.agentHandle,
|
|
553
618
|
connectionToken: hired.connectionToken,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@office-xyz/claude-code",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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"
|