@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 +46 -4
- package/cli/giz.mjs +421 -236
- package/package.json +9 -5
- package/skills/giz/SKILL.md +9 -4
- package/skills/giz/references/operations.md +6 -6
package/README.md
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# Giz CLI
|
|
2
2
|
|
|
3
|
-
Connect once in the browser, then use
|
|
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
|
-
|
|
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
|
|
35
|
-
|
|
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 {
|
|
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
|
|
67
|
-
await mkdir(path.dirname(configPath), { recursive: true })
|
|
68
|
-
await
|
|
69
|
-
|
|
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
|
|
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
|
-
|
|
92
|
-
|
|
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 || '
|
|
117
|
+
payload.error_description || payload.error || 'OAuth token exchange failed.',
|
|
103
118
|
'Run: giz auth login'
|
|
104
119
|
)
|
|
105
120
|
}
|
|
106
|
-
|
|
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,
|
|
130
|
+
async function credentials(baseUrl, rejectedToken) {
|
|
117
131
|
if (process.env.GIZ_TOKEN) return { access_token: process.env.GIZ_TOKEN, baseUrl }
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
|
|
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 (!
|
|
156
|
-
const
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
const message = payload?.message || payload?.error?.message || `Giz API returned HTTP ${response.status}.`
|
|
157
182
|
throw new CliError(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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
|
|
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
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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 { ...
|
|
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:
|
|
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.
|
|
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
|
-
|
|
231
|
-
|
|
232
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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(
|
|
259
|
-
}
|
|
260
|
-
const
|
|
261
|
-
|
|
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
|
-
|
|
277
|
-
|
|
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',
|
|
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:
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
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 ? {
|
|
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 ? {
|
|
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
|
|
489
|
-
if (subcommand === 'get') return { kind: 'result', value: await invoke(baseUrl, 'computer.get',
|
|
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}`,
|
|
537
|
+
value: await invoke(baseUrl, `computer.${subcommand}`, computer ? { computer } : {})
|
|
494
538
|
}
|
|
495
539
|
}
|
|
496
540
|
if (subcommand === 'exec') {
|
|
497
|
-
|
|
498
|
-
|
|
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, '
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
...(
|
|
567
|
+
value: await invoke(baseUrl, 'site.configure', {
|
|
568
|
+
...input,
|
|
569
|
+
...computer,
|
|
570
|
+
operation: subcommand,
|
|
571
|
+
...(siteId ? { siteId } : {})
|
|
506
572
|
})
|
|
507
573
|
}
|
|
508
574
|
}
|
|
509
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
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, '--
|
|
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(
|
|
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
|
-
|
|
718
|
+
output.write(`${JSON.stringify(value, null, 2)}\n`)
|
|
553
719
|
return
|
|
554
720
|
}
|
|
555
721
|
if (kind === 'help' || kind === 'message') {
|
|
556
|
-
|
|
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)
|
|
562
|
-
|
|
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 || [])
|
|
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
|
-
|
|
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
|
-
|
|
577
|
-
if (value.message)
|
|
578
|
-
if (value.error)
|
|
579
|
-
if (value.result)
|
|
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
|
-
|
|
747
|
+
output.write(`Run: giz job wait ${value.id}\n`)
|
|
582
748
|
}
|
|
583
749
|
return
|
|
584
750
|
}
|
|
585
|
-
|
|
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
|
|
619
|
-
giz mcp
|
|
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 —
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
669
|
-
|
|
670
|
-
|
|
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.
|
|
4
|
-
"description": "Giz CLI, MCP installer,
|
|
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
|
}
|
package/skills/giz/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: giz
|
|
3
|
-
description: Use Giz
|
|
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.
|
|
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
|
|
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,
|
|
10
|
-
| Create or change Drive content | `drive_writeFile`, `drive_makeDirectory`, `drive_movePath`, `drive_importUrl
|
|
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`, `
|
|
13
|
-
|
|
|
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.
|