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