@gizai/cli 1.1.0 → 1.3.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 +89 -1
- package/cli/codex-relay.mjs +46 -0
- package/cli/computer-identity.mjs +76 -0
- package/cli/engine.mjs +58 -0
- package/cli/giz.mjs +237 -51
- package/package.json +3 -3
- package/skills/giz/SKILL.md +61 -1
- package/skills/giz/references/operations.md +3 -0
- package/skills/giz/references/slides.md +95 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@ Connect once in the browser, then use live Giz text and media models, Drive, and
|
|
|
5
5
|
```bash
|
|
6
6
|
npm install -g @gizai/cli
|
|
7
7
|
giz auth login
|
|
8
|
-
giz chat gpt-
|
|
8
|
+
giz chat gpt-6-luna "Say hello in Korean"
|
|
9
9
|
giz generate image "A paper observatory at blue hour"
|
|
10
10
|
```
|
|
11
11
|
|
|
@@ -77,3 +77,91 @@ that runtime. Media jobs run independently and are tracked with `giz job get`.
|
|
|
77
77
|
Create a scoped API key with `giz api-key create "My app"`; its secret is shown once. Set `GIZ_TOKEN` only for ephemeral automation. Run `giz --help` for chat, models, Drive, Computer, job, upload, and raw tool commands.
|
|
78
78
|
|
|
79
79
|
Documentation: <https://www.giz.ai/mcp/>
|
|
80
|
+
|
|
81
|
+
### Managed Giz Computers
|
|
82
|
+
|
|
83
|
+
Giz Computers receive the same CLI source through the Region release overlay, at
|
|
84
|
+
`/opt/giz/managed/apps/giz-cli`, with `/opt/giz/managed/bin/giz` first on PATH.
|
|
85
|
+
No npm installation or browser login is needed inside a managed Computer:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
giz auth status
|
|
89
|
+
giz drive ls
|
|
90
|
+
giz model list
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The Region owns `/opt/giz/identity/identity.sock` and exchanges the Computer identity for a
|
|
94
|
+
15-minute access token. For this release only, when the new socket path is absent,
|
|
95
|
+
the CLI reads `/run/giz/identity.sock` to support an overlay arriving before its
|
|
96
|
+
gateway update. No environment variable can override the socket path. The CLI
|
|
97
|
+
caches it only in memory; it never saves a refresh
|
|
98
|
+
token or API key for this identity. `giz auth status` displays
|
|
99
|
+
`컴퓨터 신원으로 로그인됨 (소유자 <email>)`. A saved `giz auth login` takes priority;
|
|
100
|
+
`giz auth logout` removes that user login and returns to the Computer identity.
|
|
101
|
+
Shared Computers can access only their own Drive, model discovery/inference, and
|
|
102
|
+
jobs started by that Computer. API key, billing, organization administration,
|
|
103
|
+
destructive operations and external writes require a separate user authorization.
|
|
104
|
+
The managed `giz --version` includes the Region source revision; npm installations
|
|
105
|
+
continue to report the package version. Existing Computers receive overlay updates
|
|
106
|
+
without recreation. The Region's normal release activation attaches identity sockets
|
|
107
|
+
for existing instances, and socket replacement remains visible through the directory mount.
|
|
108
|
+
|
|
109
|
+
App 변경은 `giz app list`에서 확인한 `revision`을 명시적으로 전달합니다.
|
|
110
|
+
`run`, `publish`, `delete`에는 `--expected-revision REVISION`이 필요합니다.
|
|
111
|
+
`create`/`save`의 `--input`에는 `id`와 `expectedRevision`을 넣습니다(새 앱은 0).
|
|
112
|
+
목록을 다시 읽어 개정 번호를 자동 대체하지 않으므로, 검토 뒤 다른 변경이 생기면 서버가 충돌을 거부합니다.
|
|
113
|
+
|
|
114
|
+
아래 표는 CLI 디스패처와 도구 item에서 생성합니다. 갱신 명령:
|
|
115
|
+
`GIZ_UPDATE_CLI_SURFACE=1 pnpm --filter giz-app-giz test --runInBand --runTestsByPath server/external/cli-surface.spec.ts`.
|
|
116
|
+
일반 CI 실행은 문서를 쓰지 않고 표의 차이를 실패로 보고합니다.
|
|
117
|
+
|
|
118
|
+
<!-- cli-surface:start -->
|
|
119
|
+
| CLI 명령 | 도구 ID | MCP 이름 | 필수 입력 | scope | Agent 발견 |
|
|
120
|
+
| --- | --- | --- | --- | --- | --- |
|
|
121
|
+
| model list | model.list | model_list | - | giz.read | model.list |
|
|
122
|
+
| model get | model.get | model_get | model | giz.read | model.get |
|
|
123
|
+
| model run | model.run | model_run | model, input | giz.inference | model.run |
|
|
124
|
+
| generate image | generateImage | generateImage | prompt | giz.write | generateImage |
|
|
125
|
+
| generate video | generateVideo | generateVideo | prompt | giz.write | generateVideo |
|
|
126
|
+
| generate music | generateMusic | generateMusic | prompt | giz.write | generateMusic |
|
|
127
|
+
| generate talking-head | generateTalkingHead | generateTalkingHead | prompt, references | giz.write | generateTalkingHead |
|
|
128
|
+
| generate audio | generateAudio | generateAudio | purpose | giz.write | generateAudio |
|
|
129
|
+
| job list | job.list | job_list | - | giz.read | server job |
|
|
130
|
+
| job get | job.get | job_get | job | giz.read | server job |
|
|
131
|
+
| job wait | job.get | job_get | job | giz.read | server job |
|
|
132
|
+
| job cancel | job.cancel | job_cancel | job | giz.destructive | server job |
|
|
133
|
+
| credits | account.credits | account_credits | - | giz.read | account.credits |
|
|
134
|
+
| drive ls | drive.list | drive_list | - | giz.read | drive.list |
|
|
135
|
+
| drive list | drive.list | drive_list | - | giz.read | drive.list |
|
|
136
|
+
| drive search | drive.search | drive_search | query | giz.read | drive.search |
|
|
137
|
+
| drive read | drive.readFile | drive_readFile | path | giz.read | drive.readFile |
|
|
138
|
+
| drive write | drive.writeFile | drive_writeFile | path, content | giz.write | drive.writeFile |
|
|
139
|
+
| drive mkdir | drive.makeDirectory | drive_makeDirectory | path | giz.write | drive.makeDirectory |
|
|
140
|
+
| drive move | drive.movePath | drive_movePath | from, to | giz.write | drive.movePath |
|
|
141
|
+
| drive delete | drive.deletePath | drive_deletePath | path | giz.destructive | drive.deletePath |
|
|
142
|
+
| table list | workspace.objects | workspace_objects | - | giz.read | workspace.objects |
|
|
143
|
+
| table get | workspace.objects | workspace_objects | - | giz.read | workspace.objects |
|
|
144
|
+
| table read | table.readRows | table_readRows | table | giz.read | table.readRows |
|
|
145
|
+
| table write | table.writeRows | table_writeRows | table | giz.write | table.writeRows |
|
|
146
|
+
| table action | workspace.executeAction | workspace_executeAction | view, action | giz.execute | workspace.executeAction |
|
|
147
|
+
| computer list | computer.list | computer_list | - | giz.read | computer.list |
|
|
148
|
+
| computer get | computer.get | computer_get | - | giz.read | external only |
|
|
149
|
+
| computer start | computer.start | computer_start | - | giz.execute | external only |
|
|
150
|
+
| computer stop | computer.stop | computer_stop | - | giz.destructive | external only |
|
|
151
|
+
| computer exec | computer.exec | giz_computer_exec | - | giz.execute | computer.exec |
|
|
152
|
+
| app list | app.list | app_list | - | giz.read | app.list |
|
|
153
|
+
| app create | app.save | app_save | id, expectedRevision | giz.execute | app.save |
|
|
154
|
+
| app save | app.save | app_save | id, expectedRevision | giz.execute | app.save |
|
|
155
|
+
| app run | app.run | app_run | id, expectedRevision | giz.execute | app.run |
|
|
156
|
+
| app publish | app.publish | app_publish | id, expectedRevision, visibility | giz.write | app.publish |
|
|
157
|
+
| app open | app.open | app_open | id | giz.read | app.open |
|
|
158
|
+
| app delete | app.delete | app_delete | id, expectedRevision | giz.destructive | app.delete |
|
|
159
|
+
| capability list | capability.status | capability_status | - | giz.read | capability.status |
|
|
160
|
+
| capability search | capability.catalog | capability_catalog | kind, query | giz.read | capability.catalog |
|
|
161
|
+
| capability install | capability.install | capability_install | id, kind, query | giz.execute | capability.install |
|
|
162
|
+
| capability update | capability.manage | capability_manage | id, operation | giz.destructive | capability.manage |
|
|
163
|
+
| capability remove | capability.manage | capability_manage | id, operation | giz.destructive | capability.manage |
|
|
164
|
+
| agent start | agent.start | agent_start | agentId, prompt | giz.execute | external only |
|
|
165
|
+
| agent status | agent.status | agent_status | turnId | giz.read | agent.status |
|
|
166
|
+
| agent cancel | agent.cancel | agent_cancel | turnId | giz.execute | agent.cancel |
|
|
167
|
+
<!-- cli-surface:end -->
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Runs ONLY inside the user's Computer. stdout is the authenticated gateway wire,
|
|
2
|
+
// never credentials or provider error bodies. No listener or separate credential store.
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { execFileSync } from 'node:child_process'
|
|
7
|
+
|
|
8
|
+
const emit = value => process.stdout.write(JSON.stringify(value) + '\n')
|
|
9
|
+
try {
|
|
10
|
+
const auth = JSON.parse(await readFile(join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'auth.json'), 'utf8'))
|
|
11
|
+
const token = auth.tokens?.access_token
|
|
12
|
+
const account = auth.tokens?.account_id
|
|
13
|
+
if (auth.OPENAI_API_KEY || !token || !account) {
|
|
14
|
+
emit({ status: 401 }); process.exitCode = 1
|
|
15
|
+
} else {
|
|
16
|
+
const headers = { authorization: `Bearer ${token}`, 'chatgpt-account-id': account, 'content-type': 'application/json', originator: 'codex_cli_rs' }
|
|
17
|
+
const mode = process.argv[2]
|
|
18
|
+
if (mode === 'models') {
|
|
19
|
+
const version = execFileSync('codex', ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().split(' ').at(-1)
|
|
20
|
+
const response = await fetch(`https://chatgpt.com/backend-api/codex/models?client_version=${encodeURIComponent(version)}`, { headers, signal: AbortSignal.timeout(30000) })
|
|
21
|
+
if (!response.ok) emit({ status: response.status })
|
|
22
|
+
else {
|
|
23
|
+
const payload = await response.json()
|
|
24
|
+
emit({ status: 200, models: (payload.models || []).filter(model => model.visibility !== 'hide').map(model => ({ id: model.slug, label: model.display_name || model.slug })).filter(model => typeof model.id === 'string' && model.id.length <= 256) })
|
|
25
|
+
}
|
|
26
|
+
} else if (mode === 'responses') {
|
|
27
|
+
let input = ''
|
|
28
|
+
for await (const chunk of process.stdin) input += chunk
|
|
29
|
+
const body = JSON.parse(input)
|
|
30
|
+
body.store = false
|
|
31
|
+
body.stream = true
|
|
32
|
+
body.instructions ??= ''
|
|
33
|
+
delete body.max_output_tokens
|
|
34
|
+
delete body.temperature
|
|
35
|
+
delete body.top_p
|
|
36
|
+
const response = await fetch('https://chatgpt.com/backend-api/codex/responses', { method: 'POST', headers, body: JSON.stringify(body), signal: AbortSignal.timeout(300000) })
|
|
37
|
+
emit({ status: response.status })
|
|
38
|
+
if (response.ok) {
|
|
39
|
+
for await (const chunk of response.body) emit({ chunk: Buffer.from(chunk).toString('base64') })
|
|
40
|
+
emit({ done: true })
|
|
41
|
+
}
|
|
42
|
+
} else emit({ status: 400 })
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
emit({ status: 503 }); process.exitCode = 1
|
|
46
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { lstat, readFile } from 'node:fs/promises'
|
|
2
|
+
import { request } from 'node:http'
|
|
3
|
+
|
|
4
|
+
export const IDENTITY_SOCKET = '/opt/giz/identity/identity.sock'
|
|
5
|
+
|
|
6
|
+
// One-release bridge: the managed CLI overlay can arrive before the Region gateway migration.
|
|
7
|
+
export async function resolveIdentitySocket() {
|
|
8
|
+
try {
|
|
9
|
+
await lstat(IDENTITY_SOCKET)
|
|
10
|
+
return IDENTITY_SOCKET
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error.code !== 'ENOENT') throw error
|
|
13
|
+
return '/run/giz/identity.sock'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
let cached
|
|
17
|
+
let pending
|
|
18
|
+
|
|
19
|
+
// The release overlay owns this stamp; outside npm installations do not have it.
|
|
20
|
+
export async function managedRevision() {
|
|
21
|
+
return readFile(new URL('../REVISION', import.meta.url), 'utf8').catch(error => {
|
|
22
|
+
if (error.code !== 'ENOENT') throw error
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The CLI never accepts a socket path from options, environment, or request data.
|
|
27
|
+
export async function computerIdentity(rejectedToken) {
|
|
28
|
+
if (cached?.access_token !== rejectedToken && cached?.expires_at > Date.now() + 60_000) return cached
|
|
29
|
+
pending ??= resolveIdentitySocket().then(socketPath => new Promise((resolve, reject) => {
|
|
30
|
+
const req = request({ socketPath, path: '/token', method: 'POST',
|
|
31
|
+
headers: { 'content-length': '0' } }, res => {
|
|
32
|
+
let body = ''
|
|
33
|
+
res.setEncoding('utf8')
|
|
34
|
+
res.on('data', chunk => {
|
|
35
|
+
body += chunk
|
|
36
|
+
if (body.length > 16_384) req.destroy(new Error('컴퓨터 신원 응답이 너무 큽니다.'))
|
|
37
|
+
})
|
|
38
|
+
res.on('error', reject)
|
|
39
|
+
res.on('end', () => {
|
|
40
|
+
try {
|
|
41
|
+
const value = JSON.parse(body)
|
|
42
|
+
if (res.statusCode !== 200) throw new Error(value.error_description || value.message || '컴퓨터 신원을 사용할 수 없습니다.')
|
|
43
|
+
const url = new URL(value.baseUrl)
|
|
44
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password ||
|
|
45
|
+
url.pathname !== '/' || url.search || url.hash || !value.access_token ||
|
|
46
|
+
value.token_type !== 'Bearer' || Number(value.expires_in) <= 0 || Number(value.expires_in) > 900) {
|
|
47
|
+
throw new Error('컴퓨터 신원 응답이 올바르지 않습니다.')
|
|
48
|
+
}
|
|
49
|
+
cached = { access_token: value.access_token, baseUrl: url.origin,
|
|
50
|
+
expires_at: Date.now() + Number(value.expires_in) * 1000 }
|
|
51
|
+
resolve(cached)
|
|
52
|
+
} catch (error) { reject(error) }
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
req.on('error', async error => {
|
|
56
|
+
if (error.code !== 'ENOENT') return reject(error)
|
|
57
|
+
try {
|
|
58
|
+
await lstat(socketPath)
|
|
59
|
+
reject(new Error('컴퓨터 신원 서비스에 연결할 수 없습니다. 잠시 후 다시 시도해 주세요.'))
|
|
60
|
+
} catch (lookupError) {
|
|
61
|
+
if (lookupError.code !== 'ENOENT') return reject(lookupError)
|
|
62
|
+
try {
|
|
63
|
+
if (await managedRevision() !== undefined) {
|
|
64
|
+
reject(new Error('컴퓨터 신원을 확인할 수 없습니다. 잠시 후 다시 시도하거나 소유자에게 문의해 주세요.'))
|
|
65
|
+
} else resolve(undefined)
|
|
66
|
+
} catch (error) { reject(error) }
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
req.setTimeout(10_000, () => req.destroy(new Error('컴퓨터 신원 응답 시간이 초과되었습니다.')))
|
|
70
|
+
req.end()
|
|
71
|
+
})).catch(error => {
|
|
72
|
+
error.hint = '컴퓨터 상태를 확인하고 다시 시도해 주세요. 차단되었다면 소유자 또는 조직 관리자에게 문의해 주세요.'
|
|
73
|
+
throw error
|
|
74
|
+
}).finally(() => { pending = undefined })
|
|
75
|
+
return pending
|
|
76
|
+
}
|
package/cli/engine.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { spawn, execFileSync } from 'node:child_process'
|
|
2
|
+
import { mkdir } from 'node:fs/promises'
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
/** Local Computer execution: official binary, existing permissions, isolated reviewable worktree. */
|
|
7
|
+
export async function runEngine(engine, instruction, directory = process.cwd(), progress = () => {}) {
|
|
8
|
+
if (engine !== 'claude' || typeof instruction !== 'string' || !instruction.trim()) throw new Error('Run: giz engine run claude "instruction" [--dir path]')
|
|
9
|
+
const git = (args, cwd = directory) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 8 * 1024 * 1024 })
|
|
10
|
+
const env = { ...process.env }
|
|
11
|
+
for (const key of ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY']) delete env[key]
|
|
12
|
+
const status = spawn('claude', ['auth', 'status'], { env, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
13
|
+
let login = ''
|
|
14
|
+
status.stdout.on('data', chunk => { login += chunk })
|
|
15
|
+
await new Promise((resolve, reject) => { status.once('error', reject); status.once('close', code => code === 0 ? resolve() : reject(new Error('Log in to Claude Code in Connections.'))) })
|
|
16
|
+
const account = JSON.parse(login)
|
|
17
|
+
if (!account.loggedIn || account.authMethod !== 'claude.ai') throw new Error('A Claude subscription login is required.')
|
|
18
|
+
const root = git(['rev-parse', '--show-toplevel']).trim()
|
|
19
|
+
const common = path.resolve(root, git(['rev-parse', '--git-common-dir']).trim())
|
|
20
|
+
const worktree = path.join(common, 'giz-engine-worktrees', randomUUID())
|
|
21
|
+
await mkdir(path.dirname(worktree), { recursive: true })
|
|
22
|
+
git(['worktree', 'add', '--detach', worktree, 'HEAD'])
|
|
23
|
+
progress({ status: 'running', engine, worktree })
|
|
24
|
+
const child = spawn('claude', ['-p', instruction, '--output-format', 'stream-json', '--verbose', '--permission-mode', 'acceptEdits'],
|
|
25
|
+
{ cwd: worktree, env, detached: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
26
|
+
let output = '', summary = '', stopped = false
|
|
27
|
+
const stop = () => { stopped = true; try { process.kill(-child.pid, 'SIGTERM') } catch {} }
|
|
28
|
+
process.once('SIGTERM', stop); process.once('SIGINT', stop)
|
|
29
|
+
child.stdout.on('data', chunk => {
|
|
30
|
+
output += chunk
|
|
31
|
+
let newline
|
|
32
|
+
while ((newline = output.indexOf('\n')) >= 0) {
|
|
33
|
+
const line = output.slice(0, newline); output = output.slice(newline + 1)
|
|
34
|
+
try {
|
|
35
|
+
const event = JSON.parse(line)
|
|
36
|
+
if (event.type === 'result') summary = String(event.result || '')
|
|
37
|
+
// Do not pass raw tool inputs, prompts or authorization data into the task card.
|
|
38
|
+
progress({ status: 'running', event: event.type, engine, worktree })
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
child.stderr.resume()
|
|
43
|
+
let exitCode
|
|
44
|
+
try {
|
|
45
|
+
exitCode = await new Promise((resolve, reject) => { child.once('error', reject); child.once('close', code => resolve(code)) })
|
|
46
|
+
} finally { process.removeListener('SIGTERM', stop); process.removeListener('SIGINT', stop) }
|
|
47
|
+
const tracked = git(['diff', '--name-only', 'HEAD'], worktree).trim().split('\n').filter(Boolean)
|
|
48
|
+
const created = git(['ls-files', '--others', '--exclude-standard'], worktree).trim().split('\n').filter(Boolean)
|
|
49
|
+
let diff = git(['diff', '--binary', 'HEAD'], worktree)
|
|
50
|
+
for (const file of created) {
|
|
51
|
+
try { diff += git(['diff', '--no-index', '--binary', '--', '/dev/null', file], worktree) }
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.status !== 1) throw error
|
|
54
|
+
diff += String(error.stdout)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { status: stopped ? 'canceled' : exitCode === 0 ? 'completed' : 'failed', engine, exitCode, worktree, files: [...new Set([...tracked, ...created])], diff, summary }
|
|
58
|
+
}
|
package/cli/giz.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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
5
|
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
|
@@ -18,7 +19,7 @@ const { version: CLI_VERSION } = createRequire(import.meta.url)('../package.json
|
|
|
18
19
|
const DEFAULT_URL = 'https://www.giz.ai'
|
|
19
20
|
const CLIENT_ID = 'giz-cli'
|
|
20
21
|
const SCOPES = ['giz.read', 'giz.inference', 'giz.write', 'giz.execute', 'giz.destructive', 'offline_access']
|
|
21
|
-
const BOOLEAN_OPTIONS = new Set(['help', 'json', 'no-open', 'version'])
|
|
22
|
+
const BOOLEAN_OPTIONS = new Set(['help', 'json', 'no-open', 'version', 'device'])
|
|
22
23
|
const configPath = path.resolve(process.env.GIZ_CONFIG ||
|
|
23
24
|
path.join(os.homedir(), '.config', 'giz', 'credentials.json'))
|
|
24
25
|
|
|
@@ -113,10 +114,9 @@ async function exchangeToken(baseUrl, metadata, grant, config = {}) {
|
|
|
113
114
|
})
|
|
114
115
|
const payload = await response.json()
|
|
115
116
|
if (!response.ok) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)
|
|
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
|
|
120
120
|
}
|
|
121
121
|
return {
|
|
122
122
|
...config,
|
|
@@ -130,7 +130,12 @@ async function exchangeToken(baseUrl, metadata, grant, config = {}) {
|
|
|
130
130
|
async function credentials(baseUrl, rejectedToken) {
|
|
131
131
|
if (process.env.GIZ_TOKEN) return { access_token: process.env.GIZ_TOKEN, baseUrl }
|
|
132
132
|
const config = await readConfig()
|
|
133
|
-
if (!config?.access_token)
|
|
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
|
+
}
|
|
134
139
|
if (String(config.baseUrl).replace(/\/+$/, '') !== baseUrl) {
|
|
135
140
|
throw new CliError(
|
|
136
141
|
`Credentials belong to ${config.baseUrl}.`,
|
|
@@ -159,6 +164,8 @@ async function authenticatedFetch(baseUrl, url, init = {}) {
|
|
|
159
164
|
const signal = AbortSignal.timeout(25 * 60_000)
|
|
160
165
|
const send = () => {
|
|
161
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)
|
|
162
169
|
return fetch(url, { ...init, headers, signal: init.signal ? AbortSignal.any([init.signal, signal]) : signal })
|
|
163
170
|
}
|
|
164
171
|
let response = await send()
|
|
@@ -179,6 +186,17 @@ async function jsonResponse(response, baseUrl) {
|
|
|
179
186
|
}
|
|
180
187
|
if (!response.ok) {
|
|
181
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
|
+
}
|
|
182
200
|
throw new CliError(
|
|
183
201
|
message,
|
|
184
202
|
response.status === 403 && /scope/i.test(message)
|
|
@@ -196,7 +214,16 @@ async function api(baseUrl, pathname, data, options = {}) {
|
|
|
196
214
|
headers: { accept: 'application/json', ...(data === undefined ? {} : { 'content-type': 'application/json' }) },
|
|
197
215
|
...(data === undefined ? {} : { body: JSON.stringify(data) })
|
|
198
216
|
})
|
|
199
|
-
|
|
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`)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return payload
|
|
200
227
|
}
|
|
201
228
|
|
|
202
229
|
async function uploadMedia(baseUrl, localFile) {
|
|
@@ -215,6 +242,7 @@ async function uploadMedia(baseUrl, localFile) {
|
|
|
215
242
|
|
|
216
243
|
async function login(baseUrl, options) {
|
|
217
244
|
const metadata = await discovery(baseUrl)
|
|
245
|
+
if (options.device === true) return await deviceLogin(baseUrl, metadata)
|
|
218
246
|
const verifier = randomBytes(48).toString('base64url')
|
|
219
247
|
const challenge = createHash('sha256').update(verifier).digest('base64url')
|
|
220
248
|
const state = randomBytes(24).toString('base64url')
|
|
@@ -240,6 +268,41 @@ async function login(baseUrl, options) {
|
|
|
240
268
|
return await api(baseUrl, '/api/external/me')
|
|
241
269
|
}
|
|
242
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)
|
|
277
|
+
})
|
|
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.')
|
|
282
|
+
}
|
|
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')
|
|
304
|
+
}
|
|
305
|
+
|
|
243
306
|
async function waitForCallback(expectedState) {
|
|
244
307
|
const server = createServer()
|
|
245
308
|
server.listen(0, '127.0.0.1')
|
|
@@ -286,6 +349,23 @@ function openBrowser(url) {
|
|
|
286
349
|
child.unref()
|
|
287
350
|
}
|
|
288
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
|
+
|
|
289
369
|
function jsonInput(value) {
|
|
290
370
|
if (value === undefined || value === true) return {}
|
|
291
371
|
try {
|
|
@@ -320,6 +400,19 @@ async function applyUploads(baseUrl, input, values = []) {
|
|
|
320
400
|
return input
|
|
321
401
|
}
|
|
322
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
|
+
|
|
323
416
|
async function invoke(baseUrl, tool, input) {
|
|
324
417
|
return await api(baseUrl, `/api/tools/${encodeURIComponent(tool)}`, input)
|
|
325
418
|
}
|
|
@@ -353,8 +446,26 @@ async function serveMcp(baseUrl) {
|
|
|
353
446
|
|
|
354
447
|
async function runCommand(baseUrl, positionals, options) {
|
|
355
448
|
const [command, subcommand, ...rest] = positionals
|
|
356
|
-
if (options.version === true)
|
|
357
|
-
|
|
449
|
+
if (options.version === true) {
|
|
450
|
+
const revision = await managedRevision()
|
|
451
|
+
return { kind: 'message', value: CLI_VERSION + (revision ? ` (region ${revision.trim()})` : '') }
|
|
452
|
+
}
|
|
453
|
+
if (!command || command === 'help' || options.help === true) {
|
|
454
|
+
// A script that asks for help on a command that does not exist must fail, not read the overview as success.
|
|
455
|
+
if (command && command !== 'help' && !help().split('\n').some((line) => line.trim().split(' ')[1] === command && line.startsWith(' giz '))) {
|
|
456
|
+
throw new CliError(`Unknown command "${command}".`, 'Run: giz --help')
|
|
457
|
+
}
|
|
458
|
+
return { kind: 'help', value: help(command) }
|
|
459
|
+
}
|
|
460
|
+
if (command === 'engine' && subcommand === 'run') {
|
|
461
|
+
if (rest[0] !== 'claude') throw new CliError('Only Claude Code is an engine connection.')
|
|
462
|
+
await api(baseUrl, '/api/external/engine/claude/authorization')
|
|
463
|
+
const { runEngine } = await import('./engine.mjs')
|
|
464
|
+
const result = await runEngine(rest[0], required(rest[1], 'INSTRUCTION', 'Run: giz engine run claude "instruction"'), options.dir,
|
|
465
|
+
event => process.stdout.write(JSON.stringify(event) + '\n'))
|
|
466
|
+
if (result.status !== 'completed') process.exitCode = 1
|
|
467
|
+
return { kind: 'result', value: result }
|
|
468
|
+
}
|
|
358
469
|
if (command === 'auth' && subcommand === 'login') {
|
|
359
470
|
return { kind: 'auth', value: await login(baseUrl, options) }
|
|
360
471
|
}
|
|
@@ -400,6 +511,11 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
400
511
|
}
|
|
401
512
|
}
|
|
402
513
|
|
|
514
|
+
if (command === 'credits') {
|
|
515
|
+
// Batch scripts read the balance before paid work instead of learning it from a refusal (MCP-TOOL-SCHEMA-LIMITS).
|
|
516
|
+
if (subcommand) throw new CliError(`Unknown command "credits ${subcommand}".`, 'Run: giz credits')
|
|
517
|
+
return { kind: 'result', value: await invoke(baseUrl, 'account.credits', {}) }
|
|
518
|
+
}
|
|
403
519
|
if (command === 'model' && subcommand === 'list') {
|
|
404
520
|
return {
|
|
405
521
|
kind: 'models',
|
|
@@ -456,7 +572,6 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
456
572
|
...(rest.length ? { prompt: rest.join(' ') } : {}),
|
|
457
573
|
...(options.model ? { model: options.model } : {})
|
|
458
574
|
}, uploads)
|
|
459
|
-
required(input.prompt, 'PROMPT', `Run: giz generate ${subcommand} "your prompt"`)
|
|
460
575
|
return { kind: 'job', value: await invoke(baseUrl, tool, input) }
|
|
461
576
|
}
|
|
462
577
|
|
|
@@ -513,7 +628,7 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
513
628
|
})
|
|
514
629
|
}
|
|
515
630
|
}
|
|
516
|
-
const mapping = mappings[subcommand]
|
|
631
|
+
const mapping = mappings[subcommand === 'ls' ? 'list' : subcommand]
|
|
517
632
|
if (!mapping) throw new CliError(`Unknown drive command "${subcommand || ''}".`, 'Run: giz drive --help')
|
|
518
633
|
const [tool, input] = mapping()
|
|
519
634
|
return {
|
|
@@ -522,6 +637,37 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
522
637
|
}
|
|
523
638
|
}
|
|
524
639
|
|
|
640
|
+
if (command === 'table' && subcommand === 'list') {
|
|
641
|
+
return { kind: 'result', value: await invoke(baseUrl, 'workspace.objects', {}) }
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
if (command === 'table' && subcommand === 'get') {
|
|
645
|
+
const object = required(rest[0], 'TABLE', 'Run: giz table get task')
|
|
646
|
+
return { kind: 'result', value: await invoke(baseUrl, 'workspace.objects', { object }) }
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (command === 'table' && subcommand === 'action') {
|
|
650
|
+
const view = required(rest[0], 'TABLE', 'Run: giz table action TABLE ACTION [--id ID] --input JSON')
|
|
651
|
+
const action = required(rest[1], 'ACTION', 'Run: giz table get TABLE to see its actions')
|
|
652
|
+
const id = options.id === undefined ? undefined : String(options.id).includes(',') ? String(options.id).split(',') : String(options.id)
|
|
653
|
+
return {
|
|
654
|
+
kind: 'result',
|
|
655
|
+
value: await invoke(baseUrl, 'workspace.executeAction', { view, action, ...(id === undefined ? {} : { dataId: id }), data: jsonInput(options.input) })
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (command === 'table' && ['read', 'write'].includes(subcommand)) {
|
|
660
|
+
const table = required(rest[0], 'TABLE', `Run: giz table ${subcommand} TABLE_PATH --input JSON`)
|
|
661
|
+
return {
|
|
662
|
+
kind: 'result',
|
|
663
|
+
value: await invoke(baseUrl, subcommand === 'read' ? 'table.readRows' : 'table.writeRows', {
|
|
664
|
+
...jsonInput(options.input),
|
|
665
|
+
table,
|
|
666
|
+
...(options.computer ? { computer: options.computer } : {})
|
|
667
|
+
})
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
525
671
|
if (command === 'upload') {
|
|
526
672
|
const localFile = required(subcommand, 'LOCAL_PATH', 'Run: giz upload ./reference.png')
|
|
527
673
|
return { kind: 'result', value: await uploadMedia(baseUrl, localFile) }
|
|
@@ -552,38 +698,33 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
552
698
|
throw new CliError(`Unknown computer command "${subcommand || ''}".`, 'Run: giz computer --help')
|
|
553
699
|
}
|
|
554
700
|
|
|
555
|
-
if (command === '
|
|
701
|
+
if (command === 'app') {
|
|
556
702
|
const computer = options.computer ? { computer: options.computer } : {}
|
|
557
703
|
if (subcommand === 'list') {
|
|
558
|
-
return { kind: 'result', value: await invoke(baseUrl, '
|
|
704
|
+
return { kind: 'result', value: await invoke(baseUrl, 'app.list', computer) }
|
|
559
705
|
}
|
|
560
|
-
if (subcommand === '
|
|
706
|
+
if (subcommand === 'save' || subcommand === 'create') {
|
|
561
707
|
const input = jsonInput(options.input)
|
|
562
|
-
|
|
563
|
-
? required(rest[0], 'SITE', 'Run: giz site update SITE --input JSON')
|
|
564
|
-
: undefined
|
|
565
|
-
return {
|
|
566
|
-
kind: 'result',
|
|
567
|
-
value: await invoke(baseUrl, 'site.configure', {
|
|
568
|
-
...input,
|
|
569
|
-
...computer,
|
|
570
|
-
operation: subcommand,
|
|
571
|
-
...(siteId ? { siteId } : {})
|
|
572
|
-
})
|
|
573
|
-
}
|
|
708
|
+
return { kind: 'result', value: await invoke(baseUrl, 'app.save', { ...input, ...computer }) }
|
|
574
709
|
}
|
|
575
|
-
if (subcommand === '
|
|
576
|
-
const
|
|
577
|
-
return {
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
710
|
+
if (subcommand === 'run') {
|
|
711
|
+
const id = required(rest[0], 'APP_ID', 'Run: giz app run APP_ID --expected-revision REVISION')
|
|
712
|
+
return { kind: 'result', value: await invoke(baseUrl, 'app.run', { ...computer, id, expectedRevision: appRevision(options) }) }
|
|
713
|
+
}
|
|
714
|
+
if (subcommand === 'publish') {
|
|
715
|
+
const id = required(rest[0], 'APP_ID', 'Run: giz app publish APP_ID private|workspace|public')
|
|
716
|
+
const visibility = required(rest[1] || options.visibility, 'VISIBILITY', 'Run: giz app publish APP_ID private|workspace|public')
|
|
717
|
+
return { kind: 'result', value: await invoke(baseUrl, 'app.publish', { ...computer, id, visibility, expectedRevision: appRevision(options) }) }
|
|
718
|
+
}
|
|
719
|
+
if (subcommand === 'open') {
|
|
720
|
+
const id = required(rest[0], 'APP_ID', 'Run: giz app open APP_ID')
|
|
721
|
+
return { kind: 'result', value: await invoke(baseUrl, 'app.open', { ...computer, id }) }
|
|
581
722
|
}
|
|
582
723
|
if (subcommand === 'delete') {
|
|
583
|
-
const
|
|
584
|
-
return { kind: 'result', value: await invoke(baseUrl, '
|
|
724
|
+
const id = required(rest[0], 'APP_ID', 'Run: giz app delete APP_ID')
|
|
725
|
+
return { kind: 'result', value: await invoke(baseUrl, 'app.delete', { ...computer, id, expectedRevision: appRevision(options) }) }
|
|
585
726
|
}
|
|
586
|
-
throw new CliError(`Unknown
|
|
727
|
+
throw new CliError(`Unknown app command "${subcommand || ''}".`, 'Run: giz app --help')
|
|
587
728
|
}
|
|
588
729
|
|
|
589
730
|
if (command === 'capability') {
|
|
@@ -722,6 +863,10 @@ function print(kind, value, json, output = process.stdout) {
|
|
|
722
863
|
output.write(`${value}\n`)
|
|
723
864
|
return
|
|
724
865
|
}
|
|
866
|
+
if (kind === 'auth' && value.identity?.type === 'computer') {
|
|
867
|
+
output.write(`컴퓨터 신원으로 로그인됨 (소유자 ${value.email || value.name})\n`)
|
|
868
|
+
return
|
|
869
|
+
}
|
|
725
870
|
if (kind === 'models') {
|
|
726
871
|
const rows = value.models || []
|
|
727
872
|
for (const model of rows) output.write(`${model.id}\t${model.type}\t${model.provider || '-'}\t${model.label}\n`)
|
|
@@ -752,12 +897,22 @@ function print(kind, value, json, output = process.stdout) {
|
|
|
752
897
|
}
|
|
753
898
|
|
|
754
899
|
function help(command) {
|
|
900
|
+
if (command === 'engine') return `Usage
|
|
901
|
+
giz engine run claude "instruction" [--dir path]
|
|
902
|
+
|
|
903
|
+
Runs the official Claude Code in an isolated worktree. Connect Claude Code in Giz first.
|
|
904
|
+
Progress and the final diff, files and summary are returned. Interrupting stops the engine.`
|
|
755
905
|
if (command === 'api-key') return `Usage
|
|
756
906
|
giz api-key list
|
|
757
907
|
giz api-key create "My app" [--scopes giz.inference,giz.read] [--expires ISO_TIMESTAMP]
|
|
758
908
|
giz api-key revoke KEY_ID
|
|
759
909
|
|
|
760
910
|
The secret is shown only when the key is created.`
|
|
911
|
+
if (command === 'credits') return `Usage
|
|
912
|
+
giz credits
|
|
913
|
+
|
|
914
|
+
Prints your remaining credit (purchased and plan), when plan credit resets, and this month's spend and charges.
|
|
915
|
+
In a workspace it prints the workspace's monthly budget. Read it before paid batch work.`
|
|
761
916
|
if (command === 'chat') return `Usage
|
|
762
917
|
giz chat MODEL "your prompt"
|
|
763
918
|
giz chat MODEL --input '{"messages":[{"role":"user","content":"Hello"}]}'`
|
|
@@ -766,17 +921,24 @@ The secret is shown only when the key is created.`
|
|
|
766
921
|
giz model get MODEL
|
|
767
922
|
giz model run MODEL --input JSON [--upload FIELD=LOCAL_PATH]
|
|
768
923
|
|
|
769
|
-
Run model get first. Its live input schema is the source of truth. Repeat --upload for multiple media inputs
|
|
924
|
+
Run model get first. Its live input schema is the source of truth. Repeat --upload for multiple media inputs.
|
|
925
|
+
model run returns a job immediately. Retrieve the result with giz job wait JOB_ID, or poll with giz job get JOB_ID.
|
|
926
|
+
For speech-to-text, use model run with the audio field returned by model get. Callback/webhook delivery is not supported.`
|
|
770
927
|
if (command === 'generate') return `Usage
|
|
771
|
-
giz generate image|video|
|
|
928
|
+
giz generate image|video|music|talking-head "PROMPT" [--model MODEL]
|
|
929
|
+
giz generate audio --input '{"purpose":"speech","script":"Hello"}' [--model MODEL]
|
|
930
|
+
giz generate audio --input '{"purpose":"sound","prompt":"Rain","duration":5}'
|
|
772
931
|
|
|
773
932
|
Options
|
|
774
933
|
--image LOCAL_PATH Upload a visual reference
|
|
775
934
|
--audio LOCAL_PATH Upload speech audio
|
|
776
935
|
--upload FIELD=PATH Upload into an exact tool field
|
|
777
|
-
--input JSON
|
|
936
|
+
--input JSON Input matching the tool schema (@FILE reads a file, - reads stdin)
|
|
937
|
+
|
|
938
|
+
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.
|
|
939
|
+
Generation returns a job immediately; use giz job wait JOB_ID for its result.`
|
|
778
940
|
if (command === 'drive') return `Usage
|
|
779
|
-
giz drive
|
|
941
|
+
giz drive ls [PATH] (list alias)
|
|
780
942
|
giz drive search QUERY
|
|
781
943
|
giz drive read PATH
|
|
782
944
|
giz drive write PATH --content TEXT
|
|
@@ -784,16 +946,30 @@ Options
|
|
|
784
946
|
giz drive mkdir PATH
|
|
785
947
|
giz drive move FROM TO
|
|
786
948
|
giz drive delete PATH`
|
|
949
|
+
if (command === 'table') return `Usage
|
|
950
|
+
giz table list
|
|
951
|
+
giz table get TABLE
|
|
952
|
+
giz table read TABLE [--input JSON] [--computer COMPUTER]
|
|
953
|
+
giz table write TABLE --input JSON [--computer COMPUTER]
|
|
954
|
+
giz table action TABLE ACTION [--id ID[,ID...]] --input JSON
|
|
955
|
+
|
|
956
|
+
TABLE is a table's Drive path (for example Sales/Customers.view.ts) or a business object name from giz table list
|
|
957
|
+
(for example task, issue, requirement). giz table get shows its fields, the row changes it accepts (writes) and its
|
|
958
|
+
actions; an object without a write changes only through its actions. Use --org to work in an organization.
|
|
959
|
+
read input: {"select":["name"],"filters":[{"field":"status","operator":"eq","value":"open"}],"limit":50}
|
|
960
|
+
write input: {"create":[{"name":"Acme"}],"update":[{"id":"ROW_ID","status":"done"}],"remove":["ROW_ID"]}
|
|
961
|
+
One write is one change people can undo in the table. At most 200 rows per write.`
|
|
787
962
|
if (command === 'computer') return `Usage
|
|
788
963
|
giz computer list
|
|
789
964
|
giz computer get|start|stop [COMPUTER]
|
|
790
965
|
giz computer exec [--computer COMPUTER] [--cwd PATH] [--deadline-ms MS] [--stdin TEXT|--stdin-file PATH] -- COMMAND [ARGS...]`
|
|
791
|
-
if (command === '
|
|
792
|
-
giz
|
|
793
|
-
giz
|
|
794
|
-
giz
|
|
795
|
-
giz
|
|
796
|
-
giz
|
|
966
|
+
if (command === 'app') return `Usage
|
|
967
|
+
giz app list [--computer COMPUTER]
|
|
968
|
+
giz app create|save [--computer COMPUTER] --input JSON
|
|
969
|
+
giz app run APP_ID --expected-revision REVISION [--computer COMPUTER]
|
|
970
|
+
giz app publish APP_ID private|workspace|public --expected-revision REVISION [--computer COMPUTER]
|
|
971
|
+
giz app open APP_ID [--computer COMPUTER]
|
|
972
|
+
giz app delete APP_ID --expected-revision REVISION [--computer COMPUTER]`
|
|
797
973
|
if (command === 'capability') return `Usage
|
|
798
974
|
giz capability list [--computer COMPUTER]
|
|
799
975
|
giz capability search skill|mcp QUERY
|
|
@@ -812,26 +988,30 @@ Options
|
|
|
812
988
|
stdio (default) shares giz auth login and refreshes tokens automatically.
|
|
813
989
|
http uses the client's own OAuth login. Install the chosen client first.`
|
|
814
990
|
if (command === 'auth') return `Usage
|
|
815
|
-
giz auth login
|
|
991
|
+
giz auth login [--device]
|
|
816
992
|
giz auth status
|
|
817
993
|
giz auth logout`
|
|
818
994
|
return `Giz CLI — AI models, Drive, and Computer from any terminal
|
|
819
995
|
|
|
820
996
|
Usage
|
|
821
|
-
giz auth login
|
|
997
|
+
giz auth login [--device]
|
|
822
998
|
giz api-key list|create|revoke
|
|
823
999
|
giz chat MODEL "your prompt"
|
|
824
1000
|
giz model list [--type TYPE] [--search TEXT]
|
|
825
1001
|
giz model get MODEL
|
|
826
1002
|
giz model run MODEL --input JSON [--upload FIELD=LOCAL_PATH]
|
|
827
|
-
giz generate image|video|
|
|
1003
|
+
giz generate image|video|music|talking-head "PROMPT" [--model MODEL]
|
|
1004
|
+
giz generate audio --input JSON [--model MODEL]
|
|
828
1005
|
giz job list|get|wait|cancel [JOB]
|
|
829
|
-
giz
|
|
1006
|
+
giz credits
|
|
1007
|
+
giz drive ls|list|search|read|write|mkdir|move|delete
|
|
1008
|
+
giz table list|get|read|write|action
|
|
830
1009
|
giz upload LOCAL_PATH
|
|
831
1010
|
giz computer list|get|start|stop|exec
|
|
832
|
-
giz
|
|
1011
|
+
giz app list|create|save|run|publish|open|delete
|
|
833
1012
|
giz capability list|search|install|update|remove
|
|
834
1013
|
giz agent start|status|cancel
|
|
1014
|
+
giz engine run claude "instruction" [--dir path]
|
|
835
1015
|
giz mcp url|serve|check|add
|
|
836
1016
|
giz skill install
|
|
837
1017
|
giz invoke TOOL --input JSON
|
|
@@ -841,6 +1021,7 @@ Use giz COMMAND --help for parameters and examples.
|
|
|
841
1021
|
Global options
|
|
842
1022
|
--json Machine-readable JSON output
|
|
843
1023
|
--base-url URL Another Giz deployment
|
|
1024
|
+
--org ORGANIZATION Organization id to work in (or GIZ_ORGANIZATION)
|
|
844
1025
|
--no-open Print the login URL without opening a browser
|
|
845
1026
|
|
|
846
1027
|
Docs: https://www.giz.ai/mcp/`
|
|
@@ -849,7 +1030,12 @@ Docs: https://www.giz.ai/mcp/`
|
|
|
849
1030
|
async function main() {
|
|
850
1031
|
const { positionals, options } = parse(process.argv.slice(2))
|
|
851
1032
|
const saved = await readConfig()
|
|
852
|
-
const
|
|
1033
|
+
const requiresAuth = positionals.length && !options.help && !options.version &&
|
|
1034
|
+
!(positionals[0] === 'auth' && ['login', 'logout'].includes(positionals[1]))
|
|
1035
|
+
const computer = requiresAuth && !process.env.GIZ_TOKEN && !saved?.access_token ? await computerIdentity() : undefined
|
|
1036
|
+
const baseUrl = String(options['base-url'] || process.env.GIZ_URL || saved?.baseUrl || computer?.baseUrl || DEFAULT_URL).replace(/\/+$/, '')
|
|
1037
|
+
if (typeof options.org === 'string') process.env.GIZ_ORGANIZATION = options.org
|
|
1038
|
+
options.input = await inputText(options.input)
|
|
853
1039
|
const output = await runCommand(baseUrl, positionals, options)
|
|
854
1040
|
if (output) {
|
|
855
1041
|
print(output.kind, output.value, options.json === true)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gizai/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "Giz CLI, MCP installer, OpenAI-compatible models, Drive, and Computer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"homepage": "https://www.giz.ai/mcp/",
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
|
34
|
-
"url": "git+https://github.com/
|
|
35
|
-
"directory": "apps/
|
|
34
|
+
"url": "git+https://github.com/GizAI/giz.git",
|
|
35
|
+
"directory": "apps/giz/external"
|
|
36
36
|
},
|
|
37
37
|
"publishConfig": {
|
|
38
38
|
"access": "public"
|
package/skills/giz/SKILL.md
CHANGED
|
@@ -36,7 +36,7 @@ giz model run MODEL_ID --input '{"prompt":"..."}'
|
|
|
36
36
|
Use the OpenAI-compatible chat boundary:
|
|
37
37
|
|
|
38
38
|
```bash
|
|
39
|
-
giz chat gpt-
|
|
39
|
+
giz chat gpt-6-luna "..."
|
|
40
40
|
```
|
|
41
41
|
|
|
42
42
|
Upload local media into an exact model field:
|
|
@@ -63,3 +63,63 @@ Pass `--json` for machine output. Use `GIZ_TOKEN` only in ephemeral automation.
|
|
|
63
63
|
- Do not retry failed generations blindly. Report the provider or contract error.
|
|
64
64
|
|
|
65
65
|
Read [references/operations.md](references/operations.md) when MCP discovery is unavailable or when choosing between Drive and Computer tools.
|
|
66
|
+
|
|
67
|
+
App 변경은 `giz app list`에서 확인한 `revision`을 명시적으로 전달합니다.
|
|
68
|
+
`run`, `publish`, `delete`에는 `--expected-revision REVISION`이 필요합니다.
|
|
69
|
+
`create`/`save`의 `--input`에는 `id`와 `expectedRevision`을 넣습니다(새 앱은 0).
|
|
70
|
+
목록을 다시 읽어 개정 번호를 자동 대체하지 않으므로, 검토 뒤 다른 변경이 생기면 서버가 충돌을 거부합니다.
|
|
71
|
+
|
|
72
|
+
아래 표는 CLI 디스패처와 도구 item에서 생성합니다. 갱신 명령:
|
|
73
|
+
`GIZ_UPDATE_CLI_SURFACE=1 pnpm --filter giz-app-giz test --runInBand --runTestsByPath server/external/cli-surface.spec.ts`.
|
|
74
|
+
일반 CI 실행은 문서를 쓰지 않고 표의 차이를 실패로 보고합니다.
|
|
75
|
+
|
|
76
|
+
<!-- cli-surface:start -->
|
|
77
|
+
| CLI 명령 | 도구 ID | MCP 이름 | 필수 입력 | scope | Agent 발견 |
|
|
78
|
+
| --- | --- | --- | --- | --- | --- |
|
|
79
|
+
| model list | model.list | model_list | - | giz.read | model.list |
|
|
80
|
+
| model get | model.get | model_get | model | giz.read | model.get |
|
|
81
|
+
| model run | model.run | model_run | model, input | giz.inference | model.run |
|
|
82
|
+
| generate image | generateImage | generateImage | prompt | giz.write | generateImage |
|
|
83
|
+
| generate video | generateVideo | generateVideo | prompt | giz.write | generateVideo |
|
|
84
|
+
| generate music | generateMusic | generateMusic | prompt | giz.write | generateMusic |
|
|
85
|
+
| generate talking-head | generateTalkingHead | generateTalkingHead | prompt, references | giz.write | generateTalkingHead |
|
|
86
|
+
| generate audio | generateAudio | generateAudio | purpose | giz.write | generateAudio |
|
|
87
|
+
| job list | job.list | job_list | - | giz.read | server job |
|
|
88
|
+
| job get | job.get | job_get | job | giz.read | server job |
|
|
89
|
+
| job wait | job.get | job_get | job | giz.read | server job |
|
|
90
|
+
| job cancel | job.cancel | job_cancel | job | giz.destructive | server job |
|
|
91
|
+
| credits | account.credits | account_credits | - | giz.read | account.credits |
|
|
92
|
+
| drive ls | drive.list | drive_list | - | giz.read | drive.list |
|
|
93
|
+
| drive list | drive.list | drive_list | - | giz.read | drive.list |
|
|
94
|
+
| drive search | drive.search | drive_search | query | giz.read | drive.search |
|
|
95
|
+
| drive read | drive.readFile | drive_readFile | path | giz.read | drive.readFile |
|
|
96
|
+
| drive write | drive.writeFile | drive_writeFile | path, content | giz.write | drive.writeFile |
|
|
97
|
+
| drive mkdir | drive.makeDirectory | drive_makeDirectory | path | giz.write | drive.makeDirectory |
|
|
98
|
+
| drive move | drive.movePath | drive_movePath | from, to | giz.write | drive.movePath |
|
|
99
|
+
| drive delete | drive.deletePath | drive_deletePath | path | giz.destructive | drive.deletePath |
|
|
100
|
+
| table list | workspace.objects | workspace_objects | - | giz.read | workspace.objects |
|
|
101
|
+
| table get | workspace.objects | workspace_objects | - | giz.read | workspace.objects |
|
|
102
|
+
| table read | table.readRows | table_readRows | table | giz.read | table.readRows |
|
|
103
|
+
| table write | table.writeRows | table_writeRows | table | giz.write | table.writeRows |
|
|
104
|
+
| table action | workspace.executeAction | workspace_executeAction | view, action | giz.execute | workspace.executeAction |
|
|
105
|
+
| computer list | computer.list | computer_list | - | giz.read | computer.list |
|
|
106
|
+
| computer get | computer.get | computer_get | - | giz.read | external only |
|
|
107
|
+
| computer start | computer.start | computer_start | - | giz.execute | external only |
|
|
108
|
+
| computer stop | computer.stop | computer_stop | - | giz.destructive | external only |
|
|
109
|
+
| computer exec | computer.exec | giz_computer_exec | - | giz.execute | computer.exec |
|
|
110
|
+
| app list | app.list | app_list | - | giz.read | app.list |
|
|
111
|
+
| app create | app.save | app_save | id, expectedRevision | giz.execute | app.save |
|
|
112
|
+
| app save | app.save | app_save | id, expectedRevision | giz.execute | app.save |
|
|
113
|
+
| app run | app.run | app_run | id, expectedRevision | giz.execute | app.run |
|
|
114
|
+
| app publish | app.publish | app_publish | id, expectedRevision, visibility | giz.write | app.publish |
|
|
115
|
+
| app open | app.open | app_open | id | giz.read | app.open |
|
|
116
|
+
| app delete | app.delete | app_delete | id, expectedRevision | giz.destructive | app.delete |
|
|
117
|
+
| capability list | capability.status | capability_status | - | giz.read | capability.status |
|
|
118
|
+
| capability search | capability.catalog | capability_catalog | kind, query | giz.read | capability.catalog |
|
|
119
|
+
| capability install | capability.install | capability_install | id, kind, query | giz.execute | capability.install |
|
|
120
|
+
| capability update | capability.manage | capability_manage | id, operation | giz.destructive | capability.manage |
|
|
121
|
+
| capability remove | capability.manage | capability_manage | id, operation | giz.destructive | capability.manage |
|
|
122
|
+
| agent start | agent.start | agent_start | agentId, prompt | giz.execute | external only |
|
|
123
|
+
| agent status | agent.status | agent_status | turnId | giz.read | agent.status |
|
|
124
|
+
| agent cancel | agent.cancel | agent_cancel | turnId | giz.execute | agent.cancel |
|
|
125
|
+
<!-- cli-surface:end -->
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
| List, search, or read Drive | `drive_list`, `drive_search`, `drive_readFile` |
|
|
9
9
|
| Create or change Drive content | `drive_writeFile`, `drive_makeDirectory`, `drive_movePath`, `drive_importUrl` |
|
|
10
10
|
| Move Drive content to Trash | `drive_deletePath` |
|
|
11
|
+
| Read a table's fields and rows | `table_readRows` |
|
|
12
|
+
| Create, update or remove table rows (one undoable change per call) | `table_writeRows` |
|
|
13
|
+
| Turn rows into a new table | `table_createFromRows` |
|
|
11
14
|
| Discover and control Computers | `computer_list`, `computer_start`, `computer_stop` |
|
|
12
15
|
| Run a Computer command | `giz_computer_exec` |
|
|
13
16
|
| Find, inspect, or interact with an existing terminal | `terminal_list`, `terminal_observe`, `terminal_send` |
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Giz Presentation & Slide Authoring
|
|
2
|
+
|
|
3
|
+
The requested deliverable is a presentation: editable PPTX and, when requested,
|
|
4
|
+
PDF. Choose the design workflow from the user's intent. Image-designed proposals
|
|
5
|
+
start with complete slide images; direct SVG authoring remains an option.
|
|
6
|
+
`slides.create/read/edit/inspect/export` share one Drive document for reconstruction
|
|
7
|
+
and subsequent edits. The SVG is an intermediate representation and live preview;
|
|
8
|
+
it must not dictate or simplify the image model's design.
|
|
9
|
+
|
|
10
|
+
## Image-designed proposals
|
|
11
|
+
|
|
12
|
+
1. Read the source material and reference pages. Establish the audience, narrative,
|
|
13
|
+
approved wording, numbers and sources. Retain this content separately from image
|
|
14
|
+
recognition: generated pixels do not override approved facts.
|
|
15
|
+
2. Discover the requested image model with `model.list/model.get`. Use its actual
|
|
16
|
+
quality, size and reference-image contract. Honor the requested model; report a
|
|
17
|
+
failure instead of silently substituting the default or a cheaper model. Use
|
|
18
|
+
`model.run` with the exact retrieved input schema for model-specific quality or
|
|
19
|
+
variant controls, including subsequent reference-image cleanup.
|
|
20
|
+
3. Generate **complete slide designs**, including typography, headings, body copy,
|
|
21
|
+
diagrams, cards and artwork, through the existing image generation tools. Match
|
|
22
|
+
the reference's visual ambition, information density and aspect ratio. Pass reference images
|
|
23
|
+
to maintain composition and consistent design across pages. Save the finished
|
|
24
|
+
slide images in Drive; they are the visual targets for conversion.
|
|
25
|
+
4. Inspect each target at readable resolution. Inventory all visible components,
|
|
26
|
+
including icons and decorative marks, before cleanup; account for every one in
|
|
27
|
+
the reconstructed layers. Reconstruct its actual composition,
|
|
28
|
+
text bounds, line breaks, hierarchy, colors, borders and grouping in reference
|
|
29
|
+
pixel coordinates. Do not redesign it as a generic template. Native text uses
|
|
30
|
+
the approved source wording even where the image misspells or changes a number.
|
|
31
|
+
5. Extract photographs, logos and complex artwork with `assets` pixel crops.
|
|
32
|
+
Where editable labels overlap artwork, edit the original image through
|
|
33
|
+
the same image model and quality settings with that reference. Remove only the
|
|
34
|
+
labels and preserve the composition. Inspect the cleaned pixels for leftover glyphs and unwanted design
|
|
35
|
+
changes. Cropping alone does not clean text; flat masks cannot restore complex
|
|
36
|
+
backgrounds. Rebuild simple cards, lines, arrows and labels as native objects.
|
|
37
|
+
If retaining a background layer, remove those editable objects from its pixels
|
|
38
|
+
too: moving a card or text box must not leave its original raster version behind.
|
|
39
|
+
6. Compose the reconstructed layers in one SVG document. Match the actual reference
|
|
40
|
+
dimensions on every page, preserve stable object IDs and logical `g` groups,
|
|
41
|
+
and use visible `text`/`tspan` with explicit line baselines and page fonts.
|
|
42
|
+
Do not outline glyphs or use invisible text over full-slide screenshots.
|
|
43
|
+
Preserve notes/citations and the original design image Drive path in each page's
|
|
44
|
+
`desc` so future edits can repeat the same reference comparison. Match the
|
|
45
|
+
reference typography and geometry; do not shrink body copy merely to hide an incorrect reconstruction.
|
|
46
|
+
7. Run `slides.inspect` with `previewPages` and corresponding `referenceImages`.
|
|
47
|
+
This renders the deck once through PPTX and LibreOffice and returns each
|
|
48
|
+
original image beside its exported counterpart in the observation sequence.
|
|
49
|
+
Compare composition, artwork, typography, line breaks, content and cleanliness.
|
|
50
|
+
`info.export.textBounds` gives measured rendered lines in SVG coordinates;
|
|
51
|
+
use them to correct placement through `slides.read/edit`, then inspect again.
|
|
52
|
+
The technical `passed` flag does not measure similarity to the reference.
|
|
53
|
+
8. Export the exact inspected revision with `slides.export`. Deliver the PPTX/PDF,
|
|
54
|
+
retain the original design images and editable source, and disclose raster-only
|
|
55
|
+
areas or unresolved differences. LibreOffice rendering does not establish pixel
|
|
56
|
+
identity in Microsoft PowerPoint.
|
|
57
|
+
|
|
58
|
+
## Direct SVG authoring
|
|
59
|
+
|
|
60
|
+
When direct vector authoring suits the request, create native text, shapes, groups
|
|
61
|
+
and optional artwork directly, then use the same inspection and export path.
|
|
62
|
+
This option does not replace a requested image-model design workflow. The live
|
|
63
|
+
SVG editor and PowerPoint use different text renderers.
|
|
64
|
+
|
|
65
|
+
## Canonical source and asset placement
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"path": "Proposals/inventory.svg",
|
|
70
|
+
"content": "<svg xmlns=\"http://www.w3.org/2000/svg\" id=\"deck\" viewBox=\"0 0 1920 1080\"><svg data-slide=\"true\" id=\"slide-1\" viewBox=\"0 0 1920 1080\"><title>Inventory</title><rect width=\"1920\" height=\"1080\" fill=\"#FFFFFF\"/><image id=\"hero\" x=\"1000\" y=\"100\" width=\"800\" height=\"600\"/><g id=\"value\"><rect x=\"80\" y=\"160\" width=\"840\" height=\"320\" rx=\"16\" fill=\"#EEF4FA\"/><text id=\"headline\" x=\"120\" y=\"250\" font-family=\"Noto Sans CJK KR\" font-size=\"48\" fill=\"#123456\">재고를 한눈에 확인</text></g></svg></svg>",
|
|
71
|
+
"assets": {
|
|
72
|
+
"hero": { "path": "Proposals/artwork.png", "crop": { "left": 620, "top": 70, "width": 800, "height": 600 } }
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Asset paths are relative to the Drive root. A plain string embeds the whole image;
|
|
78
|
+
`{path,crop}` extracts exact pixels after EXIF orientation, without resizing.
|
|
79
|
+
SVG geometry controls placement. Out-of-bounds crops fail before saving. The same
|
|
80
|
+
asset map works for creation and element edits; an existing image requires its
|
|
81
|
+
replacement fingerprint. Embedded bytes make the saved document self-contained.
|
|
82
|
+
|
|
83
|
+
`slides.read` returns a compact outline. Selected text/shapes return exact XML and
|
|
84
|
+
opaque `before` values. Images and groups containing images return attributes and
|
|
85
|
+
`imageIds` instead of embedded bytes. Use `changes:[{id,before,attributes:{x:"80"}}]`
|
|
86
|
+
to change geometry without resending content; `null` removes an attribute. Use
|
|
87
|
+
`attributes:{}` together with `assets` to replace artwork. Attribute changes and
|
|
88
|
+
`after` XML replacements are mutually exclusive and share the same fingerprint/CAS.
|
|
89
|
+
Read individual child IDs when changing the contents of a group. Insert a page under the root with `slides.edit.inserts`; preserve
|
|
90
|
+
unrelated objects. Whole-document replacement requires `expectedRevision`.
|
|
91
|
+
|
|
92
|
+
Inspection example: `slides.inspect({path:"Proposals/inventory.svg", expectedRevision,
|
|
93
|
+
previewPages:[1], referenceImages:[{page:1,path:"Proposals/design-1.png"}], language:"ko-KR"})`. Export requires `path`, `expectedRevision`, `format` (`pptx` or
|
|
94
|
+
`pdf`) and a new `outputPath`. Set `language:"ko-KR"` for Korean. `reversePages`
|
|
95
|
+
changes export page order only when requested for the actual printing workflow.
|