@office-xyz/claude-code 0.1.0

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/onboarding.js ADDED
@@ -0,0 +1,379 @@
1
+ /**
2
+ * CLI Onboarding Flow for @office-xyz/claude-code
3
+ *
4
+ * Interactive terminal experience:
5
+ * Login → Create/Join Office → Name Agent → Auto Seat → Clock In
6
+ *
7
+ * Session is cached in ~/.office-xyz/session.json so subsequent runs skip login.
8
+ */
9
+
10
+ import chalk from 'chalk'
11
+ import { input, select, confirm } from '@inquirer/prompts'
12
+ import ora from 'ora'
13
+ import open from 'open'
14
+ import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'fs'
15
+ import { createRequire } from 'module'
16
+ import path from 'path'
17
+ import os from 'os'
18
+
19
+ const require = createRequire(import.meta.url)
20
+
21
+ // ── Config ────────────────────────────────────────────────────────────────
22
+
23
+ const SESSION_DIR = path.join(os.homedir(), '.office-xyz')
24
+ const SESSION_FILE = path.join(SESSION_DIR, 'session.json')
25
+
26
+ const CHAT_BRIDGE_URL =
27
+ process.env.CHAT_BRIDGE_URL ||
28
+ process.env.CHAT_BRIDGE_HTTP_URL ||
29
+ 'https://chatbridge.aladdinagi.xyz'
30
+
31
+ const PKG_NAME = '@office-xyz/claude-code'
32
+
33
+ // ── Update Check ──────────────────────────────────────────────────────────
34
+
35
+ /**
36
+ * Non-blocking version check against npm registry.
37
+ * Shows a one-line update notice if a newer version is available.
38
+ * Never throws — silently skips on any error.
39
+ */
40
+ export async function checkForUpdate() {
41
+ try {
42
+ const pkg = require('./package.json')
43
+ const currentVersion = pkg.version
44
+ if (!currentVersion) return
45
+
46
+ const controller = new AbortController()
47
+ const timeout = setTimeout(() => controller.abort(), 3000) // 3s max
48
+
49
+ const res = await fetch(`https://registry.npmjs.org/${PKG_NAME}/latest`, {
50
+ signal: controller.signal,
51
+ })
52
+ clearTimeout(timeout)
53
+
54
+ if (!res.ok) return
55
+ const data = await res.json()
56
+ const latestVersion = data.version
57
+
58
+ if (!latestVersion || latestVersion === currentVersion) return
59
+
60
+ // Simple semver compare: split into parts and compare numerically
61
+ const current = currentVersion.split('.').map(Number)
62
+ const latest = latestVersion.split('.').map(Number)
63
+ let isNewer = false
64
+ for (let i = 0; i < 3; i++) {
65
+ if ((latest[i] || 0) > (current[i] || 0)) { isNewer = true; break }
66
+ if ((latest[i] || 0) < (current[i] || 0)) break
67
+ }
68
+
69
+ if (isNewer) {
70
+ console.log('')
71
+ console.log(chalk.yellow(` ⚠ Update available: ${currentVersion} → ${latestVersion}`))
72
+ console.log(chalk.dim(` Run: npm update -g ${PKG_NAME}`))
73
+ }
74
+ } catch {
75
+ // Silent fail — network issues, registry down, etc.
76
+ }
77
+ }
78
+
79
+ // ── Banner ────────────────────────────────────────────────────────────────
80
+
81
+ export function printBanner(subtitle = 'Manage Your AI Agents') {
82
+ console.log('')
83
+ console.log(chalk.cyan(' ┌─────────────────────────────────────────┐'))
84
+ console.log(chalk.cyan(' │') + ' ' + chalk.cyan('│'))
85
+ console.log(chalk.cyan(' │') + chalk.bold.white(' ▓▓▓') + ' ' + chalk.cyan('│'))
86
+ console.log(chalk.cyan(' │') + chalk.bold.white(' ▓░░▓') + chalk.bold.white(' Virtual Office') + ' ' + chalk.cyan('│'))
87
+ console.log(chalk.cyan(' │') + chalk.bold.white(' ▓▓▓▓') + chalk.dim(` ${subtitle}`) + ' ' + chalk.cyan('│'))
88
+ console.log(chalk.cyan(' │') + chalk.bold.white(' ██') + ' ' + chalk.cyan('│'))
89
+ console.log(chalk.cyan(' │') + chalk.dim(' ▓▓▓▓ office.xyz') + ' ' + chalk.cyan('│'))
90
+ console.log(chalk.cyan(' │') + ' ' + chalk.cyan('│'))
91
+ console.log(chalk.cyan(' └─────────────────────────────────────────┘'))
92
+ console.log('')
93
+ }
94
+
95
+ export function printClockInBanner({ agentHandle, model, seat, workspace }) {
96
+ console.log('')
97
+ console.log(chalk.cyan(' ┌─────────────────────────────────────────┐'))
98
+ console.log(chalk.cyan(' │') + chalk.green.bold(' ✓ Clocked in to Virtual Office') + ' ' + chalk.cyan('│'))
99
+ console.log(chalk.cyan(' │') + ' ' + chalk.cyan('│'))
100
+ console.log(chalk.cyan(' │') + chalk.dim(' Agent: ') + chalk.bold.white(agentHandle))
101
+ console.log(chalk.cyan(' │') + chalk.dim(' Model: ') + chalk.white(model || 'Claude Opus 4.6'))
102
+ if (seat) {
103
+ console.log(chalk.cyan(' │') + chalk.dim(' Seat: ') + chalk.white(seat))
104
+ }
105
+ console.log(chalk.cyan(' │') + chalk.dim(' Workspace: ') + chalk.white(workspace || process.cwd()))
106
+ console.log(chalk.cyan(' │') + ' ' + chalk.cyan('│'))
107
+ console.log(chalk.cyan(' │') + chalk.yellow(' Press Ctrl+C to clock out') + ' ' + chalk.cyan('│'))
108
+ console.log(chalk.cyan(' └─────────────────────────────────────────┘'))
109
+ console.log('')
110
+ }
111
+
112
+ // ── Session Storage ───────────────────────────────────────────────────────
113
+
114
+ export function loadSession() {
115
+ try {
116
+ if (!existsSync(SESSION_FILE)) return null
117
+ const data = JSON.parse(readFileSync(SESSION_FILE, 'utf-8'))
118
+ // Check expiry
119
+ if (data.expiresAt && new Date(data.expiresAt).getTime() < Date.now()) {
120
+ return null
121
+ }
122
+ return data
123
+ } catch {
124
+ return null
125
+ }
126
+ }
127
+
128
+ export function saveSession(session) {
129
+ try {
130
+ if (!existsSync(SESSION_DIR)) {
131
+ mkdirSync(SESSION_DIR, { recursive: true })
132
+ }
133
+ writeFileSync(SESSION_FILE, JSON.stringify(session, null, 2), 'utf-8')
134
+ } catch (err) {
135
+ console.warn(chalk.dim(`[session] Failed to save: ${err.message}`))
136
+ }
137
+ }
138
+
139
+ export function clearSession() {
140
+ try {
141
+ if (existsSync(SESSION_FILE)) {
142
+ writeFileSync(SESSION_FILE, '{}', 'utf-8')
143
+ }
144
+ } catch { /* ignore */ }
145
+ }
146
+
147
+ // ── API Helpers ───────────────────────────────────────────────────────────
148
+
149
+ async function api(method, path, body = null, sessionToken = null) {
150
+ const url = `${CHAT_BRIDGE_URL}${path}`
151
+ const headers = { 'Content-Type': 'application/json' }
152
+ if (sessionToken) headers['x-cli-session'] = sessionToken
153
+
154
+ const options = { method, headers }
155
+ if (body) options.body = JSON.stringify(body)
156
+
157
+ const res = await fetch(url, options)
158
+ const data = await res.json()
159
+
160
+ if (!res.ok) {
161
+ throw new Error(data.error || data.message || `API error ${res.status}`)
162
+ }
163
+ return data
164
+ }
165
+
166
+ // ── Browser Login Flow ────────────────────────────────────────────────────
167
+
168
+ async function browserLogin() {
169
+ const spinner = ora('Starting login...').start()
170
+
171
+ try {
172
+ const { loginUrl, pollToken } = await api('POST', '/api/cli/auth/start')
173
+ spinner.stop()
174
+
175
+ console.log(chalk.dim(' Opening browser for login...'))
176
+ await open(loginUrl)
177
+ console.log(chalk.dim(' Waiting for authentication...'))
178
+ console.log(chalk.dim(` (If browser didn't open, visit: ${loginUrl})`))
179
+ console.log('')
180
+
181
+ const pollSpinner = ora('Waiting for login...').start()
182
+ const POLL_INTERVAL = 1500
183
+ const POLL_TIMEOUT = 5 * 60 * 1000
184
+
185
+ const startTime = Date.now()
186
+ while (Date.now() - startTime < POLL_TIMEOUT) {
187
+ await new Promise(r => setTimeout(r, POLL_INTERVAL))
188
+ const result = await api('GET', `/api/cli/auth/poll?token=${pollToken}`)
189
+
190
+ if (result.status === 'complete') {
191
+ pollSpinner.succeed(chalk.green(`Logged in as ${result.session.email || result.session.userId}`))
192
+ return { session: result.session, offices: result.offices || [] }
193
+ }
194
+ if (result.status === 'expired') {
195
+ pollSpinner.fail('Login expired. Please try again.')
196
+ process.exit(1)
197
+ }
198
+ }
199
+
200
+ pollSpinner.fail('Login timed out. Please try again.')
201
+ process.exit(1)
202
+ } catch (err) {
203
+ spinner.fail(`Login failed: ${err.message}`)
204
+ process.exit(1)
205
+ }
206
+ }
207
+
208
+ // ── Office Selection / Creation ───────────────────────────────────────────
209
+
210
+ async function selectOrCreateOffice(offices, sessionToken) {
211
+ if (offices.length === 0) {
212
+ // No offices — must create
213
+ console.log(chalk.dim(' You don\'t have any offices yet. Let\'s create one!'))
214
+ console.log('')
215
+ return createOffice(sessionToken)
216
+ }
217
+
218
+ // Show offices with "Create new" option
219
+ const choices = [
220
+ ...offices.map(o => ({
221
+ name: `${o.domain || o.slug || o.displayName} ${chalk.dim(`${o.agentCount || 0} agents`)}`,
222
+ value: o.id,
223
+ })),
224
+ {
225
+ name: chalk.cyan('+ Create a new office'),
226
+ value: '__create__',
227
+ },
228
+ ]
229
+
230
+ const choice = await select({
231
+ message: 'Select office:',
232
+ choices,
233
+ })
234
+
235
+ if (choice === '__create__') {
236
+ return createOffice(sessionToken)
237
+ }
238
+
239
+ const selected = offices.find(o => o.id === choice)
240
+ return {
241
+ officeId: selected.id,
242
+ domain: selected.domain || selected.slug,
243
+ }
244
+ }
245
+
246
+ async function createOffice(sessionToken) {
247
+ const name = await input({
248
+ message: 'Office name:',
249
+ validate: (v) => v.trim().length > 0 || 'Name is required',
250
+ })
251
+
252
+ let slug = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '')
253
+
254
+ // Check availability
255
+ const spinner = ora(`Checking ${slug}.office.xyz...`).start()
256
+ try {
257
+ const check = await api('GET', `/api/cli/office/slug-check?slug=${encodeURIComponent(slug)}`)
258
+ if (!check.available) {
259
+ spinner.warn(`${slug}.office.xyz is taken`)
260
+ slug = await input({
261
+ message: 'Choose a different slug:',
262
+ validate: (v) => /^[a-z0-9][a-z0-9-]*$/.test(v) || 'Lowercase alphanumeric + hyphens only',
263
+ })
264
+ } else {
265
+ spinner.succeed(`${slug}.office.xyz is available`)
266
+ }
267
+ } catch {
268
+ spinner.stop()
269
+ }
270
+
271
+ // Create
272
+ const createSpinner = ora('Creating office...').start()
273
+ try {
274
+ const result = await api('POST', '/api/cli/office/create', { name, slug }, sessionToken)
275
+ createSpinner.succeed(`Office created: ${chalk.bold(result.domain)}`)
276
+ return { officeId: result.officeId, domain: result.domain }
277
+ } catch (err) {
278
+ createSpinner.fail(`Failed to create office: ${err.message}`)
279
+ process.exit(1)
280
+ }
281
+ }
282
+
283
+ // ── Agent Hire ────────────────────────────────────────────────────────────
284
+
285
+ async function hireAgent(officeId, sessionToken) {
286
+ const agentName = await input({
287
+ message: 'Name your Claude Code agent:',
288
+ validate: (v) => {
289
+ const name = v.trim().toLowerCase()
290
+ if (!name) return 'Name is required'
291
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) return 'Lowercase alphanumeric + hyphens only'
292
+ return true
293
+ },
294
+ transformer: (v) => v.toLowerCase(),
295
+ })
296
+
297
+ const spinner = ora('Setting up agent...').start()
298
+ try {
299
+ const result = await api('POST', '/api/cli/office/hire', {
300
+ officeId,
301
+ agentName: agentName.trim().toLowerCase(),
302
+ provider: 'claude-code',
303
+ }, sessionToken)
304
+
305
+ spinner.succeed(`Agent ready: ${chalk.bold(result.agentHandle)}${result.seat ? chalk.dim(` (seat: ${result.seat})`) : ''}`)
306
+ return result
307
+ } catch (err) {
308
+ spinner.fail(`Failed to create agent: ${err.message}`)
309
+ process.exit(1)
310
+ }
311
+ }
312
+
313
+ // ── Main Onboarding Flow ─────────────────────────────────────────────────
314
+
315
+ /**
316
+ * Run the interactive onboarding flow.
317
+ * Returns { agent, token } to pass to the connection logic.
318
+ *
319
+ * @returns {{ agent: string, token: string, seat?: string }}
320
+ */
321
+ export async function runOnboarding() {
322
+ printBanner()
323
+
324
+ // Check for updates (non-blocking, runs in background)
325
+ checkForUpdate()
326
+
327
+ // 1. Check cached session
328
+ const cached = loadSession()
329
+
330
+ if (cached?.sessionToken && cached?.lastAgent?.handle && cached?.lastAgent?.connectionToken) {
331
+ // Quick reconnect path
332
+ const spinner = ora('Validating session...').start()
333
+ try {
334
+ const validation = await api('GET', '/api/cli/auth/session', null, cached.sessionToken)
335
+ if (validation.success) {
336
+ spinner.succeed(`Welcome back, ${chalk.bold(cached.email || cached.displayName || 'user')}!`)
337
+ console.log(chalk.dim(` Reconnecting as ${cached.lastAgent.handle}...`))
338
+ return {
339
+ agent: cached.lastAgent.handle,
340
+ token: cached.lastAgent.connectionToken,
341
+ seat: cached.lastAgent.seat,
342
+ }
343
+ }
344
+ } catch {
345
+ // Session expired — fall through to login
346
+ }
347
+ spinner.stop()
348
+ }
349
+
350
+ // 2. Browser login
351
+ const { session, offices } = await browserLogin()
352
+
353
+ // 3. Select or create office
354
+ const { officeId, domain } = await selectOrCreateOffice(offices, session.sessionToken)
355
+
356
+ // 4. Name and hire agent
357
+ const hired = await hireAgent(officeId, session.sessionToken)
358
+
359
+ // 5. Save session
360
+ saveSession({
361
+ userId: session.userId,
362
+ email: session.email,
363
+ displayName: session.displayName,
364
+ sessionToken: session.sessionToken,
365
+ expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
366
+ lastOffice: domain,
367
+ lastAgent: {
368
+ handle: hired.agentHandle,
369
+ connectionToken: hired.connectionToken,
370
+ seat: hired.seat,
371
+ },
372
+ })
373
+
374
+ return {
375
+ agent: hired.agentHandle,
376
+ token: hired.connectionToken,
377
+ seat: hired.seat,
378
+ }
379
+ }
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "@office-xyz/claude-code",
3
+ "version": "0.1.0",
4
+ "description": "Connect Claude Code to your Virtual Office — manage your AI agents from the terminal",
5
+ "type": "module",
6
+ "bin": {
7
+ "vo-claude": "./index.js",
8
+ "claude-code": "./index.js"
9
+ },
10
+ "main": "./index.js",
11
+ "files": [
12
+ "index.js",
13
+ "onboarding.js",
14
+ "README.md",
15
+ "LICENSE",
16
+ "CHANGELOG.md"
17
+ ],
18
+ "scripts": {
19
+ "start": "node index.js",
20
+ "dev": "node --watch index.js",
21
+ "prepublishOnly": "node -e \"require('fs').existsSync('LICENSE') || process.exit(1)\" && node --check index.js && node --check onboarding.js"
22
+ },
23
+ "dependencies": {
24
+ "chalk": "^5.3.0",
25
+ "ws": "^8.18.0",
26
+ "yargs": "^17.7.2",
27
+ "@inquirer/prompts": "^7.0.0",
28
+ "ora": "^8.0.0",
29
+ "open": "^10.0.0"
30
+ },
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "repository": {
35
+ "type": "git",
36
+ "url": "https://github.com/AGIoffice/office.xyz",
37
+ "directory": "manager-host-sdk/local-host"
38
+ },
39
+ "homepage": "https://office.xyz",
40
+ "bugs": {
41
+ "url": "https://github.com/AGIoffice/office.xyz/issues"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ },
46
+ "keywords": [
47
+ "virtual-office",
48
+ "office-xyz",
49
+ "ai-agent",
50
+ "claude-code",
51
+ "claude",
52
+ "anthropic",
53
+ "agent-management"
54
+ ],
55
+ "license": "MIT"
56
+ }