@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 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,4 @@
1
+ interface:
2
+ display_name: "Giz"
3
+ short_description: "Use Giz media, Drive, and Computer tools"
4
+ default_prompt: "Use Giz to create media or work with my Drive and Computer."
@@ -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`.