@gizai/cli 1.0.1 → 1.2.1
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 +133 -4
- package/cli/codex-relay.mjs +46 -0
- package/cli/computer-identity.mjs +76 -0
- package/cli/engine.mjs +58 -0
- package/cli/giz.mjs +602 -249
- package/package.json +10 -6
- package/skills/giz/SKILL.md +68 -4
- package/skills/giz/references/operations.md +9 -6
- package/skills/giz/references/slides.md +95 -0
package/cli/giz.mjs
CHANGED
|
@@ -1,22 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { computerIdentity, managedRevision } from './computer-identity.mjs'
|
|
2
3
|
import { createHash, randomBytes } from 'node:crypto'
|
|
3
4
|
import { openAsBlob } from 'node:fs'
|
|
4
|
-
import {
|
|
5
|
+
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
|
5
6
|
import { createServer } from 'node:http'
|
|
7
|
+
import { once } from 'node:events'
|
|
6
8
|
import { createRequire } from 'node:module'
|
|
7
9
|
import os from 'node:os'
|
|
8
10
|
import path from 'node:path'
|
|
9
11
|
import { spawn, spawnSync } from 'node:child_process'
|
|
10
12
|
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { lock } from 'proper-lockfile'
|
|
14
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
15
|
+
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'
|
|
16
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
11
17
|
|
|
12
18
|
const { version: CLI_VERSION } = createRequire(import.meta.url)('../package.json')
|
|
13
19
|
const DEFAULT_URL = 'https://www.giz.ai'
|
|
14
|
-
const CALLBACK_URL = 'http://127.0.0.1:53682/callback'
|
|
15
20
|
const CLIENT_ID = 'giz-cli'
|
|
16
|
-
const SCOPES = ['giz.read', 'giz.write', 'giz.execute', 'giz.destructive', 'offline_access']
|
|
17
|
-
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')
|
|
21
|
+
const SCOPES = ['giz.read', 'giz.inference', 'giz.write', 'giz.execute', 'giz.destructive', 'offline_access']
|
|
22
|
+
const BOOLEAN_OPTIONS = new Set(['help', 'json', 'no-open', 'version', 'device'])
|
|
23
|
+
const configPath = path.resolve(process.env.GIZ_CONFIG ||
|
|
24
|
+
path.join(os.homedir(), '.config', 'giz', 'credentials.json'))
|
|
20
25
|
|
|
21
26
|
class CliError extends Error {
|
|
22
27
|
constructor(message, hint, status) {
|
|
@@ -65,14 +70,27 @@ async function readConfig() {
|
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
async function
|
|
69
|
-
await mkdir(path.dirname(configPath), { recursive: true })
|
|
70
|
-
await
|
|
71
|
-
|
|
73
|
+
async function updateConfig(update) {
|
|
74
|
+
await mkdir(path.dirname(configPath), { recursive: true, mode: 0o700 })
|
|
75
|
+
const release = await lock(configPath, { realpath: false, retries: { retries: 120, minTimeout: 100, maxTimeout: 500 } })
|
|
76
|
+
const temporary = `${configPath}.${randomBytes(8).toString('hex')}`
|
|
77
|
+
try {
|
|
78
|
+
const config = await update(await readConfig())
|
|
79
|
+
if (config) {
|
|
80
|
+
await writeFile(temporary, `${JSON.stringify(config, null, 2)}\n`, { mode: 0o600, flag: 'wx' })
|
|
81
|
+
await rename(temporary, configPath)
|
|
82
|
+
} else {
|
|
83
|
+
await unlink(configPath).catch((error) => { if (error.code !== 'ENOENT') throw error })
|
|
84
|
+
}
|
|
85
|
+
return config
|
|
86
|
+
} finally {
|
|
87
|
+
try { await unlink(temporary).catch((error) => { if (error.code !== 'ENOENT') throw error }) }
|
|
88
|
+
finally { await release() }
|
|
89
|
+
}
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
async function discovery(baseUrl) {
|
|
75
|
-
const response = await fetch(new URL('/.well-known/oauth-authorization-server', baseUrl))
|
|
93
|
+
const response = await fetch(new URL('/.well-known/oauth-authorization-server', baseUrl), { signal: AbortSignal.timeout(30_000) })
|
|
76
94
|
if (!response.ok) {
|
|
77
95
|
throw new CliError(
|
|
78
96
|
`OAuth discovery failed: HTTP ${response.status}.`,
|
|
@@ -82,143 +100,158 @@ async function discovery(baseUrl) {
|
|
|
82
100
|
return await response.json()
|
|
83
101
|
}
|
|
84
102
|
|
|
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)
|
|
103
|
+
async function exchangeToken(baseUrl, metadata, grant, config = {}) {
|
|
90
104
|
const body = new URLSearchParams({
|
|
91
|
-
grant_type: 'refresh_token',
|
|
92
105
|
client_id: CLIENT_ID,
|
|
93
|
-
|
|
94
|
-
|
|
106
|
+
resource: new URL('/api/mcp', baseUrl).href,
|
|
107
|
+
...grant
|
|
95
108
|
})
|
|
96
109
|
const response = await fetch(metadata.token_endpoint, {
|
|
97
110
|
method: 'POST',
|
|
98
111
|
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
99
|
-
body
|
|
112
|
+
body,
|
|
113
|
+
signal: AbortSignal.timeout(30_000)
|
|
100
114
|
})
|
|
101
115
|
const payload = await response.json()
|
|
102
116
|
if (!response.ok) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
)
|
|
117
|
+
const error = new CliError(payload.error_description || payload.error || 'OAuth token exchange failed.', 'Run: giz auth login')
|
|
118
|
+
error.oauthError = payload.error
|
|
119
|
+
throw error
|
|
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
|
-
|
|
121
|
-
if (!config?.access_token)
|
|
132
|
+
const config = await readConfig()
|
|
133
|
+
if (!config?.access_token) {
|
|
134
|
+
const computer = await computerIdentity(rejectedToken)
|
|
135
|
+
if (!computer) throw new CliError('Not authenticated.', 'Run: giz auth login')
|
|
136
|
+
if (computer.baseUrl !== baseUrl) throw new CliError('컴퓨터 신원은 다른 서버에서 사용할 수 없습니다.', '사용자 계정으로 giz auth login을 실행해 주세요.')
|
|
137
|
+
return computer
|
|
138
|
+
}
|
|
122
139
|
if (String(config.baseUrl).replace(/\/+$/, '') !== baseUrl) {
|
|
123
140
|
throw new CliError(
|
|
124
141
|
`Credentials belong to ${config.baseUrl}.`,
|
|
125
142
|
`Run: giz auth login --base-url ${baseUrl}`
|
|
126
143
|
)
|
|
127
144
|
}
|
|
128
|
-
|
|
145
|
+
const expired = (value) => value.access_token === rejectedToken || Number(value.expires_at || 0) <= Date.now() + 60_000
|
|
146
|
+
if (expired(config)) {
|
|
147
|
+
return await updateConfig(async (config) => {
|
|
148
|
+
if (config?.baseUrl !== baseUrl) throw new CliError('Giz credentials changed.', 'Run: giz auth login')
|
|
149
|
+
if (expired(config)) {
|
|
150
|
+
if (!config.refresh_token) throw new CliError('Giz session expired.', 'Run: giz auth login')
|
|
151
|
+
return await exchangeToken(baseUrl, await discovery(baseUrl), {
|
|
152
|
+
grant_type: 'refresh_token', refresh_token: config.refresh_token
|
|
153
|
+
}, config)
|
|
154
|
+
}
|
|
155
|
+
return config
|
|
156
|
+
})
|
|
157
|
+
}
|
|
129
158
|
return config
|
|
130
159
|
}
|
|
131
160
|
|
|
132
|
-
async function
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
},
|
|
142
|
-
...(data === undefined ? {} : { body: JSON.stringify(data) })
|
|
143
|
-
})
|
|
144
|
-
const text = await response.text()
|
|
145
|
-
let payload
|
|
146
|
-
try {
|
|
147
|
-
payload = text ? JSON.parse(text) : null
|
|
148
|
-
} catch {
|
|
149
|
-
payload = { message: text.slice(0, 500) }
|
|
150
|
-
}
|
|
151
|
-
return { response, payload }
|
|
161
|
+
async function authenticatedFetch(baseUrl, url, init = {}) {
|
|
162
|
+
const headers = new Headers(init.headers)
|
|
163
|
+
let auth = await credentials(baseUrl)
|
|
164
|
+
const signal = AbortSignal.timeout(25 * 60_000)
|
|
165
|
+
const send = () => {
|
|
166
|
+
headers.set('authorization', `Bearer ${auth.access_token}`)
|
|
167
|
+
// The server resolves membership and roles; the CLI only names the organization, as the web's selector does.
|
|
168
|
+
if (process.env.GIZ_ORGANIZATION) headers.set('x-giz-organization', process.env.GIZ_ORGANIZATION)
|
|
169
|
+
return fetch(url, { ...init, headers, signal: init.signal ? AbortSignal.any([init.signal, signal]) : signal })
|
|
152
170
|
}
|
|
153
|
-
let
|
|
154
|
-
if (
|
|
155
|
-
|
|
171
|
+
let response = await send()
|
|
172
|
+
if (response.status === 401 && !process.env.GIZ_TOKEN) {
|
|
173
|
+
await response.body?.cancel()
|
|
174
|
+
auth = await credentials(baseUrl, auth.access_token)
|
|
175
|
+
response = await send()
|
|
156
176
|
}
|
|
157
|
-
|
|
158
|
-
|
|
177
|
+
return response
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function jsonResponse(response, baseUrl) {
|
|
181
|
+
const text = await response.text()
|
|
182
|
+
let payload
|
|
183
|
+
try { payload = JSON.parse(text) } catch {
|
|
184
|
+
if (response.ok) throw new CliError('Giz API returned invalid JSON.', `Request: ${new URL(response.url).pathname}`)
|
|
185
|
+
payload = { message: text.slice(0, 500) }
|
|
186
|
+
}
|
|
187
|
+
if (!response.ok) {
|
|
188
|
+
const message = payload?.message || payload?.error?.message || `Giz API returned HTTP ${response.status}.`
|
|
189
|
+
// Older servers have only the legacy message; classify that exact contract without hiding other 404s.
|
|
190
|
+
if (response.status === 404 && (payload?.code === 'EXTERNAL_TOOL_UNAVAILABLE' ||
|
|
191
|
+
(new URL(response.url).pathname.startsWith('/api/tools/') && /^Unknown Giz tool: /.test(message)))) {
|
|
192
|
+
const compatibility = payload?.code === 'EXTERNAL_TOOL_UNAVAILABLE'
|
|
193
|
+
? message : '서버 버전은 확인할 수 없습니다(구버전 서버).'
|
|
194
|
+
throw new CliError(
|
|
195
|
+
`이 서버는 요청한 명령을 지원하지 않습니다. CLI ${CLI_VERSION}. ${compatibility}`,
|
|
196
|
+
`서버 주소(${baseUrl})와 지원 버전을 확인해 주세요. 지원하는 서버는 --base-url로 선택할 수 있습니다.`,
|
|
197
|
+
response.status
|
|
198
|
+
)
|
|
199
|
+
}
|
|
159
200
|
throw new CliError(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
201
|
+
message,
|
|
202
|
+
response.status === 403 && /scope/i.test(message)
|
|
203
|
+
? (process.env.GIZ_TOKEN ? 'Provide a GIZ_TOKEN with the required scope.' : `Run: giz auth login --base-url ${baseUrl}`)
|
|
204
|
+
: `Request: ${new URL(response.url).pathname}`,
|
|
205
|
+
response.status
|
|
163
206
|
)
|
|
164
207
|
}
|
|
165
|
-
return
|
|
208
|
+
return payload
|
|
166
209
|
}
|
|
167
210
|
|
|
168
|
-
async function
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
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) }
|
|
211
|
+
async function api(baseUrl, pathname, data, options = {}) {
|
|
212
|
+
const response = await authenticatedFetch(baseUrl, new URL(pathname, baseUrl), {
|
|
213
|
+
method: options.method || (data === undefined ? 'GET' : 'POST'),
|
|
214
|
+
headers: { accept: 'application/json', ...(data === undefined ? {} : { 'content-type': 'application/json' }) },
|
|
215
|
+
...(data === undefined ? {} : { body: JSON.stringify(data) })
|
|
216
|
+
})
|
|
217
|
+
const payload = await jsonResponse(response, baseUrl)
|
|
218
|
+
if (pathname === '/api/external/me' && payload.minimumCliVersion) {
|
|
219
|
+
const current = CLI_VERSION.split('.').map(Number)
|
|
220
|
+
const minimum = String(payload.minimumCliVersion).split('.').map(Number)
|
|
221
|
+
const differing = minimum.findIndex((part, index) => part !== current[index])
|
|
222
|
+
if (differing >= 0 && current[differing] < minimum[differing]) {
|
|
223
|
+
process.stderr.write(`Warning: Giz CLI ${CLI_VERSION} is below minimum ${payload.minimumCliVersion}; update @gizai/cli.\n`)
|
|
194
224
|
}
|
|
195
|
-
return { response, payload }
|
|
196
225
|
}
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
226
|
+
return payload
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function uploadMedia(baseUrl, localFile) {
|
|
230
|
+
const filePath = path.resolve(String(localFile))
|
|
231
|
+
const body = new FormData()
|
|
232
|
+
body.append('file', await openAsBlob(filePath), path.basename(filePath))
|
|
233
|
+
const response = await authenticatedFetch(baseUrl, new URL('/api/storage/media', baseUrl), {
|
|
234
|
+
method: 'POST', headers: { accept: 'application/json' }, body
|
|
235
|
+
})
|
|
236
|
+
const payload = await jsonResponse(response, baseUrl)
|
|
237
|
+
const url = String(payload?.url || '').trim()
|
|
238
|
+
const drivePath = String(payload?.path || '').trim()
|
|
207
239
|
if (!url || !drivePath) throw new CliError('Giz upload returned an incomplete file reference.', 'Try the upload again.')
|
|
208
|
-
return { ...
|
|
240
|
+
return { ...payload, url: new URL(url, baseUrl).href }
|
|
209
241
|
}
|
|
210
242
|
|
|
211
243
|
async function login(baseUrl, options) {
|
|
212
244
|
const metadata = await discovery(baseUrl)
|
|
245
|
+
if (options.device === true) return await deviceLogin(baseUrl, metadata)
|
|
213
246
|
const verifier = randomBytes(48).toString('base64url')
|
|
214
247
|
const challenge = createHash('sha256').update(verifier).digest('base64url')
|
|
215
248
|
const state = randomBytes(24).toString('base64url')
|
|
216
|
-
const callback = waitForCallback(state)
|
|
249
|
+
const callback = await waitForCallback(state)
|
|
217
250
|
const authorization = new URL(metadata.authorization_endpoint)
|
|
218
251
|
for (const [name, value] of Object.entries({
|
|
219
252
|
response_type: 'code',
|
|
220
253
|
client_id: CLIENT_ID,
|
|
221
|
-
redirect_uri:
|
|
254
|
+
redirect_uri: callback.url,
|
|
222
255
|
scope: SCOPES.join(' '),
|
|
223
256
|
resource: new URL('/api/mcp', baseUrl).href,
|
|
224
257
|
code_challenge: challenge,
|
|
@@ -226,76 +259,86 @@ async function login(baseUrl, options) {
|
|
|
226
259
|
prompt: 'consent',
|
|
227
260
|
state
|
|
228
261
|
})) authorization.searchParams.set(name, value)
|
|
229
|
-
process.
|
|
262
|
+
process.stderr.write(`Opening Giz sign-in:\n${authorization}\n`)
|
|
230
263
|
if (options.open !== 'false' && options['no-open'] !== true) openBrowser(authorization.href)
|
|
231
|
-
const code = await callback
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
})
|
|
264
|
+
const code = await callback.code
|
|
265
|
+
await updateConfig(() => exchangeToken(baseUrl, metadata, {
|
|
266
|
+
grant_type: 'authorization_code', redirect_uri: callback.url, code, code_verifier: verifier
|
|
267
|
+
}))
|
|
268
|
+
return await api(baseUrl, '/api/external/me')
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function deviceLogin(baseUrl, metadata) {
|
|
272
|
+
if (!metadata.device_authorization_endpoint) throw new CliError('OAuth device authorization is not supported.')
|
|
273
|
+
const response = await fetch(metadata.device_authorization_endpoint, {
|
|
274
|
+
method: 'POST', headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
275
|
+
body: new URLSearchParams({ client_id: CLIENT_ID, scope: SCOPES.join(' '), resource: new URL('/api/mcp', baseUrl).href }),
|
|
276
|
+
signal: AbortSignal.timeout(30_000)
|
|
243
277
|
})
|
|
244
|
-
const
|
|
245
|
-
if (!response.ok)
|
|
246
|
-
|
|
278
|
+
const device = await response.json()
|
|
279
|
+
if (!response.ok) throw new CliError(device.error_description || device.error || 'OAuth device authorization failed.')
|
|
280
|
+
if (!device.device_code || !device.user_code || !device.verification_uri || !(Number(device.expires_in) > 0)) {
|
|
281
|
+
throw new CliError('OAuth device authorization returned an incomplete response.')
|
|
247
282
|
}
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
283
|
+
process.stderr.write(`Giz sign-in: ${device.verification_uri_complete || device.verification_uri}\nCode: ${device.user_code}\n`)
|
|
284
|
+
let interval = Number(device.interval ?? 5) * 1000
|
|
285
|
+
if (!Number.isFinite(interval) || interval <= 0) throw new CliError('OAuth device authorization returned an invalid interval.')
|
|
286
|
+
const deadline = Date.now() + Number(device.expires_in) * 1000
|
|
287
|
+
while (Date.now() < deadline) {
|
|
288
|
+
const remaining = deadline - Date.now()
|
|
289
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(interval, remaining)))
|
|
290
|
+
if (interval >= remaining || Date.now() >= deadline) break
|
|
291
|
+
try {
|
|
292
|
+
await updateConfig(() => exchangeToken(baseUrl, metadata, {
|
|
293
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code', device_code: device.device_code
|
|
294
|
+
}))
|
|
295
|
+
} catch (error) {
|
|
296
|
+
if (error.oauthError === 'slow_down') interval += 5000
|
|
297
|
+
else if (error.name === 'TimeoutError') interval *= 2
|
|
298
|
+
else if (error.oauthError !== 'authorization_pending') throw error
|
|
299
|
+
continue
|
|
300
|
+
}
|
|
301
|
+
return await api(baseUrl, '/api/external/me')
|
|
302
|
+
}
|
|
303
|
+
throw new CliError('expired_token: Giz device code expired.', 'Run: giz auth login --device')
|
|
254
304
|
}
|
|
255
305
|
|
|
256
|
-
function waitForCallback(expectedState) {
|
|
257
|
-
|
|
258
|
-
|
|
306
|
+
async function waitForCallback(expectedState) {
|
|
307
|
+
const server = createServer()
|
|
308
|
+
server.listen(0, '127.0.0.1')
|
|
309
|
+
await once(server, 'listening')
|
|
310
|
+
const callbackUrl = `http://127.0.0.1:${server.address().port}/callback`
|
|
311
|
+
const code = new Promise((resolve, reject) => {
|
|
312
|
+
const finish = (error, value) => {
|
|
313
|
+
clearTimeout(timer)
|
|
259
314
|
server.close()
|
|
260
|
-
reject(
|
|
261
|
-
}
|
|
262
|
-
const
|
|
263
|
-
|
|
315
|
+
error ? reject(error) : resolve(value)
|
|
316
|
+
}
|
|
317
|
+
const timer = setTimeout(() => finish(new CliError('Giz sign-in timed out.', 'Run: giz auth login')), 5 * 60 * 1000)
|
|
318
|
+
server.on('request', (request, response) => {
|
|
319
|
+
const url = new URL(request.url || '/', callbackUrl)
|
|
264
320
|
if (url.pathname !== '/callback') {
|
|
265
321
|
response.writeHead(404).end('Not found')
|
|
266
322
|
return
|
|
267
323
|
}
|
|
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
324
|
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'))
|
|
325
|
+
response.writeHead(400).end('This sign-in link has expired. Use the link from your current giz auth login.')
|
|
326
|
+
return
|
|
288
327
|
}
|
|
328
|
+
const code = url.searchParams.get('code')
|
|
329
|
+
const error = url.searchParams.get('error') || !code
|
|
330
|
+
? new CliError(url.searchParams.get('error_description') || url.searchParams.get('error') || 'OAuth callback did not include a code.', 'Run: giz auth login')
|
|
331
|
+
: undefined
|
|
332
|
+
response.setHeader('content-type', 'text/html; charset=utf-8')
|
|
333
|
+
response.setHeader('connection', 'close')
|
|
334
|
+
response.end(error
|
|
335
|
+
? '<h1>Giz connection failed</h1><p>Return to the terminal for details.</p>'
|
|
336
|
+
: '<h1>Giz connected</h1><p>You can close this window and return to the terminal.</p>')
|
|
337
|
+
finish(error, code)
|
|
289
338
|
})
|
|
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')
|
|
339
|
+
server.on('error', finish)
|
|
298
340
|
})
|
|
341
|
+
return { url: callbackUrl, code }
|
|
299
342
|
}
|
|
300
343
|
|
|
301
344
|
function openBrowser(url) {
|
|
@@ -306,6 +349,23 @@ function openBrowser(url) {
|
|
|
306
349
|
child.unref()
|
|
307
350
|
}
|
|
308
351
|
|
|
352
|
+
// --input @FILE and --input - (stdin) carry JSON too large for one process argument (Linux caps one at 128 KiB).
|
|
353
|
+
async function inputText(value) {
|
|
354
|
+
if (value === '-') {
|
|
355
|
+
let text = ''
|
|
356
|
+
for await (const chunk of process.stdin) text += chunk
|
|
357
|
+
return text
|
|
358
|
+
}
|
|
359
|
+
if (typeof value === 'string' && value.startsWith('@')) {
|
|
360
|
+
try {
|
|
361
|
+
return await readFile(value.slice(1), 'utf8')
|
|
362
|
+
} catch {
|
|
363
|
+
throw new CliError(`Could not read ${value.slice(1)}.`, 'Pass --input @PATH to an existing JSON file.')
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
return value
|
|
367
|
+
}
|
|
368
|
+
|
|
309
369
|
function jsonInput(value) {
|
|
310
370
|
if (value === undefined || value === true) return {}
|
|
311
371
|
try {
|
|
@@ -340,14 +400,66 @@ async function applyUploads(baseUrl, input, values = []) {
|
|
|
340
400
|
return input
|
|
341
401
|
}
|
|
342
402
|
|
|
403
|
+
// Mutations use the user's reviewed App revision; fetching it silently would bypass conflict protection.
|
|
404
|
+
function appRevision(options) {
|
|
405
|
+
const raw = options['expected-revision']
|
|
406
|
+
if (raw === undefined || raw === '') {
|
|
407
|
+
throw new CliError('개정 번호가 필요합니다.', 'giz app list에서 확인한 revision을 --expected-revision으로 전달해 주세요.')
|
|
408
|
+
}
|
|
409
|
+
const revision = Number(raw)
|
|
410
|
+
if (typeof raw !== 'string' || !/^\d+$/.test(raw) || !Number.isSafeInteger(revision) || revision < 1 || revision > 9_007_199_254_740_990) {
|
|
411
|
+
throw new CliError('개정 번호는 1 이상의 안전한 정수여야 합니다.', 'giz app list에서 현재 revision을 확인해 주세요.')
|
|
412
|
+
}
|
|
413
|
+
return revision
|
|
414
|
+
}
|
|
415
|
+
|
|
343
416
|
async function invoke(baseUrl, tool, input) {
|
|
344
417
|
return await api(baseUrl, `/api/tools/${encodeURIComponent(tool)}`, input)
|
|
345
418
|
}
|
|
346
419
|
|
|
420
|
+
function mcpTransport(baseUrl) {
|
|
421
|
+
return new StreamableHTTPClientTransport(new URL('/api/mcp', baseUrl), {
|
|
422
|
+
fetch: (url, init) => authenticatedFetch(baseUrl, url, init)
|
|
423
|
+
})
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
async function serveMcp(baseUrl) {
|
|
427
|
+
const remote = mcpTransport(baseUrl)
|
|
428
|
+
const stdio = new StdioServerTransport()
|
|
429
|
+
try {
|
|
430
|
+
await remote.start()
|
|
431
|
+
await new Promise((resolve, reject) => {
|
|
432
|
+
remote.onerror = stdio.onerror = reject
|
|
433
|
+
remote.onmessage = (message) => {
|
|
434
|
+
if (message.result?.protocolVersion) remote.setProtocolVersion(message.result.protocolVersion)
|
|
435
|
+
stdio.send(message).catch(reject)
|
|
436
|
+
}
|
|
437
|
+
stdio.onmessage = (message) => { remote.send(message).catch(reject) }
|
|
438
|
+
process.stdin.once('end', resolve)
|
|
439
|
+
stdio.start().catch(reject)
|
|
440
|
+
})
|
|
441
|
+
} finally {
|
|
442
|
+
await remote.close()
|
|
443
|
+
await stdio.close()
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
347
447
|
async function runCommand(baseUrl, positionals, options) {
|
|
348
448
|
const [command, subcommand, ...rest] = positionals
|
|
349
|
-
if (options.version === true)
|
|
449
|
+
if (options.version === true) {
|
|
450
|
+
const revision = await managedRevision()
|
|
451
|
+
return { kind: 'message', value: CLI_VERSION + (revision ? ` (region ${revision.trim()})` : '') }
|
|
452
|
+
}
|
|
350
453
|
if (!command || command === 'help' || options.help === true) return { kind: 'help', value: help(command) }
|
|
454
|
+
if (command === 'engine' && subcommand === 'run') {
|
|
455
|
+
if (rest[0] !== 'claude') throw new CliError('Only Claude Code is an engine connection.')
|
|
456
|
+
await api(baseUrl, '/api/external/engine/claude/authorization')
|
|
457
|
+
const { runEngine } = await import('./engine.mjs')
|
|
458
|
+
const result = await runEngine(rest[0], required(rest[1], 'INSTRUCTION', 'Run: giz engine run claude "instruction"'), options.dir,
|
|
459
|
+
event => process.stdout.write(JSON.stringify(event) + '\n'))
|
|
460
|
+
if (result.status !== 'completed') process.exitCode = 1
|
|
461
|
+
return { kind: 'result', value: result }
|
|
462
|
+
}
|
|
351
463
|
if (command === 'auth' && subcommand === 'login') {
|
|
352
464
|
return { kind: 'auth', value: await login(baseUrl, options) }
|
|
353
465
|
}
|
|
@@ -355,24 +467,43 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
355
467
|
return { kind: 'auth', value: await api(baseUrl, '/api/external/me') }
|
|
356
468
|
}
|
|
357
469
|
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
|
|
470
|
+
await updateConfig(async (config) => {
|
|
471
|
+
if (config?.refresh_token) {
|
|
472
|
+
const metadata = await discovery(config.baseUrl)
|
|
473
|
+
const response = await fetch(metadata.revocation_endpoint, {
|
|
474
|
+
method: 'POST', signal: AbortSignal.timeout(30_000),
|
|
475
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
476
|
+
body: new URLSearchParams({ token: config.refresh_token, token_type_hint: 'refresh_token', client_id: CLIENT_ID })
|
|
368
477
|
})
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
await unlink(configPath).catch((error) => {
|
|
372
|
-
if (error?.code !== 'ENOENT') throw error
|
|
478
|
+
if (!response.ok) throw new CliError(`Giz sign-out failed: HTTP ${response.status}.`, 'Run: giz auth logout')
|
|
479
|
+
}
|
|
373
480
|
})
|
|
374
481
|
return { kind: 'message', value: 'Signed out of Giz on this device.' }
|
|
375
482
|
}
|
|
483
|
+
if (command === 'api-key' && subcommand === 'list') {
|
|
484
|
+
return { kind: 'result', value: await api(baseUrl, '/api/external/api-keys') }
|
|
485
|
+
}
|
|
486
|
+
if (command === 'api-key' && subcommand === 'create') {
|
|
487
|
+
const name = required(rest.join(' ') || options.name, 'NAME', 'Run: giz api-key create "My app"')
|
|
488
|
+
const scopes = options.scopes
|
|
489
|
+
? String(options.scopes).split(',').map((scope) => scope.trim()).filter(Boolean)
|
|
490
|
+
: undefined
|
|
491
|
+
return {
|
|
492
|
+
kind: 'result',
|
|
493
|
+
value: await api(baseUrl, '/api/external/api-keys', {
|
|
494
|
+
name,
|
|
495
|
+
...(scopes ? { scopes } : {}),
|
|
496
|
+
...(options.expires ? { expiresAt: options.expires } : {})
|
|
497
|
+
})
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
if (command === 'api-key' && subcommand === 'revoke') {
|
|
501
|
+
const id = required(rest[0], 'KEY', 'Run: giz api-key revoke key_...')
|
|
502
|
+
return {
|
|
503
|
+
kind: 'result',
|
|
504
|
+
value: await api(baseUrl, `/api/external/api-keys/${encodeURIComponent(id)}`, undefined, { method: 'DELETE' })
|
|
505
|
+
}
|
|
506
|
+
}
|
|
376
507
|
|
|
377
508
|
if (command === 'model' && subcommand === 'list') {
|
|
378
509
|
return {
|
|
@@ -395,6 +526,22 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
395
526
|
if (command === 'models') {
|
|
396
527
|
throw new CliError('Unknown command "models".', 'Did you mean: giz model list')
|
|
397
528
|
}
|
|
529
|
+
if (command === 'chat') {
|
|
530
|
+
const model = required(options.model || subcommand, 'MODEL', 'Run: giz chat MODEL "your prompt"')
|
|
531
|
+
const input = jsonInput(options.input)
|
|
532
|
+
const prompt = rest.join(' ').trim()
|
|
533
|
+
const messages = input.messages || (prompt ? [{ role: 'user', content: prompt }] : undefined)
|
|
534
|
+
required(messages, 'MESSAGES', 'Pass a prompt or --input \'{"messages":[...]}\'')
|
|
535
|
+
return {
|
|
536
|
+
kind: 'result',
|
|
537
|
+
value: await api(baseUrl, '/v1/chat/completions', {
|
|
538
|
+
...input,
|
|
539
|
+
model,
|
|
540
|
+
messages,
|
|
541
|
+
stream: false
|
|
542
|
+
})
|
|
543
|
+
}
|
|
544
|
+
}
|
|
398
545
|
|
|
399
546
|
if (command === 'generate' && ['image', 'video', 'audio', 'music', 'talking-head'].includes(subcommand)) {
|
|
400
547
|
const tool = {
|
|
@@ -414,7 +561,6 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
414
561
|
...(rest.length ? { prompt: rest.join(' ') } : {}),
|
|
415
562
|
...(options.model ? { model: options.model } : {})
|
|
416
563
|
}, uploads)
|
|
417
|
-
required(input.prompt, 'PROMPT', `Run: giz generate ${subcommand} "your prompt"`)
|
|
418
564
|
return { kind: 'job', value: await invoke(baseUrl, tool, input) }
|
|
419
565
|
}
|
|
420
566
|
|
|
@@ -433,7 +579,7 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
433
579
|
const id = required(rest[0], 'JOB', 'Run: giz job wait JOB')
|
|
434
580
|
while (true) {
|
|
435
581
|
const job = await invoke(baseUrl, 'job.get', { job: id })
|
|
436
|
-
if (['completed', 'failed', 'cancelled'].includes(job.status)) return { kind: 'job', value: job }
|
|
582
|
+
if (['completed', 'failed', 'cancelled'].includes(job.status)) return { kind: 'job', value: job, exitCode: job.status === 'completed' ? 0 : 1 }
|
|
437
583
|
if (process.stderr.isTTY) process.stderr.write(`\r${job.id} ${job.status} ${job.message || ''}`.slice(0, 120))
|
|
438
584
|
await new Promise((resolve) => setTimeout(resolve, 1000))
|
|
439
585
|
}
|
|
@@ -467,16 +613,47 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
467
613
|
path: drivePath,
|
|
468
614
|
content: String(content),
|
|
469
615
|
encoding,
|
|
470
|
-
...(options.computer ? {
|
|
616
|
+
...(options.computer ? { computer: options.computer } : {})
|
|
471
617
|
})
|
|
472
618
|
}
|
|
473
619
|
}
|
|
474
|
-
const mapping = mappings[subcommand]
|
|
620
|
+
const mapping = mappings[subcommand === 'ls' ? 'list' : subcommand]
|
|
475
621
|
if (!mapping) throw new CliError(`Unknown drive command "${subcommand || ''}".`, 'Run: giz drive --help')
|
|
476
622
|
const [tool, input] = mapping()
|
|
477
623
|
return {
|
|
478
624
|
kind: 'result',
|
|
479
|
-
value: await invoke(baseUrl, tool, { ...input, ...(options.computer ? {
|
|
625
|
+
value: await invoke(baseUrl, tool, { ...input, ...(options.computer ? { computer: options.computer } : {}) })
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
if (command === 'table' && subcommand === 'list') {
|
|
630
|
+
return { kind: 'result', value: await invoke(baseUrl, 'workspace.objects', {}) }
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
if (command === 'table' && subcommand === 'get') {
|
|
634
|
+
const object = required(rest[0], 'TABLE', 'Run: giz table get task')
|
|
635
|
+
return { kind: 'result', value: await invoke(baseUrl, 'workspace.objects', { object }) }
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (command === 'table' && subcommand === 'action') {
|
|
639
|
+
const view = required(rest[0], 'TABLE', 'Run: giz table action TABLE ACTION [--id ID] --input JSON')
|
|
640
|
+
const action = required(rest[1], 'ACTION', 'Run: giz table get TABLE to see its actions')
|
|
641
|
+
const id = options.id === undefined ? undefined : String(options.id).includes(',') ? String(options.id).split(',') : String(options.id)
|
|
642
|
+
return {
|
|
643
|
+
kind: 'result',
|
|
644
|
+
value: await invoke(baseUrl, 'workspace.executeAction', { view, action, ...(id === undefined ? {} : { dataId: id }), data: jsonInput(options.input) })
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (command === 'table' && ['read', 'write'].includes(subcommand)) {
|
|
649
|
+
const table = required(rest[0], 'TABLE', `Run: giz table ${subcommand} TABLE_PATH --input JSON`)
|
|
650
|
+
return {
|
|
651
|
+
kind: 'result',
|
|
652
|
+
value: await invoke(baseUrl, subcommand === 'read' ? 'table.readRows' : 'table.writeRows', {
|
|
653
|
+
...jsonInput(options.input),
|
|
654
|
+
table,
|
|
655
|
+
...(options.computer ? { computer: options.computer } : {})
|
|
656
|
+
})
|
|
480
657
|
}
|
|
481
658
|
}
|
|
482
659
|
|
|
@@ -487,28 +664,128 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
487
664
|
|
|
488
665
|
if (command === 'computer') {
|
|
489
666
|
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',
|
|
667
|
+
const computer = options.computer || (subcommand === 'exec' ? undefined : rest[0])
|
|
668
|
+
if (subcommand === 'get') return { kind: 'result', value: await invoke(baseUrl, 'computer.get', computer ? { computer } : {}) }
|
|
492
669
|
if (subcommand === 'start' || subcommand === 'stop') {
|
|
493
670
|
return {
|
|
494
671
|
kind: 'result',
|
|
495
|
-
value: await invoke(baseUrl, `computer.${subcommand}`,
|
|
672
|
+
value: await invoke(baseUrl, `computer.${subcommand}`, computer ? { computer } : {})
|
|
496
673
|
}
|
|
497
674
|
}
|
|
498
675
|
if (subcommand === 'exec') {
|
|
499
|
-
|
|
500
|
-
|
|
676
|
+
if (!rest.length) throw new CliError('COMMAND is required.', 'Run: giz computer exec -- ls -la')
|
|
677
|
+
const stdin = options['stdin-file'] ? await readFile(String(options['stdin-file']), 'utf8') : options.stdin
|
|
678
|
+
const value = await invoke(baseUrl, 'computer.exec', {
|
|
679
|
+
command: rest,
|
|
680
|
+
...(stdin !== undefined ? { stdin: String(stdin) } : {}),
|
|
681
|
+
...(options['deadline-ms'] !== undefined ? { deadlineMs: Number(options['deadline-ms']) } : {}),
|
|
682
|
+
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
683
|
+
...(computer ? { computer } : {})
|
|
684
|
+
})
|
|
685
|
+
return { kind: 'result', value, exitCode: value.exitCode }
|
|
686
|
+
}
|
|
687
|
+
throw new CliError(`Unknown computer command "${subcommand || ''}".`, 'Run: giz computer --help')
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
if (command === 'app') {
|
|
691
|
+
const computer = options.computer ? { computer: options.computer } : {}
|
|
692
|
+
if (subcommand === 'list') {
|
|
693
|
+
return { kind: 'result', value: await invoke(baseUrl, 'app.list', computer) }
|
|
694
|
+
}
|
|
695
|
+
if (subcommand === 'save' || subcommand === 'create') {
|
|
696
|
+
const input = jsonInput(options.input)
|
|
697
|
+
return { kind: 'result', value: await invoke(baseUrl, 'app.save', { ...input, ...computer }) }
|
|
698
|
+
}
|
|
699
|
+
if (subcommand === 'run') {
|
|
700
|
+
const id = required(rest[0], 'APP_ID', 'Run: giz app run APP_ID --expected-revision REVISION')
|
|
701
|
+
return { kind: 'result', value: await invoke(baseUrl, 'app.run', { ...computer, id, expectedRevision: appRevision(options) }) }
|
|
702
|
+
}
|
|
703
|
+
if (subcommand === 'publish') {
|
|
704
|
+
const id = required(rest[0], 'APP_ID', 'Run: giz app publish APP_ID private|workspace|public')
|
|
705
|
+
const visibility = required(rest[1] || options.visibility, 'VISIBILITY', 'Run: giz app publish APP_ID private|workspace|public')
|
|
706
|
+
return { kind: 'result', value: await invoke(baseUrl, 'app.publish', { ...computer, id, visibility, expectedRevision: appRevision(options) }) }
|
|
707
|
+
}
|
|
708
|
+
if (subcommand === 'open') {
|
|
709
|
+
const id = required(rest[0], 'APP_ID', 'Run: giz app open APP_ID')
|
|
710
|
+
return { kind: 'result', value: await invoke(baseUrl, 'app.open', { ...computer, id }) }
|
|
711
|
+
}
|
|
712
|
+
if (subcommand === 'delete') {
|
|
713
|
+
const id = required(rest[0], 'APP_ID', 'Run: giz app delete APP_ID')
|
|
714
|
+
return { kind: 'result', value: await invoke(baseUrl, 'app.delete', { ...computer, id, expectedRevision: appRevision(options) }) }
|
|
715
|
+
}
|
|
716
|
+
throw new CliError(`Unknown app command "${subcommand || ''}".`, 'Run: giz app --help')
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
if (command === 'capability') {
|
|
720
|
+
if (subcommand === 'list') {
|
|
501
721
|
return {
|
|
502
722
|
kind: 'result',
|
|
503
|
-
value: await invoke(baseUrl, '
|
|
504
|
-
|
|
505
|
-
sessionId: String(options.session || 'giz-cli'),
|
|
506
|
-
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
507
|
-
...(options.computer ? { computerId: options.computer } : {})
|
|
723
|
+
value: await invoke(baseUrl, 'capability.status', {
|
|
724
|
+
...(options.computer ? { computer: options.computer } : {})
|
|
508
725
|
})
|
|
509
726
|
}
|
|
510
727
|
}
|
|
511
|
-
|
|
728
|
+
if (subcommand === 'search') {
|
|
729
|
+
const kind = required(rest[0], 'KIND', 'Run: giz capability search skill|mcp QUERY')
|
|
730
|
+
const query = required(rest.slice(1).join(' '), 'QUERY', 'Run: giz capability search skill|mcp QUERY')
|
|
731
|
+
return {
|
|
732
|
+
kind: 'result',
|
|
733
|
+
value: await invoke(baseUrl, 'capability.catalog', {
|
|
734
|
+
kind,
|
|
735
|
+
query,
|
|
736
|
+
...(options.limit ? { limit: Number(options.limit) } : {})
|
|
737
|
+
})
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (subcommand === 'install') {
|
|
741
|
+
const kind = required(rest[0], 'KIND', 'Run: giz capability install skill|mcp ID --query QUERY')
|
|
742
|
+
const id = required(rest[1], 'ID', 'Run: giz capability install skill|mcp ID --query QUERY')
|
|
743
|
+
const query = required(options.query, 'QUERY', 'Repeat the catalog query with --query QUERY.')
|
|
744
|
+
return {
|
|
745
|
+
kind: 'result',
|
|
746
|
+
value: await invoke(baseUrl, 'capability.install', {
|
|
747
|
+
kind,
|
|
748
|
+
id,
|
|
749
|
+
query,
|
|
750
|
+
...(options.computer ? { computer: options.computer } : {})
|
|
751
|
+
})
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
if (subcommand === 'update' || subcommand === 'remove') {
|
|
755
|
+
const id = required(rest[0], 'ID', `Run: giz capability ${subcommand} ID`)
|
|
756
|
+
return {
|
|
757
|
+
kind: 'result',
|
|
758
|
+
value: await invoke(baseUrl, 'capability.manage', {
|
|
759
|
+
id,
|
|
760
|
+
operation: subcommand,
|
|
761
|
+
...(options.computer ? { computer: options.computer } : {})
|
|
762
|
+
})
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
throw new CliError(`Unknown capability command "${subcommand || ''}".`, 'Run: giz capability --help')
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (command === 'agent') {
|
|
769
|
+
if (subcommand === 'start') {
|
|
770
|
+
return {
|
|
771
|
+
kind: 'result',
|
|
772
|
+
value: await invoke(baseUrl, 'agent.start', {
|
|
773
|
+
...jsonInput(options.input),
|
|
774
|
+
...(options.computer ? { computer: options.computer } : {})
|
|
775
|
+
})
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (subcommand === 'status' || subcommand === 'cancel') {
|
|
779
|
+
const turnId = required(rest[0], 'TURN', `Run: giz agent ${subcommand} TURN`)
|
|
780
|
+
return {
|
|
781
|
+
kind: 'result',
|
|
782
|
+
value: await invoke(baseUrl, `agent.${subcommand}`, {
|
|
783
|
+
turnId,
|
|
784
|
+
...(subcommand === 'status' && options.wait ? { wait: true } : {})
|
|
785
|
+
})
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
throw new CliError(`Unknown agent command "${subcommand || ''}".`, 'Run: giz agent --help')
|
|
512
789
|
}
|
|
513
790
|
|
|
514
791
|
if (command === 'invoke') {
|
|
@@ -519,90 +796,133 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
519
796
|
if (command === 'mcp' && subcommand === 'url') {
|
|
520
797
|
return { kind: 'message', value: new URL('/api/mcp', baseUrl).href }
|
|
521
798
|
}
|
|
799
|
+
if (command === 'mcp' && subcommand === 'serve') {
|
|
800
|
+
await serveMcp(baseUrl)
|
|
801
|
+
return
|
|
802
|
+
}
|
|
803
|
+
if (command === 'mcp' && subcommand === 'check') {
|
|
804
|
+
const client = new Client({ name: 'giz-cli', version: CLI_VERSION })
|
|
805
|
+
try {
|
|
806
|
+
await client.connect(mcpTransport(baseUrl))
|
|
807
|
+
const { tools } = await client.listTools()
|
|
808
|
+
if (!tools.length) throw new CliError('Giz MCP returned zero tools.', 'Check your Giz connection and scopes.')
|
|
809
|
+
return { kind: 'result', value: { server: client.getServerVersion(), tools: tools.map((tool) => tool.name) } }
|
|
810
|
+
} finally { await client.close() }
|
|
811
|
+
}
|
|
522
812
|
if (command === 'mcp' && subcommand === 'add') {
|
|
523
813
|
const client = required(rest[0], 'CLIENT', 'Run: giz mcp add codex|claude')
|
|
814
|
+
if (!['codex', 'claude'].includes(client)) throw new CliError(`Unsupported MCP client "${client}".`, 'Supported clients: codex, claude')
|
|
815
|
+
const transport = options.transport || 'stdio'
|
|
816
|
+
if (!['stdio', 'http'].includes(transport)) throw new CliError('MCP transport must be stdio or http.')
|
|
524
817
|
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')
|
|
818
|
+
const target = transport === 'stdio'
|
|
819
|
+
? ['--env', `GIZ_CONFIG=${configPath}`, '--', process.execPath, fileURLToPath(import.meta.url), 'mcp', 'serve', '--base-url', baseUrl]
|
|
820
|
+
: client === 'codex' ? ['--url', endpoint, '--oauth-resource', endpoint] : [endpoint]
|
|
821
|
+
runExternal(client, ['mcp', 'add', ...(client === 'claude' ? ['--scope', 'user', '--transport', transport] : []), 'giz', ...target],
|
|
822
|
+
`Install ${client === 'codex' ? 'Codex: npm install -g @openai/codex' : 'Claude Code: npm install -g @anthropic-ai/claude-code'}`, transport === 'http')
|
|
823
|
+
return { kind: 'message', value: `Giz was added to ${client} (${transport}). ${transport === 'stdio'
|
|
824
|
+
? 'Run giz auth login, then giz mcp check --json before starting a headless session.'
|
|
825
|
+
: client === 'claude' ? 'Open Claude Code and run /mcp to sign in.' : 'Complete the browser approval opened by Codex.'}` }
|
|
534
826
|
}
|
|
535
827
|
|
|
536
828
|
if (command === 'skill' && subcommand === 'install') {
|
|
537
829
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'skills')
|
|
538
|
-
runExternal('npx', ['--yes', 'skills', 'add', root, '--
|
|
830
|
+
runExternal('npx', ['--yes', 'skills', 'add', root, '--skill', '*', '--agent', 'universal', '--copy', '-y'], 'Install Node.js 20 or newer first.')
|
|
539
831
|
return { kind: 'message', value: 'Giz Skill installed for available AI agents.' }
|
|
540
832
|
}
|
|
541
833
|
|
|
542
834
|
throw new CliError(`Unknown command "${[command, subcommand].filter(Boolean).join(' ')}".`, 'Run: giz --help')
|
|
543
835
|
}
|
|
544
836
|
|
|
545
|
-
function runExternal(command, args, hint) {
|
|
546
|
-
const result = spawnSync(command, args, { stdio: 'inherit' })
|
|
837
|
+
function runExternal(command, args, hint, interactive = false) {
|
|
838
|
+
const result = spawnSync(command, args, { encoding: 'utf8', stdio: interactive ? ['inherit', 2, 2] : ['inherit', 'pipe', 'pipe'] })
|
|
547
839
|
if (result.error?.code === 'ENOENT') throw new CliError(`${command} is not installed.`, hint)
|
|
548
|
-
if (result.status !== 0) throw new CliError(
|
|
840
|
+
if (result.error || result.status !== 0) throw new CliError(
|
|
841
|
+
`${command} failed (${result.error?.code || result.signal || result.status}): ${(result.stderr || result.stdout || result.error?.message || '').trim()}`
|
|
842
|
+
)
|
|
843
|
+
if (result.stderr || result.stdout) process.stderr.write(result.stderr + result.stdout)
|
|
549
844
|
}
|
|
550
845
|
|
|
551
|
-
function print(kind, value, json) {
|
|
846
|
+
function print(kind, value, json, output = process.stdout) {
|
|
552
847
|
if (json) {
|
|
553
|
-
|
|
848
|
+
output.write(`${JSON.stringify(value, null, 2)}\n`)
|
|
554
849
|
return
|
|
555
850
|
}
|
|
556
851
|
if (kind === 'help' || kind === 'message') {
|
|
557
|
-
|
|
852
|
+
output.write(`${value}\n`)
|
|
853
|
+
return
|
|
854
|
+
}
|
|
855
|
+
if (kind === 'auth' && value.identity?.type === 'computer') {
|
|
856
|
+
output.write(`컴퓨터 신원으로 로그인됨 (소유자 ${value.email || value.name})\n`)
|
|
558
857
|
return
|
|
559
858
|
}
|
|
560
859
|
if (kind === 'models') {
|
|
561
860
|
const rows = value.models || []
|
|
562
|
-
for (const model of rows)
|
|
563
|
-
|
|
861
|
+
for (const model of rows) output.write(`${model.id}\t${model.type}\t${model.provider || '-'}\t${model.label}\n`)
|
|
862
|
+
output.write(`\n${rows.length} models\n`)
|
|
564
863
|
return
|
|
565
864
|
}
|
|
566
865
|
if (kind === 'jobs') {
|
|
567
|
-
for (const job of value.jobs || [])
|
|
866
|
+
for (const job of value.jobs || []) output.write(`${job.id}\t${job.status}\t${job.tool}\t${job.createdAt}\n`)
|
|
568
867
|
return
|
|
569
868
|
}
|
|
570
869
|
if (kind === 'computers') {
|
|
571
870
|
for (const computer of value.computers || []) {
|
|
572
|
-
|
|
871
|
+
output.write(`${computer.id}\t${computer.status || '-'}\t${computer.isDefault ? 'default' : '-'}\t${computer.name}\n`)
|
|
573
872
|
}
|
|
574
873
|
return
|
|
575
874
|
}
|
|
576
875
|
if (kind === 'job') {
|
|
577
|
-
|
|
578
|
-
if (value.message)
|
|
579
|
-
if (value.error)
|
|
580
|
-
if (value.result)
|
|
876
|
+
output.write(`${value.id} ${value.status}\n`)
|
|
877
|
+
if (value.message) output.write(`${value.message}\n`)
|
|
878
|
+
if (value.error) output.write(`Error: ${value.error}\n`)
|
|
879
|
+
if (value.result) output.write(`${JSON.stringify(value.result, null, 2)}\n`)
|
|
581
880
|
if (!['completed', 'failed', 'cancelled'].includes(value.status)) {
|
|
582
|
-
|
|
881
|
+
output.write(`Run: giz job wait ${value.id}\n`)
|
|
583
882
|
}
|
|
584
883
|
return
|
|
585
884
|
}
|
|
586
|
-
|
|
885
|
+
output.write(`${JSON.stringify(value, null, 2)}\n`)
|
|
587
886
|
}
|
|
588
887
|
|
|
589
888
|
function help(command) {
|
|
889
|
+
if (command === 'engine') return `Usage
|
|
890
|
+
giz engine run claude "instruction" [--dir path]
|
|
891
|
+
|
|
892
|
+
Runs the official Claude Code in an isolated worktree. Connect Claude Code in Giz first.
|
|
893
|
+
Progress and the final diff, files and summary are returned. Interrupting stops the engine.`
|
|
894
|
+
if (command === 'api-key') return `Usage
|
|
895
|
+
giz api-key list
|
|
896
|
+
giz api-key create "My app" [--scopes giz.inference,giz.read] [--expires ISO_TIMESTAMP]
|
|
897
|
+
giz api-key revoke KEY_ID
|
|
898
|
+
|
|
899
|
+
The secret is shown only when the key is created.`
|
|
900
|
+
if (command === 'chat') return `Usage
|
|
901
|
+
giz chat MODEL "your prompt"
|
|
902
|
+
giz chat MODEL --input '{"messages":[{"role":"user","content":"Hello"}]}'`
|
|
590
903
|
if (command === 'model') return `Usage
|
|
591
904
|
giz model list [--type TYPE] [--search TEXT]
|
|
592
905
|
giz model get MODEL
|
|
593
906
|
giz model run MODEL --input JSON [--upload FIELD=LOCAL_PATH]
|
|
594
907
|
|
|
595
|
-
Run model get first. Its live input schema is the source of truth. Repeat --upload for multiple media inputs
|
|
908
|
+
Run model get first. Its live input schema is the source of truth. Repeat --upload for multiple media inputs.
|
|
909
|
+
model run returns a job immediately. Retrieve the result with giz job wait JOB_ID, or poll with giz job get JOB_ID.
|
|
910
|
+
For speech-to-text, use model run with the audio field returned by model get. Callback/webhook delivery is not supported.`
|
|
596
911
|
if (command === 'generate') return `Usage
|
|
597
|
-
giz generate image|video|
|
|
912
|
+
giz generate image|video|music|talking-head "PROMPT" [--model MODEL]
|
|
913
|
+
giz generate audio --input '{"purpose":"speech","script":"Hello"}' [--model MODEL]
|
|
914
|
+
giz generate audio --input '{"purpose":"sound","prompt":"Rain","duration":5}'
|
|
598
915
|
|
|
599
916
|
Options
|
|
600
917
|
--image LOCAL_PATH Upload a visual reference
|
|
601
918
|
--audio LOCAL_PATH Upload speech audio
|
|
602
919
|
--upload FIELD=PATH Upload into an exact tool field
|
|
603
|
-
--input JSON
|
|
920
|
+
--input JSON Input matching the tool schema (@FILE reads a file, - reads stdin)
|
|
921
|
+
|
|
922
|
+
For model-specific audio options, inspect giz model get MODEL and pass supported fields in settings (for example settings.speed), not at the top level.
|
|
923
|
+
Generation returns a job immediately; use giz job wait JOB_ID for its result.`
|
|
604
924
|
if (command === 'drive') return `Usage
|
|
605
|
-
giz drive
|
|
925
|
+
giz drive ls [PATH] (list alias)
|
|
606
926
|
giz drive search QUERY
|
|
607
927
|
giz drive read PATH
|
|
608
928
|
giz drive write PATH --content TEXT
|
|
@@ -610,47 +930,81 @@ Options
|
|
|
610
930
|
giz drive mkdir PATH
|
|
611
931
|
giz drive move FROM TO
|
|
612
932
|
giz drive delete PATH`
|
|
933
|
+
if (command === 'table') return `Usage
|
|
934
|
+
giz table list
|
|
935
|
+
giz table get TABLE
|
|
936
|
+
giz table read TABLE [--input JSON] [--computer COMPUTER]
|
|
937
|
+
giz table write TABLE --input JSON [--computer COMPUTER]
|
|
938
|
+
giz table action TABLE ACTION [--id ID[,ID...]] --input JSON
|
|
939
|
+
|
|
940
|
+
TABLE is a table's Drive path (for example Sales/Customers.view.ts) or a business object name from giz table list
|
|
941
|
+
(for example task, issue, requirement). giz table get shows its fields, the row changes it accepts (writes) and its
|
|
942
|
+
actions; an object without a write changes only through its actions. Use --org to work in an organization.
|
|
943
|
+
read input: {"select":["name"],"filters":[{"field":"status","operator":"eq","value":"open"}],"limit":50}
|
|
944
|
+
write input: {"create":[{"name":"Acme"}],"update":[{"id":"ROW_ID","status":"done"}],"remove":["ROW_ID"]}
|
|
945
|
+
One write is one change people can undo in the table. At most 200 rows per write.`
|
|
613
946
|
if (command === 'computer') return `Usage
|
|
614
947
|
giz computer list
|
|
615
948
|
giz computer get|start|stop [COMPUTER]
|
|
616
|
-
giz computer exec [--computer COMPUTER] [--cwd PATH] -- COMMAND [ARGS...]`
|
|
949
|
+
giz computer exec [--computer COMPUTER] [--cwd PATH] [--deadline-ms MS] [--stdin TEXT|--stdin-file PATH] -- COMMAND [ARGS...]`
|
|
950
|
+
if (command === 'app') return `Usage
|
|
951
|
+
giz app list [--computer COMPUTER]
|
|
952
|
+
giz app create|save [--computer COMPUTER] --input JSON
|
|
953
|
+
giz app run APP_ID --expected-revision REVISION [--computer COMPUTER]
|
|
954
|
+
giz app publish APP_ID private|workspace|public --expected-revision REVISION [--computer COMPUTER]
|
|
955
|
+
giz app open APP_ID [--computer COMPUTER]
|
|
956
|
+
giz app delete APP_ID --expected-revision REVISION [--computer COMPUTER]`
|
|
957
|
+
if (command === 'capability') return `Usage
|
|
958
|
+
giz capability list [--computer COMPUTER]
|
|
959
|
+
giz capability search skill|mcp QUERY
|
|
960
|
+
giz capability install skill|mcp ID --query QUERY [--computer COMPUTER]
|
|
961
|
+
giz capability update|remove ID [--computer COMPUTER]`
|
|
962
|
+
if (command === 'agent') return `Usage
|
|
963
|
+
giz agent start [--computer COMPUTER] --input JSON
|
|
964
|
+
giz agent status TURN [--wait]
|
|
965
|
+
giz agent cancel TURN`
|
|
617
966
|
if (command === 'mcp') return `Usage
|
|
618
967
|
giz mcp url
|
|
619
|
-
giz mcp
|
|
620
|
-
giz mcp
|
|
968
|
+
giz mcp serve
|
|
969
|
+
giz mcp check --json
|
|
970
|
+
giz mcp add codex|claude [--transport stdio|http]
|
|
971
|
+
|
|
972
|
+
stdio (default) shares giz auth login and refreshes tokens automatically.
|
|
973
|
+
http uses the client's own OAuth login. Install the chosen client first.`
|
|
621
974
|
if (command === 'auth') return `Usage
|
|
622
|
-
giz auth login
|
|
975
|
+
giz auth login [--device]
|
|
623
976
|
giz auth status
|
|
624
977
|
giz auth logout`
|
|
625
|
-
return `Giz CLI —
|
|
978
|
+
return `Giz CLI — AI models, Drive, and Computer from any terminal
|
|
626
979
|
|
|
627
980
|
Usage
|
|
628
|
-
giz auth login
|
|
981
|
+
giz auth login [--device]
|
|
982
|
+
giz api-key list|create|revoke
|
|
983
|
+
giz chat MODEL "your prompt"
|
|
629
984
|
giz model list [--type TYPE] [--search TEXT]
|
|
630
985
|
giz model get MODEL
|
|
631
986
|
giz model run MODEL --input JSON [--upload FIELD=LOCAL_PATH]
|
|
632
|
-
giz generate image|video|
|
|
987
|
+
giz generate image|video|music|talking-head "PROMPT" [--model MODEL]
|
|
988
|
+
giz generate audio --input JSON [--model MODEL]
|
|
633
989
|
giz job list|get|wait|cancel [JOB]
|
|
634
|
-
giz drive list|search|read|write|mkdir|move|delete
|
|
990
|
+
giz drive ls|list|search|read|write|mkdir|move|delete
|
|
991
|
+
giz table list|get|read|write|action
|
|
635
992
|
giz upload LOCAL_PATH
|
|
636
993
|
giz computer list|get|start|stop|exec
|
|
637
|
-
giz
|
|
994
|
+
giz app list|create|save|run|publish|open|delete
|
|
995
|
+
giz capability list|search|install|update|remove
|
|
996
|
+
giz agent start|status|cancel
|
|
997
|
+
giz engine run claude "instruction" [--dir path]
|
|
998
|
+
giz mcp url|serve|check|add
|
|
638
999
|
giz skill install
|
|
639
1000
|
giz invoke TOOL --input JSON
|
|
640
1001
|
|
|
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
|
|
1002
|
+
Use giz COMMAND --help for parameters and examples.
|
|
650
1003
|
|
|
651
1004
|
Global options
|
|
652
1005
|
--json Machine-readable JSON output
|
|
653
1006
|
--base-url URL Another Giz deployment
|
|
1007
|
+
--org ORGANIZATION Organization id to work in (or GIZ_ORGANIZATION)
|
|
654
1008
|
--no-open Print the login URL without opening a browser
|
|
655
1009
|
|
|
656
1010
|
Docs: https://www.giz.ai/mcp/`
|
|
@@ -659,24 +1013,23 @@ Docs: https://www.giz.ai/mcp/`
|
|
|
659
1013
|
async function main() {
|
|
660
1014
|
const { positionals, options } = parse(process.argv.slice(2))
|
|
661
1015
|
const saved = await readConfig()
|
|
662
|
-
const
|
|
1016
|
+
const requiresAuth = positionals.length && !options.help && !options.version &&
|
|
1017
|
+
!(positionals[0] === 'auth' && ['login', 'logout'].includes(positionals[1]))
|
|
1018
|
+
const computer = requiresAuth && !process.env.GIZ_TOKEN && !saved?.access_token ? await computerIdentity() : undefined
|
|
1019
|
+
const baseUrl = String(options['base-url'] || process.env.GIZ_URL || saved?.baseUrl || computer?.baseUrl || DEFAULT_URL).replace(/\/+$/, '')
|
|
1020
|
+
if (typeof options.org === 'string') process.env.GIZ_ORGANIZATION = options.org
|
|
1021
|
+
options.input = await inputText(options.input)
|
|
663
1022
|
const output = await runCommand(baseUrl, positionals, options)
|
|
664
|
-
|
|
1023
|
+
if (output) {
|
|
1024
|
+
print(output.kind, output.value, options.json === true)
|
|
1025
|
+
process.exitCode = output.exitCode ?? (output.value?.isError ? 1 : 0)
|
|
1026
|
+
}
|
|
665
1027
|
}
|
|
666
1028
|
|
|
667
1029
|
main().catch((error) => {
|
|
668
1030
|
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
|
-
}
|
|
1031
|
+
const json = options.json === true
|
|
1032
|
+
const failure = { message: String(error?.message || error), hint: error?.hint, status: error?.status }
|
|
1033
|
+
print('message', json ? { error: failure } : `Error: ${failure.message}${failure.hint ? `\nHint: ${failure.hint}` : ''}`, json, process.stderr)
|
|
681
1034
|
process.exitCode = 1
|
|
682
1035
|
})
|