@gizai/cli 1.0.0 → 1.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/README.md CHANGED
@@ -1,10 +1,11 @@
1
1
  # Giz CLI
2
2
 
3
- Connect once in the browser, then use every live Giz non-LLM model, Drive, and Computer from a terminal or AI agent.
3
+ Connect once in the browser, then use live Giz text and media models, Drive, and Computer from a terminal or AI agent.
4
4
 
5
5
  ```bash
6
6
  npm install -g @gizai/cli
7
7
  giz auth login
8
+ giz chat gpt-5-6-luna "Say hello in Korean"
8
9
  giz generate image "A paper observatory at blue hour"
9
10
  ```
10
11
 
@@ -18,20 +19,61 @@ giz model run ltx2-distilled \
18
19
  --upload image=./first-frame.png
19
20
  ```
20
21
 
21
- Remote MCP:
22
+ MCP for Claude Code and Codex (install the chosen client first):
22
23
 
23
24
  ```bash
25
+ giz auth login
24
26
  giz mcp add codex
25
27
  giz mcp add claude
28
+ giz mcp check --json
26
29
  ```
27
30
 
31
+ These commands register `giz mcp serve` over stdio. It forwards the server's live
32
+ MCP protocol and shares the CLI's rotating OAuth credentials. Tokens are not
33
+ copied into Claude or Codex configuration. A custom `GIZ_CONFIG` path is preserved.
34
+
35
+ For clients with their own OAuth session, use `giz mcp add claude --transport http`
36
+ and sign in through `/mcp`, or `giz mcp add codex --transport http`. The remote
37
+ endpoint also supports OAuth discovery and scoped API keys. A manually copied
38
+ OAuth access token expires after one hour; updating CLI credentials does not
39
+ update a static client header.
40
+
41
+ For a headless Claude Code check, after login and registration:
42
+
43
+ ```bash
44
+ claude -p 'Use model_list to list one Giz image model.' --allowedTools mcp__giz__model_list
45
+ ```
46
+
47
+ `giz mcp check --json` checks the MCP handshake and lists actual tool names without
48
+ running a model. Claude's `--verbose --output-format stream-json` also shows MCP
49
+ connection state. If registration reports an existing `giz` entry, remove that
50
+ entry with the client's `mcp remove` command before adding the desired transport.
51
+ The exact client failure is returned on stderr.
52
+
28
53
  Agent Skill:
29
54
 
30
55
  ```bash
31
56
  giz skill install
32
57
  ```
33
58
 
34
- The CLI stores rotating OAuth credentials in `~/.config/giz/credentials.json` with mode `0600`. It never asks for or stores a Giz password.
35
- Set `GIZ_TOKEN` only for ephemeral automation. Run `giz --help` for Drive, Computer, job, upload, and raw tool commands.
59
+ The CLI stores rotating OAuth credentials as unencrypted JSON in
60
+ `~/.config/giz/credentials.json` with mode `0600` (owner access only), using atomic
61
+ writes and serialized sign-in, refresh, and sign-out. Failed token revocation is
62
+ reported before local credentials are removed. It never stores a Giz password.
63
+ The sign-in callback binds a fresh loopback port; use the URL printed by the
64
+ current login. Progress goes to stderr, leaving `--json` stdout machine-readable.
65
+
66
+ After updating from 1.0.1, run `giz auth login` again to consent to `giz.inference`.
67
+ Refreshing an old grant cannot add this scope. `giz auth status --json` shows
68
+ granted scopes; a missing scope error names the required permission.
69
+
70
+ `giz computer exec --deadline-ms 120000 -- COMMAND ARGS...` uses the bounded
71
+ Computer command API (2 minutes by default, up to 15 minutes). The CLI preserves
72
+ the remote command's exit code; `giz job wait` exits with status 1 for failed or
73
+ cancelled jobs. JSON results remain available on stdout. Free Computers have a runtime limit; check the current
74
+ Computer plan before starting long workflows. MCP authentication does not extend
75
+ that runtime. Media jobs run independently and are tracked with `giz job get`.
76
+
77
+ Create a scoped API key with `giz api-key create "My app"`; its secret is shown once. Set `GIZ_TOKEN` only for ephemeral automation. Run `giz --help` for chat, models, Drive, Computer, job, upload, and raw tool commands.
36
78
 
37
79
  Documentation: <https://www.giz.ai/mcp/>
package/cli/giz.mjs CHANGED
@@ -1,20 +1,26 @@
1
1
  #!/usr/bin/env node
2
2
  import { createHash, randomBytes } from 'node:crypto'
3
3
  import { openAsBlob } from 'node:fs'
4
- import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises'
4
+ import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
5
5
  import { createServer } from 'node:http'
6
+ import { once } from 'node:events'
7
+ import { createRequire } from 'node:module'
6
8
  import os from 'node:os'
7
9
  import path from 'node:path'
8
10
  import { spawn, spawnSync } from 'node:child_process'
9
11
  import { fileURLToPath } from 'node:url'
12
+ import { lock } from 'proper-lockfile'
13
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js'
14
+ import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
15
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
10
16
 
17
+ const { version: CLI_VERSION } = createRequire(import.meta.url)('../package.json')
11
18
  const DEFAULT_URL = 'https://www.giz.ai'
12
- const CALLBACK_URL = 'http://127.0.0.1:53682/callback'
13
19
  const CLIENT_ID = 'giz-cli'
14
- const SCOPES = ['giz.read', 'giz.write', 'giz.execute', 'giz.destructive', 'offline_access']
20
+ const SCOPES = ['giz.read', 'giz.inference', 'giz.write', 'giz.execute', 'giz.destructive', 'offline_access']
15
21
  const BOOLEAN_OPTIONS = new Set(['help', 'json', 'no-open', 'version'])
16
- const configPath = process.env.GIZ_CONFIG ||
17
- path.join(os.homedir(), '.config', 'giz', 'credentials.json')
22
+ const configPath = path.resolve(process.env.GIZ_CONFIG ||
23
+ path.join(os.homedir(), '.config', 'giz', 'credentials.json'))
18
24
 
19
25
  class CliError extends Error {
20
26
  constructor(message, hint, status) {
@@ -63,14 +69,27 @@ async function readConfig() {
63
69
  }
64
70
  }
65
71
 
66
- async function saveConfig(config) {
67
- await mkdir(path.dirname(configPath), { recursive: true })
68
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 })
69
- await chmod(configPath, 0o600)
72
+ async function updateConfig(update) {
73
+ await mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 })
74
+ const release = await lock(configPath, { realpath: false, retries: { retries: 120, minTimeout: 100, maxTimeout: 500 } })
75
+ const temporary = `${configPath}.${randomBytes(8).toString('hex')}`
76
+ try {
77
+ const config = await update(await readConfig())
78
+ if (config) {
79
+ await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600, flag: 'wx' })
80
+ await rename(temporary, configPath)
81
+ } else {
82
+ await unlink(configPath).catch((error) => { if (error.code !== 'ENOENT') throw error })
83
+ }
84
+ return config
85
+ } finally {
86
+ try { await unlink(temporary).catch((error) => { if (error.code !== 'ENOENT') throw error }) }
87
+ finally { await release() }
88
+ }
70
89
  }
71
90
 
72
91
  async function discovery(baseUrl) {
73
- const response = await fetch(new URL('/.well-known/oauth-authorization-server', baseUrl))
92
+ const response = await fetch(new URL('/.well-known/oauth-authorization-server', baseUrl), { signal: AbortSignal.timeout(30_000) })
74
93
  if (!response.ok) {
75
94
  throw new CliError(
76
95
  `OAuth discovery failed: HTTP ${response.status}.`,
@@ -80,42 +99,37 @@ async function discovery(baseUrl) {
80
99
  return await response.json()
81
100
  }
82
101
 
83
- async function refresh(config) {
84
- if (!config?.refresh_token) {
85
- throw new CliError('Not authenticated.', 'Run: giz auth login')
86
- }
87
- const metadata = await discovery(config.baseUrl)
102
+ async function exchangeToken(baseUrl, metadata, grant, config = {}) {
88
103
  const body = new URLSearchParams({
89
- grant_type: 'refresh_token',
90
104
  client_id: CLIENT_ID,
91
- refresh_token: config.refresh_token,
92
- resource: new URL('/api/mcp', config.baseUrl).href
105
+ resource: new URL('/api/mcp', baseUrl).href,
106
+ ...grant
93
107
  })
94
108
  const response = await fetch(metadata.token_endpoint, {
95
109
  method: 'POST',
96
110
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
97
- body
111
+ body,
112
+ signal: AbortSignal.timeout(30_000)
98
113
  })
99
114
  const payload = await response.json()
100
115
  if (!response.ok) {
101
116
  throw new CliError(
102
- payload.error_description || payload.error || 'Giz session could not be refreshed.',
117
+ payload.error_description || payload.error || 'OAuth token exchange failed.',
103
118
  'Run: giz auth login'
104
119
  )
105
120
  }
106
- const next = {
121
+ return {
107
122
  ...config,
123
+ baseUrl,
108
124
  ...payload,
109
125
  refresh_token: payload.refresh_token || config.refresh_token,
110
126
  expires_at: Date.now() + Number(payload.expires_in || 3600) * 1000
111
127
  }
112
- await saveConfig(next)
113
- return next
114
128
  }
115
129
 
116
- async function credentials(baseUrl, forceRefresh = false) {
130
+ async function credentials(baseUrl, rejectedToken) {
117
131
  if (process.env.GIZ_TOKEN) return { access_token: process.env.GIZ_TOKEN, baseUrl }
118
- let config = await readConfig()
132
+ const config = await readConfig()
119
133
  if (!config?.access_token) throw new CliError('Not authenticated.', 'Run: giz auth login')
120
134
  if (String(config.baseUrl).replace(/\/+$/, '') !== baseUrl) {
121
135
  throw new CliError(
@@ -123,87 +137,80 @@ async function credentials(baseUrl, forceRefresh = false) {
123
137
  `Run: giz auth login --base-url ${baseUrl}`
124
138
  )
125
139
  }
126
- if (forceRefresh || Number(config.expires_at || 0) <= Date.now() + 60_000) config = await refresh(config)
140
+ const expired = (value) => value.access_token === rejectedToken || Number(value.expires_at || 0) <= Date.now() + 60_000
141
+ if (expired(config)) {
142
+ return await updateConfig(async (config) => {
143
+ if (config?.baseUrl !== baseUrl) throw new CliError('Giz credentials changed.', 'Run: giz auth login')
144
+ if (expired(config)) {
145
+ if (!config.refresh_token) throw new CliError('Giz session expired.', 'Run: giz auth login')
146
+ return await exchangeToken(baseUrl, await discovery(baseUrl), {
147
+ grant_type: 'refresh_token', refresh_token: config.refresh_token
148
+ }, config)
149
+ }
150
+ return config
151
+ })
152
+ }
127
153
  return config
128
154
  }
129
155
 
130
- async function api(baseUrl, pathname, data, options = {}) {
131
- const run = async (forceRefresh = false) => {
132
- const auth = await credentials(baseUrl, forceRefresh)
133
- const response = await fetch(new URL(pathname, baseUrl), {
134
- method: data === undefined ? 'GET' : 'POST',
135
- headers: {
136
- accept: 'application/json',
137
- ...(data === undefined ? {} : { 'content-type': 'application/json' }),
138
- authorization: `Bearer ${auth.access_token}`
139
- },
140
- ...(data === undefined ? {} : { body: JSON.stringify(data) })
141
- })
142
- const text = await response.text()
143
- let payload
144
- try {
145
- payload = text ? JSON.parse(text) : null
146
- } catch {
147
- payload = { message: text.slice(0, 500) }
148
- }
149
- return { response, payload }
150
- }
151
- let result = await run()
152
- if (result.response.status === 401 && !process.env.GIZ_TOKEN && !options.refreshed) {
153
- result = await run(true)
156
+ async function authenticatedFetch(baseUrl, url, init = {}) {
157
+ const headers = new Headers(init.headers)
158
+ let auth = await credentials(baseUrl)
159
+ const signal = AbortSignal.timeout(25 * 60_000)
160
+ const send = () => {
161
+ headers.set('authorization', `Bearer ${auth.access_token}`)
162
+ return fetch(url, { ...init, headers, signal: init.signal ? AbortSignal.any([init.signal, signal]) : signal })
163
+ }
164
+ let response = await send()
165
+ if (response.status === 401 && !process.env.GIZ_TOKEN) {
166
+ await response.body?.cancel()
167
+ auth = await credentials(baseUrl, auth.access_token)
168
+ response = await send()
169
+ }
170
+ return response
171
+ }
172
+
173
+ async function jsonResponse(response, baseUrl) {
174
+ const text = await response.text()
175
+ let payload
176
+ try { payload = JSON.parse(text) } catch {
177
+ if (response.ok) throw new CliError('Giz API returned invalid JSON.', `Request: ${new URL(response.url).pathname}`)
178
+ payload = { message: text.slice(0, 500) }
154
179
  }
155
- if (!result.response.ok) {
156
- const required = result.response.status === 403 ? result.payload?.message : undefined
180
+ if (!response.ok) {
181
+ const message = payload?.message || payload?.error?.message || `Giz API returned HTTP ${response.status}.`
157
182
  throw new CliError(
158
- result.payload?.message || `Giz API returned HTTP ${result.response.status}.`,
159
- required?.includes('scope') ? 'Run: giz auth login' : 'Run with --json for the complete error.',
160
- result.response.status
183
+ message,
184
+ response.status === 403 && /scope/i.test(message)
185
+ ? (process.env.GIZ_TOKEN ? 'Provide a GIZ_TOKEN with the required scope.' : `Run: giz auth login --base-url ${baseUrl}`)
186
+ : `Request: ${new URL(response.url).pathname}`,
187
+ response.status
161
188
  )
162
189
  }
163
- return result.payload
190
+ return payload
191
+ }
192
+
193
+ async function api(baseUrl, pathname, data, options = {}) {
194
+ const response = await authenticatedFetch(baseUrl, new URL(pathname, baseUrl), {
195
+ method: options.method || (data === undefined ? 'GET' : 'POST'),
196
+ headers: { accept: 'application/json', ...(data === undefined ? {} : { 'content-type': 'application/json' }) },
197
+ ...(data === undefined ? {} : { body: JSON.stringify(data) })
198
+ })
199
+ return await jsonResponse(response, baseUrl)
164
200
  }
165
201
 
166
202
  async function uploadMedia(baseUrl, localFile) {
167
203
  const filePath = path.resolve(String(localFile))
168
- const run = async (forceRefresh = false) => {
169
- const auth = await credentials(baseUrl, forceRefresh)
170
- let file
171
- try {
172
- file = await openAsBlob(filePath)
173
- } catch (error) {
174
- throw new CliError(`Could not open ${filePath}.`, String(error?.message || error))
175
- }
176
- const body = new FormData()
177
- body.append('file', file, path.basename(filePath))
178
- const response = await fetch(new URL('/api/storage/media', baseUrl), {
179
- method: 'POST',
180
- headers: {
181
- accept: 'application/json',
182
- authorization: `Bearer ${auth.access_token}`
183
- },
184
- body
185
- })
186
- const text = await response.text()
187
- let payload
188
- try {
189
- payload = text ? JSON.parse(text) : null
190
- } catch {
191
- payload = { message: text.slice(0, 500) }
192
- }
193
- return { response, payload }
194
- }
195
- let result = await run()
196
- if (result.response.status === 401 && !process.env.GIZ_TOKEN) result = await run(true)
197
- if (!result.response.ok) {
198
- throw new CliError(
199
- result.payload?.message || `Giz upload returned HTTP ${result.response.status}.`,
200
- 'Check the file and your Giz storage access.'
201
- )
202
- }
203
- const url = String(result.payload?.url || '').trim()
204
- const drivePath = String(result.payload?.path || '').trim()
204
+ const body = new FormData()
205
+ body.append('file', await openAsBlob(filePath), path.basename(filePath))
206
+ const response = await authenticatedFetch(baseUrl, new URL('/api/storage/media', baseUrl), {
207
+ method: 'POST', headers: { accept: 'application/json' }, body
208
+ })
209
+ const payload = await jsonResponse(response, baseUrl)
210
+ const url = String(payload?.url || '').trim()
211
+ const drivePath = String(payload?.path || '').trim()
205
212
  if (!url || !drivePath) throw new CliError('Giz upload returned an incomplete file reference.', 'Try the upload again.')
206
- return { ...result.payload, url: new URL(url, baseUrl).href }
213
+ return { ...payload, url: new URL(url, baseUrl).href }
207
214
  }
208
215
 
209
216
  async function login(baseUrl, options) {
@@ -211,12 +218,12 @@ async function login(baseUrl, options) {
211
218
  const verifier = randomBytes(48).toString('base64url')
212
219
  const challenge = createHash('sha256').update(verifier).digest('base64url')
213
220
  const state = randomBytes(24).toString('base64url')
214
- const callback = waitForCallback(state)
221
+ const callback = await waitForCallback(state)
215
222
  const authorization = new URL(metadata.authorization_endpoint)
216
223
  for (const [name, value] of Object.entries({
217
224
  response_type: 'code',
218
225
  client_id: CLIENT_ID,
219
- redirect_uri: CALLBACK_URL,
226
+ redirect_uri: callback.url,
220
227
  scope: SCOPES.join(' '),
221
228
  resource: new URL('/api/mcp', baseUrl).href,
222
229
  code_challenge: challenge,
@@ -224,76 +231,51 @@ async function login(baseUrl, options) {
224
231
  prompt: 'consent',
225
232
  state
226
233
  })) authorization.searchParams.set(name, value)
227
- process.stdout.write(`Opening Giz sign-in:\n${authorization}\n`)
234
+ process.stderr.write(`Opening Giz sign-in:\n${authorization}\n`)
228
235
  if (options.open !== 'false' && options['no-open'] !== true) openBrowser(authorization.href)
229
- const code = await callback
230
- const response = await fetch(metadata.token_endpoint, {
231
- method: 'POST',
232
- headers: { 'content-type': 'application/x-www-form-urlencoded' },
233
- body: new URLSearchParams({
234
- grant_type: 'authorization_code',
235
- client_id: CLIENT_ID,
236
- redirect_uri: CALLBACK_URL,
237
- code,
238
- code_verifier: verifier,
239
- resource: new URL('/api/mcp', baseUrl).href
240
- })
241
- })
242
- const payload = await response.json()
243
- if (!response.ok) {
244
- throw new CliError(payload.error_description || payload.error || 'OAuth token exchange failed.', 'Run: giz auth login')
245
- }
246
- await saveConfig({
247
- baseUrl,
248
- ...payload,
249
- expires_at: Date.now() + Number(payload.expires_in || 3600) * 1000
250
- })
236
+ const code = await callback.code
237
+ await updateConfig(() => exchangeToken(baseUrl, metadata, {
238
+ grant_type: 'authorization_code', redirect_uri: callback.url, code, code_verifier: verifier
239
+ }))
251
240
  return await api(baseUrl, '/api/external/me')
252
241
  }
253
242
 
254
- function waitForCallback(expectedState) {
255
- return new Promise((resolve, reject) => {
256
- const timer = setTimeout(() => {
243
+ async function waitForCallback(expectedState) {
244
+ const server = createServer()
245
+ server.listen(0, '127.0.0.1')
246
+ await once(server, 'listening')
247
+ const callbackUrl = `http://127.0.0.1:${server.address().port}/callback`
248
+ const code = new Promise((resolve, reject) => {
249
+ const finish = (error, value) => {
250
+ clearTimeout(timer)
257
251
  server.close()
258
- reject(new CliError('Giz sign-in timed out.', 'Run: giz auth login'))
259
- }, 5 * 60 * 1000)
260
- const server = createServer((request, response) => {
261
- const url = new URL(request.url || '/', CALLBACK_URL)
252
+ error ? reject(error) : resolve(value)
253
+ }
254
+ const timer = setTimeout(() => finish(new CliError('Giz sign-in timed out.', 'Run: giz auth login')), 5 * 60 * 1000)
255
+ server.on('request', (request, response) => {
256
+ const url = new URL(request.url || '/', callbackUrl)
262
257
  if (url.pathname !== '/callback') {
263
258
  response.writeHead(404).end('Not found')
264
259
  return
265
260
  }
266
- const finish = (error, code) => {
267
- clearTimeout(timer)
268
- response.setHeader('content-type', 'text/html; charset=utf-8')
269
- response.end(error
270
- ? '<h1>Giz connection failed</h1><p>Return to the terminal for details.</p>'
271
- : '<h1>Giz connected</h1><p>You can close this window and return to the terminal.</p>')
272
- server.close()
273
- error ? reject(error) : resolve(code)
274
- }
275
261
  if (url.searchParams.get('state') !== expectedState) {
276
- finish(new CliError('OAuth state did not match.', 'Run: giz auth login'))
277
- } else if (url.searchParams.get('error')) {
278
- finish(new CliError(
279
- url.searchParams.get('error_description') || url.searchParams.get('error'),
280
- 'Run: giz auth login'
281
- ))
282
- } else if (!url.searchParams.get('code')) {
283
- finish(new CliError('OAuth callback did not include a code.', 'Run: giz auth login'))
284
- } else {
285
- finish(undefined, url.searchParams.get('code'))
262
+ response.writeHead(400).end('This sign-in link has expired. Use the link from your current giz auth login.')
263
+ return
286
264
  }
265
+ const code = url.searchParams.get('code')
266
+ const error = url.searchParams.get('error') || !code
267
+ ? new CliError(url.searchParams.get('error_description') || url.searchParams.get('error') || 'OAuth callback did not include a code.', 'Run: giz auth login')
268
+ : undefined
269
+ response.setHeader('content-type', 'text/html; charset=utf-8')
270
+ response.setHeader('connection', 'close')
271
+ response.end(error
272
+ ? '<h1>Giz connection failed</h1><p>Return to the terminal for details.</p>'
273
+ : '<h1>Giz connected</h1><p>You can close this window and return to the terminal.</p>')
274
+ finish(error, code)
287
275
  })
288
- server.on('error', (error) => {
289
- clearTimeout(timer)
290
- reject(new CliError(
291
- `Could not listen on ${CALLBACK_URL}: ${error.message}`,
292
- 'Close the process using port 53682 and run: giz auth login'
293
- ))
294
- })
295
- server.listen(53682, '127.0.0.1')
276
+ server.on('error', finish)
296
277
  })
278
+ return { url: callbackUrl, code }
297
279
  }
298
280
 
299
281
  function openBrowser(url) {
@@ -342,9 +324,36 @@ async function invoke(baseUrl, tool, input) {
342
324
  return await api(baseUrl, `/api/tools/${encodeURIComponent(tool)}`, input)
343
325
  }
344
326
 
327
+ function mcpTransport(baseUrl) {
328
+ return new StreamableHTTPClientTransport(new URL('/api/mcp', baseUrl), {
329
+ fetch: (url, init) => authenticatedFetch(baseUrl, url, init)
330
+ })
331
+ }
332
+
333
+ async function serveMcp(baseUrl) {
334
+ const remote = mcpTransport(baseUrl)
335
+ const stdio = new StdioServerTransport()
336
+ try {
337
+ await remote.start()
338
+ await new Promise((resolve, reject) => {
339
+ remote.onerror = stdio.onerror = reject
340
+ remote.onmessage = (message) => {
341
+ if (message.result?.protocolVersion) remote.setProtocolVersion(message.result.protocolVersion)
342
+ stdio.send(message).catch(reject)
343
+ }
344
+ stdio.onmessage = (message) => { remote.send(message).catch(reject) }
345
+ process.stdin.once('end', resolve)
346
+ stdio.start().catch(reject)
347
+ })
348
+ } finally {
349
+ await remote.close()
350
+ await stdio.close()
351
+ }
352
+ }
353
+
345
354
  async function runCommand(baseUrl, positionals, options) {
346
355
  const [command, subcommand, ...rest] = positionals
347
- if (options.version === true) return { kind: 'message', value: '1.0.0' }
356
+ if (options.version === true) return { kind: 'message', value: CLI_VERSION }
348
357
  if (!command || command === 'help' || options.help === true) return { kind: 'help', value: help(command) }
349
358
  if (command === 'auth' && subcommand === 'login') {
350
359
  return { kind: 'auth', value: await login(baseUrl, options) }
@@ -353,24 +362,43 @@ async function runCommand(baseUrl, positionals, options) {
353
362
  return { kind: 'auth', value: await api(baseUrl, '/api/external/me') }
354
363
  }
355
364
  if (command === 'auth' && subcommand === 'logout') {
356
- const config = await readConfig()
357
- if (config?.refresh_token) {
358
- const metadata = await discovery(baseUrl)
359
- await fetch(metadata.revocation_endpoint, {
360
- method: 'POST',
361
- headers: { 'content-type': 'application/x-www-form-urlencoded' },
362
- body: new URLSearchParams({
363
- token: config.refresh_token,
364
- token_type_hint: 'refresh_token',
365
- client_id: CLIENT_ID
365
+ await updateConfig(async (config) => {
366
+ if (config?.refresh_token) {
367
+ const metadata = await discovery(config.baseUrl)
368
+ const response = await fetch(metadata.revocation_endpoint, {
369
+ method: 'POST', signal: AbortSignal.timeout(30_000),
370
+ headers: { 'content-type': 'application/x-www-form-urlencoded' },
371
+ body: new URLSearchParams({ token: config.refresh_token, token_type_hint: 'refresh_token', client_id: CLIENT_ID })
366
372
  })
367
- })
368
- }
369
- await unlink(configPath).catch((error) => {
370
- if (error?.code !== 'ENOENT') throw error
373
+ if (!response.ok) throw new CliError(`Giz sign-out failed: HTTP ${response.status}.`, 'Run: giz auth logout')
374
+ }
371
375
  })
372
376
  return { kind: 'message', value: 'Signed out of Giz on this device.' }
373
377
  }
378
+ if (command === 'api-key' && subcommand === 'list') {
379
+ return { kind: 'result', value: await api(baseUrl, '/api/external/api-keys') }
380
+ }
381
+ if (command === 'api-key' && subcommand === 'create') {
382
+ const name = required(rest.join(' ') || options.name, 'NAME', 'Run: giz api-key create "My app"')
383
+ const scopes = options.scopes
384
+ ? String(options.scopes).split(',').map((scope) => scope.trim()).filter(Boolean)
385
+ : undefined
386
+ return {
387
+ kind: 'result',
388
+ value: await api(baseUrl, '/api/external/api-keys', {
389
+ name,
390
+ ...(scopes ? { scopes } : {}),
391
+ ...(options.expires ? { expiresAt: options.expires } : {})
392
+ })
393
+ }
394
+ }
395
+ if (command === 'api-key' && subcommand === 'revoke') {
396
+ const id = required(rest[0], 'KEY', 'Run: giz api-key revoke key_...')
397
+ return {
398
+ kind: 'result',
399
+ value: await api(baseUrl, `/api/external/api-keys/${encodeURIComponent(id)}`, undefined, { method: 'DELETE' })
400
+ }
401
+ }
374
402
 
375
403
  if (command === 'model' && subcommand === 'list') {
376
404
  return {
@@ -393,6 +421,22 @@ async function runCommand(baseUrl, positionals, options) {
393
421
  if (command === 'models') {
394
422
  throw new CliError('Unknown command "models".', 'Did you mean: giz model list')
395
423
  }
424
+ if (command === 'chat') {
425
+ const model = required(options.model || subcommand, 'MODEL', 'Run: giz chat MODEL "your prompt"')
426
+ const input = jsonInput(options.input)
427
+ const prompt = rest.join(' ').trim()
428
+ const messages = input.messages || (prompt ? [{ role: 'user', content: prompt }] : undefined)
429
+ required(messages, 'MESSAGES', 'Pass a prompt or --input \'{"messages":[...]}\'')
430
+ return {
431
+ kind: 'result',
432
+ value: await api(baseUrl, '/v1/chat/completions', {
433
+ ...input,
434
+ model,
435
+ messages,
436
+ stream: false
437
+ })
438
+ }
439
+ }
396
440
 
397
441
  if (command === 'generate' && ['image', 'video', 'audio', 'music', 'talking-head'].includes(subcommand)) {
398
442
  const tool = {
@@ -431,7 +475,7 @@ async function runCommand(baseUrl, positionals, options) {
431
475
  const id = required(rest[0], 'JOB', 'Run: giz job wait JOB')
432
476
  while (true) {
433
477
  const job = await invoke(baseUrl, 'job.get', { job: id })
434
- if (['completed', 'failed', 'cancelled'].includes(job.status)) return { kind: 'job', value: job }
478
+ if (['completed', 'failed', 'cancelled'].includes(job.status)) return { kind: 'job', value: job, exitCode: job.status === 'completed' ? 0 : 1 }
435
479
  if (process.stderr.isTTY) process.stderr.write(`\r${job.id} ${job.status} ${job.message || ''}`.slice(0, 120))
436
480
  await new Promise((resolve) => setTimeout(resolve, 1000))
437
481
  }
@@ -465,7 +509,7 @@ async function runCommand(baseUrl, positionals, options) {
465
509
  path: drivePath,
466
510
  content: String(content),
467
511
  encoding,
468
- ...(options.computer ? { computerId: options.computer } : {})
512
+ ...(options.computer ? { computer: options.computer } : {})
469
513
  })
470
514
  }
471
515
  }
@@ -474,7 +518,7 @@ async function runCommand(baseUrl, positionals, options) {
474
518
  const [tool, input] = mapping()
475
519
  return {
476
520
  kind: 'result',
477
- value: await invoke(baseUrl, tool, { ...input, ...(options.computer ? { computerId: options.computer } : {}) })
521
+ value: await invoke(baseUrl, tool, { ...input, ...(options.computer ? { computer: options.computer } : {}) })
478
522
  }
479
523
  }
480
524
 
@@ -485,28 +529,133 @@ async function runCommand(baseUrl, positionals, options) {
485
529
 
486
530
  if (command === 'computer') {
487
531
  if (subcommand === 'list') return { kind: 'computers', value: await invoke(baseUrl, 'computer.list', {}) }
488
- const computerId = rest[0] || options.computer
489
- if (subcommand === 'get') return { kind: 'result', value: await invoke(baseUrl, 'computer.get', computerId ? { computerId } : {}) }
532
+ const computer = options.computer || (subcommand === 'exec' ? undefined : rest[0])
533
+ if (subcommand === 'get') return { kind: 'result', value: await invoke(baseUrl, 'computer.get', computer ? { computer } : {}) }
490
534
  if (subcommand === 'start' || subcommand === 'stop') {
491
535
  return {
492
536
  kind: 'result',
493
- value: await invoke(baseUrl, `computer.${subcommand}`, computerId ? { computerId } : {})
537
+ value: await invoke(baseUrl, `computer.${subcommand}`, computer ? { computer } : {})
494
538
  }
495
539
  }
496
540
  if (subcommand === 'exec') {
497
- const commandArgs = rest
498
- if (!commandArgs.length) throw new CliError('COMMAND is required.', 'Run: giz computer exec -- ls -la')
541
+ if (!rest.length) throw new CliError('COMMAND is required.', 'Run: giz computer exec -- ls -la')
542
+ const stdin = options['stdin-file'] ? await readFile(String(options['stdin-file']), 'utf8') : options.stdin
543
+ const value = await invoke(baseUrl, 'computer.exec', {
544
+ command: rest,
545
+ ...(stdin !== undefined ? { stdin: String(stdin) } : {}),
546
+ ...(options['deadline-ms'] !== undefined ? { deadlineMs: Number(options['deadline-ms']) } : {}),
547
+ ...(options.cwd ? { cwd: options.cwd } : {}),
548
+ ...(computer ? { computer } : {})
549
+ })
550
+ return { kind: 'result', value, exitCode: value.exitCode }
551
+ }
552
+ throw new CliError(`Unknown computer command "${subcommand || ''}".`, 'Run: giz computer --help')
553
+ }
554
+
555
+ if (command === 'site') {
556
+ const computer = options.computer ? { computer: options.computer } : {}
557
+ if (subcommand === 'list') {
558
+ return { kind: 'result', value: await invoke(baseUrl, 'site.list', computer) }
559
+ }
560
+ if (subcommand === 'create' || subcommand === 'update') {
561
+ const input = jsonInput(options.input)
562
+ const siteId = subcommand === 'update'
563
+ ? required(rest[0], 'SITE', 'Run: giz site update SITE --input JSON')
564
+ : undefined
499
565
  return {
500
566
  kind: 'result',
501
- value: await invoke(baseUrl, 'container.exec', {
502
- command: commandArgs,
503
- sessionId: String(options.session || 'giz-cli'),
504
- ...(options.cwd ? { cwd: options.cwd } : {}),
505
- ...(options.computer ? { computerId: options.computer } : {})
567
+ value: await invoke(baseUrl, 'site.configure', {
568
+ ...input,
569
+ ...computer,
570
+ operation: subcommand,
571
+ ...(siteId ? { siteId } : {})
506
572
  })
507
573
  }
508
574
  }
509
- throw new CliError(`Unknown computer command "${subcommand || ''}".`, 'Run: giz computer --help')
575
+ if (subcommand === 'verify') {
576
+ const siteId = required(rest[0], 'SITE', 'Run: giz site verify SITE')
577
+ return {
578
+ kind: 'result',
579
+ value: await invoke(baseUrl, 'site.configure', { ...computer, operation: 'verify', siteId })
580
+ }
581
+ }
582
+ if (subcommand === 'delete') {
583
+ const siteId = required(rest[0], 'SITE', 'Run: giz site delete SITE')
584
+ return { kind: 'result', value: await invoke(baseUrl, 'site.delete', { ...computer, siteId }) }
585
+ }
586
+ throw new CliError(`Unknown site command "${subcommand || ''}".`, 'Run: giz site --help')
587
+ }
588
+
589
+ if (command === 'capability') {
590
+ if (subcommand === 'list') {
591
+ return {
592
+ kind: 'result',
593
+ value: await invoke(baseUrl, 'capability.status', {
594
+ ...(options.computer ? { computer: options.computer } : {})
595
+ })
596
+ }
597
+ }
598
+ if (subcommand === 'search') {
599
+ const kind = required(rest[0], 'KIND', 'Run: giz capability search skill|mcp QUERY')
600
+ const query = required(rest.slice(1).join(' '), 'QUERY', 'Run: giz capability search skill|mcp QUERY')
601
+ return {
602
+ kind: 'result',
603
+ value: await invoke(baseUrl, 'capability.catalog', {
604
+ kind,
605
+ query,
606
+ ...(options.limit ? { limit: Number(options.limit) } : {})
607
+ })
608
+ }
609
+ }
610
+ if (subcommand === 'install') {
611
+ const kind = required(rest[0], 'KIND', 'Run: giz capability install skill|mcp ID --query QUERY')
612
+ const id = required(rest[1], 'ID', 'Run: giz capability install skill|mcp ID --query QUERY')
613
+ const query = required(options.query, 'QUERY', 'Repeat the catalog query with --query QUERY.')
614
+ return {
615
+ kind: 'result',
616
+ value: await invoke(baseUrl, 'capability.install', {
617
+ kind,
618
+ id,
619
+ query,
620
+ ...(options.computer ? { computer: options.computer } : {})
621
+ })
622
+ }
623
+ }
624
+ if (subcommand === 'update' || subcommand === 'remove') {
625
+ const id = required(rest[0], 'ID', `Run: giz capability ${subcommand} ID`)
626
+ return {
627
+ kind: 'result',
628
+ value: await invoke(baseUrl, 'capability.manage', {
629
+ id,
630
+ operation: subcommand,
631
+ ...(options.computer ? { computer: options.computer } : {})
632
+ })
633
+ }
634
+ }
635
+ throw new CliError(`Unknown capability command "${subcommand || ''}".`, 'Run: giz capability --help')
636
+ }
637
+
638
+ if (command === 'agent') {
639
+ if (subcommand === 'start') {
640
+ return {
641
+ kind: 'result',
642
+ value: await invoke(baseUrl, 'agent.start', {
643
+ ...jsonInput(options.input),
644
+ ...(options.computer ? { computer: options.computer } : {})
645
+ })
646
+ }
647
+ }
648
+ if (subcommand === 'status' || subcommand === 'cancel') {
649
+ const turnId = required(rest[0], 'TURN', `Run: giz agent ${subcommand} TURN`)
650
+ return {
651
+ kind: 'result',
652
+ value: await invoke(baseUrl, `agent.${subcommand}`, {
653
+ turnId,
654
+ ...(subcommand === 'status' && options.wait ? { wait: true } : {})
655
+ })
656
+ }
657
+ }
658
+ throw new CliError(`Unknown agent command "${subcommand || ''}".`, 'Run: giz agent --help')
510
659
  }
511
660
 
512
661
  if (command === 'invoke') {
@@ -517,75 +666,101 @@ async function runCommand(baseUrl, positionals, options) {
517
666
  if (command === 'mcp' && subcommand === 'url') {
518
667
  return { kind: 'message', value: new URL('/api/mcp', baseUrl).href }
519
668
  }
669
+ if (command === 'mcp' && subcommand === 'serve') {
670
+ await serveMcp(baseUrl)
671
+ return
672
+ }
673
+ if (command === 'mcp' && subcommand === 'check') {
674
+ const client = new Client({ name: 'giz-cli', version: CLI_VERSION })
675
+ try {
676
+ await client.connect(mcpTransport(baseUrl))
677
+ const { tools } = await client.listTools()
678
+ if (!tools.length) throw new CliError('Giz MCP returned zero tools.', 'Check your Giz connection and scopes.')
679
+ return { kind: 'result', value: { server: client.getServerVersion(), tools: tools.map((tool) => tool.name) } }
680
+ } finally { await client.close() }
681
+ }
520
682
  if (command === 'mcp' && subcommand === 'add') {
521
683
  const client = required(rest[0], 'CLIENT', 'Run: giz mcp add codex|claude')
684
+ if (!['codex', 'claude'].includes(client)) throw new CliError(`Unsupported MCP client "${client}".`, 'Supported clients: codex, claude')
685
+ const transport = options.transport || 'stdio'
686
+ if (!['stdio', 'http'].includes(transport)) throw new CliError('MCP transport must be stdio or http.')
522
687
  const endpoint = new URL('/api/mcp', baseUrl).href
523
- if (client === 'codex') {
524
- runExternal('codex', ['mcp', 'add', 'giz', '--url', endpoint, '--oauth-resource', endpoint], 'Install Codex CLI first.')
525
- runExternal('codex', ['mcp', 'login', 'giz', '--scopes', SCOPES.join(',')], 'Run: codex mcp login giz')
526
- return { kind: 'message', value: 'Giz is connected to Codex.' }
527
- }
528
- if (client === 'claude') {
529
- runExternal('claude', ['mcp', 'add', '--transport', 'http', '--scope', 'user', 'giz', endpoint], 'Install Claude Code first.')
530
- return { kind: 'message', value: 'Giz was added to Claude Code. Open Claude Code and run /mcp to sign in.' }
531
- }
532
- throw new CliError(`Unsupported MCP client "${client}".`, 'Supported clients: codex, claude')
688
+ const target = transport === 'stdio'
689
+ ? ['--env', `GIZ_CONFIG=${configPath}`, '--', process.execPath, fileURLToPath(import.meta.url), 'mcp', 'serve', '--base-url', baseUrl]
690
+ : client === 'codex' ? ['--url', endpoint, '--oauth-resource', endpoint] : [endpoint]
691
+ runExternal(client, ['mcp', 'add', ...(client === 'claude' ? ['--scope', 'user', '--transport', transport] : []), 'giz', ...target],
692
+ `Install ${client === 'codex' ? 'Codex: npm install -g @openai/codex' : 'Claude Code: npm install -g @anthropic-ai/claude-code'}`, transport === 'http')
693
+ return { kind: 'message', value: `Giz was added to ${client} (${transport}). ${transport === 'stdio'
694
+ ? 'Run giz auth login, then giz mcp check --json before starting a headless session.'
695
+ : client === 'claude' ? 'Open Claude Code and run /mcp to sign in.' : 'Complete the browser approval opened by Codex.'}` }
533
696
  }
534
697
 
535
698
  if (command === 'skill' && subcommand === 'install') {
536
699
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'skills')
537
- runExternal('npx', ['--yes', 'skills', 'add', root, '--all'], 'Install Node.js 20 or newer first.')
700
+ runExternal('npx', ['--yes', 'skills', 'add', root, '--skill', '*', '--agent', 'universal', '--copy', '-y'], 'Install Node.js 20 or newer first.')
538
701
  return { kind: 'message', value: 'Giz Skill installed for available AI agents.' }
539
702
  }
540
703
 
541
704
  throw new CliError(`Unknown command "${[command, subcommand].filter(Boolean).join(' ')}".`, 'Run: giz --help')
542
705
  }
543
706
 
544
- function runExternal(command, args, hint) {
545
- const result = spawnSync(command, args, { stdio: 'inherit' })
707
+ function runExternal(command, args, hint, interactive = false) {
708
+ const result = spawnSync(command, args, { encoding: 'utf8', stdio: interactive ? ['inherit', 2, 2] : ['inherit', 'pipe', 'pipe'] })
546
709
  if (result.error?.code === 'ENOENT') throw new CliError(`${command} is not installed.`, hint)
547
- if (result.status !== 0) throw new CliError(`${command} exited with status ${result.status}.`, hint)
710
+ if (result.error || result.status !== 0) throw new CliError(
711
+ `${command} failed (${result.error?.code || result.signal || result.status}): ${(result.stderr || result.stdout || result.error?.message || '').trim()}`
712
+ )
713
+ if (result.stderr || result.stdout) process.stderr.write(result.stderr + result.stdout)
548
714
  }
549
715
 
550
- function print(kind, value, json) {
716
+ function print(kind, value, json, output = process.stdout) {
551
717
  if (json) {
552
- process.stdout.write(`${JSON.stringify(value, null, 2)}\n`)
718
+ output.write(`${JSON.stringify(value, null, 2)}\n`)
553
719
  return
554
720
  }
555
721
  if (kind === 'help' || kind === 'message') {
556
- process.stdout.write(`${value}\n`)
722
+ output.write(`${value}\n`)
557
723
  return
558
724
  }
559
725
  if (kind === 'models') {
560
726
  const rows = value.models || []
561
- for (const model of rows) process.stdout.write(`${model.id}\t${model.type}\t${model.provider || '-'}\t${model.label}\n`)
562
- process.stdout.write(`\n${rows.length} models\n`)
727
+ for (const model of rows) output.write(`${model.id}\t${model.type}\t${model.provider || '-'}\t${model.label}\n`)
728
+ output.write(`\n${rows.length} models\n`)
563
729
  return
564
730
  }
565
731
  if (kind === 'jobs') {
566
- for (const job of value.jobs || []) process.stdout.write(`${job.id}\t${job.status}\t${job.tool}\t${job.createdAt}\n`)
732
+ for (const job of value.jobs || []) output.write(`${job.id}\t${job.status}\t${job.tool}\t${job.createdAt}\n`)
567
733
  return
568
734
  }
569
735
  if (kind === 'computers') {
570
736
  for (const computer of value.computers || []) {
571
- process.stdout.write(`${computer.id}\t${computer.status || '-'}\t${computer.isDefault ? 'default' : '-'}\t${computer.name}\n`)
737
+ output.write(`${computer.id}\t${computer.status || '-'}\t${computer.isDefault ? 'default' : '-'}\t${computer.name}\n`)
572
738
  }
573
739
  return
574
740
  }
575
741
  if (kind === 'job') {
576
- process.stdout.write(`${value.id} ${value.status}\n`)
577
- if (value.message) process.stdout.write(`${value.message}\n`)
578
- if (value.error) process.stdout.write(`Error: ${value.error}\n`)
579
- if (value.result) process.stdout.write(`${JSON.stringify(value.result, null, 2)}\n`)
742
+ output.write(`${value.id} ${value.status}\n`)
743
+ if (value.message) output.write(`${value.message}\n`)
744
+ if (value.error) output.write(`Error: ${value.error}\n`)
745
+ if (value.result) output.write(`${JSON.stringify(value.result, null, 2)}\n`)
580
746
  if (!['completed', 'failed', 'cancelled'].includes(value.status)) {
581
- process.stdout.write(`Run: giz job wait ${value.id}\n`)
747
+ output.write(`Run: giz job wait ${value.id}\n`)
582
748
  }
583
749
  return
584
750
  }
585
- process.stdout.write(`${JSON.stringify(value, null, 2)}\n`)
751
+ output.write(`${JSON.stringify(value, null, 2)}\n`)
586
752
  }
587
753
 
588
754
  function help(command) {
755
+ if (command === 'api-key') return `Usage
756
+ giz api-key list
757
+ giz api-key create "My app" [--scopes giz.inference,giz.read] [--expires ISO_TIMESTAMP]
758
+ giz api-key revoke KEY_ID
759
+
760
+ The secret is shown only when the key is created.`
761
+ if (command === 'chat') return `Usage
762
+ giz chat MODEL "your prompt"
763
+ giz chat MODEL --input '{"messages":[{"role":"user","content":"Hello"}]}'`
589
764
  if (command === 'model') return `Usage
590
765
  giz model list [--type TYPE] [--search TEXT]
591
766
  giz model get MODEL
@@ -612,19 +787,40 @@ Options
612
787
  if (command === 'computer') return `Usage
613
788
  giz computer list
614
789
  giz computer get|start|stop [COMPUTER]
615
- giz computer exec [--computer COMPUTER] [--cwd PATH] -- COMMAND [ARGS...]`
790
+ giz computer exec [--computer COMPUTER] [--cwd PATH] [--deadline-ms MS] [--stdin TEXT|--stdin-file PATH] -- COMMAND [ARGS...]`
791
+ if (command === 'site') return `Usage
792
+ giz site list [--computer COMPUTER]
793
+ giz site create [--computer COMPUTER] --input JSON
794
+ giz site update SITE [--computer COMPUTER] --input JSON
795
+ giz site verify SITE [--computer COMPUTER]
796
+ giz site delete SITE [--computer COMPUTER]`
797
+ if (command === 'capability') return `Usage
798
+ giz capability list [--computer COMPUTER]
799
+ giz capability search skill|mcp QUERY
800
+ giz capability install skill|mcp ID --query QUERY [--computer COMPUTER]
801
+ giz capability update|remove ID [--computer COMPUTER]`
802
+ if (command === 'agent') return `Usage
803
+ giz agent start [--computer COMPUTER] --input JSON
804
+ giz agent status TURN [--wait]
805
+ giz agent cancel TURN`
616
806
  if (command === 'mcp') return `Usage
617
807
  giz mcp url
618
- giz mcp add codex
619
- giz mcp add claude`
808
+ giz mcp serve
809
+ giz mcp check --json
810
+ giz mcp add codex|claude [--transport stdio|http]
811
+
812
+ stdio (default) shares giz auth login and refreshes tokens automatically.
813
+ http uses the client's own OAuth login. Install the chosen client first.`
620
814
  if (command === 'auth') return `Usage
621
815
  giz auth login
622
816
  giz auth status
623
817
  giz auth logout`
624
- return `Giz CLI — non-LLM models, Drive, and Computer from any terminal
818
+ return `Giz CLI — AI models, Drive, and Computer from any terminal
625
819
 
626
820
  Usage
627
821
  giz auth login
822
+ giz api-key list|create|revoke
823
+ giz chat MODEL "your prompt"
628
824
  giz model list [--type TYPE] [--search TEXT]
629
825
  giz model get MODEL
630
826
  giz model run MODEL --input JSON [--upload FIELD=LOCAL_PATH]
@@ -633,19 +829,14 @@ Usage
633
829
  giz drive list|search|read|write|mkdir|move|delete
634
830
  giz upload LOCAL_PATH
635
831
  giz computer list|get|start|stop|exec
636
- giz mcp add codex|claude
832
+ giz site list|create|update|verify|delete
833
+ giz capability list|search|install|update|remove
834
+ giz agent start|status|cancel
835
+ giz mcp url|serve|check|add
637
836
  giz skill install
638
837
  giz invoke TOOL --input JSON
639
838
 
640
- Examples
641
- giz auth login
642
- giz generate image "A paper observatory at blue hour"
643
- giz model list --type video-generation
644
- giz model run ltx2-distilled --input '{"prompt":"A slow dolly in"}' --upload image=./first-frame.png
645
- giz generate talking-head "Welcome to Giz" --image ./face.png
646
- giz drive write notes/brief.md --file ./brief.md
647
- giz computer exec --computer ctr_123 -- python --version
648
- giz mcp add codex
839
+ Use giz COMMAND --help for parameters and examples.
649
840
 
650
841
  Global options
651
842
  --json Machine-readable JSON output
@@ -660,22 +851,16 @@ async function main() {
660
851
  const saved = await readConfig()
661
852
  const baseUrl = String(options['base-url'] || process.env.GIZ_URL || saved?.baseUrl || DEFAULT_URL).replace(/\/+$/, '')
662
853
  const output = await runCommand(baseUrl, positionals, options)
663
- print(output.kind, output.value, options.json === true)
854
+ if (output) {
855
+ print(output.kind, output.value, options.json === true)
856
+ process.exitCode = output.exitCode ?? (output.value?.isError ? 1 : 0)
857
+ }
664
858
  }
665
859
 
666
860
  main().catch((error) => {
667
861
  const { options } = parse(process.argv.slice(2))
668
- if (options.json === true) {
669
- process.stderr.write(`${JSON.stringify({
670
- error: {
671
- message: String(error?.message || error),
672
- ...(error?.hint ? { hint: error.hint } : {}),
673
- ...(error?.status ? { status: error.status } : {})
674
- }
675
- }, null, 2)}\n`)
676
- } else {
677
- process.stderr.write(`Error: ${String(error?.message || error)}\n`)
678
- if (error?.hint) process.stderr.write(`Hint: ${error.hint}\n`)
679
- }
862
+ const json = options.json === true
863
+ const failure = { message: String(error?.message || error), hint: error?.hint, status: error?.status }
864
+ print('message', json ? { error: failure } : `Error: ${failure.message}${failure.hint ? `\nHint: ${failure.hint}` : ''}`, json, process.stderr)
680
865
  process.exitCode = 1
681
866
  })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gizai/cli",
3
- "version": "1.0.0",
4
- "description": "Giz CLI, MCP installer, and agent Skills for non-LLM models, Drive, and Computer",
3
+ "version": "1.1.0",
4
+ "description": "Giz CLI, MCP installer, OpenAI-compatible models, Drive, and Computer",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "giz": "cli/giz.mjs"
@@ -12,7 +12,11 @@
12
12
  "README.md"
13
13
  ],
14
14
  "engines": {
15
- "node": ">=20"
15
+ "node": ">=20.12"
16
+ },
17
+ "dependencies": {
18
+ "@modelcontextprotocol/sdk": "1.29.0",
19
+ "proper-lockfile": "4.1.2"
16
20
  },
17
21
  "keywords": [
18
22
  "giz",
@@ -27,13 +31,13 @@
27
31
  "homepage": "https://www.giz.ai/mcp/",
28
32
  "repository": {
29
33
  "type": "git",
30
- "url": "https://github.com/exinno/giz.git",
34
+ "url": "git+https://github.com/exinno/giz.git",
31
35
  "directory": "apps/space/external"
32
36
  },
33
37
  "publishConfig": {
34
38
  "access": "public"
35
39
  },
36
40
  "scripts": {
37
- "check": "node --check cli/giz.mjs"
41
+ "check": "node --check cli/giz.mjs && node --test test/*.test.mjs"
38
42
  }
39
43
  }
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: giz
3
- description: Use Giz non-LLM image, video, audio, music, speech, Drive, and Computer capabilities from an AI agent. Trigger when the user asks to generate or transform media with Giz, inspect or modify Giz Drive files, operate a Giz Computer or terminal, or discover live Giz models and their parameters.
3
+ description: Use Giz text, image, video, audio, speech, embedding, reranking, Drive, and Computer capabilities from an AI agent. Trigger when the user asks to run an AI model through Giz, generate or transform media, embed or rerank content, inspect or modify Giz Drive files, operate a Giz Computer or terminal, or discover live Giz models and their parameters.
4
4
  ---
5
5
 
6
6
  # Giz
@@ -11,10 +11,10 @@ Use the `giz` CLI or the configured Giz MCP server. Prefer MCP when available an
11
11
 
12
12
  1. Run `model_list` before selecting an unfamiliar media model.
13
13
  2. Run `model_get` and treat its live input schema as the source of truth.
14
- 3. Call `model_run` with the exact model ID and supported inputs. For simple prompt-only work, use `generate_image`, `generate_video`, `generate_audio`, `generate_music`, or `generate_talking_head`.
14
+ 3. Call `model_run` with the exact model ID and supported inputs.
15
15
  4. Generation returns a durable job. Poll `job_get`; do not resubmit while it is queued or processing.
16
16
  5. Preserve completed Drive URLs and paths. Use Drive tools to inspect or organize results.
17
- 6. Use Computer tools only when the task requires a shell or runtime. Reuse one terminal `sessionId` per conversation.
17
+ 6. Use `giz_computer_exec` for commands; it runs outside persistent terminals and has a bounded deadline. Use `terminal_list`, `terminal_observe`, and `terminal_send` only to inspect or interact with an existing visible session. Pass a known exact Computer name, slug, or ID directly as `computer`; use `computer_list` only when the target is unknown or ambiguous.
18
18
  7. Ask for explicit confirmation immediately before destructive Drive removal, Computer runtime changes, or commands with material side effects.
19
19
 
20
20
  ## CLI
@@ -33,6 +33,12 @@ giz model get MODEL_ID
33
33
  giz model run MODEL_ID --input '{"prompt":"..."}'
34
34
  ```
35
35
 
36
+ Use the OpenAI-compatible chat boundary:
37
+
38
+ ```bash
39
+ giz chat gpt-5-6-luna "..."
40
+ ```
41
+
36
42
  Upload local media into an exact model field:
37
43
 
38
44
  ```bash
@@ -52,7 +58,6 @@ Pass `--json` for machine output. Use `GIZ_TOKEN` only in ephemeral automation.
52
58
  ## Boundaries
53
59
 
54
60
  - Do not guess model ids, parameter names, enum values, or costs.
55
- - Do not use text-generation models; this integration intentionally exposes non-LLM models only.
56
61
  - Do not turn argv arrays into shell strings.
57
62
  - Do not read binary Drive content unless the task needs it; prefer returned URLs and metadata.
58
63
  - Do not retry failed generations blindly. Report the provider or contract error.
@@ -4,13 +4,13 @@
4
4
  |---|---|
5
5
  | Discover non-LLM models and fields | `model_list`, `model_get` |
6
6
  | Run any live non-LLM model | `model_run` |
7
- | Generate common media with automatic routing | `generate_image`, `generate_video`, `generate_audio`, `generate_music`, `generate_talking_head` |
8
7
  | Track durable generation | `job_list`, `job_get`, `job_cancel` |
9
- | List, search, inspect, or read Drive | `drive_list`, `drive_search`, `drive_getMetadata`, `drive_readFile` |
10
- | Create or change Drive content | `drive_writeFile`, `drive_makeDirectory`, `drive_movePath`, `drive_importUrl`, `drive_createNote`, `drive_editNote` |
8
+ | List, search, or read Drive | `drive_list`, `drive_search`, `drive_readFile` |
9
+ | Create or change Drive content | `drive_writeFile`, `drive_makeDirectory`, `drive_movePath`, `drive_importUrl` |
11
10
  | Move Drive content to Trash | `drive_deletePath` |
12
- | Discover and control Computers | `computer_list`, `computer_get`, `computer_start`, `computer_stop` |
13
- | Execute or open a terminal | `container_exec`, `terminal_open` |
11
+ | Discover and control Computers | `computer_list`, `computer_start`, `computer_stop` |
12
+ | Run a Computer command | `giz_computer_exec` |
13
+ | Find, inspect, or interact with an existing terminal | `terminal_list`, `terminal_observe`, `terminal_send` |
14
14
  | Inspect or transform media | `media_probe`, `media_process` |
15
15
 
16
- Always prefer the live `tools/list` result over this map. Streamable HTTP MCP is `/api/mcp`; ChatGPT OpenAPI is `/api/openapi.json`; the authenticated HTTP manifest is `/api/tools`.
16
+ Always prefer the live `tools/list` result over this map. Streamable HTTP MCP is `/api/mcp`; ChatGPT OpenAPI is `/api/openapi.json`; the authenticated HTTP manifest is `/api/tools`. `giz_computer_exec` accepts an exact Computer name, slug, or ID as `computer`, argv `command`, optional `stdin`, `cwd`, and `deadlineMs`; do not send `timeoutMs`, `yieldMs`, `sessionId`, or shell command strings. Omit `computer` for the default Computer and use `computer_list` only when the target is unknown or ambiguous.