@office-xyz/claude-code 0.1.1 → 0.1.3

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/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  All notable changes to `@office-xyz/claude-code` will be documented in this file.
4
4
 
5
+ ## [0.1.2] - 2026-02-15
6
+
7
+ ### Changed
8
+ - Integrate public repo sync into npm publish workflow
9
+ - Fall back to Cloudflare URL for SSE/WebSocket connections
10
+
5
11
  ## [0.1.1] - 2026-02-15
6
12
 
7
13
  ### Fixed
package/index.js CHANGED
@@ -23,6 +23,28 @@ import { execSync } from 'child_process'
23
23
  import path from 'path'
24
24
  import os from 'os'
25
25
  import { fileURLToPath } from 'url'
26
+ // Inline tool name normalizer (can't import from parent CJS package — ESM/CJS conflict)
27
+ // Maps raw CLI tool names to standard frontend TOOL_CONFIG keys
28
+ const TOOL_NAME_MAP = {
29
+ // Codex CLI
30
+ command_execution: 'Bash', file_edit: 'FileEdit', file_read: 'FileRead',
31
+ file_change: 'FileEdit', file_write: 'FileWrite', web_search: 'WebSearch',
32
+ mcp_call: 'Mcp', mcp_tool_call: 'Mcp', unknown_tool: 'default',
33
+ // Gemini CLI
34
+ shell: 'Bash', edit: 'Edit', read: 'Read', write: 'Write',
35
+ search_files: 'Grep', list_files: 'LS', web_fetch: 'WebFetch', google_search: 'WebSearch',
36
+ // Kimi / DeepSeek / Qwen CLI
37
+ execute_command: 'Bash', read_file: 'Read', write_file: 'Write', edit_file: 'Edit',
38
+ search: 'Grep', run_command: 'Bash', code_edit: 'FileEdit', code_search: 'Grep',
39
+ terminal: 'Bash', file_operation: 'FileEdit',
40
+ // General aliases
41
+ bash: 'Bash', grep: 'Grep', glob: 'Glob', ls: 'LS',
42
+ }
43
+ function normalizeToolName(rawName) {
44
+ if (!rawName || typeof rawName !== 'string') return 'default'
45
+ const lower = rawName.toLowerCase()
46
+ return TOOL_NAME_MAP[lower] || TOOL_NAME_MAP[rawName] || rawName
47
+ }
26
48
 
27
49
  const __filename = fileURLToPath(import.meta.url)
28
50
  const __dirname = path.dirname(__filename)
@@ -224,6 +246,8 @@ if (argv.agent) {
224
246
  let wsRef = null
225
247
  let reconnectAttempts = 0
226
248
  const MAX_RECONNECT_DELAY_MS = 30_000
249
+ let registryHeartbeatTimer = null
250
+ const REGISTRY_HEARTBEAT_INTERVAL_MS = 30_000 // Report liveness to Registry every 30s
227
251
  const workspace = path.resolve(argv.workspace)
228
252
  const model = argv.model || providerConfig.defaultModel
229
253
 
@@ -506,14 +530,16 @@ async function handleMessage(message) {
506
530
  const emitToolStart = ({ toolUseId, toolName, input, timestamp }) => {
507
531
  const normalizedId = toolUseId || `tool-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
508
532
  const startedAt = Number.isFinite(timestamp) ? timestamp : Date.now()
509
- activeToolsById.set(normalizedId, { toolName: toolName || 'tool', input, startedAt })
533
+ // Normalize tool names for consistent frontend display across all CLI providers
534
+ const normalized = normalizeToolName(toolName || 'tool')
535
+ activeToolsById.set(normalizedId, { toolName: normalized, input, startedAt })
510
536
  sendJSON({
511
537
  type: 'tool_event',
512
538
  sessionId,
513
539
  commandId,
514
540
  event: {
515
541
  eventType: 'tool_start',
516
- toolName: toolName || 'tool',
542
+ toolName: normalized,
517
543
  toolUseId: normalizedId,
518
544
  input,
519
545
  status: 'running',
@@ -527,7 +553,8 @@ async function handleMessage(message) {
527
553
  if (finalizedToolIds.has(toolUseId) && !force) return
528
554
  finalizedToolIds.add(toolUseId)
529
555
  const endedAt = Number.isFinite(timestamp) ? timestamp : Date.now()
530
- const resolvedTool = toolName || activeToolsById.get(toolUseId)?.toolName || 'tool'
556
+ const rawTool = toolName || activeToolsById.get(toolUseId)?.toolName || 'tool'
557
+ const resolvedTool = normalizeToolName(rawTool)
531
558
  const resolvedInput = input ?? activeToolsById.get(toolUseId)?.input
532
559
  const status = error ? 'error' : 'completed'
533
560
  const compactResult = compactToolValue(result)
@@ -582,6 +609,29 @@ async function handleMessage(message) {
582
609
  return
583
610
  }
584
611
 
612
+ // Input JSON deltas — accumulate tool input as it streams in
613
+ // Claude CLI sends tool input progressively: content_block_start has input: {},
614
+ // then input_json_delta events carry the actual JSON. Without accumulating these,
615
+ // tool indicators show "(no command)" because input is empty.
616
+ if (streamEvent?.type === 'content_block_delta' && streamEvent?.delta?.type === 'input_json_delta') {
617
+ const partialJson = streamEvent.delta.partial_json || ''
618
+ if (blockIndex !== null) {
619
+ const toolState = activeToolsByIndex.get(blockIndex)
620
+ if (toolState) {
621
+ // Accumulate raw JSON string — we'll parse the complete object later
622
+ if (!toolState._inputJsonBuffer) toolState._inputJsonBuffer = ''
623
+ toolState._inputJsonBuffer += partialJson
624
+ // Try to parse accumulated JSON to update input (best-effort)
625
+ try {
626
+ toolState.input = JSON.parse(toolState._inputJsonBuffer)
627
+ } catch {
628
+ // Not complete JSON yet — that's fine, keep accumulating
629
+ }
630
+ }
631
+ }
632
+ return
633
+ }
634
+
585
635
  // Thinking deltas
586
636
  if (streamEvent?.type === 'content_block_delta' && streamEvent?.delta?.type === 'thinking_delta') {
587
637
  const deltaText = streamEvent.delta.thinking || streamEvent.delta.text || ''
@@ -707,12 +757,35 @@ async function handleMessage(message) {
707
757
 
708
758
  // Message event with assistant content
709
759
  if (event.type === 'message' && event.message?.role === 'assistant') {
710
- // Extract text from content blocks
711
760
  const content = event.message.content || []
712
761
  for (const block of content) {
762
+ // Extract text blocks
713
763
  if (block.type === 'text' && block.text) {
714
764
  if (!fullText) fullText = block.text
715
765
  }
766
+ // Extract complete tool_use input — the assistant message contains the
767
+ // fully accumulated input (unlike content_block_start which has {}).
768
+ // This allows us to retroactively update tool events with real input data.
769
+ if (block.type === 'tool_use' && block.id && block.input) {
770
+ const existing = activeToolsById.get(block.id)
771
+ if (existing && (!existing.input || Object.keys(existing.input).length === 0)) {
772
+ existing.input = block.input
773
+ // Re-emit tool_start with complete input so frontend can display details
774
+ sendJSON({
775
+ type: 'tool_event',
776
+ sessionId,
777
+ commandId,
778
+ event: {
779
+ eventType: 'tool_start',
780
+ toolName: existing.toolName,
781
+ toolUseId: block.id,
782
+ input: block.input,
783
+ status: 'running',
784
+ timestamp: existing.startedAt || now,
785
+ },
786
+ })
787
+ }
788
+ }
716
789
  }
717
790
  return
718
791
  }
@@ -937,6 +1010,11 @@ function connect() {
937
1010
 
938
1011
  sendHostMeta(ws)
939
1012
 
1013
+ // Registry heartbeat: report liveness independently of WS ping/pong.
1014
+ // This lets Chat Bridge know the agent process is alive even during
1015
+ // brief WS reconnection windows.
1016
+ startRegistryHeartbeat()
1017
+
940
1018
  // Build system prompt and register MCP server (first-class citizen setup)
941
1019
  log(chalk.blue('Setting up agent identity and tools...'))
942
1020
  if (!cachedSystemPrompt) {
@@ -980,6 +1058,8 @@ function connect() {
980
1058
  log(chalk.red(`Disconnected (${code} ${reasonStr})`))
981
1059
  wsRef = null
982
1060
  stopHeartbeat()
1061
+ // Keep registry heartbeat running during reconnection — it's independent
1062
+ // of the WS connection and tells Chat Bridge the process is still alive.
983
1063
 
984
1064
  // Kill all active CLI processes on disconnect
985
1065
  for (const [, entry] of activeChildren) {
@@ -1012,6 +1092,53 @@ function scheduleReconnect() {
1012
1092
  setTimeout(connect, delay)
1013
1093
  }
1014
1094
 
1095
+ // ── Registry Heartbeat ────────────────────────────────────────────────────
1096
+ // Reports agent liveness to Registry via Chat Bridge, independent of WS state.
1097
+ // This ensures Chat Bridge knows the agent process is alive even during
1098
+ // WS reconnection (up to 30s exponential backoff window).
1099
+
1100
+ function startRegistryHeartbeat() {
1101
+ if (registryHeartbeatTimer) return // already running
1102
+ const agentHandle = argv.agent
1103
+ if (!agentHandle) return
1104
+
1105
+ const chatBridgeHttpUrl =
1106
+ process.env.CHAT_BRIDGE_HTTP_URL ||
1107
+ process.env.CHAT_BRIDGE_URL ||
1108
+ process.env.CHAT_BRIDGE_BASE_URL ||
1109
+ 'https://chatbridge.aladdinagi.xyz'
1110
+
1111
+ const sendHeartbeat = async () => {
1112
+ try {
1113
+ const url = `${chatBridgeHttpUrl}/api/${encodeURIComponent(agentHandle)}/heartbeat`
1114
+ const resp = await fetch(url, {
1115
+ method: 'POST',
1116
+ headers: { 'Content-Type': 'application/json' },
1117
+ body: JSON.stringify({ timestamp: new Date().toISOString() }),
1118
+ signal: AbortSignal.timeout(10000),
1119
+ })
1120
+ if (!resp.ok) {
1121
+ const text = await resp.text().catch(() => '')
1122
+ log(chalk.dim(`[heartbeat] Registry heartbeat failed: ${resp.status} ${text}`))
1123
+ }
1124
+ } catch (err) {
1125
+ // Silent — heartbeat failures are non-critical
1126
+ log(chalk.dim(`[heartbeat] ${err.message}`))
1127
+ }
1128
+ }
1129
+
1130
+ // Send immediately, then every 30s
1131
+ sendHeartbeat()
1132
+ registryHeartbeatTimer = setInterval(sendHeartbeat, REGISTRY_HEARTBEAT_INTERVAL_MS)
1133
+ }
1134
+
1135
+ function stopRegistryHeartbeat() {
1136
+ if (registryHeartbeatTimer) {
1137
+ clearInterval(registryHeartbeatTimer)
1138
+ registryHeartbeatTimer = null
1139
+ }
1140
+ }
1141
+
1015
1142
  // ── Local Device Connection (file system access for Workspace Panel) ───────
1016
1143
  // Second WebSocket to Chat Bridge /local-agent — registers as a local device
1017
1144
  // so the web UI can browse local files via Workspace Panel.
@@ -1185,7 +1312,8 @@ async function executeLocalTool(toolName, params) {
1185
1312
  function shutdown() {
1186
1313
  console.log('')
1187
1314
  log(chalk.yellow('Clocking out...'))
1188
- // Unregister MCP server so it doesn't linger in ~/.claude.json
1315
+ // Stop registry heartbeat and unregister MCP server
1316
+ stopRegistryHeartbeat()
1189
1317
  unregisterMcpServer()
1190
1318
  for (const [, entry] of activeChildren) {
1191
1319
  try { entry?.child?.kill('SIGTERM') } catch { /* ignore */ }
package/onboarding.js CHANGED
@@ -30,6 +30,55 @@ const CHAT_BRIDGE_URL =
30
30
 
31
31
  const PKG_NAME = '@office-xyz/claude-code'
32
32
 
33
+ // ── Pre-flight Check ──────────────────────────────────────────────────────
34
+
35
+ async function checkClaudeCodeCli() {
36
+ const { execSync } = await import('child_process')
37
+
38
+ // Check if claude is installed
39
+ let version = null
40
+ try {
41
+ version = execSync('claude --version', { encoding: 'utf-8', timeout: 5000 }).trim()
42
+ } catch {
43
+ console.log(chalk.red(' Claude Code CLI is not installed.'))
44
+ console.log('')
45
+ console.log(chalk.white(' Install it:'))
46
+ console.log(chalk.cyan(' curl -fsSL https://claude.ai/install.sh | bash'))
47
+ console.log('')
48
+ console.log(chalk.dim(' Or via npm: npm install -g @anthropic-ai/claude-code'))
49
+ console.log('')
50
+ process.exit(1)
51
+ }
52
+
53
+ console.log(chalk.dim(` Detected: Claude Code ${version}`))
54
+
55
+ // Check if logged in by running a quick command
56
+ try {
57
+ const result = execSync('claude -p "ping" --output-format text --max-turns 1', {
58
+ encoding: 'utf-8',
59
+ timeout: 15000,
60
+ env: { ...process.env, ANTHROPIC_API_KEY: undefined },
61
+ })
62
+ // If we get any response, auth works
63
+ console.log(chalk.dim(' Auth: Claude login session ✓'))
64
+ } catch (err) {
65
+ const msg = (err.stderr || err.stdout || err.message || '').toLowerCase()
66
+ if (msg.includes('credit') || msg.includes('balance') || msg.includes('unauthorized') || msg.includes('auth')) {
67
+ console.log(chalk.red(' Claude Code is not logged in.'))
68
+ console.log('')
69
+ console.log(chalk.white(' Run this first:'))
70
+ console.log(chalk.cyan(' claude login'))
71
+ console.log('')
72
+ console.log(chalk.dim(' You need a Claude Pro, Max, or Team subscription.'))
73
+ console.log('')
74
+ process.exit(1)
75
+ }
76
+ // Other errors (timeout, etc.) — proceed anyway, might work
77
+ console.log(chalk.dim(' Auth: could not verify (will try anyway)'))
78
+ }
79
+ console.log('')
80
+ }
81
+
33
82
  // ── Update Check ──────────────────────────────────────────────────────────
34
83
 
35
84
  /**
@@ -104,6 +153,8 @@ export function printClockInBanner({ agentHandle, model, seat, workspace }) {
104
153
  }
105
154
  console.log(chalk.cyan(' │') + chalk.dim(' Workspace: ') + chalk.white(workspace || process.cwd()))
106
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('│'))
107
158
  console.log(chalk.cyan(' │') + chalk.yellow(' Press Ctrl+C to clock out') + ' ' + chalk.cyan('│'))
108
159
  console.log(chalk.cyan(' └─────────────────────────────────────────┘'))
109
160
  console.log('')
@@ -280,9 +331,135 @@ async function createOffice(sessionToken) {
280
331
  }
281
332
  }
282
333
 
283
- // ── Agent Hire ────────────────────────────────────────────────────────────
334
+ // ── Role Selection ────────────────────────────────────────────────────────
335
+
336
+ const ROLE_CATEGORIES = [
337
+ { id: 'business', icon: '💼', label: 'Business', description: 'Operations, Marketing, Sales, Support, Executive, HR' },
338
+ { id: 'science', icon: '🔬', label: 'Science', description: 'Research, Data Science, Bioinformatics, Lab, Clinical' },
339
+ { id: 'developer', icon: '💻', label: 'Developer', description: 'Full-Stack, Frontend, Backend, DevOps, AI Engineering' },
340
+ { id: 'education', icon: '📖', label: 'Education', description: 'Learning, Tutoring, Knowledge Exploration' },
341
+ ]
342
+
343
+ const ROLES = [
344
+ // Business
345
+ { id: 'operations', category: 'business', icon: '📈', label: 'Operations' },
346
+ { id: 'marketing', category: 'business', icon: '📣', label: 'Marketing' },
347
+ { id: 'sales', category: 'business', icon: '🤝', label: 'Sales' },
348
+ { id: 'support', category: 'business', icon: '💬', label: 'Support' },
349
+ { id: 'executive', category: 'business', icon: '👔', label: 'Executive' },
350
+ { id: 'hr', category: 'business', icon: '👥', label: 'HR' },
351
+ // Science
352
+ { id: 'researcher', category: 'science', icon: '🔬', label: 'Researcher' },
353
+ { id: 'data-scientist', category: 'science', icon: '📊', label: 'Data Scientist' },
354
+ { id: 'bioinformatics', category: 'science', icon: '🧬', label: 'Bioinformatics' },
355
+ { id: 'lab-manager', category: 'science', icon: '🧪', label: 'Lab Manager' },
356
+ { id: 'clinical', category: 'science', icon: '🏥', label: 'Clinical' },
357
+ // Developer
358
+ { id: 'fullstack', category: 'developer', icon: '🖥️', label: 'Full-Stack' },
359
+ { id: 'frontend', category: 'developer', icon: '🎨', label: 'Frontend' },
360
+ { id: 'backend', category: 'developer', icon: '⚙️', label: 'Backend' },
361
+ { id: 'devops', category: 'developer', icon: '🔧', label: 'DevOps' },
362
+ { id: 'ai-engineer', category: 'developer', icon: '🤖', label: 'AI Engineer' },
363
+ // Education
364
+ { id: 'learner', category: 'education', icon: '📖', label: 'Learner' },
365
+ ]
366
+
367
+ async function selectRole() {
368
+ const category = await select({
369
+ message: 'What does your agent do?',
370
+ choices: ROLE_CATEGORIES.map(c => ({
371
+ name: `${c.icon} ${c.label} ${chalk.dim(c.description)}`,
372
+ value: c.id,
373
+ })),
374
+ })
375
+
376
+ const rolesInCategory = ROLES.filter(r => r.category === category)
377
+
378
+ const roleId = await select({
379
+ message: 'Select a role:',
380
+ choices: rolesInCategory.map(r => ({
381
+ name: `${r.icon} ${r.label}`,
382
+ value: r.id,
383
+ })),
384
+ })
284
385
 
285
- async function hireAgent(officeId, sessionToken) {
386
+ const role = ROLES.find(r => r.id === roleId)
387
+ return { roleId, roleCategory: category, roleLabel: role?.label || roleId }
388
+ }
389
+
390
+ // ── Agent Selection / Hire ─────────────────────────────────────────────────
391
+
392
+ const PROVIDER_LABELS = {
393
+ claude: 'Claude Code',
394
+ anthropic: 'Claude',
395
+ openai: 'Codex',
396
+ gemini: 'Gemini',
397
+ deepseek: 'DeepSeek',
398
+ qwen: 'Qwen',
399
+ kimi: 'Kimi',
400
+ ollama: 'Ollama',
401
+ }
402
+
403
+ async function selectOrHireAgent(officeId, sessionToken) {
404
+ // 1. Check for existing agents in this office
405
+ let existingAgents = []
406
+ try {
407
+ const data = await api('GET', `/api/cli/office/${encodeURIComponent(officeId)}/agents`, null, sessionToken)
408
+ existingAgents = data.agents || []
409
+ } catch {
410
+ // Continue — will go straight to hire
411
+ }
412
+
413
+ // Only show local Claude agents — cloud agents and non-Claude agents can't be clocked in from this CLI
414
+ const localAgents = existingAgents.filter(a =>
415
+ a.deploymentMode === 'local' &&
416
+ (a.provider === 'claude' || a.provider === 'claude-code' || a.provider === 'anthropic')
417
+ )
418
+
419
+ if (localAgents.length > 0) {
420
+ // Show local agents + option to hire new
421
+ const providerLabel = (p) => PROVIDER_LABELS[p] || p || 'unknown'
422
+
423
+ const choices = [
424
+ ...localAgents.map(a => ({
425
+ name: `${a.name} ${chalk.dim(`${a.role} · ${providerLabel(a.provider)} · ${a.seat || 'no seat'}`)}`,
426
+ value: a.id,
427
+ })),
428
+ {
429
+ name: chalk.cyan('+ Hire a new agent'),
430
+ value: '__hire__',
431
+ },
432
+ ]
433
+
434
+ const choice = await select({
435
+ message: 'Select an agent to clock in:',
436
+ choices,
437
+ })
438
+
439
+ if (choice !== '__hire__') {
440
+ // Reconnect existing agent
441
+ const agent = localAgents.find(a => a.id === choice)
442
+ const spinner = ora('Reconnecting...').start()
443
+ try {
444
+ const result = await api('POST', '/api/cli/office/hire', {
445
+ officeId,
446
+ agentName: agent.name,
447
+ provider: 'claude-code',
448
+ }, sessionToken)
449
+
450
+ spinner.succeed(`Reconnected: ${chalk.bold(result.agentHandle)} ${chalk.dim(`(${agent.role})`)}${result.seat ? chalk.dim(` · seat: ${result.seat}`) : ''}`)
451
+ return result
452
+ } catch (err) {
453
+ spinner.fail(`Failed to reconnect: ${err.message}`)
454
+ process.exit(1)
455
+ }
456
+ }
457
+ }
458
+
459
+ // 2. Hire new agent: select role
460
+ const { roleId, roleCategory, roleLabel } = await selectRole()
461
+
462
+ // 3. Name agent
286
463
  const agentName = await input({
287
464
  message: 'Name your Claude Code agent:',
288
465
  validate: (v) => {
@@ -294,15 +471,18 @@ async function hireAgent(officeId, sessionToken) {
294
471
  transformer: (v) => v.toLowerCase(),
295
472
  })
296
473
 
474
+ // 4. Hire
297
475
  const spinner = ora('Setting up agent...').start()
298
476
  try {
299
477
  const result = await api('POST', '/api/cli/office/hire', {
300
478
  officeId,
301
479
  agentName: agentName.trim().toLowerCase(),
302
480
  provider: 'claude-code',
481
+ roleId,
482
+ roleCategory,
303
483
  }, sessionToken)
304
484
 
305
- spinner.succeed(`Agent ready: ${chalk.bold(result.agentHandle)}${result.seat ? chalk.dim(` (seat: ${result.seat})`) : ''}`)
485
+ spinner.succeed(`Agent ready: ${chalk.bold(result.agentHandle)} ${chalk.dim(`(${roleLabel})`)}${result.seat ? chalk.dim(` · seat: ${result.seat}`) : ''}`)
306
486
  return result
307
487
  } catch (err) {
308
488
  spinner.fail(`Failed to create agent: ${err.message}`)
@@ -324,6 +504,9 @@ export async function runOnboarding() {
324
504
  // Check for updates (non-blocking, runs in background)
325
505
  checkForUpdate()
326
506
 
507
+ // 0. Pre-flight: check Claude Code CLI is installed and logged in
508
+ await checkClaudeCodeCli()
509
+
327
510
  // 1. Check cached session
328
511
  const cached = loadSession()
329
512
 
@@ -335,6 +518,7 @@ export async function runOnboarding() {
335
518
  if (validation.success) {
336
519
  spinner.succeed(`Welcome back, ${chalk.bold(cached.email || cached.displayName || 'user')}!`)
337
520
  console.log(chalk.dim(` Reconnecting as ${cached.lastAgent.handle}...`))
521
+ console.log(chalk.dim(` Web interface: ${chalk.underline.cyan('https://beta.office.xyz')}`))
338
522
  return {
339
523
  agent: cached.lastAgent.handle,
340
524
  token: cached.lastAgent.connectionToken,
@@ -353,8 +537,8 @@ export async function runOnboarding() {
353
537
  // 3. Select or create office
354
538
  const { officeId, domain } = await selectOrCreateOffice(offices, session.sessionToken)
355
539
 
356
- // 4. Name and hire agent
357
- const hired = await hireAgent(officeId, session.sessionToken)
540
+ // 4. Select existing agent or hire new
541
+ const hired = await selectOrHireAgent(officeId, session.sessionToken)
358
542
 
359
543
  // 5. Save session
360
544
  saveSession({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@office-xyz/claude-code",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Connect Claude Code to your Virtual Office — manage your AI agents from the terminal",
5
5
  "type": "module",
6
6
  "bin": {