@gizai/cli 2.1.2 → 2.2.1

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/cli/giz.mjs ADDED
@@ -0,0 +1,1129 @@
1
+ #!/usr/bin/env node
2
+ import { computerIdentity, managedRevision } from './computer-identity.mjs'
3
+ import { checkForUpdate, explicitUpdate, installSource, maybeNotify } from './update.mjs'
4
+ import { runToolchain, toolchainCommand } from './toolchain.mjs'
5
+ import { createHash, randomBytes } from 'node:crypto'
6
+ import { openAsBlob, statSync } from 'node:fs'
7
+ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
8
+ import { createServer } from 'node:http'
9
+ import { once } from 'node:events'
10
+ import { createRequire } from 'node:module'
11
+ import os from 'node:os'
12
+ import path from 'node:path'
13
+ import { spawn, spawnSync } from 'node:child_process'
14
+ import { fileURLToPath } from 'node:url'
15
+ import { lock } from 'proper-lockfile'
16
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
17
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
18
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
19
+
20
+ const { version: CLI_VERSION } = createRequire(import.meta.url)('../package.json')
21
+ const DEFAULT_URL = 'https://www.giz.ai'
22
+ const CLIENT_ID = 'giz-cli'
23
+ const SCOPES = ['giz.read', 'giz.inference', 'giz.write', 'giz.execute', 'giz.destructive', 'offline_access']
24
+ const BOOLEAN_OPTIONS = new Set(['help', 'json', 'no-open', 'version', 'device'])
25
+ const configPath = path.resolve(process.env.GIZ_CONFIG ||
26
+ path.join(os.homedir(), '.config', 'giz', 'credentials.json'))
27
+
28
+ class CliError extends Error {
29
+ constructor(message, hint, status) {
30
+ super(message)
31
+ this.hint = hint
32
+ this.status = status
33
+ }
34
+ }
35
+
36
+ function parse(argv) {
37
+ const positionals = []
38
+ const options = {}
39
+ for (let index = 0; index < argv.length; index += 1) {
40
+ const value = argv[index]
41
+ if (value === '--') {
42
+ positionals.push(...argv.slice(index + 1))
43
+ break
44
+ }
45
+ if (!value.startsWith('--')) {
46
+ positionals.push(value)
47
+ continue
48
+ }
49
+ const [rawName, inline] = value.slice(2).split(/=(.*)/s, 2)
50
+ const next = argv[index + 1]
51
+ const parsed = inline !== undefined
52
+ ? inline
53
+ : BOOLEAN_OPTIONS.has(rawName) || !next || next.startsWith('--')
54
+ ? true
55
+ : next
56
+ if (rawName === 'upload') {
57
+ options.upload = [...(Array.isArray(options.upload) ? options.upload : []), parsed]
58
+ } else {
59
+ options[rawName] = parsed
60
+ }
61
+ if (inline === undefined && parsed !== true) index += 1
62
+ }
63
+ return { positionals, options }
64
+ }
65
+
66
+ async function readConfig() {
67
+ try {
68
+ return JSON.parse(await readFile(configPath, 'utf8'))
69
+ } catch (error) {
70
+ if (error?.code === 'ENOENT') return undefined
71
+ throw new CliError('Could not read Giz credentials.', `Remove or repair ${configPath}.`)
72
+ }
73
+ }
74
+
75
+ async function updateConfig(update) {
76
+ await mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 })
77
+ const release = await lock(configPath, { realpath: false, retries: { retries: 120, minTimeout: 100, maxTimeout: 500 } })
78
+ const temporary = `${configPath}.${randomBytes(8).toString('hex')}`
79
+ try {
80
+ const config = await update(await readConfig())
81
+ if (config) {
82
+ await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600, flag: 'wx' })
83
+ await rename(temporary, configPath)
84
+ } else {
85
+ await unlink(configPath).catch((error) => { if (error.code !== 'ENOENT') throw error })
86
+ }
87
+ return config
88
+ } finally {
89
+ try { await unlink(temporary).catch((error) => { if (error.code !== 'ENOENT') throw error }) }
90
+ finally { await release() }
91
+ }
92
+ }
93
+
94
+ async function discovery(baseUrl) {
95
+ const response = await fetch(new URL('/.well-known/oauth-authorization-server', baseUrl), { signal: AbortSignal.timeout(30_000) })
96
+ if (!response.ok) {
97
+ throw new CliError(
98
+ `OAuth discovery failed: HTTP ${response.status}.`,
99
+ `Check ${new URL('/.well-known/oauth-authorization-server', baseUrl)}.`
100
+ )
101
+ }
102
+ return await response.json()
103
+ }
104
+
105
+ async function exchangeToken(baseUrl, metadata, grant, config = {}) {
106
+ const body = new URLSearchParams({
107
+ client_id: CLIENT_ID,
108
+ resource: new URL('/api/mcp', baseUrl).href,
109
+ ...grant
110
+ })
111
+ const response = await fetch(metadata.token_endpoint, {
112
+ method: 'POST',
113
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
114
+ body,
115
+ signal: AbortSignal.timeout(30_000)
116
+ })
117
+ const payload = await response.json()
118
+ if (!response.ok) {
119
+ const error = new CliError(payload.error_description || payload.error || 'OAuth token exchange failed.', 'Run: giz auth login')
120
+ error.oauthError = payload.error
121
+ throw error
122
+ }
123
+ return {
124
+ ...config,
125
+ baseUrl,
126
+ ...payload,
127
+ refresh_token: payload.refresh_token || config.refresh_token,
128
+ expires_at: Date.now() + Number(payload.expires_in || 3600) * 1000
129
+ }
130
+ }
131
+
132
+ async function credentials(baseUrl, rejectedToken) {
133
+ if (process.env.GIZ_TOKEN) return { access_token: process.env.GIZ_TOKEN, baseUrl }
134
+ const config = await readConfig()
135
+ if (!config?.access_token) {
136
+ const computer = await computerIdentity(rejectedToken)
137
+ if (!computer) throw new CliError('Not authenticated.', 'Run: giz auth login')
138
+ if (computer.baseUrl !== baseUrl) throw new CliError('A Computer identity cannot be used with another server.', 'Run giz auth login with your user account.')
139
+ return computer
140
+ }
141
+ if (String(config.baseUrl).replace(/\/+$/, '') !== baseUrl) {
142
+ throw new CliError(
143
+ `Credentials belong to ${config.baseUrl}.`,
144
+ `Run: giz auth login --base-url ${baseUrl}`
145
+ )
146
+ }
147
+ const expired = (value) => value.access_token === rejectedToken || Number(value.expires_at || 0) <= Date.now() + 60_000
148
+ if (expired(config)) {
149
+ return await updateConfig(async (config) => {
150
+ if (config?.baseUrl !== baseUrl) throw new CliError('Giz credentials changed.', 'Run: giz auth login')
151
+ if (expired(config)) {
152
+ if (!config.refresh_token) throw new CliError('Giz session expired.', 'Run: giz auth login')
153
+ return await exchangeToken(baseUrl, await discovery(baseUrl), {
154
+ grant_type: 'refresh_token', refresh_token: config.refresh_token
155
+ }, config)
156
+ }
157
+ return config
158
+ })
159
+ }
160
+ return config
161
+ }
162
+
163
+ async function authenticatedFetch(baseUrl, url, init = {}) {
164
+ const headers = new Headers(init.headers)
165
+ let auth = await credentials(baseUrl)
166
+ const signal = AbortSignal.timeout(25 * 60_000)
167
+ const send = () => {
168
+ headers.set('authorization', `Bearer ${auth.access_token}`)
169
+ // The server resolves membership and roles; the CLI only names the organization, as the web's selector does.
170
+ if (process.env.GIZ_ORGANIZATION) headers.set('x-giz-organization', process.env.GIZ_ORGANIZATION)
171
+ return fetch(url, { ...init, headers, signal: init.signal ? AbortSignal.any([init.signal, signal]) : signal })
172
+ }
173
+ let response = await send()
174
+ if (response.status === 401 && !process.env.GIZ_TOKEN) {
175
+ await response.body?.cancel()
176
+ auth = await credentials(baseUrl, auth.access_token)
177
+ response = await send()
178
+ }
179
+ return response
180
+ }
181
+
182
+ async function jsonResponse(response, baseUrl) {
183
+ const text = await response.text()
184
+ let payload
185
+ try { payload = JSON.parse(text) } catch {
186
+ if (response.ok) throw new CliError('Giz API returned invalid JSON.', `Request: ${new URL(response.url).pathname}`)
187
+ payload = { message: text.slice(0, 500) }
188
+ }
189
+ if (!response.ok) {
190
+ const message = payload?.message || payload?.error?.message || `Giz API returned HTTP ${response.status}.`
191
+ // Older servers have only the legacy message; classify that exact contract without hiding other 404s.
192
+ if (response.status === 404 && (payload?.code === 'EXTERNAL_TOOL_UNAVAILABLE' ||
193
+ (new URL(response.url).pathname.startsWith('/api/tools/') && /^Unknown Giz tool: /.test(message)))) {
194
+ const compatibility = payload?.code === 'EXTERNAL_TOOL_UNAVAILABLE'
195
+ ? message : 'The server version is unknown (older server).'
196
+ throw new CliError(
197
+ `This server does not support the requested command. CLI ${CLI_VERSION}. ${compatibility}`,
198
+ `Check the server address (${baseUrl}) and its supported version; choose a supporting server with --base-url.`,
199
+ response.status
200
+ )
201
+ }
202
+ throw new CliError(
203
+ message,
204
+ response.status === 403 && /scope/i.test(message)
205
+ ? (process.env.GIZ_TOKEN ? 'Provide a GIZ_TOKEN with the required scope.' : `Run: giz auth login --base-url ${baseUrl}`)
206
+ : `Request: ${new URL(response.url).pathname}`,
207
+ response.status
208
+ )
209
+ }
210
+ return payload
211
+ }
212
+
213
+ async function api(baseUrl, pathname, data, options = {}) {
214
+ const response = await authenticatedFetch(baseUrl, new URL(pathname, baseUrl), {
215
+ method: options.method || (data === undefined ? 'GET' : 'POST'),
216
+ headers: { accept: 'application/json', ...(data === undefined ? {} : { 'content-type': 'application/json' }) },
217
+ ...(data === undefined ? {} : { body: JSON.stringify(data) })
218
+ })
219
+ const payload = await jsonResponse(response, baseUrl)
220
+ if (pathname === '/api/external/me' && payload.minimumCliVersion) {
221
+ const current = CLI_VERSION.split('.').map(Number)
222
+ const minimum = String(payload.minimumCliVersion).split('.').map(Number)
223
+ const differing = minimum.findIndex((part, index) => part !== current[index])
224
+ if (differing >= 0 && current[differing] < minimum[differing]) {
225
+ process.stderr.write(`Warning: Giz CLI ${CLI_VERSION} is below minimum ${payload.minimumCliVersion}; update @gizai/cli.\n`)
226
+ }
227
+ }
228
+ return payload
229
+ }
230
+
231
+ async function uploadMedia(baseUrl, localFile) {
232
+ const filePath = path.resolve(String(localFile))
233
+ // openAsBlob reports a missing or unreadable path only as "Unable to open file as blob".
234
+ let isFile = false
235
+ try { isFile = statSync(filePath).isFile() } catch {}
236
+ if (!isFile) throw new CliError(`Cannot read the file to upload: ${filePath}`, 'Check the path; it is resolved from the current directory.')
237
+ const body = new FormData()
238
+ body.append('file', await openAsBlob(filePath), path.basename(filePath))
239
+ const response = await authenticatedFetch(baseUrl, new URL('/api/storage/media', baseUrl), {
240
+ method: 'POST', headers: { accept: 'application/json' }, body
241
+ })
242
+ const payload = await jsonResponse(response, baseUrl)
243
+ const url = String(payload?.url || '').trim()
244
+ const drivePath = String(payload?.path || '').trim()
245
+ if (!url || !drivePath) throw new CliError('Giz upload returned an incomplete file reference.', 'Try the upload again.')
246
+ return { ...payload, url: new URL(url, baseUrl).href }
247
+ }
248
+
249
+ async function login(baseUrl, options) {
250
+ const metadata = await discovery(baseUrl)
251
+ if (options.device === true) return await deviceLogin(baseUrl, metadata)
252
+ const verifier = randomBytes(48).toString('base64url')
253
+ const challenge = createHash('sha256').update(verifier).digest('base64url')
254
+ const state = randomBytes(24).toString('base64url')
255
+ const callback = await waitForCallback(state)
256
+ const authorization = new URL(metadata.authorization_endpoint)
257
+ for (const [name, value] of Object.entries({
258
+ response_type: 'code',
259
+ client_id: CLIENT_ID,
260
+ redirect_uri: callback.url,
261
+ scope: SCOPES.join(' '),
262
+ resource: new URL('/api/mcp', baseUrl).href,
263
+ code_challenge: challenge,
264
+ code_challenge_method: 'S256',
265
+ prompt: 'consent',
266
+ state
267
+ })) authorization.searchParams.set(name, value)
268
+ process.stderr.write(`Opening Giz sign-in:\n${authorization}\n`)
269
+ if (options.open !== 'false' && options['no-open'] !== true) openBrowser(authorization.href)
270
+ const code = await callback.code
271
+ await updateConfig(() => exchangeToken(baseUrl, metadata, {
272
+ grant_type: 'authorization_code', redirect_uri: callback.url, code, code_verifier: verifier
273
+ }))
274
+ return await api(baseUrl, '/api/external/me')
275
+ }
276
+
277
+ async function deviceLogin(baseUrl, metadata) {
278
+ if (!metadata.device_authorization_endpoint) throw new CliError('OAuth device authorization is not supported.')
279
+ const response = await fetch(metadata.device_authorization_endpoint, {
280
+ method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' },
281
+ body: new URLSearchParams({ client_id: CLIENT_ID, scope: SCOPES.join(' '), resource: new URL('/api/mcp', baseUrl).href }),
282
+ signal: AbortSignal.timeout(30_000)
283
+ })
284
+ const device = await response.json()
285
+ if (!response.ok) throw new CliError(device.error_description || device.error || 'OAuth device authorization failed.')
286
+ if (!device.device_code || !device.user_code || !device.verification_uri || !(Number(device.expires_in) > 0)) {
287
+ throw new CliError('OAuth device authorization returned an incomplete response.')
288
+ }
289
+ process.stderr.write(`Giz sign-in: ${device.verification_uri_complete || device.verification_uri}\nCode: ${device.user_code}\n`)
290
+ let interval = Number(device.interval ?? 5) * 1000
291
+ if (!Number.isFinite(interval) || interval <= 0) throw new CliError('OAuth device authorization returned an invalid interval.')
292
+ const deadline = Date.now() + Number(device.expires_in) * 1000
293
+ while (Date.now() < deadline) {
294
+ const remaining = deadline - Date.now()
295
+ await new Promise((resolve) => setTimeout(resolve, Math.min(interval, remaining)))
296
+ if (interval >= remaining || Date.now() >= deadline) break
297
+ try {
298
+ await updateConfig(() => exchangeToken(baseUrl, metadata, {
299
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: device.device_code
300
+ }))
301
+ } catch (error) {
302
+ if (error.oauthError === 'slow_down') interval += 5000
303
+ else if (error.name === 'TimeoutError') interval *= 2
304
+ else if (error.oauthError !== 'authorization_pending') throw error
305
+ continue
306
+ }
307
+ return await api(baseUrl, '/api/external/me')
308
+ }
309
+ throw new CliError('expired_token: Giz device code expired.', 'Run: giz auth login --device')
310
+ }
311
+
312
+ async function waitForCallback(expectedState) {
313
+ const server = createServer()
314
+ server.listen(0, '127.0.0.1')
315
+ await once(server, 'listening')
316
+ const callbackUrl = `http://127.0.0.1:${server.address().port}/callback`
317
+ const code = new Promise((resolve, reject) => {
318
+ const finish = (error, value) => {
319
+ clearTimeout(timer)
320
+ server.close()
321
+ error ? reject(error) : resolve(value)
322
+ }
323
+ const timer = setTimeout(() => finish(new CliError('Giz sign-in timed out.', 'Run: giz auth login')), 5 * 60 * 1000)
324
+ server.on('request', (request, response) => {
325
+ const url = new URL(request.url || '/', callbackUrl)
326
+ if (url.pathname !== '/callback') {
327
+ response.writeHead(404).end('Not found')
328
+ return
329
+ }
330
+ if (url.searchParams.get('state') !== expectedState) {
331
+ response.writeHead(400).end('This sign-in link has expired. Use the link from your current giz auth login.')
332
+ return
333
+ }
334
+ const code = url.searchParams.get('code')
335
+ const error = url.searchParams.get('error') || !code
336
+ ? new CliError(url.searchParams.get('error_description') || url.searchParams.get('error') || 'OAuth callback did not include a code.', 'Run: giz auth login')
337
+ : undefined
338
+ response.setHeader('content-type', 'text/html; charset=utf-8')
339
+ response.setHeader('connection', 'close')
340
+ response.end(error
341
+ ? '<h1>Giz connection failed</h1><p>Return to the terminal for details.</p>'
342
+ : '<h1>Giz connected</h1><p>You can close this window and return to the terminal.</p>')
343
+ finish(error, code)
344
+ })
345
+ server.on('error', finish)
346
+ })
347
+ return { url: callbackUrl, code }
348
+ }
349
+
350
+ function openBrowser(url) {
351
+ // Not `cmd /c start`: cmd splits the URL at its first `&`, so Windows sign-in lost client_id and every later parameter.
352
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'rundll32' : 'xdg-open'
353
+ const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url]
354
+ const child = spawn(command, args, { detached: true, stdio: 'ignore' })
355
+ child.on('error', () => {})
356
+ child.unref()
357
+ }
358
+
359
+ // --input @FILE and --input - (stdin) carry JSON too large for one process argument (Linux caps one at 128 KiB).
360
+ async function inputText(value) {
361
+ if (value === '-') {
362
+ let text = ''
363
+ for await (const chunk of process.stdin) text += chunk
364
+ return text
365
+ }
366
+ if (typeof value === 'string' && value.startsWith('@')) {
367
+ try {
368
+ return await readFile(value.slice(1), 'utf8')
369
+ } catch {
370
+ throw new CliError(`Could not read ${value.slice(1)}.`, 'Pass --input @PATH to an existing JSON file.')
371
+ }
372
+ }
373
+ return value
374
+ }
375
+
376
+ function jsonInput(value) {
377
+ if (value === undefined || value === true) return {}
378
+ try {
379
+ const parsed = JSON.parse(value)
380
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error()
381
+ return parsed
382
+ } catch {
383
+ throw new CliError('--input must be one JSON object.', 'Example: --input \'{"prompt":"A quiet ocean"}\'')
384
+ }
385
+ }
386
+
387
+ function required(value, name, example) {
388
+ if (value) return value
389
+ throw new CliError(`${name} is required.`, example)
390
+ }
391
+
392
+ async function applyUploads(baseUrl, input, values = []) {
393
+ for (const value of values.filter((item) => item !== undefined)) {
394
+ const separator = String(value).indexOf('=')
395
+ if (separator < 1 || separator === String(value).length - 1) {
396
+ throw new CliError(
397
+ '--upload must be FIELD=LOCAL_PATH.',
398
+ 'Example: --upload image=./first-frame.png'
399
+ )
400
+ }
401
+ const field = String(value).slice(0, separator).trim()
402
+ const localFile = String(value).slice(separator + 1).trim()
403
+ const stored = await uploadMedia(baseUrl, localFile)
404
+ const current = input[field]
405
+ input[field] = current === undefined ? stored.url : Array.isArray(current) ? [...current, stored.url] : [current, stored.url]
406
+ }
407
+ return input
408
+ }
409
+
410
+ // Mutations use the user's reviewed App revision; fetching it silently would bypass conflict protection.
411
+ function appRevision(options) {
412
+ const raw = options['expected-revision']
413
+ if (raw === undefined || raw === '') {
414
+ throw new CliError('A revision is required.', 'Pass the revision from giz app list as --expected-revision.')
415
+ }
416
+ const revision = Number(raw)
417
+ if (typeof raw !== 'string' || !/^\d+$/.test(raw) || !Number.isSafeInteger(revision) || revision < 1 || revision > 9_007_199_254_740_990) {
418
+ throw new CliError('The revision must be a safe integer of 1 or more.', 'Check the current revision with giz app list.')
419
+ }
420
+ return revision
421
+ }
422
+
423
+ async function invoke(baseUrl, tool, input) {
424
+ return await api(baseUrl, `/api/tools/${encodeURIComponent(tool)}`, input)
425
+ }
426
+
427
+ function mcpTransport(baseUrl) {
428
+ return new StreamableHTTPClientTransport(new URL('/api/mcp', baseUrl), {
429
+ fetch: (url, init) => authenticatedFetch(baseUrl, url, init)
430
+ })
431
+ }
432
+
433
+ async function serveMcp(baseUrl) {
434
+ const remote = mcpTransport(baseUrl)
435
+ const stdio = new StdioServerTransport()
436
+ try {
437
+ await remote.start()
438
+ await new Promise((resolve, reject) => {
439
+ remote.onerror = stdio.onerror = reject
440
+ remote.onmessage = (message) => {
441
+ if (message.result?.protocolVersion) remote.setProtocolVersion(message.result.protocolVersion)
442
+ stdio.send(message).catch(reject)
443
+ }
444
+ stdio.onmessage = (message) => { remote.send(message).catch(reject) }
445
+ process.stdin.once('end', resolve)
446
+ stdio.start().catch(reject)
447
+ })
448
+ } finally {
449
+ await remote.close()
450
+ await stdio.close()
451
+ }
452
+ }
453
+
454
+ async function runCommand(baseUrl, positionals, options) {
455
+ const [command, subcommand, ...rest] = positionals
456
+ if (command === '__update-check') {
457
+ await checkForUpdate({ file: options.cache, url: options.registry })
458
+ return undefined
459
+ }
460
+ if (command === 'update' && options.help === true) return { kind: 'help', value: help('update') }
461
+ if (command === 'update' && !subcommand) {
462
+ const result = await explicitUpdate({ version: CLI_VERSION, source: installSource() })
463
+ return { kind: 'message', value: result.message }
464
+ }
465
+ if (options.version === true) {
466
+ const revision = await managedRevision()
467
+ return { kind: 'message', value: CLI_VERSION + (revision ? ` (region ${revision.trim()})` : '') }
468
+ }
469
+ if (!command || command === 'help' || options.help === true) {
470
+ // A script that asks for help on a command that does not exist must fail, not read the overview as success.
471
+ if (command && !gizCommand(command)) {
472
+ throw new CliError(`Unknown command "${command}".`, 'Run: giz --help')
473
+ }
474
+ return { kind: 'help', value: help(command) }
475
+ }
476
+ if (command === 'engine' && subcommand === 'run') {
477
+ if (rest[0] !== 'claude') throw new CliError('Only Claude Code is an engine connection.')
478
+ await api(baseUrl, '/api/external/engine/claude/authorization')
479
+ const { runEngine } = await import('./engine.mjs')
480
+ const result = await runEngine(rest[0], required(rest[1], 'INSTRUCTION', 'Run: giz engine run claude "instruction"'), options.dir,
481
+ event => process.stdout.write(JSON.stringify(event) + '\n'))
482
+ if (result.status !== 'completed') process.exitCode = 1
483
+ return { kind: 'result', value: result }
484
+ }
485
+ if (command === 'auth' && subcommand === 'login') {
486
+ return { kind: 'auth', value: await login(baseUrl, options) }
487
+ }
488
+ if (command === 'auth' && subcommand === 'status') {
489
+ return { kind: 'auth', value: await api(baseUrl, '/api/external/me') }
490
+ }
491
+ if (command === 'auth' && subcommand === 'logout') {
492
+ await updateConfig(async (config) => {
493
+ if (config?.refresh_token) {
494
+ const metadata = await discovery(config.baseUrl)
495
+ const response = await fetch(metadata.revocation_endpoint, {
496
+ method: 'POST', signal: AbortSignal.timeout(30_000),
497
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
498
+ body: new URLSearchParams({ token: config.refresh_token, token_type_hint: 'refresh_token', client_id: CLIENT_ID })
499
+ })
500
+ if (!response.ok) throw new CliError(`Giz sign-out failed: HTTP ${response.status}.`, 'Run: giz auth logout')
501
+ }
502
+ })
503
+ return { kind: 'message', value: 'Signed out of Giz on this device.' }
504
+ }
505
+ if (command === 'api-key' && subcommand === 'list') {
506
+ return { kind: 'result', value: await api(baseUrl, '/api/external/api-keys') }
507
+ }
508
+ if (command === 'api-key' && subcommand === 'create') {
509
+ const name = required(rest.join(' ') || options.name, 'NAME', 'Run: giz api-key create "My app"')
510
+ const scopes = options.scopes
511
+ ? String(options.scopes).split(',').map((scope) => scope.trim()).filter(Boolean)
512
+ : undefined
513
+ return {
514
+ kind: 'result',
515
+ value: await api(baseUrl, '/api/external/api-keys', {
516
+ name,
517
+ ...(scopes ? { scopes } : {}),
518
+ ...(options.expires ? { expiresAt: options.expires } : {})
519
+ })
520
+ }
521
+ }
522
+ if (command === 'api-key' && subcommand === 'revoke') {
523
+ const id = required(rest[0], 'KEY', 'Run: giz api-key revoke key_...')
524
+ return {
525
+ kind: 'result',
526
+ value: await api(baseUrl, `/api/external/api-keys/${encodeURIComponent(id)}`, undefined, { method: 'DELETE' })
527
+ }
528
+ }
529
+
530
+ if (command === 'credits') {
531
+ // Batch scripts read the balance before paid work instead of learning it from a refusal (MCP-TOOL-SCHEMA-LIMITS).
532
+ if (subcommand) throw new CliError(`Unknown command "credits ${subcommand}".`, 'Run: giz credits')
533
+ return { kind: 'result', value: await invoke(baseUrl, 'account.credits', {}) }
534
+ }
535
+ if (command === 'model' && subcommand === 'list') {
536
+ return {
537
+ kind: 'models',
538
+ value: await invoke(baseUrl, 'model.list', {
539
+ ...(options.type ? { type: options.type } : {}),
540
+ ...(options.search ? { search: options.search } : {})
541
+ })
542
+ }
543
+ }
544
+ if (command === 'model' && subcommand === 'get') {
545
+ const model = required(rest[0] || options.model, 'MODEL', 'Run: giz model get MODEL')
546
+ return { kind: 'model', value: await invoke(baseUrl, 'model.get', { model }) }
547
+ }
548
+ if (command === 'model' && subcommand === 'run') {
549
+ const model = required(rest[0] || options.model, 'MODEL', 'Run: giz model run MODEL --input JSON')
550
+ const input = await applyUploads(baseUrl, jsonInput(options.input), options.upload)
551
+ return { kind: 'job', value: await invoke(baseUrl, 'model.run', { model, input }) }
552
+ }
553
+ if (command === 'models') {
554
+ throw new CliError('Unknown command "models".', 'Did you mean: giz model list')
555
+ }
556
+ if (command === 'chat') {
557
+ const model = required(options.model || subcommand, 'MODEL', 'Run: giz chat MODEL "your prompt"')
558
+ const input = jsonInput(options.input)
559
+ const prompt = rest.join(' ').trim()
560
+ const messages = input.messages || (prompt ? [{ role: 'user', content: prompt }] : undefined)
561
+ required(messages, 'MESSAGES', 'Pass a prompt or --input \'{"messages":[...]}\'')
562
+ return {
563
+ kind: 'result',
564
+ value: await api(baseUrl, '/v1/chat/completions', {
565
+ ...input,
566
+ model,
567
+ messages,
568
+ stream: false
569
+ })
570
+ }
571
+ }
572
+
573
+ if (command === 'generate' && ['image', 'video', 'audio', 'music', 'talking-head'].includes(subcommand)) {
574
+ const tool = {
575
+ image: 'generateImage',
576
+ video: 'generateVideo',
577
+ audio: 'generateAudio',
578
+ music: 'generateMusic',
579
+ 'talking-head': 'generateTalkingHead'
580
+ }[subcommand]
581
+ const uploads = [
582
+ ...(Array.isArray(options.upload) ? options.upload : []),
583
+ ...(options.image ? [`references=${options.image}`] : []),
584
+ ...(options.audio ? [`audio=${options.audio}`] : [])
585
+ ]
586
+ const input = await applyUploads(baseUrl, {
587
+ ...jsonInput(options.input),
588
+ ...(rest.length ? { prompt: rest.join(' ') } : {}),
589
+ ...(options.model ? { model: options.model } : {})
590
+ }, uploads)
591
+ return { kind: 'job', value: await invoke(baseUrl, tool, input) }
592
+ }
593
+
594
+ if (command === 'job' && subcommand === 'list') {
595
+ return { kind: 'jobs', value: await invoke(baseUrl, 'job.list', options.limit ? { limit: Number(options.limit) } : {}) }
596
+ }
597
+ if (command === 'job' && subcommand === 'get') {
598
+ const job = required(rest[0], 'JOB', 'Run: giz job get JOB')
599
+ return { kind: 'job', value: await invoke(baseUrl, 'job.get', { job }) }
600
+ }
601
+ if (command === 'job' && subcommand === 'cancel') {
602
+ const job = required(rest[0], 'JOB', 'Run: giz job cancel JOB')
603
+ return { kind: 'job', value: await invoke(baseUrl, 'job.cancel', { job }) }
604
+ }
605
+ if (command === 'job' && subcommand === 'wait') {
606
+ const id = required(rest[0], 'JOB', 'Run: giz job wait JOB')
607
+ while (true) {
608
+ const job = await invoke(baseUrl, 'job.get', { job: id })
609
+ if (['completed', 'failed', 'cancelled'].includes(job.status)) return { kind: 'job', value: job, exitCode: job.status === 'completed' ? 0 : 1 }
610
+ if (process.stderr.isTTY) process.stderr.write(`\r${job.id} ${job.status} ${job.message || ''}`.slice(0, 120))
611
+ await new Promise((resolve) => setTimeout(resolve, 1000))
612
+ }
613
+ }
614
+
615
+ if (command === 'drive') {
616
+ const mappings = {
617
+ list: () => ['drive.list', { path: rest[0] || '' }],
618
+ search: () => ['drive.search', { query: required(rest.join(' '), 'QUERY', 'Run: giz drive search QUERY') }],
619
+ read: () => ['drive.readFile', { path: required(rest[0], 'PATH', 'Run: giz drive read PATH') }],
620
+ mkdir: () => ['drive.makeDirectory', { path: required(rest[0], 'PATH', 'Run: giz drive mkdir PATH') }],
621
+ move: () => ['drive.movePath', {
622
+ from: required(rest[0], 'FROM', 'Run: giz drive move FROM TO'),
623
+ to: required(rest[1], 'TO', 'Run: giz drive move FROM TO')
624
+ }],
625
+ delete: () => ['drive.deletePath', { path: required(rest[0], 'PATH', 'Run: giz drive delete PATH') }]
626
+ }
627
+ if (subcommand === 'write') {
628
+ const drivePath = required(rest[0], 'PATH', 'Run: giz drive write PATH --content TEXT')
629
+ let content = options.content
630
+ let encoding = 'utf8'
631
+ if (options.file) {
632
+ const buffer = await readFile(String(options.file))
633
+ content = buffer.toString('base64')
634
+ encoding = 'base64'
635
+ }
636
+ required(content, 'CONTENT', 'Use --content TEXT or --file LOCAL_PATH.')
637
+ return {
638
+ kind: 'result',
639
+ value: await invoke(baseUrl, 'drive.writeFile', {
640
+ path: drivePath,
641
+ content: String(content),
642
+ encoding,
643
+ ...(options.computer ? { computer: options.computer } : {})
644
+ })
645
+ }
646
+ }
647
+ const mapping = mappings[subcommand === 'ls' ? 'list' : subcommand]
648
+ if (!mapping) throw new CliError(`Unknown drive command "${subcommand || ''}".`, 'Run: giz drive --help')
649
+ const [tool, input] = mapping()
650
+ return {
651
+ kind: 'result',
652
+ value: await invoke(baseUrl, tool, { ...input, ...(options.computer ? { computer: options.computer } : {}) })
653
+ }
654
+ }
655
+
656
+ if (command === 'table' && subcommand === 'list') {
657
+ return { kind: 'result', value: await invoke(baseUrl, 'workspace.objects', {}) }
658
+ }
659
+
660
+ if (command === 'table' && subcommand === 'get') {
661
+ const object = required(rest[0], 'TABLE', 'Run: giz table get task')
662
+ return { kind: 'result', value: await invoke(baseUrl, 'workspace.objects', { object }) }
663
+ }
664
+
665
+ if (command === 'table' && subcommand === 'action') {
666
+ const view = required(rest[0], 'TABLE', 'Run: giz table action TABLE ACTION [--id ID] --input JSON')
667
+ const action = required(rest[1], 'ACTION', 'Run: giz table get TABLE to see its actions')
668
+ // Namespaced table tools use their canonical external contract and table ownership checks.
669
+ if (action.startsWith('table.')) {
670
+ if (options.id !== undefined) throw new CliError('Table tools do not take --id.', 'Put row IDs in --input.')
671
+ return { kind: 'result', value: await invoke(baseUrl, action, { ...jsonInput(options.input), table: view }) }
672
+ }
673
+ const id = options.id === undefined ? undefined : String(options.id).includes(',') ? String(options.id).split(',') : String(options.id)
674
+ return {
675
+ kind: 'result',
676
+ value: await invoke(baseUrl, 'workspace.executeAction', { view, action, ...(id === undefined ? {} : { dataId: id }), data: jsonInput(options.input) })
677
+ }
678
+ }
679
+
680
+ if (command === 'table' && ['read', 'write'].includes(subcommand)) {
681
+ const table = required(rest[0], 'TABLE', `Run: giz table ${subcommand} TABLE_PATH --input JSON`)
682
+ return {
683
+ kind: 'result',
684
+ value: await invoke(baseUrl, subcommand === 'read' ? 'table.readRows' : 'table.writeRows', {
685
+ ...jsonInput(options.input),
686
+ table,
687
+ ...(options.computer ? { computer: options.computer } : {})
688
+ })
689
+ }
690
+ }
691
+
692
+ if (command === 'upload') {
693
+ const localFile = required(subcommand, 'LOCAL_PATH', 'Run: giz upload ./reference.png')
694
+ return { kind: 'result', value: await uploadMedia(baseUrl, localFile) }
695
+ }
696
+
697
+ if (command === 'computer') {
698
+ if (subcommand === 'list') return { kind: 'computers', value: await invoke(baseUrl, 'computer.list', {}) }
699
+ const computer = options.computer || (subcommand === 'exec' ? undefined : rest[0])
700
+ if (subcommand === 'get') return { kind: 'result', value: await invoke(baseUrl, 'computer.get', computer ? { computer } : {}) }
701
+ if (subcommand === 'start' || subcommand === 'stop') {
702
+ return {
703
+ kind: 'result',
704
+ value: await invoke(baseUrl, `computer.${subcommand}`, computer ? { computer } : {})
705
+ }
706
+ }
707
+ if (subcommand === 'exec') {
708
+ if (!rest.length) throw new CliError('COMMAND is required.', 'Run: giz computer exec -- ls -la')
709
+ const stdin = options['stdin-file'] ? await readFile(String(options['stdin-file']), 'utf8') : options.stdin
710
+ const value = await invoke(baseUrl, 'computer.exec', {
711
+ command: rest,
712
+ ...(stdin !== undefined ? { stdin: String(stdin) } : {}),
713
+ ...(options['deadline-ms'] !== undefined ? { deadlineMs: Number(options['deadline-ms']) } : {}),
714
+ ...(options.cwd ? { cwd: options.cwd } : {}),
715
+ ...(computer ? { computer } : {})
716
+ })
717
+ return { kind: 'result', value, exitCode: value.exitCode }
718
+ }
719
+ throw new CliError(`Unknown computer command "${subcommand || ''}".`, 'Run: giz computer --help')
720
+ }
721
+
722
+ if (command === 'app') {
723
+ const computer = options.computer ? { computer: options.computer } : {}
724
+ if (subcommand === 'list') {
725
+ return { kind: 'result', value: await invoke(baseUrl, 'app.list', computer) }
726
+ }
727
+ if (subcommand === 'save' || subcommand === 'create') {
728
+ const input = jsonInput(options.input)
729
+ return { kind: 'result', value: await invoke(baseUrl, 'app.save', { ...input, ...computer }) }
730
+ }
731
+ if (subcommand === 'run') {
732
+ const id = required(rest[0], 'APP_ID', 'Run: giz app run APP_ID --expected-revision REVISION')
733
+ return { kind: 'result', value: await invoke(baseUrl, 'app.run', { ...computer, id, expectedRevision: appRevision(options) }) }
734
+ }
735
+ if (subcommand === 'publish') {
736
+ const id = required(rest[0], 'APP_ID', 'Run: giz app publish APP_ID private|workspace|public')
737
+ const visibility = required(rest[1] || options.visibility, 'VISIBILITY', 'Run: giz app publish APP_ID private|workspace|public')
738
+ return { kind: 'result', value: await invoke(baseUrl, 'app.publish', { ...computer, id, visibility, expectedRevision: appRevision(options) }) }
739
+ }
740
+ if (subcommand === 'open') {
741
+ const id = required(rest[0], 'APP_ID', 'Run: giz app open APP_ID')
742
+ return { kind: 'result', value: await invoke(baseUrl, 'app.open', { ...computer, id }) }
743
+ }
744
+ if (subcommand === 'delete') {
745
+ const id = required(rest[0], 'APP_ID', 'Run: giz app delete APP_ID')
746
+ return { kind: 'result', value: await invoke(baseUrl, 'app.delete', { ...computer, id, expectedRevision: appRevision(options) }) }
747
+ }
748
+ throw new CliError(`Unknown app command "${subcommand || ''}".`, 'Run: giz app --help')
749
+ }
750
+
751
+ if (command === 'connection') {
752
+ if (subcommand === 'list') {
753
+ return {
754
+ kind: 'result',
755
+ value: await invoke(baseUrl, 'capability.status', {
756
+ ...(options.computer ? { computer: options.computer } : {})
757
+ })
758
+ }
759
+ }
760
+ throw new CliError(`Unknown connection command "${subcommand || ''}".`, 'Run: giz connection --help')
761
+ }
762
+
763
+ if (command === 'plugin') {
764
+ const computer = options.computer ? { computer: options.computer } : {}
765
+ if (subcommand === 'list' || subcommand === 'search') {
766
+ const query = subcommand === 'search'
767
+ ? required(rest.join(' '), 'QUERY', 'Run: giz plugin search QUERY [--origin catalog|skill|mcp]')
768
+ : undefined
769
+ return {
770
+ kind: 'result',
771
+ value: await invoke(baseUrl, 'plugin.catalog', {
772
+ ...(query ? { query } : { origin: 'installed' }),
773
+ ...(query && options.origin ? { origin: options.origin } : {}),
774
+ ...(options.limit ? { limit: Number(options.limit) } : {}),
775
+ ...computer
776
+ })
777
+ }
778
+ }
779
+ if (subcommand === 'inspect') {
780
+ const drivePath = required(rest[0], 'PATH', 'Run: giz plugin inspect DRIVE_PATH')
781
+ return { kind: 'result', value: await invoke(baseUrl, 'plugin.catalog', { path: drivePath, ...computer }) }
782
+ }
783
+ if (subcommand === 'install') {
784
+ const source = options.path
785
+ ? { path: options.path }
786
+ : { id: required(rest[0], 'ID', 'Run: giz plugin install ID --query QUERY, or giz plugin install --path DRIVE_PATH') }
787
+ return {
788
+ kind: 'result',
789
+ value: await invoke(baseUrl, 'plugin.install', {
790
+ ...source,
791
+ ...(options.query ? { query: options.query } : {}),
792
+ ...computer
793
+ })
794
+ }
795
+ }
796
+ if (['update', 'enable', 'disable', 'remove'].includes(subcommand)) {
797
+ const name = required(rest[0], 'NAME', `Run: giz plugin ${subcommand} NAME`)
798
+ return { kind: 'result', value: await invoke(baseUrl, 'plugin.manage', { operation: subcommand, name, ...computer }) }
799
+ }
800
+ if (subcommand === 'publish') {
801
+ const drivePath = required(rest[0], 'PATH', 'Run: giz plugin publish DRIVE_PATH --visibility organization|public')
802
+ const visibility = required(options.visibility, 'VISIBILITY', 'Add --visibility organization|public.')
803
+ return {
804
+ kind: 'result',
805
+ value: await invoke(baseUrl, 'plugin.manage', {
806
+ operation: 'publish',
807
+ path: drivePath,
808
+ visibility,
809
+ ...(options.organization ? { organizationId: options.organization } : {}),
810
+ ...computer
811
+ })
812
+ }
813
+ }
814
+ if (subcommand === 'unpublish') {
815
+ const releaseId = required(rest[0], 'RELEASE', 'Run: giz plugin unpublish RELEASE')
816
+ return { kind: 'result', value: await invoke(baseUrl, 'plugin.manage', { operation: 'unpublish', releaseId }) }
817
+ }
818
+ throw new CliError(`Unknown plugin command "${subcommand || ''}".`, 'Run: giz plugin --help')
819
+ }
820
+
821
+ if (command === 'agent') {
822
+ if (subcommand === 'start') {
823
+ return {
824
+ kind: 'result',
825
+ value: await invoke(baseUrl, 'agent.start', {
826
+ ...jsonInput(options.input),
827
+ ...(options.computer ? { computer: options.computer } : {})
828
+ })
829
+ }
830
+ }
831
+ if (subcommand === 'status' || subcommand === 'cancel') {
832
+ const turnId = required(rest[0], 'TURN', `Run: giz agent ${subcommand} TURN`)
833
+ return {
834
+ kind: 'result',
835
+ value: await invoke(baseUrl, `agent.${subcommand}`, {
836
+ turnId,
837
+ ...(subcommand === 'status' && options.wait ? { wait: true } : {})
838
+ })
839
+ }
840
+ }
841
+ throw new CliError(`Unknown agent command "${subcommand || ''}".`, 'Run: giz agent --help')
842
+ }
843
+
844
+ if (command === 'invoke') {
845
+ const tool = required(subcommand, 'TOOL', 'Run: giz invoke TOOL --input JSON')
846
+ return { kind: 'result', value: await invoke(baseUrl, tool, jsonInput(options.input)) }
847
+ }
848
+
849
+ if (command === 'mcp' && subcommand === 'url') {
850
+ return { kind: 'message', value: new URL('/api/mcp', baseUrl).href }
851
+ }
852
+ if (command === 'mcp' && subcommand === 'serve') {
853
+ await serveMcp(baseUrl)
854
+ return
855
+ }
856
+ if (command === 'mcp' && subcommand === 'check') {
857
+ const client = new Client({ name: 'giz-cli', version: CLI_VERSION })
858
+ try {
859
+ await client.connect(mcpTransport(baseUrl))
860
+ const { tools } = await client.listTools()
861
+ if (!tools.length) throw new CliError('Giz MCP returned zero tools.', 'Check your Giz connection and scopes.')
862
+ return { kind: 'result', value: { server: client.getServerVersion(), tools: tools.map((tool) => tool.name) } }
863
+ } finally { await client.close() }
864
+ }
865
+ if (command === 'mcp' && subcommand === 'add') {
866
+ const client = required(rest[0], 'CLIENT', 'Run: giz mcp add codex|claude')
867
+ if (!['codex', 'claude'].includes(client)) throw new CliError(`Unsupported MCP client "${client}".`, 'Supported clients: codex, claude')
868
+ const transport = options.transport || 'stdio'
869
+ if (!['stdio', 'http'].includes(transport)) throw new CliError('MCP transport must be stdio or http.')
870
+ const endpoint = new URL('/api/mcp', baseUrl).href
871
+ const target = transport === 'stdio'
872
+ ? ['--env', `GIZ_CONFIG=${configPath}`, '--', process.execPath, fileURLToPath(import.meta.url), 'mcp', 'serve', '--base-url', baseUrl]
873
+ : client === 'codex' ? ['--url', endpoint, '--oauth-resource', endpoint] : [endpoint]
874
+ runExternal(client, ['mcp', 'add', ...(client === 'claude' ? ['--scope', 'user', '--transport', transport] : []), 'giz', ...target],
875
+ `Install ${client === 'codex' ? 'Codex: npm install -g @openai/codex' : 'Claude Code: npm install -g @anthropic-ai/claude-code'}`, transport === 'http')
876
+ return { kind: 'message', value: `Giz was added to ${client} (${transport}). ${transport === 'stdio'
877
+ ? 'Run giz auth login, then giz mcp check --json before starting a headless session.'
878
+ : client === 'claude' ? 'Open Claude Code and run /mcp to sign in.' : 'Complete the browser approval opened by Codex.'}` }
879
+ }
880
+
881
+ if (command === 'skill' && subcommand === 'install') {
882
+ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'skills')
883
+ runExternal('npx', ['--yes', 'skills', 'add', root, '--skill', '*', '--agent', 'universal', '--copy', '-y'], 'Install Node.js 20 or newer first.')
884
+ return { kind: 'message', value: 'Giz Skill installed for available AI agents.' }
885
+ }
886
+
887
+ throw new CliError(`Unknown command "${[command, subcommand].filter(Boolean).join(' ')}".`, 'Run: giz --help')
888
+ }
889
+
890
+ function runExternal(command, args, hint, interactive = false) {
891
+ const result = spawnSync(command, args, { encoding: 'utf8', stdio: interactive ? ['inherit', 2, 2] : ['inherit', 'pipe', 'pipe'] })
892
+ if (result.error?.code === 'ENOENT') throw new CliError(`${command} is not installed.`, hint)
893
+ if (result.error || result.status !== 0) throw new CliError(
894
+ `${command} failed (${result.error?.code || result.signal || result.status}): ${(result.stderr || result.stdout || result.error?.message || '').trim()}`
895
+ )
896
+ if (result.stderr || result.stdout) process.stderr.write(result.stderr + result.stdout)
897
+ }
898
+
899
+ function print(kind, value, json, output = process.stdout) {
900
+ if (json) {
901
+ output.write(`${JSON.stringify(value, null, 2)}\n`)
902
+ return
903
+ }
904
+ if (kind === 'help' || kind === 'message') {
905
+ output.write(`${value}\n`)
906
+ return
907
+ }
908
+ if (kind === 'auth' && value.identity?.type === 'computer') {
909
+ output.write(`Signed in as a Computer identity (owner ${value.email || value.name})\n`)
910
+ return
911
+ }
912
+ if (kind === 'models') {
913
+ const rows = value.models || []
914
+ for (const model of rows) output.write(`${model.id}\t${model.type}\t${model.provider || '-'}\t${model.label}\n`)
915
+ output.write(`\n${rows.length} models\n`)
916
+ return
917
+ }
918
+ if (kind === 'jobs') {
919
+ for (const job of value.jobs || []) output.write(`${job.id}\t${job.status}\t${job.tool}\t${job.createdAt}\n`)
920
+ return
921
+ }
922
+ if (kind === 'computers') {
923
+ for (const computer of value.computers || []) {
924
+ output.write(`${computer.id}\t${computer.status || '-'}\t${computer.isDefault ? 'default' : '-'}\t${computer.name}\n`)
925
+ }
926
+ return
927
+ }
928
+ if (kind === 'job') {
929
+ output.write(`${value.id} ${value.status}\n`)
930
+ if (value.message) output.write(`${value.message}\n`)
931
+ if (value.error) output.write(`Error: ${value.error}\n`)
932
+ if (value.result) output.write(`${JSON.stringify(value.result, null, 2)}\n`)
933
+ if (!['completed', 'failed', 'cancelled'].includes(value.status)) {
934
+ output.write(`Run: giz job wait ${value.id}\n`)
935
+ }
936
+ return
937
+ }
938
+ output.write(`${JSON.stringify(value, null, 2)}\n`)
939
+ }
940
+
941
+ function help(command) {
942
+ if (command === 'engine') return `Usage
943
+ giz engine run claude "instruction" [--dir path]
944
+
945
+ Runs the official Claude Code in an isolated worktree. Connect Claude Code in Giz first.
946
+ Progress and the final diff, files and summary are returned. Interrupting stops the engine.`
947
+ if (command === 'api-key') return `Usage
948
+ giz api-key list
949
+ giz api-key create "My app" [--scopes giz.inference,giz.read] [--expires ISO_TIMESTAMP]
950
+ giz api-key revoke KEY_ID
951
+
952
+ The secret is shown only when the key is created.`
953
+ if (command === 'credits') return `Usage
954
+ giz credits
955
+
956
+ Prints your remaining credit (purchased and plan), when plan credit resets, and this month's spend and charges.
957
+ In a workspace it prints the workspace's monthly budget. Read it before paid batch work.`
958
+ if (command === 'chat') return `Usage
959
+ giz chat MODEL "your prompt"
960
+ giz chat MODEL --input '{"messages":[{"role":"user","content":"Hello"}]}'`
961
+ if (command === 'model') return `Usage
962
+ giz model list [--type TYPE] [--search TEXT]
963
+ giz model get MODEL
964
+ giz model run MODEL --input JSON [--upload FIELD=LOCAL_PATH]
965
+
966
+ Run model get first. Its live input schema is the source of truth. Repeat --upload for multiple media inputs.
967
+ model run returns a job immediately. Retrieve the result with giz job wait JOB_ID, or poll with giz job get JOB_ID.
968
+ For speech-to-text, use model run with the audio field returned by model get. Callback/webhook delivery is not supported.`
969
+ if (command === 'generate') return `Usage
970
+ giz generate image|video|music|talking-head "PROMPT" [--model MODEL]
971
+ giz generate audio --input '{"purpose":"speech","script":"Hello"}' [--model MODEL]
972
+ giz generate audio --input '{"purpose":"sound","prompt":"Rain","duration":5}'
973
+
974
+ Options
975
+ --image LOCAL_PATH Upload a visual reference
976
+ --audio LOCAL_PATH Upload speech audio
977
+ --upload FIELD=PATH Upload into an exact tool field
978
+ --input JSON Input matching the tool schema (@FILE reads a file, - reads stdin)
979
+
980
+ For model-specific audio options, inspect giz model get MODEL and pass supported fields in settings (for example settings.speed), not at the top level.
981
+ Generation returns a job immediately; use giz job wait JOB_ID for its result.`
982
+ if (command === 'drive') return `Usage
983
+ giz drive ls [PATH] (list alias)
984
+ giz drive search QUERY
985
+ giz drive read PATH
986
+ giz drive write PATH --content TEXT
987
+ giz drive write PATH --file LOCAL_PATH
988
+ giz drive mkdir PATH
989
+ giz drive move FROM TO
990
+ giz drive delete PATH`
991
+ if (command === 'table') return `Usage
992
+ giz table list
993
+ giz table get TABLE
994
+ giz table read TABLE [--input JSON] [--computer COMPUTER]
995
+ giz table write TABLE --input JSON [--computer COMPUTER]
996
+ giz table action TABLE ACTION [--id ID[,ID...]] --input JSON
997
+
998
+ TABLE is a table's Drive path (for example Sales/Customers.view.ts) or a business object name from giz table list
999
+ (for example task, issue, requirement). giz table get shows its fields, the row changes it accepts (writes) and its
1000
+ actions. Namespaced table actions (such as table.updateSchema) use the table tool contract; an object without a write changes only through its actions. Use --org to work in an organization.
1001
+ read input: {"select":["name"],"filters":[{"field":"status","operator":"eq","value":"open"}],"limit":50}
1002
+ write input: {"create":[{"name":"Acme"}],"update":[{"id":"ROW_ID","status":"done"}],"remove":["ROW_ID"]}
1003
+ One write is one change people can undo in the table. At most 200 rows per write.`
1004
+ if (command === 'computer') return `Usage
1005
+ giz computer list
1006
+ giz computer get|start|stop [COMPUTER]
1007
+ giz computer exec [--computer COMPUTER] [--cwd PATH] [--deadline-ms MS] [--stdin TEXT|--stdin-file PATH] -- COMMAND [ARGS...]`
1008
+ if (command === 'app') return `Usage
1009
+ giz app list [--computer COMPUTER]
1010
+ giz app create|save [--computer COMPUTER] --input JSON
1011
+ giz app run APP_ID --expected-revision REVISION [--computer COMPUTER]
1012
+ giz app publish APP_ID private|workspace|public --expected-revision REVISION [--computer COMPUTER]
1013
+ giz app open APP_ID [--computer COMPUTER]
1014
+ giz app delete APP_ID --expected-revision REVISION [--computer COMPUTER]`
1015
+ if (command === 'plugin') return `Usage
1016
+ giz plugin list [--computer COMPUTER]
1017
+ giz plugin search QUERY [--origin catalog|skill|mcp] [--limit N]
1018
+ giz plugin inspect DRIVE_PATH
1019
+ giz plugin install ID --query QUERY [--computer COMPUTER]
1020
+ giz plugin install --path DRIVE_PATH [--computer COMPUTER]
1021
+ giz plugin update|enable|disable|remove NAME [--computer COMPUTER]
1022
+ giz plugin publish DRIVE_PATH --visibility organization|public [--organization ORG]
1023
+ giz plugin unpublish RELEASE
1024
+
1025
+ A plugin (Agent Plugins 1.0) bundles skills, MCP servers and Agents; it is what you install, turn off and share.`
1026
+ if (command === 'connection') return `Usage
1027
+ giz connection list [--computer COMPUTER]`
1028
+ if (command === 'agent') return `Usage
1029
+ giz agent start [--computer COMPUTER] --input JSON
1030
+ giz agent status TURN [--wait]
1031
+ giz agent cancel TURN`
1032
+ if (command === 'mcp') return `Usage
1033
+ giz mcp url
1034
+ giz mcp serve
1035
+ giz mcp check --json
1036
+ giz mcp add codex|claude [--transport stdio|http]
1037
+
1038
+ stdio (default) shares giz auth login and refreshes tokens automatically.
1039
+ http uses the client's own OAuth login. Install the chosen client first.`
1040
+ if (command === 'update') return `Usage\n giz update\n\nUpdates @gizai/cli with its owning package manager.`
1041
+ if (command === 'auth') return `Usage
1042
+ giz auth login [--device]
1043
+ giz auth status
1044
+ giz auth logout`
1045
+ return `Giz CLI — AI models, Drive, and Computer from any terminal
1046
+
1047
+ Usage
1048
+ giz auth login [--device]
1049
+ giz update
1050
+ giz api-key list|create|revoke
1051
+ giz chat MODEL "your prompt"
1052
+ giz model list [--type TYPE] [--search TEXT]
1053
+ giz model get MODEL
1054
+ giz model run MODEL --input JSON [--upload FIELD=LOCAL_PATH]
1055
+ giz generate image|video|music|talking-head "PROMPT" [--model MODEL]
1056
+ giz generate audio --input JSON [--model MODEL]
1057
+ giz job list|get|wait|cancel [JOB]
1058
+ giz credits
1059
+ giz drive ls|list|search|read|write|mkdir|move|delete
1060
+ giz table list|get|read|write|action
1061
+ giz upload LOCAL_PATH
1062
+ giz computer list|get|start|stop|exec
1063
+ giz app list|create|save|run|publish|open|delete
1064
+ giz plugin list|search|inspect|install|update|enable|disable|remove|publish|unpublish
1065
+ giz connection list
1066
+ giz agent start|status|cancel
1067
+ giz engine run claude "instruction" [--dir path]
1068
+ giz mcp url|serve|check|add
1069
+ giz skill install
1070
+ giz invoke TOOL --input JSON
1071
+
1072
+ Use giz COMMAND --help for parameters and examples.
1073
+
1074
+ Gizlang (the toolchain is installed on first use, with its size shown)
1075
+ init, run, build, test, check, deploy, and every other command; gizdb runs the database server.
1076
+ Docs: https://gizstack.com/docs
1077
+
1078
+ Global options
1079
+ --json Machine-readable JSON output
1080
+ --base-url URL Another Giz deployment
1081
+ --org ORGANIZATION Organization id to work in (or GIZ_ORGANIZATION)
1082
+ --no-open Print the login URL without opening a browser
1083
+
1084
+ Docs: https://www.giz.ai/mcp/`
1085
+ }
1086
+
1087
+ // giz.ai's commands are the ones its overview lists; every other command is the Gizlang toolchain's (toolchain.mjs).
1088
+ function gizCommand(name) {
1089
+ return ['help', 'models', '__update-check'].includes(name) ||
1090
+ help().split('\n').some((line) => line.startsWith(' giz ') && line.trim().split(' ')[1] === name)
1091
+ }
1092
+
1093
+ async function main() {
1094
+ const { positionals, options } = parse(process.argv.slice(2))
1095
+ const [first, second] = positionals
1096
+ const asked = first === 'help' ? second : first
1097
+ if (asked && !gizCommand(asked) && toolchainCommand(asked)) {
1098
+ await runToolchain('giz', process.argv.slice(2))
1099
+ return
1100
+ }
1101
+ if (positionals[0] === '__update-check') {
1102
+ await runCommand('', positionals, options)
1103
+ return
1104
+ }
1105
+ const source = installSource()
1106
+ const saved = await readConfig()
1107
+ const updateMode = ['notify', 'off'].includes(saved?.updates?.mode) ? saved.updates.mode : 'notify'
1108
+ void maybeNotify({ version: CLI_VERSION, source, mode: updateMode, env: { ...process.env, GIZ_JSON: options.json === true ? '1' : process.env.GIZ_JSON } }).catch(() => {})
1109
+ const requiresAuth = positionals.length && !options.help && !options.version &&
1110
+ !(positionals[0] === 'auth' && ['login', 'logout'].includes(positionals[1])) &&
1111
+ !['update', '__update-check'].includes(positionals[0])
1112
+ const computer = requiresAuth && !process.env.GIZ_TOKEN && !saved?.access_token ? await computerIdentity() : undefined
1113
+ const baseUrl = String(options['base-url'] || process.env.GIZ_URL || saved?.baseUrl || computer?.baseUrl || DEFAULT_URL).replace(/\/+$/, '')
1114
+ if (typeof options.org === 'string') process.env.GIZ_ORGANIZATION = options.org
1115
+ options.input = await inputText(options.input)
1116
+ const output = await runCommand(baseUrl, positionals, options)
1117
+ if (output) {
1118
+ print(output.kind, output.value, options.json === true)
1119
+ process.exitCode = output.exitCode ?? (output.value?.isError ? 1 : 0)
1120
+ }
1121
+ }
1122
+
1123
+ main().catch((error) => {
1124
+ const { options } = parse(process.argv.slice(2))
1125
+ const json = options.json === true
1126
+ const failure = { message: String(error?.message || error), hint: error?.hint, status: error?.status }
1127
+ print('message', json ? { error: failure } : `Error: ${failure.message}${failure.hint ? `\nHint: ${failure.hint}` : ''}`, json, process.stderr)
1128
+ process.exitCode = 1
1129
+ })