@gizai/cli 1.0.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 +37 -0
- package/cli/giz.mjs +681 -0
- package/package.json +39 -0
- package/skills/giz/SKILL.md +60 -0
- package/skills/giz/agents/openai.yaml +4 -0
- package/skills/giz/references/operations.md +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Giz CLI
|
|
2
|
+
|
|
3
|
+
Connect once in the browser, then use every live Giz non-LLM model, Drive, and Computer from a terminal or AI agent.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g @gizai/cli
|
|
7
|
+
giz auth login
|
|
8
|
+
giz generate image "A paper observatory at blue hour"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Discover a model's live inputs, upload local media, and run it:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
giz model list --type video-generation
|
|
15
|
+
giz model get ltx2-distilled
|
|
16
|
+
giz model run ltx2-distilled \
|
|
17
|
+
--input '{"prompt":"A slow camera push through morning fog"}' \
|
|
18
|
+
--upload image=./first-frame.png
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Remote MCP:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
giz mcp add codex
|
|
25
|
+
giz mcp add claude
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Agent Skill:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
giz skill install
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
The CLI stores rotating OAuth credentials in `~/.config/giz/credentials.json` with mode `0600`. It never asks for or stores a Giz password.
|
|
35
|
+
Set `GIZ_TOKEN` only for ephemeral automation. Run `giz --help` for Drive, Computer, job, upload, and raw tool commands.
|
|
36
|
+
|
|
37
|
+
Documentation: <https://www.giz.ai/mcp/>
|
package/cli/giz.mjs
ADDED
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash, randomBytes } from 'node:crypto'
|
|
3
|
+
import { openAsBlob } from 'node:fs'
|
|
4
|
+
import { chmod, mkdir, readFile, unlink, writeFile } from 'node:fs/promises'
|
|
5
|
+
import { createServer } from 'node:http'
|
|
6
|
+
import os from 'node:os'
|
|
7
|
+
import path from 'node:path'
|
|
8
|
+
import { spawn, spawnSync } from 'node:child_process'
|
|
9
|
+
import { fileURLToPath } from 'node:url'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_URL = 'https://www.giz.ai'
|
|
12
|
+
const CALLBACK_URL = 'http://127.0.0.1:53682/callback'
|
|
13
|
+
const CLIENT_ID = 'giz-cli'
|
|
14
|
+
const SCOPES = ['giz.read', 'giz.write', 'giz.execute', 'giz.destructive', 'offline_access']
|
|
15
|
+
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')
|
|
18
|
+
|
|
19
|
+
class CliError extends Error {
|
|
20
|
+
constructor(message, hint, status) {
|
|
21
|
+
super(message)
|
|
22
|
+
this.hint = hint
|
|
23
|
+
this.status = status
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parse(argv) {
|
|
28
|
+
const positionals = []
|
|
29
|
+
const options = {}
|
|
30
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
31
|
+
const value = argv[index]
|
|
32
|
+
if (value === '--') {
|
|
33
|
+
positionals.push(...argv.slice(index + 1))
|
|
34
|
+
break
|
|
35
|
+
}
|
|
36
|
+
if (!value.startsWith('--')) {
|
|
37
|
+
positionals.push(value)
|
|
38
|
+
continue
|
|
39
|
+
}
|
|
40
|
+
const [rawName, inline] = value.slice(2).split(/=(.*)/s, 2)
|
|
41
|
+
const next = argv[index + 1]
|
|
42
|
+
const parsed = inline !== undefined
|
|
43
|
+
? inline
|
|
44
|
+
: BOOLEAN_OPTIONS.has(rawName) || !next || next.startsWith('--')
|
|
45
|
+
? true
|
|
46
|
+
: next
|
|
47
|
+
if (rawName === 'upload') {
|
|
48
|
+
options.upload = [...(Array.isArray(options.upload) ? options.upload : []), parsed]
|
|
49
|
+
} else {
|
|
50
|
+
options[rawName] = parsed
|
|
51
|
+
}
|
|
52
|
+
if (inline === undefined && parsed !== true) index += 1
|
|
53
|
+
}
|
|
54
|
+
return { positionals, options }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function readConfig() {
|
|
58
|
+
try {
|
|
59
|
+
return JSON.parse(await readFile(configPath, 'utf8'))
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (error?.code === 'ENOENT') return undefined
|
|
62
|
+
throw new CliError('Could not read Giz credentials.', `Remove or repair ${configPath}.`)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function saveConfig(config) {
|
|
67
|
+
await mkdir(path.dirname(configPath), { recursive: true })
|
|
68
|
+
await writeFile(configPath, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600 })
|
|
69
|
+
await chmod(configPath, 0o600)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function discovery(baseUrl) {
|
|
73
|
+
const response = await fetch(new URL('/.well-known/oauth-authorization-server', baseUrl))
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new CliError(
|
|
76
|
+
`OAuth discovery failed: HTTP ${response.status}.`,
|
|
77
|
+
`Check ${new URL('/.well-known/oauth-authorization-server', baseUrl)}.`
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
return await response.json()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function refresh(config) {
|
|
84
|
+
if (!config?.refresh_token) {
|
|
85
|
+
throw new CliError('Not authenticated.', 'Run: giz auth login')
|
|
86
|
+
}
|
|
87
|
+
const metadata = await discovery(config.baseUrl)
|
|
88
|
+
const body = new URLSearchParams({
|
|
89
|
+
grant_type: 'refresh_token',
|
|
90
|
+
client_id: CLIENT_ID,
|
|
91
|
+
refresh_token: config.refresh_token,
|
|
92
|
+
resource: new URL('/api/mcp', config.baseUrl).href
|
|
93
|
+
})
|
|
94
|
+
const response = await fetch(metadata.token_endpoint, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
97
|
+
body
|
|
98
|
+
})
|
|
99
|
+
const payload = await response.json()
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
throw new CliError(
|
|
102
|
+
payload.error_description || payload.error || 'Giz session could not be refreshed.',
|
|
103
|
+
'Run: giz auth login'
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
const next = {
|
|
107
|
+
...config,
|
|
108
|
+
...payload,
|
|
109
|
+
refresh_token: payload.refresh_token || config.refresh_token,
|
|
110
|
+
expires_at: Date.now() + Number(payload.expires_in || 3600) * 1000
|
|
111
|
+
}
|
|
112
|
+
await saveConfig(next)
|
|
113
|
+
return next
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function credentials(baseUrl, forceRefresh = false) {
|
|
117
|
+
if (process.env.GIZ_TOKEN) return { access_token: process.env.GIZ_TOKEN, baseUrl }
|
|
118
|
+
let config = await readConfig()
|
|
119
|
+
if (!config?.access_token) throw new CliError('Not authenticated.', 'Run: giz auth login')
|
|
120
|
+
if (String(config.baseUrl).replace(/\/+$/, '') !== baseUrl) {
|
|
121
|
+
throw new CliError(
|
|
122
|
+
`Credentials belong to ${config.baseUrl}.`,
|
|
123
|
+
`Run: giz auth login --base-url ${baseUrl}`
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
if (forceRefresh || Number(config.expires_at || 0) <= Date.now() + 60_000) config = await refresh(config)
|
|
127
|
+
return config
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function api(baseUrl, pathname, data, options = {}) {
|
|
131
|
+
const run = async (forceRefresh = false) => {
|
|
132
|
+
const auth = await credentials(baseUrl, forceRefresh)
|
|
133
|
+
const response = await fetch(new URL(pathname, baseUrl), {
|
|
134
|
+
method: data === undefined ? 'GET' : 'POST',
|
|
135
|
+
headers: {
|
|
136
|
+
accept: 'application/json',
|
|
137
|
+
...(data === undefined ? {} : { 'content-type': 'application/json' }),
|
|
138
|
+
authorization: `Bearer ${auth.access_token}`
|
|
139
|
+
},
|
|
140
|
+
...(data === undefined ? {} : { body: JSON.stringify(data) })
|
|
141
|
+
})
|
|
142
|
+
const text = await response.text()
|
|
143
|
+
let payload
|
|
144
|
+
try {
|
|
145
|
+
payload = text ? JSON.parse(text) : null
|
|
146
|
+
} catch {
|
|
147
|
+
payload = { message: text.slice(0, 500) }
|
|
148
|
+
}
|
|
149
|
+
return { response, payload }
|
|
150
|
+
}
|
|
151
|
+
let result = await run()
|
|
152
|
+
if (result.response.status === 401 && !process.env.GIZ_TOKEN && !options.refreshed) {
|
|
153
|
+
result = await run(true)
|
|
154
|
+
}
|
|
155
|
+
if (!result.response.ok) {
|
|
156
|
+
const required = result.response.status === 403 ? result.payload?.message : undefined
|
|
157
|
+
throw new CliError(
|
|
158
|
+
result.payload?.message || `Giz API returned HTTP ${result.response.status}.`,
|
|
159
|
+
required?.includes('scope') ? 'Run: giz auth login' : 'Run with --json for the complete error.',
|
|
160
|
+
result.response.status
|
|
161
|
+
)
|
|
162
|
+
}
|
|
163
|
+
return result.payload
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
async function uploadMedia(baseUrl, localFile) {
|
|
167
|
+
const filePath = path.resolve(String(localFile))
|
|
168
|
+
const run = async (forceRefresh = false) => {
|
|
169
|
+
const auth = await credentials(baseUrl, forceRefresh)
|
|
170
|
+
let file
|
|
171
|
+
try {
|
|
172
|
+
file = await openAsBlob(filePath)
|
|
173
|
+
} catch (error) {
|
|
174
|
+
throw new CliError(`Could not open ${filePath}.`, String(error?.message || error))
|
|
175
|
+
}
|
|
176
|
+
const body = new FormData()
|
|
177
|
+
body.append('file', file, path.basename(filePath))
|
|
178
|
+
const response = await fetch(new URL('/api/storage/media', baseUrl), {
|
|
179
|
+
method: 'POST',
|
|
180
|
+
headers: {
|
|
181
|
+
accept: 'application/json',
|
|
182
|
+
authorization: `Bearer ${auth.access_token}`
|
|
183
|
+
},
|
|
184
|
+
body
|
|
185
|
+
})
|
|
186
|
+
const text = await response.text()
|
|
187
|
+
let payload
|
|
188
|
+
try {
|
|
189
|
+
payload = text ? JSON.parse(text) : null
|
|
190
|
+
} catch {
|
|
191
|
+
payload = { message: text.slice(0, 500) }
|
|
192
|
+
}
|
|
193
|
+
return { response, payload }
|
|
194
|
+
}
|
|
195
|
+
let result = await run()
|
|
196
|
+
if (result.response.status === 401 && !process.env.GIZ_TOKEN) result = await run(true)
|
|
197
|
+
if (!result.response.ok) {
|
|
198
|
+
throw new CliError(
|
|
199
|
+
result.payload?.message || `Giz upload returned HTTP ${result.response.status}.`,
|
|
200
|
+
'Check the file and your Giz storage access.'
|
|
201
|
+
)
|
|
202
|
+
}
|
|
203
|
+
const url = String(result.payload?.url || '').trim()
|
|
204
|
+
const drivePath = String(result.payload?.path || '').trim()
|
|
205
|
+
if (!url || !drivePath) throw new CliError('Giz upload returned an incomplete file reference.', 'Try the upload again.')
|
|
206
|
+
return { ...result.payload, url: new URL(url, baseUrl).href }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function login(baseUrl, options) {
|
|
210
|
+
const metadata = await discovery(baseUrl)
|
|
211
|
+
const verifier = randomBytes(48).toString('base64url')
|
|
212
|
+
const challenge = createHash('sha256').update(verifier).digest('base64url')
|
|
213
|
+
const state = randomBytes(24).toString('base64url')
|
|
214
|
+
const callback = waitForCallback(state)
|
|
215
|
+
const authorization = new URL(metadata.authorization_endpoint)
|
|
216
|
+
for (const [name, value] of Object.entries({
|
|
217
|
+
response_type: 'code',
|
|
218
|
+
client_id: CLIENT_ID,
|
|
219
|
+
redirect_uri: CALLBACK_URL,
|
|
220
|
+
scope: SCOPES.join(' '),
|
|
221
|
+
resource: new URL('/api/mcp', baseUrl).href,
|
|
222
|
+
code_challenge: challenge,
|
|
223
|
+
code_challenge_method: 'S256',
|
|
224
|
+
prompt: 'consent',
|
|
225
|
+
state
|
|
226
|
+
})) authorization.searchParams.set(name, value)
|
|
227
|
+
process.stdout.write(`Opening Giz sign-in:\n${authorization}\n`)
|
|
228
|
+
if (options.open !== 'false' && options['no-open'] !== true) openBrowser(authorization.href)
|
|
229
|
+
const code = await callback
|
|
230
|
+
const response = await fetch(metadata.token_endpoint, {
|
|
231
|
+
method: 'POST',
|
|
232
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
233
|
+
body: new URLSearchParams({
|
|
234
|
+
grant_type: 'authorization_code',
|
|
235
|
+
client_id: CLIENT_ID,
|
|
236
|
+
redirect_uri: CALLBACK_URL,
|
|
237
|
+
code,
|
|
238
|
+
code_verifier: verifier,
|
|
239
|
+
resource: new URL('/api/mcp', baseUrl).href
|
|
240
|
+
})
|
|
241
|
+
})
|
|
242
|
+
const payload = await response.json()
|
|
243
|
+
if (!response.ok) {
|
|
244
|
+
throw new CliError(payload.error_description || payload.error || 'OAuth token exchange failed.', 'Run: giz auth login')
|
|
245
|
+
}
|
|
246
|
+
await saveConfig({
|
|
247
|
+
baseUrl,
|
|
248
|
+
...payload,
|
|
249
|
+
expires_at: Date.now() + Number(payload.expires_in || 3600) * 1000
|
|
250
|
+
})
|
|
251
|
+
return await api(baseUrl, '/api/external/me')
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function waitForCallback(expectedState) {
|
|
255
|
+
return new Promise((resolve, reject) => {
|
|
256
|
+
const timer = setTimeout(() => {
|
|
257
|
+
server.close()
|
|
258
|
+
reject(new CliError('Giz sign-in timed out.', 'Run: giz auth login'))
|
|
259
|
+
}, 5 * 60 * 1000)
|
|
260
|
+
const server = createServer((request, response) => {
|
|
261
|
+
const url = new URL(request.url || '/', CALLBACK_URL)
|
|
262
|
+
if (url.pathname !== '/callback') {
|
|
263
|
+
response.writeHead(404).end('Not found')
|
|
264
|
+
return
|
|
265
|
+
}
|
|
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
|
+
if (url.searchParams.get('state') !== expectedState) {
|
|
276
|
+
finish(new CliError('OAuth state did not match.', 'Run: giz auth login'))
|
|
277
|
+
} else if (url.searchParams.get('error')) {
|
|
278
|
+
finish(new CliError(
|
|
279
|
+
url.searchParams.get('error_description') || url.searchParams.get('error'),
|
|
280
|
+
'Run: giz auth login'
|
|
281
|
+
))
|
|
282
|
+
} else if (!url.searchParams.get('code')) {
|
|
283
|
+
finish(new CliError('OAuth callback did not include a code.', 'Run: giz auth login'))
|
|
284
|
+
} else {
|
|
285
|
+
finish(undefined, url.searchParams.get('code'))
|
|
286
|
+
}
|
|
287
|
+
})
|
|
288
|
+
server.on('error', (error) => {
|
|
289
|
+
clearTimeout(timer)
|
|
290
|
+
reject(new CliError(
|
|
291
|
+
`Could not listen on ${CALLBACK_URL}: ${error.message}`,
|
|
292
|
+
'Close the process using port 53682 and run: giz auth login'
|
|
293
|
+
))
|
|
294
|
+
})
|
|
295
|
+
server.listen(53682, '127.0.0.1')
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function openBrowser(url) {
|
|
300
|
+
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open'
|
|
301
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url]
|
|
302
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' })
|
|
303
|
+
child.on('error', () => {})
|
|
304
|
+
child.unref()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function jsonInput(value) {
|
|
308
|
+
if (value === undefined || value === true) return {}
|
|
309
|
+
try {
|
|
310
|
+
const parsed = JSON.parse(value)
|
|
311
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error()
|
|
312
|
+
return parsed
|
|
313
|
+
} catch {
|
|
314
|
+
throw new CliError('--input must be one JSON object.', 'Example: --input \'{"prompt":"A quiet ocean"}\'')
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function required(value, name, example) {
|
|
319
|
+
if (value) return value
|
|
320
|
+
throw new CliError(`${name} is required.`, example)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async function applyUploads(baseUrl, input, values = []) {
|
|
324
|
+
for (const value of values.filter((item) => item !== undefined)) {
|
|
325
|
+
const separator = String(value).indexOf('=')
|
|
326
|
+
if (separator < 1 || separator === String(value).length - 1) {
|
|
327
|
+
throw new CliError(
|
|
328
|
+
'--upload must be FIELD=LOCAL_PATH.',
|
|
329
|
+
'Example: --upload image=./first-frame.png'
|
|
330
|
+
)
|
|
331
|
+
}
|
|
332
|
+
const field = String(value).slice(0, separator).trim()
|
|
333
|
+
const localFile = String(value).slice(separator + 1).trim()
|
|
334
|
+
const stored = await uploadMedia(baseUrl, localFile)
|
|
335
|
+
const current = input[field]
|
|
336
|
+
input[field] = current === undefined ? stored.url : Array.isArray(current) ? [...current, stored.url] : [current, stored.url]
|
|
337
|
+
}
|
|
338
|
+
return input
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
async function invoke(baseUrl, tool, input) {
|
|
342
|
+
return await api(baseUrl, `/api/tools/${encodeURIComponent(tool)}`, input)
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function runCommand(baseUrl, positionals, options) {
|
|
346
|
+
const [command, subcommand, ...rest] = positionals
|
|
347
|
+
if (options.version === true) return { kind: 'message', value: '1.0.0' }
|
|
348
|
+
if (!command || command === 'help' || options.help === true) return { kind: 'help', value: help(command) }
|
|
349
|
+
if (command === 'auth' && subcommand === 'login') {
|
|
350
|
+
return { kind: 'auth', value: await login(baseUrl, options) }
|
|
351
|
+
}
|
|
352
|
+
if (command === 'auth' && subcommand === 'status') {
|
|
353
|
+
return { kind: 'auth', value: await api(baseUrl, '/api/external/me') }
|
|
354
|
+
}
|
|
355
|
+
if (command === 'auth' && subcommand === 'logout') {
|
|
356
|
+
const config = await readConfig()
|
|
357
|
+
if (config?.refresh_token) {
|
|
358
|
+
const metadata = await discovery(baseUrl)
|
|
359
|
+
await fetch(metadata.revocation_endpoint, {
|
|
360
|
+
method: 'POST',
|
|
361
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
362
|
+
body: new URLSearchParams({
|
|
363
|
+
token: config.refresh_token,
|
|
364
|
+
token_type_hint: 'refresh_token',
|
|
365
|
+
client_id: CLIENT_ID
|
|
366
|
+
})
|
|
367
|
+
})
|
|
368
|
+
}
|
|
369
|
+
await unlink(configPath).catch((error) => {
|
|
370
|
+
if (error?.code !== 'ENOENT') throw error
|
|
371
|
+
})
|
|
372
|
+
return { kind: 'message', value: 'Signed out of Giz on this device.' }
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (command === 'model' && subcommand === 'list') {
|
|
376
|
+
return {
|
|
377
|
+
kind: 'models',
|
|
378
|
+
value: await invoke(baseUrl, 'model.list', {
|
|
379
|
+
...(options.type ? { type: options.type } : {}),
|
|
380
|
+
...(options.search ? { search: options.search } : {})
|
|
381
|
+
})
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (command === 'model' && subcommand === 'get') {
|
|
385
|
+
const model = required(rest[0] || options.model, 'MODEL', 'Run: giz model get MODEL')
|
|
386
|
+
return { kind: 'model', value: await invoke(baseUrl, 'model.get', { model }) }
|
|
387
|
+
}
|
|
388
|
+
if (command === 'model' && subcommand === 'run') {
|
|
389
|
+
const model = required(rest[0] || options.model, 'MODEL', 'Run: giz model run MODEL --input JSON')
|
|
390
|
+
const input = await applyUploads(baseUrl, jsonInput(options.input), options.upload)
|
|
391
|
+
return { kind: 'job', value: await invoke(baseUrl, 'model.run', { model, input }) }
|
|
392
|
+
}
|
|
393
|
+
if (command === 'models') {
|
|
394
|
+
throw new CliError('Unknown command "models".', 'Did you mean: giz model list')
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (command === 'generate' && ['image', 'video', 'audio', 'music', 'talking-head'].includes(subcommand)) {
|
|
398
|
+
const tool = {
|
|
399
|
+
image: 'generateImage',
|
|
400
|
+
video: 'generateVideo',
|
|
401
|
+
audio: 'generateAudio',
|
|
402
|
+
music: 'generateMusic',
|
|
403
|
+
'talking-head': 'generateTalkingHead'
|
|
404
|
+
}[subcommand]
|
|
405
|
+
const uploads = [
|
|
406
|
+
...(Array.isArray(options.upload) ? options.upload : []),
|
|
407
|
+
...(options.image ? [`references=${options.image}`] : []),
|
|
408
|
+
...(options.audio ? [`audio=${options.audio}`] : [])
|
|
409
|
+
]
|
|
410
|
+
const input = await applyUploads(baseUrl, {
|
|
411
|
+
...jsonInput(options.input),
|
|
412
|
+
...(rest.length ? { prompt: rest.join(' ') } : {}),
|
|
413
|
+
...(options.model ? { model: options.model } : {})
|
|
414
|
+
}, uploads)
|
|
415
|
+
required(input.prompt, 'PROMPT', `Run: giz generate ${subcommand} "your prompt"`)
|
|
416
|
+
return { kind: 'job', value: await invoke(baseUrl, tool, input) }
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (command === 'job' && subcommand === 'list') {
|
|
420
|
+
return { kind: 'jobs', value: await invoke(baseUrl, 'job.list', options.limit ? { limit: Number(options.limit) } : {}) }
|
|
421
|
+
}
|
|
422
|
+
if (command === 'job' && subcommand === 'get') {
|
|
423
|
+
const job = required(rest[0], 'JOB', 'Run: giz job get JOB')
|
|
424
|
+
return { kind: 'job', value: await invoke(baseUrl, 'job.get', { job }) }
|
|
425
|
+
}
|
|
426
|
+
if (command === 'job' && subcommand === 'cancel') {
|
|
427
|
+
const job = required(rest[0], 'JOB', 'Run: giz job cancel JOB')
|
|
428
|
+
return { kind: 'job', value: await invoke(baseUrl, 'job.cancel', { job }) }
|
|
429
|
+
}
|
|
430
|
+
if (command === 'job' && subcommand === 'wait') {
|
|
431
|
+
const id = required(rest[0], 'JOB', 'Run: giz job wait JOB')
|
|
432
|
+
while (true) {
|
|
433
|
+
const job = await invoke(baseUrl, 'job.get', { job: id })
|
|
434
|
+
if (['completed', 'failed', 'cancelled'].includes(job.status)) return { kind: 'job', value: job }
|
|
435
|
+
if (process.stderr.isTTY) process.stderr.write(`\r${job.id} ${job.status} ${job.message || ''}`.slice(0, 120))
|
|
436
|
+
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (command === 'drive') {
|
|
441
|
+
const mappings = {
|
|
442
|
+
list: () => ['drive.list', { path: rest[0] || '' }],
|
|
443
|
+
search: () => ['drive.search', { query: required(rest.join(' '), 'QUERY', 'Run: giz drive search QUERY') }],
|
|
444
|
+
read: () => ['drive.readFile', { path: required(rest[0], 'PATH', 'Run: giz drive read PATH') }],
|
|
445
|
+
mkdir: () => ['drive.makeDirectory', { path: required(rest[0], 'PATH', 'Run: giz drive mkdir PATH') }],
|
|
446
|
+
move: () => ['drive.movePath', {
|
|
447
|
+
from: required(rest[0], 'FROM', 'Run: giz drive move FROM TO'),
|
|
448
|
+
to: required(rest[1], 'TO', 'Run: giz drive move FROM TO')
|
|
449
|
+
}],
|
|
450
|
+
delete: () => ['drive.deletePath', { path: required(rest[0], 'PATH', 'Run: giz drive delete PATH') }]
|
|
451
|
+
}
|
|
452
|
+
if (subcommand === 'write') {
|
|
453
|
+
const drivePath = required(rest[0], 'PATH', 'Run: giz drive write PATH --content TEXT')
|
|
454
|
+
let content = options.content
|
|
455
|
+
let encoding = 'utf8'
|
|
456
|
+
if (options.file) {
|
|
457
|
+
const buffer = await readFile(String(options.file))
|
|
458
|
+
content = buffer.toString('base64')
|
|
459
|
+
encoding = 'base64'
|
|
460
|
+
}
|
|
461
|
+
required(content, 'CONTENT', 'Use --content TEXT or --file LOCAL_PATH.')
|
|
462
|
+
return {
|
|
463
|
+
kind: 'result',
|
|
464
|
+
value: await invoke(baseUrl, 'drive.writeFile', {
|
|
465
|
+
path: drivePath,
|
|
466
|
+
content: String(content),
|
|
467
|
+
encoding,
|
|
468
|
+
...(options.computer ? { computerId: options.computer } : {})
|
|
469
|
+
})
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const mapping = mappings[subcommand]
|
|
473
|
+
if (!mapping) throw new CliError(`Unknown drive command "${subcommand || ''}".`, 'Run: giz drive --help')
|
|
474
|
+
const [tool, input] = mapping()
|
|
475
|
+
return {
|
|
476
|
+
kind: 'result',
|
|
477
|
+
value: await invoke(baseUrl, tool, { ...input, ...(options.computer ? { computerId: options.computer } : {}) })
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
if (command === 'upload') {
|
|
482
|
+
const localFile = required(subcommand, 'LOCAL_PATH', 'Run: giz upload ./reference.png')
|
|
483
|
+
return { kind: 'result', value: await uploadMedia(baseUrl, localFile) }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (command === 'computer') {
|
|
487
|
+
if (subcommand === 'list') return { kind: 'computers', value: await invoke(baseUrl, 'computer.list', {}) }
|
|
488
|
+
const computerId = rest[0] || options.computer
|
|
489
|
+
if (subcommand === 'get') return { kind: 'result', value: await invoke(baseUrl, 'computer.get', computerId ? { computerId } : {}) }
|
|
490
|
+
if (subcommand === 'start' || subcommand === 'stop') {
|
|
491
|
+
return {
|
|
492
|
+
kind: 'result',
|
|
493
|
+
value: await invoke(baseUrl, `computer.${subcommand}`, computerId ? { computerId } : {})
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
if (subcommand === 'exec') {
|
|
497
|
+
const commandArgs = rest
|
|
498
|
+
if (!commandArgs.length) throw new CliError('COMMAND is required.', 'Run: giz computer exec -- ls -la')
|
|
499
|
+
return {
|
|
500
|
+
kind: 'result',
|
|
501
|
+
value: await invoke(baseUrl, 'container.exec', {
|
|
502
|
+
command: commandArgs,
|
|
503
|
+
sessionId: String(options.session || 'giz-cli'),
|
|
504
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
505
|
+
...(options.computer ? { computerId: options.computer } : {})
|
|
506
|
+
})
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
throw new CliError(`Unknown computer command "${subcommand || ''}".`, 'Run: giz computer --help')
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (command === 'invoke') {
|
|
513
|
+
const tool = required(subcommand, 'TOOL', 'Run: giz invoke TOOL --input JSON')
|
|
514
|
+
return { kind: 'result', value: await invoke(baseUrl, tool, jsonInput(options.input)) }
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
if (command === 'mcp' && subcommand === 'url') {
|
|
518
|
+
return { kind: 'message', value: new URL('/api/mcp', baseUrl).href }
|
|
519
|
+
}
|
|
520
|
+
if (command === 'mcp' && subcommand === 'add') {
|
|
521
|
+
const client = required(rest[0], 'CLIENT', 'Run: giz mcp add codex|claude')
|
|
522
|
+
const endpoint = new URL('/api/mcp', baseUrl).href
|
|
523
|
+
if (client === 'codex') {
|
|
524
|
+
runExternal('codex', ['mcp', 'add', 'giz', '--url', endpoint, '--oauth-resource', endpoint], 'Install Codex CLI first.')
|
|
525
|
+
runExternal('codex', ['mcp', 'login', 'giz', '--scopes', SCOPES.join(',')], 'Run: codex mcp login giz')
|
|
526
|
+
return { kind: 'message', value: 'Giz is connected to Codex.' }
|
|
527
|
+
}
|
|
528
|
+
if (client === 'claude') {
|
|
529
|
+
runExternal('claude', ['mcp', 'add', '--transport', 'http', '--scope', 'user', 'giz', endpoint], 'Install Claude Code first.')
|
|
530
|
+
return { kind: 'message', value: 'Giz was added to Claude Code. Open Claude Code and run /mcp to sign in.' }
|
|
531
|
+
}
|
|
532
|
+
throw new CliError(`Unsupported MCP client "${client}".`, 'Supported clients: codex, claude')
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
if (command === 'skill' && subcommand === 'install') {
|
|
536
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'skills')
|
|
537
|
+
runExternal('npx', ['--yes', 'skills', 'add', root, '--all'], 'Install Node.js 20 or newer first.')
|
|
538
|
+
return { kind: 'message', value: 'Giz Skill installed for available AI agents.' }
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
throw new CliError(`Unknown command "${[command, subcommand].filter(Boolean).join(' ')}".`, 'Run: giz --help')
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function runExternal(command, args, hint) {
|
|
545
|
+
const result = spawnSync(command, args, { stdio: 'inherit' })
|
|
546
|
+
if (result.error?.code === 'ENOENT') throw new CliError(`${command} is not installed.`, hint)
|
|
547
|
+
if (result.status !== 0) throw new CliError(`${command} exited with status ${result.status}.`, hint)
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function print(kind, value, json) {
|
|
551
|
+
if (json) {
|
|
552
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`)
|
|
553
|
+
return
|
|
554
|
+
}
|
|
555
|
+
if (kind === 'help' || kind === 'message') {
|
|
556
|
+
process.stdout.write(`${value}\n`)
|
|
557
|
+
return
|
|
558
|
+
}
|
|
559
|
+
if (kind === 'models') {
|
|
560
|
+
const rows = value.models || []
|
|
561
|
+
for (const model of rows) process.stdout.write(`${model.id}\t${model.type}\t${model.provider || '-'}\t${model.label}\n`)
|
|
562
|
+
process.stdout.write(`\n${rows.length} models\n`)
|
|
563
|
+
return
|
|
564
|
+
}
|
|
565
|
+
if (kind === 'jobs') {
|
|
566
|
+
for (const job of value.jobs || []) process.stdout.write(`${job.id}\t${job.status}\t${job.tool}\t${job.createdAt}\n`)
|
|
567
|
+
return
|
|
568
|
+
}
|
|
569
|
+
if (kind === 'computers') {
|
|
570
|
+
for (const computer of value.computers || []) {
|
|
571
|
+
process.stdout.write(`${computer.id}\t${computer.status || '-'}\t${computer.isDefault ? 'default' : '-'}\t${computer.name}\n`)
|
|
572
|
+
}
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
if (kind === 'job') {
|
|
576
|
+
process.stdout.write(`${value.id} ${value.status}\n`)
|
|
577
|
+
if (value.message) process.stdout.write(`${value.message}\n`)
|
|
578
|
+
if (value.error) process.stdout.write(`Error: ${value.error}\n`)
|
|
579
|
+
if (value.result) process.stdout.write(`${JSON.stringify(value.result, null, 2)}\n`)
|
|
580
|
+
if (!['completed', 'failed', 'cancelled'].includes(value.status)) {
|
|
581
|
+
process.stdout.write(`Run: giz job wait ${value.id}\n`)
|
|
582
|
+
}
|
|
583
|
+
return
|
|
584
|
+
}
|
|
585
|
+
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`)
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
function help(command) {
|
|
589
|
+
if (command === 'model') return `Usage
|
|
590
|
+
giz model list [--type TYPE] [--search TEXT]
|
|
591
|
+
giz model get MODEL
|
|
592
|
+
giz model run MODEL --input JSON [--upload FIELD=LOCAL_PATH]
|
|
593
|
+
|
|
594
|
+
Run model get first. Its live input schema is the source of truth. Repeat --upload for multiple media inputs.`
|
|
595
|
+
if (command === 'generate') return `Usage
|
|
596
|
+
giz generate image|video|audio|music|talking-head "PROMPT" [--model MODEL]
|
|
597
|
+
|
|
598
|
+
Options
|
|
599
|
+
--image LOCAL_PATH Upload a visual reference
|
|
600
|
+
--audio LOCAL_PATH Upload speech audio
|
|
601
|
+
--upload FIELD=PATH Upload into an exact tool field
|
|
602
|
+
--input JSON Additional generation parameters`
|
|
603
|
+
if (command === 'drive') return `Usage
|
|
604
|
+
giz drive list [PATH]
|
|
605
|
+
giz drive search QUERY
|
|
606
|
+
giz drive read PATH
|
|
607
|
+
giz drive write PATH --content TEXT
|
|
608
|
+
giz drive write PATH --file LOCAL_PATH
|
|
609
|
+
giz drive mkdir PATH
|
|
610
|
+
giz drive move FROM TO
|
|
611
|
+
giz drive delete PATH`
|
|
612
|
+
if (command === 'computer') return `Usage
|
|
613
|
+
giz computer list
|
|
614
|
+
giz computer get|start|stop [COMPUTER]
|
|
615
|
+
giz computer exec [--computer COMPUTER] [--cwd PATH] -- COMMAND [ARGS...]`
|
|
616
|
+
if (command === 'mcp') return `Usage
|
|
617
|
+
giz mcp url
|
|
618
|
+
giz mcp add codex
|
|
619
|
+
giz mcp add claude`
|
|
620
|
+
if (command === 'auth') return `Usage
|
|
621
|
+
giz auth login
|
|
622
|
+
giz auth status
|
|
623
|
+
giz auth logout`
|
|
624
|
+
return `Giz CLI — non-LLM models, Drive, and Computer from any terminal
|
|
625
|
+
|
|
626
|
+
Usage
|
|
627
|
+
giz auth login
|
|
628
|
+
giz model list [--type TYPE] [--search TEXT]
|
|
629
|
+
giz model get MODEL
|
|
630
|
+
giz model run MODEL --input JSON [--upload FIELD=LOCAL_PATH]
|
|
631
|
+
giz generate image|video|audio|music "PROMPT" [--model MODEL]
|
|
632
|
+
giz job list|get|wait|cancel [JOB]
|
|
633
|
+
giz drive list|search|read|write|mkdir|move|delete
|
|
634
|
+
giz upload LOCAL_PATH
|
|
635
|
+
giz computer list|get|start|stop|exec
|
|
636
|
+
giz mcp add codex|claude
|
|
637
|
+
giz skill install
|
|
638
|
+
giz invoke TOOL --input JSON
|
|
639
|
+
|
|
640
|
+
Examples
|
|
641
|
+
giz auth login
|
|
642
|
+
giz generate image "A paper observatory at blue hour"
|
|
643
|
+
giz model list --type video-generation
|
|
644
|
+
giz model run ltx2-distilled --input '{"prompt":"A slow dolly in"}' --upload image=./first-frame.png
|
|
645
|
+
giz generate talking-head "Welcome to Giz" --image ./face.png
|
|
646
|
+
giz drive write notes/brief.md --file ./brief.md
|
|
647
|
+
giz computer exec --computer ctr_123 -- python --version
|
|
648
|
+
giz mcp add codex
|
|
649
|
+
|
|
650
|
+
Global options
|
|
651
|
+
--json Machine-readable JSON output
|
|
652
|
+
--base-url URL Another Giz deployment
|
|
653
|
+
--no-open Print the login URL without opening a browser
|
|
654
|
+
|
|
655
|
+
Docs: https://www.giz.ai/mcp/`
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async function main() {
|
|
659
|
+
const { positionals, options } = parse(process.argv.slice(2))
|
|
660
|
+
const saved = await readConfig()
|
|
661
|
+
const baseUrl = String(options['base-url'] || process.env.GIZ_URL || saved?.baseUrl || DEFAULT_URL).replace(/\/+$/, '')
|
|
662
|
+
const output = await runCommand(baseUrl, positionals, options)
|
|
663
|
+
print(output.kind, output.value, options.json === true)
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
main().catch((error) => {
|
|
667
|
+
const { options } = parse(process.argv.slice(2))
|
|
668
|
+
if (options.json === true) {
|
|
669
|
+
process.stderr.write(`${JSON.stringify({
|
|
670
|
+
error: {
|
|
671
|
+
message: String(error?.message || error),
|
|
672
|
+
...(error?.hint ? { hint: error.hint } : {}),
|
|
673
|
+
...(error?.status ? { status: error.status } : {})
|
|
674
|
+
}
|
|
675
|
+
}, null, 2)}\n`)
|
|
676
|
+
} else {
|
|
677
|
+
process.stderr.write(`Error: ${String(error?.message || error)}\n`)
|
|
678
|
+
if (error?.hint) process.stderr.write(`Hint: ${error.hint}\n`)
|
|
679
|
+
}
|
|
680
|
+
process.exitCode = 1
|
|
681
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gizai/cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Giz CLI, MCP installer, and agent Skills for non-LLM models, Drive, and Computer",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"giz": "cli/giz.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"cli",
|
|
11
|
+
"skills",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=20"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"giz",
|
|
19
|
+
"mcp",
|
|
20
|
+
"ai",
|
|
21
|
+
"image",
|
|
22
|
+
"video",
|
|
23
|
+
"audio",
|
|
24
|
+
"agent"
|
|
25
|
+
],
|
|
26
|
+
"license": "UNLICENSED",
|
|
27
|
+
"homepage": "https://www.giz.ai/mcp/",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/exinno/giz.git",
|
|
31
|
+
"directory": "apps/space/external"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"check": "node --check cli/giz.mjs"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: giz
|
|
3
|
+
description: Use Giz non-LLM image, video, audio, music, speech, Drive, and Computer capabilities from an AI agent. Trigger when the user asks to generate or transform media with Giz, inspect or modify Giz Drive files, operate a Giz Computer or terminal, or discover live Giz models and their parameters.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Giz
|
|
7
|
+
|
|
8
|
+
Use the `giz` CLI or the configured Giz MCP server. Prefer MCP when available and the CLI when working in a shell.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Run `model_list` before selecting an unfamiliar media model.
|
|
13
|
+
2. Run `model_get` and treat its live input schema as the source of truth.
|
|
14
|
+
3. Call `model_run` with the exact model ID and supported inputs. For simple prompt-only work, use `generate_image`, `generate_video`, `generate_audio`, `generate_music`, or `generate_talking_head`.
|
|
15
|
+
4. Generation returns a durable job. Poll `job_get`; do not resubmit while it is queued or processing.
|
|
16
|
+
5. Preserve completed Drive URLs and paths. Use Drive tools to inspect or organize results.
|
|
17
|
+
6. Use Computer tools only when the task requires a shell or runtime. Reuse one terminal `sessionId` per conversation.
|
|
18
|
+
7. Ask for explicit confirmation immediately before destructive Drive removal, Computer runtime changes, or commands with material side effects.
|
|
19
|
+
|
|
20
|
+
## CLI
|
|
21
|
+
|
|
22
|
+
Connect once in a browser:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
giz auth login
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Discover and generate:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
giz model list --type image-generation
|
|
32
|
+
giz model get MODEL_ID
|
|
33
|
+
giz model run MODEL_ID --input '{"prompt":"..."}'
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Upload local media into an exact model field:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
giz model run MODEL_ID \
|
|
40
|
+
--input '{"prompt":"Animate this frame"}' \
|
|
41
|
+
--upload image=./frame.png
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Invoke an exact item-driven tool:
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
giz invoke drive.readFile --input '{"path":"notes/brief.md"}'
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Pass `--json` for machine output. Use `GIZ_TOKEN` only in ephemeral automation.
|
|
51
|
+
|
|
52
|
+
## Boundaries
|
|
53
|
+
|
|
54
|
+
- 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
|
+
- Do not turn argv arrays into shell strings.
|
|
57
|
+
- Do not read binary Drive content unless the task needs it; prefer returned URLs and metadata.
|
|
58
|
+
- Do not retry failed generations blindly. Report the provider or contract error.
|
|
59
|
+
|
|
60
|
+
Read [references/operations.md](references/operations.md) when MCP discovery is unavailable or when choosing between Drive and Computer tools.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Giz operation map
|
|
2
|
+
|
|
3
|
+
| Goal | Operation |
|
|
4
|
+
|---|---|
|
|
5
|
+
| Discover non-LLM models and fields | `model_list`, `model_get` |
|
|
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
|
+
| Track durable generation | `job_list`, `job_get`, `job_cancel` |
|
|
9
|
+
| List, search, inspect, or read Drive | `drive_list`, `drive_search`, `drive_getMetadata`, `drive_readFile` |
|
|
10
|
+
| Create or change Drive content | `drive_writeFile`, `drive_makeDirectory`, `drive_movePath`, `drive_importUrl`, `drive_createNote`, `drive_editNote` |
|
|
11
|
+
| Move Drive content to Trash | `drive_deletePath` |
|
|
12
|
+
| Discover and control Computers | `computer_list`, `computer_get`, `computer_start`, `computer_stop` |
|
|
13
|
+
| Execute or open a terminal | `container_exec`, `terminal_open` |
|
|
14
|
+
| Inspect or transform media | `media_probe`, `media_process` |
|
|
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`.
|