@gizai/cli 1.0.1 → 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,22 +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'
6
7
  import { createRequire } from 'node:module'
7
8
  import os from 'node:os'
8
9
  import path from 'node:path'
9
10
  import { spawn, spawnSync } from 'node:child_process'
10
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'
11
16
 
12
17
  const { version: CLI_VERSION } = createRequire(import.meta.url)('../package.json')
13
18
  const DEFAULT_URL = 'https://www.giz.ai'
14
- const CALLBACK_URL = 'http://127.0.0.1:53682/callback'
15
19
  const CLIENT_ID = 'giz-cli'
16
- 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']
17
21
  const BOOLEAN_OPTIONS = new Set(['help', 'json', 'no-open', 'version'])
18
- const configPath = process.env.GIZ_CONFIG ||
19
- 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'))
20
24
 
21
25
  class CliError extends Error {
22
26
  constructor(message, hint, status) {
@@ -65,14 +69,27 @@ async function readConfig() {
65
69
  }
66
70
  }
67
71
 
68
- async function saveConfig(config) {
69
- await mkdir(path.dirname(configPath), { recursive: true })
70
- await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 })
71
- 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
+ }
72
89
  }
73
90
 
74
91
  async function discovery(baseUrl) {
75
- 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) })
76
93
  if (!response.ok) {
77
94
  throw new CliError(
78
95
  `OAuth discovery failed: HTTP ${response.status}.`,
@@ -82,42 +99,37 @@ async function discovery(baseUrl) {
82
99
  return await response.json()
83
100
  }
84
101
 
85
- async function refresh(config) {
86
- if (!config?.refresh_token) {
87
- throw new CliError('Not authenticated.', 'Run: giz auth login')
88
- }
89
- const metadata = await discovery(config.baseUrl)
102
+ async function exchangeToken(baseUrl, metadata, grant, config = {}) {
90
103
  const body = new URLSearchParams({
91
- grant_type: 'refresh_token',
92
104
  client_id: CLIENT_ID,
93
- refresh_token: config.refresh_token,
94
- resource: new URL('/api/mcp', config.baseUrl).href
105
+ resource: new URL('/api/mcp', baseUrl).href,
106
+ ...grant
95
107
  })
96
108
  const response = await fetch(metadata.token_endpoint, {
97
109
  method: 'POST',
98
110
  headers: { 'content-type': 'application/x-www-form-urlencoded' },
99
- body
111
+ body,
112
+ signal: AbortSignal.timeout(30_000)
100
113
  })
101
114
  const payload = await response.json()
102
115
  if (!response.ok) {
103
116
  throw new CliError(
104
- payload.error_description || payload.error || 'Giz session could not be refreshed.',
117
+ payload.error_description || payload.error || 'OAuth token exchange failed.',
105
118
  'Run: giz auth login'
106
119
  )
107
120
  }
108
- const next = {
121
+ return {
109
122
  ...config,
123
+ baseUrl,
110
124
  ...payload,
111
125
  refresh_token: payload.refresh_token || config.refresh_token,
112
126
  expires_at: Date.now() + Number(payload.expires_in || 3600) * 1000
113
127
  }
114
- await saveConfig(next)
115
- return next
116
128
  }
117
129
 
118
- async function credentials(baseUrl, forceRefresh = false) {
130
+ async function credentials(baseUrl, rejectedToken) {
119
131
  if (process.env.GIZ_TOKEN) return { access_token: process.env.GIZ_TOKEN, baseUrl }
120
- let config = await readConfig()
132
+ const config = await readConfig()
121
133
  if (!config?.access_token) throw new CliError('Not authenticated.', 'Run: giz auth login')
122
134
  if (String(config.baseUrl).replace(/\/+$/, '') !== baseUrl) {
123
135
  throw new CliError(
@@ -125,87 +137,80 @@ async function credentials(baseUrl, forceRefresh = false) {
125
137
  `Run: giz auth login --base-url ${baseUrl}`
126
138
  )
127
139
  }
128
- 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
+ }
129
153
  return config
130
154
  }
131
155
 
132
- async function api(baseUrl, pathname, data, options = {}) {
133
- const run = async (forceRefresh = false) => {
134
- const auth = await credentials(baseUrl, forceRefresh)
135
- const response = await fetch(new URL(pathname, baseUrl), {
136
- method: data === undefined ? 'GET' : 'POST',
137
- headers: {
138
- accept: 'application/json',
139
- ...(data === undefined ? {} : { 'content-type': 'application/json' }),
140
- authorization: `Bearer ${auth.access_token}`
141
- },
142
- ...(data === undefined ? {} : { body: JSON.stringify(data) })
143
- })
144
- const text = await response.text()
145
- let payload
146
- try {
147
- payload = text ? JSON.parse(text) : null
148
- } catch {
149
- payload = { message: text.slice(0, 500) }
150
- }
151
- return { response, payload }
152
- }
153
- let result = await run()
154
- if (result.response.status === 401 && !process.env.GIZ_TOKEN && !options.refreshed) {
155
- 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) }
156
179
  }
157
- if (!result.response.ok) {
158
- 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}.`
159
182
  throw new CliError(
160
- result.payload?.message || `Giz API returned HTTP ${result.response.status}.`,
161
- required?.includes('scope') ? 'Run: giz auth login' : 'Run with --json for the complete error.',
162
- 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
163
188
  )
164
189
  }
165
- 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)
166
200
  }
167
201
 
168
202
  async function uploadMedia(baseUrl, localFile) {
169
203
  const filePath = path.resolve(String(localFile))
170
- const run = async (forceRefresh = false) => {
171
- const auth = await credentials(baseUrl, forceRefresh)
172
- let file
173
- try {
174
- file = await openAsBlob(filePath)
175
- } catch (error) {
176
- throw new CliError(`Could not open ${filePath}.`, String(error?.message || error))
177
- }
178
- const body = new FormData()
179
- body.append('file', file, path.basename(filePath))
180
- const response = await fetch(new URL('/api/storage/media', baseUrl), {
181
- method: 'POST',
182
- headers: {
183
- accept: 'application/json',
184
- authorization: `Bearer ${auth.access_token}`
185
- },
186
- body
187
- })
188
- const text = await response.text()
189
- let payload
190
- try {
191
- payload = text ? JSON.parse(text) : null
192
- } catch {
193
- payload = { message: text.slice(0, 500) }
194
- }
195
- return { response, payload }
196
- }
197
- let result = await run()
198
- if (result.response.status === 401 && !process.env.GIZ_TOKEN) result = await run(true)
199
- if (!result.response.ok) {
200
- throw new CliError(
201
- result.payload?.message || `Giz upload returned HTTP ${result.response.status}.`,
202
- 'Check the file and your Giz storage access.'
203
- )
204
- }
205
- const url = String(result.payload?.url || '').trim()
206
- 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()
207
212
  if (!url || !drivePath) throw new CliError('Giz upload returned an incomplete file reference.', 'Try the upload again.')
208
- return { ...result.payload, url: new URL(url, baseUrl).href }
213
+ return { ...payload, url: new URL(url, baseUrl).href }
209
214
  }
210
215
 
211
216
  async function login(baseUrl, options) {
@@ -213,12 +218,12 @@ async function login(baseUrl, options) {
213
218
  const verifier = randomBytes(48).toString('base64url')
214
219
  const challenge = createHash('sha256').update(verifier).digest('base64url')
215
220
  const state = randomBytes(24).toString('base64url')
216
- const callback = waitForCallback(state)
221
+ const callback = await waitForCallback(state)
217
222
  const authorization = new URL(metadata.authorization_endpoint)
218
223
  for (const [name, value] of Object.entries({
219
224
  response_type: 'code',
220
225
  client_id: CLIENT_ID,
221
- redirect_uri: CALLBACK_URL,
226
+ redirect_uri: callback.url,
222
227
  scope: SCOPES.join(' '),
223
228
  resource: new URL('/api/mcp', baseUrl).href,
224
229
  code_challenge: challenge,
@@ -226,76 +231,51 @@ async function login(baseUrl, options) {
226
231
  prompt: 'consent',
227
232
  state
228
233
  })) authorization.searchParams.set(name, value)
229
- process.stdout.write(`Opening Giz sign-in:\n${authorization}\n`)
234
+ process.stderr.write(`Opening Giz sign-in:\n${authorization}\n`)
230
235
  if (options.open !== 'false' && options['no-open'] !== true) openBrowser(authorization.href)
231
- const code = await callback
232
- const response = await fetch(metadata.token_endpoint, {
233
- method: 'POST',
234
- headers: { 'content-type': 'application/x-www-form-urlencoded' },
235
- body: new URLSearchParams({
236
- grant_type: 'authorization_code',
237
- client_id: CLIENT_ID,
238
- redirect_uri: CALLBACK_URL,
239
- code,
240
- code_verifier: verifier,
241
- resource: new URL('/api/mcp', baseUrl).href
242
- })
243
- })
244
- const payload = await response.json()
245
- if (!response.ok) {
246
- throw new CliError(payload.error_description || payload.error || 'OAuth token exchange failed.', 'Run: giz auth login')
247
- }
248
- await saveConfig({
249
- baseUrl,
250
- ...payload,
251
- expires_at: Date.now() + Number(payload.expires_in || 3600) * 1000
252
- })
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
+ }))
253
240
  return await api(baseUrl, '/api/external/me')
254
241
  }
255
242
 
256
- function waitForCallback(expectedState) {
257
- return new Promise((resolve, reject) => {
258
- 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)
259
251
  server.close()
260
- reject(new CliError('Giz sign-in timed out.', 'Run: giz auth login'))
261
- }, 5 * 60 * 1000)
262
- const server = createServer((request, response) => {
263
- 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)
264
257
  if (url.pathname !== '/callback') {
265
258
  response.writeHead(404).end('Not found')
266
259
  return
267
260
  }
268
- const finish = (error, code) => {
269
- clearTimeout(timer)
270
- response.setHeader('content-type', 'text/html; charset=utf-8')
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
- server.close()
275
- error ? reject(error) : resolve(code)
276
- }
277
261
  if (url.searchParams.get('state') !== expectedState) {
278
- finish(new CliError('OAuth state did not match.', 'Run: giz auth login'))
279
- } else if (url.searchParams.get('error')) {
280
- finish(new CliError(
281
- url.searchParams.get('error_description') || url.searchParams.get('error'),
282
- 'Run: giz auth login'
283
- ))
284
- } else if (!url.searchParams.get('code')) {
285
- finish(new CliError('OAuth callback did not include a code.', 'Run: giz auth login'))
286
- } else {
287
- 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
288
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)
289
275
  })
290
- server.on('error', (error) => {
291
- clearTimeout(timer)
292
- reject(new CliError(
293
- `Could not listen on ${CALLBACK_URL}: ${error.message}`,
294
- 'Close the process using port 53682 and run: giz auth login'
295
- ))
296
- })
297
- server.listen(53682, '127.0.0.1')
276
+ server.on('error', finish)
298
277
  })
278
+ return { url: callbackUrl, code }
299
279
  }
300
280
 
301
281
  function openBrowser(url) {
@@ -344,6 +324,33 @@ async function invoke(baseUrl, tool, input) {
344
324
  return await api(baseUrl, `/api/tools/${encodeURIComponent(tool)}`, input)
345
325
  }
346
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
+
347
354
  async function runCommand(baseUrl, positionals, options) {
348
355
  const [command, subcommand, ...rest] = positionals
349
356
  if (options.version === true) return { kind: 'message', value: CLI_VERSION }
@@ -355,24 +362,43 @@ async function runCommand(baseUrl, positionals, options) {
355
362
  return { kind: 'auth', value: await api(baseUrl, '/api/external/me') }
356
363
  }
357
364
  if (command === 'auth' && subcommand === 'logout') {
358
- const config = await readConfig()
359
- if (config?.refresh_token) {
360
- const metadata = await discovery(baseUrl)
361
- await fetch(metadata.revocation_endpoint, {
362
- method: 'POST',
363
- headers: { 'content-type': 'application/x-www-form-urlencoded' },
364
- body: new URLSearchParams({
365
- token: config.refresh_token,
366
- token_type_hint: 'refresh_token',
367
- 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 })
368
372
  })
369
- })
370
- }
371
- await unlink(configPath).catch((error) => {
372
- 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
+ }
373
375
  })
374
376
  return { kind: 'message', value: 'Signed out of Giz on this device.' }
375
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
+ }
376
402
 
377
403
  if (command === 'model' && subcommand === 'list') {
378
404
  return {
@@ -395,6 +421,22 @@ async function runCommand(baseUrl, positionals, options) {
395
421
  if (command === 'models') {
396
422
  throw new CliError('Unknown command "models".', 'Did you mean: giz model list')
397
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
+ }
398
440
 
399
441
  if (command === 'generate' && ['image', 'video', 'audio', 'music', 'talking-head'].includes(subcommand)) {
400
442
  const tool = {
@@ -433,7 +475,7 @@ async function runCommand(baseUrl, positionals, options) {
433
475
  const id = required(rest[0], 'JOB', 'Run: giz job wait JOB')
434
476
  while (true) {
435
477
  const job = await invoke(baseUrl, 'job.get', { job: id })
436
- 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 }
437
479
  if (process.stderr.isTTY) process.stderr.write(`\r${job.id} ${job.status} ${job.message || ''}`.slice(0, 120))
438
480
  await new Promise((resolve) => setTimeout(resolve, 1000))
439
481
  }
@@ -467,7 +509,7 @@ async function runCommand(baseUrl, positionals, options) {
467
509
  path: drivePath,
468
510
  content: String(content),
469
511
  encoding,
470
- ...(options.computer ? { computerId: options.computer } : {})
512
+ ...(options.computer ? { computer: options.computer } : {})
471
513
  })
472
514
  }
473
515
  }
@@ -476,7 +518,7 @@ async function runCommand(baseUrl, positionals, options) {
476
518
  const [tool, input] = mapping()
477
519
  return {
478
520
  kind: 'result',
479
- value: await invoke(baseUrl, tool, { ...input, ...(options.computer ? { computerId: options.computer } : {}) })
521
+ value: await invoke(baseUrl, tool, { ...input, ...(options.computer ? { computer: options.computer } : {}) })
480
522
  }
481
523
  }
482
524
 
@@ -487,28 +529,133 @@ async function runCommand(baseUrl, positionals, options) {
487
529
 
488
530
  if (command === 'computer') {
489
531
  if (subcommand === 'list') return { kind: 'computers', value: await invoke(baseUrl, 'computer.list', {}) }
490
- const computerId = rest[0] || options.computer
491
- 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 } : {}) }
492
534
  if (subcommand === 'start' || subcommand === 'stop') {
493
535
  return {
494
536
  kind: 'result',
495
- value: await invoke(baseUrl, `computer.${subcommand}`, computerId ? { computerId } : {})
537
+ value: await invoke(baseUrl, `computer.${subcommand}`, computer ? { computer } : {})
496
538
  }
497
539
  }
498
540
  if (subcommand === 'exec') {
499
- const commandArgs = rest
500
- 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
501
565
  return {
502
566
  kind: 'result',
503
- value: await invoke(baseUrl, 'container.exec', {
504
- command: commandArgs,
505
- sessionId: String(options.session || 'giz-cli'),
506
- ...(options.cwd ? { cwd: options.cwd } : {}),
507
- ...(options.computer ? { computerId: options.computer } : {})
567
+ value: await invoke(baseUrl, 'site.configure', {
568
+ ...input,
569
+ ...computer,
570
+ operation: subcommand,
571
+ ...(siteId ? { siteId } : {})
508
572
  })
509
573
  }
510
574
  }
511
- 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')
512
659
  }
513
660
 
514
661
  if (command === 'invoke') {
@@ -519,74 +666,101 @@ async function runCommand(baseUrl, positionals, options) {
519
666
  if (command === 'mcp' && subcommand === 'url') {
520
667
  return { kind: 'message', value: new URL('/api/mcp', baseUrl).href }
521
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
+ }
522
682
  if (command === 'mcp' && subcommand === 'add') {
523
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.')
524
687
  const endpoint = new URL('/api/mcp', baseUrl).href
525
- if (client === 'codex') {
526
- runExternal('codex', ['mcp', 'add', 'giz', '--url', endpoint, '--oauth-resource', endpoint], 'Install Codex CLI first.')
527
- return { kind: 'message', value: 'Giz was added to Codex. Complete the browser approval opened by Codex.' }
528
- }
529
- if (client === 'claude') {
530
- runExternal('claude', ['mcp', 'add', '--transport', 'http', '--scope', 'user', 'giz', endpoint], 'Install Claude Code first.')
531
- return { kind: 'message', value: 'Giz was added to Claude Code. Open Claude Code and run /mcp to sign in.' }
532
- }
533
- 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.'}` }
534
696
  }
535
697
 
536
698
  if (command === 'skill' && subcommand === 'install') {
537
699
  const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'skills')
538
- 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.')
539
701
  return { kind: 'message', value: 'Giz Skill installed for available AI agents.' }
540
702
  }
541
703
 
542
704
  throw new CliError(`Unknown command "${[command, subcommand].filter(Boolean).join(' ')}".`, 'Run: giz --help')
543
705
  }
544
706
 
545
- function runExternal(command, args, hint) {
546
- 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'] })
547
709
  if (result.error?.code === 'ENOENT') throw new CliError(`${command} is not installed.`, hint)
548
- 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)
549
714
  }
550
715
 
551
- function print(kind, value, json) {
716
+ function print(kind, value, json, output = process.stdout) {
552
717
  if (json) {
553
- process.stdout.write(`${JSON.stringify(value, null, 2)}\n`)
718
+ output.write(`${JSON.stringify(value, null, 2)}\n`)
554
719
  return
555
720
  }
556
721
  if (kind === 'help' || kind === 'message') {
557
- process.stdout.write(`${value}\n`)
722
+ output.write(`${value}\n`)
558
723
  return
559
724
  }
560
725
  if (kind === 'models') {
561
726
  const rows = value.models || []
562
- for (const model of rows) process.stdout.write(`${model.id}\t${model.type}\t${model.provider || '-'}\t${model.label}\n`)
563
- 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`)
564
729
  return
565
730
  }
566
731
  if (kind === 'jobs') {
567
- 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`)
568
733
  return
569
734
  }
570
735
  if (kind === 'computers') {
571
736
  for (const computer of value.computers || []) {
572
- 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`)
573
738
  }
574
739
  return
575
740
  }
576
741
  if (kind === 'job') {
577
- process.stdout.write(`${value.id} ${value.status}\n`)
578
- if (value.message) process.stdout.write(`${value.message}\n`)
579
- if (value.error) process.stdout.write(`Error: ${value.error}\n`)
580
- 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`)
581
746
  if (!['completed', 'failed', 'cancelled'].includes(value.status)) {
582
- process.stdout.write(`Run: giz job wait ${value.id}\n`)
747
+ output.write(`Run: giz job wait ${value.id}\n`)
583
748
  }
584
749
  return
585
750
  }
586
- process.stdout.write(`${JSON.stringify(value, null, 2)}\n`)
751
+ output.write(`${JSON.stringify(value, null, 2)}\n`)
587
752
  }
588
753
 
589
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"}]}'`
590
764
  if (command === 'model') return `Usage
591
765
  giz model list [--type TYPE] [--search TEXT]
592
766
  giz model get MODEL
@@ -613,19 +787,40 @@ Options
613
787
  if (command === 'computer') return `Usage
614
788
  giz computer list
615
789
  giz computer get|start|stop [COMPUTER]
616
- 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`
617
806
  if (command === 'mcp') return `Usage
618
807
  giz mcp url
619
- giz mcp add codex
620
- 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.`
621
814
  if (command === 'auth') return `Usage
622
815
  giz auth login
623
816
  giz auth status
624
817
  giz auth logout`
625
- return `Giz CLI — non-LLM models, Drive, and Computer from any terminal
818
+ return `Giz CLI — AI models, Drive, and Computer from any terminal
626
819
 
627
820
  Usage
628
821
  giz auth login
822
+ giz api-key list|create|revoke
823
+ giz chat MODEL "your prompt"
629
824
  giz model list [--type TYPE] [--search TEXT]
630
825
  giz model get MODEL
631
826
  giz model run MODEL --input JSON [--upload FIELD=LOCAL_PATH]
@@ -634,19 +829,14 @@ Usage
634
829
  giz drive list|search|read|write|mkdir|move|delete
635
830
  giz upload LOCAL_PATH
636
831
  giz computer list|get|start|stop|exec
637
- 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
638
836
  giz skill install
639
837
  giz invoke TOOL --input JSON
640
838
 
641
- Examples
642
- giz auth login
643
- giz generate image "A paper observatory at blue hour"
644
- giz model list --type video-generation
645
- giz model run ltx2-distilled --input '{"prompt":"A slow dolly in"}' --upload image=./first-frame.png
646
- giz generate talking-head "Welcome to Giz" --image ./face.png
647
- giz drive write notes/brief.md --file ./brief.md
648
- giz computer exec --computer ctr_123 -- python --version
649
- giz mcp add codex
839
+ Use giz COMMAND --help for parameters and examples.
650
840
 
651
841
  Global options
652
842
  --json Machine-readable JSON output
@@ -661,22 +851,16 @@ async function main() {
661
851
  const saved = await readConfig()
662
852
  const baseUrl = String(options['base-url'] || process.env.GIZ_URL || saved?.baseUrl || DEFAULT_URL).replace(/\/+$/, '')
663
853
  const output = await runCommand(baseUrl, positionals, options)
664
- 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
+ }
665
858
  }
666
859
 
667
860
  main().catch((error) => {
668
861
  const { options } = parse(process.argv.slice(2))
669
- if (options.json === true) {
670
- process.stderr.write(`${JSON.stringify({
671
- error: {
672
- message: String(error?.message || error),
673
- ...(error?.hint ? { hint: error.hint } : {}),
674
- ...(error?.status ? { status: error.status } : {})
675
- }
676
- }, null, 2)}\n`)
677
- } else {
678
- process.stderr.write(`Error: ${String(error?.message || error)}\n`)
679
- if (error?.hint) process.stderr.write(`Hint: ${error.hint}\n`)
680
- }
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)
681
865
  process.exitCode = 1
682
866
  })
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gizai/cli",
3
- "version": "1.0.1",
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",
@@ -34,6 +38,6 @@
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.