@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 +46 -4
- package/cli/giz.mjs +418 -234
- package/package.json +8 -4
- 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,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 {
|
|
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
|
|
69
|
-
await mkdir(path.dirname(configPath), { recursive: true })
|
|
70
|
-
await
|
|
71
|
-
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
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 || '
|
|
117
|
+
payload.error_description || payload.error || 'OAuth token exchange failed.',
|
|
105
118
|
'Run: giz auth login'
|
|
106
119
|
)
|
|
107
120
|
}
|
|
108
|
-
|
|
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,
|
|
130
|
+
async function credentials(baseUrl, rejectedToken) {
|
|
119
131
|
if (process.env.GIZ_TOKEN) return { access_token: process.env.GIZ_TOKEN, baseUrl }
|
|
120
|
-
|
|
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
|
-
|
|
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
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
|
|
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 (!
|
|
158
|
-
const
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
const message = payload?.message || payload?.error?.message || `Giz API returned HTTP ${response.status}.`
|
|
159
182
|
throw new CliError(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
|
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
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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 { ...
|
|
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:
|
|
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.
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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(
|
|
261
|
-
}
|
|
262
|
-
const
|
|
263
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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',
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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 ? {
|
|
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 ? {
|
|
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
|
|
491
|
-
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 } : {}) }
|
|
492
534
|
if (subcommand === 'start' || subcommand === 'stop') {
|
|
493
535
|
return {
|
|
494
536
|
kind: 'result',
|
|
495
|
-
value: await invoke(baseUrl, `computer.${subcommand}`,
|
|
537
|
+
value: await invoke(baseUrl, `computer.${subcommand}`, computer ? { computer } : {})
|
|
496
538
|
}
|
|
497
539
|
}
|
|
498
540
|
if (subcommand === 'exec') {
|
|
499
|
-
|
|
500
|
-
|
|
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, '
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
...(
|
|
567
|
+
value: await invoke(baseUrl, 'site.configure', {
|
|
568
|
+
...input,
|
|
569
|
+
...computer,
|
|
570
|
+
operation: subcommand,
|
|
571
|
+
...(siteId ? { siteId } : {})
|
|
508
572
|
})
|
|
509
573
|
}
|
|
510
574
|
}
|
|
511
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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, '--
|
|
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(
|
|
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
|
-
|
|
718
|
+
output.write(`${JSON.stringify(value, null, 2)}\n`)
|
|
554
719
|
return
|
|
555
720
|
}
|
|
556
721
|
if (kind === 'help' || kind === 'message') {
|
|
557
|
-
|
|
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)
|
|
563
|
-
|
|
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 || [])
|
|
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
|
-
|
|
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
|
-
|
|
578
|
-
if (value.message)
|
|
579
|
-
if (value.error)
|
|
580
|
-
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`)
|
|
581
746
|
if (!['completed', 'failed', 'cancelled'].includes(value.status)) {
|
|
582
|
-
|
|
747
|
+
output.write(`Run: giz job wait ${value.id}\n`)
|
|
583
748
|
}
|
|
584
749
|
return
|
|
585
750
|
}
|
|
586
|
-
|
|
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
|
|
620
|
-
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.`
|
|
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 —
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
670
|
-
|
|
671
|
-
|
|
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
|
|
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",
|
|
@@ -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
|
}
|
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.
|