@office-xyz/claude-code 0.1.1 → 0.1.4
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 +6 -0
- package/index.js +133 -5
- package/onboarding.js +189 -5
- package/package.json +1 -1
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
//
|
|
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
|
-
// ──
|
|
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
|
-
|
|
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(`
|
|
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.
|
|
357
|
-
const hired = await
|
|
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({
|