@office-xyz/claude-code 0.1.8 → 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.
Files changed (3) hide show
  1. package/index.js +109 -28
  2. package/onboarding.js +91 -26
  3. package/package.json +1 -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
  }
@@ -257,16 +264,28 @@ const model = argv.model || providerConfig.defaultModel
257
264
 
258
265
  // ── Session tracking ───────────────────────────────────────────────────────
259
266
  // Map VO sessionId → Claude session_id for conversation continuity.
260
- // Cleared on clock-in to avoid stale sessions that ignore --append-system-prompt.
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).
261
270
  const SESSION_MAP_FILE = path.join(os.tmpdir(), `vo-sessions-${(argv.agent || 'pending').replace(/\./g, '-')}.json`)
262
271
  const sessionMap = new Map()
263
272
 
264
- // Clear stale sessions on startup — stale sessions from previous clock-ins
265
- // ignore new --append-system-prompt and MCP tools.
273
+ // Load persisted sessions from previous clock-in (if any)
266
274
  try {
267
- require('fs').unlinkSync(SESSION_MAP_FILE)
268
- // Will be re-created when first session is mapped
269
- } catch { /* no file to delete */ }
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
+ }
270
289
 
271
290
  // Track active command processes PER SESSION for concurrent conversation support.
272
291
  // Key: sessionId, Value: { child, commandId }. Different clients (web, Telegram)
@@ -516,7 +535,7 @@ async function handleMessage(message) {
516
535
  // Kill previous command for THIS SESSION only. Other sessions continue in parallel.
517
536
  const prev = sessionId ? activeChildren.get(sessionId) : null
518
537
  if (prev?.child) {
519
- log(chalk.dim(`[${sessionLabel}] Killing previous command for same session`))
538
+ log(chalk.dim(`[${sessionId}] Killing previous command for same session`))
520
539
  sendJSON({
521
540
  type: 'streaming.aborted',
522
541
  sessionId,
@@ -538,6 +557,9 @@ async function handleMessage(message) {
538
557
  const claudeSessionId = sessionId ? sessionMap.get(sessionId) : null
539
558
  if (claudeSessionId && providerConfig.resumeFlag) {
540
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})`))
541
563
  }
542
564
  // System prompt + platform context injection.
543
565
  // shell: false — no escaping needed, args passed directly.
@@ -734,6 +756,11 @@ async function handleMessage(message) {
734
756
  return
735
757
  }
736
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
+
737
764
  // Thinking deltas
738
765
  if (streamEvent?.type === 'content_block_delta' && streamEvent?.delta?.type === 'thinking_delta') {
739
766
  const deltaText = streamEvent.delta.thinking || streamEvent.delta.text || ''
@@ -788,19 +815,42 @@ async function handleMessage(message) {
788
815
  return
789
816
  }
790
817
 
791
- if (block?.type === 'tool_use') {
792
- const toolUseId = block.id || `tool-${now}-${Math.random().toString(36).slice(2, 8)}`
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)}`
793
833
  if (blockIndex !== null) {
794
834
  activeToolsByIndex.set(blockIndex, {
795
835
  toolUseId,
796
- toolName: block.name || 'tool',
797
- input: block.input,
836
+ toolName: rawName,
837
+ input: block.input || {},
798
838
  })
799
839
  }
800
840
  emitToolStart({
801
841
  toolUseId,
802
- toolName: block.name || 'tool',
803
- 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: {},
804
854
  timestamp: now,
805
855
  })
806
856
  }
@@ -865,10 +915,11 @@ async function handleMessage(message) {
865
915
  if (block.type === 'text' && block.text) {
866
916
  if (!fullText) fullText = block.text
867
917
  }
868
- // Extract complete tool_use input — the assistant message contains the
918
+ // Extract complete tool input — the assistant message contains the
869
919
  // fully accumulated input (unlike content_block_start which has {}).
870
- // This allows us to retroactively update tool events with real input data.
871
- if (block.type === 'tool_use' && block.id && block.input) {
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) {
872
923
  const existing = activeToolsById.get(block.id)
873
924
  if (existing && (!existing.input || Object.keys(existing.input).length === 0)) {
874
925
  existing.input = block.input
@@ -921,6 +972,39 @@ async function handleMessage(message) {
921
972
  }
922
973
  return
923
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
+ }
924
1008
  } catch {
925
1009
  // Not JSON or unrecognized format — ignore
926
1010
  }
@@ -947,6 +1031,7 @@ async function handleMessage(message) {
947
1031
  // Store Claude session_id for conversation continuity
948
1032
  if (resultSessionId && sessionId) {
949
1033
  sessionMap.set(sessionId, resultSessionId)
1034
+ persistSessionMap()
950
1035
  log(chalk.dim(`Session mapped: ${sessionId} → ${resultSessionId}`))
951
1036
  }
952
1037
 
@@ -1132,18 +1217,14 @@ function connect() {
1132
1217
  if (mcpRegistered) mcpConfigPath = 'registered' // flag to skip re-registration
1133
1218
  }
1134
1219
 
1135
- // Banner
1136
- console.log('')
1137
- console.log(chalk.bold.cyan(' ╔══════════════════════════════════════╗'))
1138
- console.log(chalk.bold.cyan(' ║ Clocked in to Virtual Office ║'))
1139
- console.log(chalk.bold.cyan(' ╚══════════════════════════════════════╝'))
1140
- console.log(chalk.dim(` Agent: ${argv.agent}`))
1141
- console.log(chalk.dim(` Provider: ${argv.provider}`))
1142
- console.log(chalk.dim(` Workspace: ${workspace}`))
1143
- console.log(chalk.dim(` Identity: ${cachedSystemPrompt ? 'loaded' : 'default'}`))
1144
- console.log(chalk.dim(` Tools: ${mcpRegistered ? 'VO MCP registered ✓' : 'basic only'}`))
1145
- console.log(chalk.dim(` Press Ctrl+C to clock out`))
1146
- 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
+ })
1147
1228
  })
1148
1229
 
1149
1230
  ws.on('message', (data) => {
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
- console.log(chalk.cyan(' ┌─────────────────────────────────────────┐'))
133
- console.log(chalk.cyan(' ') + ' ' + chalk.cyan(''))
134
- console.log(chalk.cyan(' │') + chalk.bold.white(' ▓▓▓') + ' ' + chalk.cyan('│'))
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
- console.log(chalk.cyan(' ┌─────────────────────────────────────────┐'))
147
- console.log(chalk.cyan(' │') + chalk.green.bold(' ✓ Clocked in to Virtual Office') + ' ' + chalk.cyan(''))
148
- console.log(chalk.cyan(' │') + ' ' + chalk.cyan(''))
149
- console.log(chalk.cyan(' ') + chalk.dim(' Agent: ') + chalk.bold.white(agentHandle))
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 && cached?.lastAgent?.connectionToken) {
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: cached.lastAgent.connectionToken,
525
- seat: cached.lastAgent.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.8",
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": {