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