@gizai/cli 1.3.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,33 @@
1
1
  # Giz CLI
2
2
 
3
- Connect once in the browser, then use live Giz text and media models, Drive, and Computer from a terminal or AI agent.
3
+ The official command-line interface for [giz.ai](https://www.giz.ai). Run Giz text, image, video and audio models, manage Drive files, tables, Apps and Computers, and connect Claude Code, Codex and other AI agents to your Giz account, all from a terminal.
4
+
5
+ [Documentation](https://www.giz.ai/mcp/) · [giz.ai](https://www.giz.ai) · [Sign up](https://www.giz.ai)
6
+
7
+ ## Install
8
+
9
+ Requires Node.js 20.12 or later and a [giz.ai](https://www.giz.ai) account.
4
10
 
5
11
  ```bash
6
12
  npm install -g @gizai/cli
7
- giz auth login
8
- giz chat gpt-6-luna "Say hello in Korean"
13
+ giz --version
14
+ ```
15
+
16
+ Update later with `giz update` (or `npm install -g @gizai/cli`).
17
+
18
+ ## Quick start
19
+
20
+ ```bash
21
+ giz auth login # opens your browser, one-time sign-in
22
+ giz chat gpt-6-luna "Say hello in Korean" # chat with a text model
9
23
  giz generate image "A paper observatory at blue hour"
24
+ giz credits # check your balance
25
+ giz --help # every command
10
26
  ```
11
27
 
12
- Discover a model's live inputs, upload local media, and run it:
28
+ ## Run any model
29
+
30
+ Discover a model's live inputs, upload local files, and run it:
13
31
 
14
32
  ```bash
15
33
  giz model list --type video-generation
@@ -19,104 +37,89 @@ giz model run ltx2-distilled \
19
37
  --upload image=./first-frame.png
20
38
  ```
21
39
 
22
- MCP for Claude Code and Codex (install the chosen client first):
40
+ Media jobs run independently. Track them with `giz job get JOB` or block with `giz job wait JOB` (exit status 1 if the job failed or was cancelled). Add `--json` to any command for machine-readable output on stdout; progress goes to stderr.
41
+
42
+ ## Use Giz from Claude Code, Codex and other agents
43
+
44
+ Register the Giz MCP server with your agent (install the client first):
23
45
 
24
46
  ```bash
25
47
  giz auth login
26
- giz mcp add codex
27
48
  giz mcp add claude
28
- giz mcp check --json
49
+ giz mcp add codex
50
+ giz mcp check --json # verifies the MCP handshake and lists tool names
29
51
  ```
30
52
 
31
- These commands register `giz mcp serve` over stdio. It forwards the server's live
32
- MCP protocol and shares the CLI's rotating OAuth credentials. Tokens are not
33
- copied into Claude or Codex configuration. A custom `GIZ_CONFIG` path is preserved.
53
+ This registers `giz mcp serve` over stdio. It forwards Giz's live MCP protocol and shares the CLI's credentials, so no token is copied into the client's configuration. For clients that run their own OAuth session, use `giz mcp add claude --transport http` (then sign in with `/mcp`) or `giz mcp add codex --transport http`. The remote endpoint is `https://www.giz.ai/api/mcp`; it supports OAuth discovery and scoped API keys.
34
54
 
35
- For clients with their own OAuth session, use `giz mcp add claude --transport http`
36
- and sign in through `/mcp`, or `giz mcp add codex --transport http`. The remote
37
- endpoint also supports OAuth discovery and scoped API keys. A manually copied
38
- OAuth access token expires after one hour; updating CLI credentials does not
39
- update a static client header.
55
+ Install the Giz agent skill with `giz skill install`. The package root is also an [Agent Plugins](https://agent-plugins.org) 1.0 plugin (`plugin.json`, the `giz` skill and `mcp.json`), so a client that installs plugins gets the skill and the MCP server together.
40
56
 
41
- For a headless Claude Code check, after login and registration:
57
+ ## Drive, Computers and Apps
42
58
 
43
59
  ```bash
44
- claude -p 'Use model_list to list one Giz image model.' --allowedTools mcp__giz__model_list
60
+ giz drive ls
61
+ giz drive read /notes/todo.md
62
+ giz computer list
63
+ giz computer exec --deadline-ms 120000 -- ls -la # up to 15 minutes; the remote exit code is preserved
64
+ giz app list
45
65
  ```
46
66
 
47
- `giz mcp check --json` checks the MCP handshake and lists actual tool names without
48
- running a model. Claude's `--verbose --output-format stream-json` also shows MCP
49
- connection state. If registration reports an existing `giz` entry, remove that
50
- entry with the client's `mcp remove` command before adding the desired transport.
51
- The exact client failure is returned on stderr.
67
+ App changes are explicit: pass the `revision` shown by `giz app list` as `--expected-revision REVISION` to `run`, `publish` and `delete`, and as `expectedRevision` in the `--input` of `create` and `save` (use 0 for a new App). A concurrent change is rejected by the server instead of being overwritten.
68
+
69
+ Free Computers have a runtime limit; check your plan before starting long workflows. Signing in does not extend that runtime.
70
+
71
+ ### Inside a Giz Computer
72
+
73
+ `giz` is already installed in every Giz Computer, and no login is needed: the Computer signs in as itself and can reach only its own Drive, model discovery and inference, and jobs it started. API keys, billing, organization administration, destructive operations and external writes need a separate user sign-in with `giz auth login`. `giz auth status` shows who you are.
52
74
 
53
- Agent Skill:
75
+ ## Plugins
54
76
 
55
77
  ```bash
56
- giz skill install
78
+ giz plugin search QUERY [--origin skill|mcp]
79
+ giz plugin install ID --query QUERY
80
+ giz plugin list
81
+ giz connection list # connected apps
57
82
  ```
58
83
 
59
- The CLI stores rotating OAuth credentials as unencrypted JSON in
60
- `~/.config/giz/credentials.json` with mode `0600` (owner access only), using atomic
61
- writes and serialized sign-in, refresh, and sign-out. Failed token revocation is
62
- reported before local credentials are removed. It never stores a Giz password.
63
- The sign-in callback binds a fresh loopback port; use the URL printed by the
64
- current login. Progress goes to stderr, leaving `--json` stdout machine-readable.
84
+ Also available: `plugin inspect`, `update`, `remove`, `enable`, `disable`, `publish` and `unpublish`.
65
85
 
66
- After updating from 1.0.1, run `giz auth login` again to consent to `giz.inference`.
67
- Refreshing an old grant cannot add this scope. `giz auth status --json` shows
68
- granted scopes; a missing scope error names the required permission.
86
+ ## Authentication and security
69
87
 
70
- `giz computer exec --deadline-ms 120000 -- COMMAND ARGS...` uses the bounded
71
- Computer command API (2 minutes by default, up to 15 minutes). The CLI preserves
72
- the remote command's exit code; `giz job wait` exits with status 1 for failed or
73
- cancelled jobs. JSON results remain available on stdout. Free Computers have a runtime limit; check the current
74
- Computer plan before starting long workflows. MCP authentication does not extend
75
- that runtime. Media jobs run independently and are tracked with `giz job get`.
88
+ - `giz auth login` uses browser OAuth with a loopback callback; the CLI never sees or stores your password.
89
+ - Credentials are stored as JSON in `~/.config/giz/credentials.json` with mode `0600` (owner only), written atomically. `GIZ_CONFIG` overrides the path. `giz auth logout` revokes the token and removes the file.
90
+ - `giz auth status --json` shows the granted scopes; a missing scope error names the permission it needs. After updating from 1.0.1, sign in again to grant `giz.inference`.
91
+ - Create a scoped API key with `giz api-key create "My app"` (the secret is shown once). Set `GIZ_TOKEN` only for short-lived automation.
92
+ - An OAuth access token copied by hand expires after one hour. Prefer `giz mcp add`, which refreshes it for you.
76
93
 
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.
94
+ ## Configuration
78
95
 
79
- Documentation: <https://www.giz.ai/mcp/>
96
+ | Variable | Purpose |
97
+ | --- | --- |
98
+ | `GIZ_TOKEN` | Use this API key or token instead of the stored login |
99
+ | `GIZ_CONFIG` | Path of the credentials file |
100
+ | `GIZ_NO_UPDATE_CHECK=1` | Turn off the update notice (or set `updates.mode` to `off` in `credentials.json`) |
80
101
 
81
- ### Managed Giz Computers
102
+ The update check is cached for up to 24 hours and prints only in interactive terminals.
82
103
 
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:
104
+ ## Upgrading from 1.x
86
105
 
87
- ```sh
88
- giz auth status
89
- giz drive ls
90
- giz model list
91
- ```
106
+ Version 2.0 removes `giz capability` with no alias. Skills, MCP servers and Agents now install as one plugin.
107
+
108
+ | 1.x | 2.0 |
109
+ | --- | --- |
110
+ | `giz capability list` | `giz connection list` (connected apps), `giz plugin list` (installed plugins) |
111
+ | `giz capability search skill\|mcp QUERY` | `giz plugin search QUERY [--origin skill\|mcp]` |
112
+ | `giz capability install skill\|mcp ID --query QUERY` | `giz plugin install ID --query QUERY` |
113
+ | `giz capability update\|remove ID` | `giz plugin update\|remove NAME` |
92
114
 
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 실행은 문서를 쓰지 않고 표의 차이를 실패로 보고합니다.
115
+ New in 2.0: `plugin inspect`, `install --path`, `enable`, `disable`, `publish` and `unpublish`. A Giz server that supports plugins reports 2.0.0 as its minimum CLI, so 1.x prints an update warning there.
116
+
117
+ ## Command reference
118
+
119
+ Generated from the CLI dispatcher and the Giz tool descriptors.
117
120
 
118
121
  <!-- cli-surface:start -->
119
- | CLI 명령 | 도구 ID | MCP 이름 | 필수 입력 | scope | Agent 발견 |
122
+ | Command | Tool ID | MCP tool | Required input | Scope | Agent discovery |
120
123
  | --- | --- | --- | --- | --- | --- |
121
124
  | model list | model.list | model_list | - | giz.read | model.list |
122
125
  | model get | model.get | model_get | model | giz.read | model.get |
@@ -156,12 +159,26 @@ App 변경은 `giz app list`에서 확인한 `revision`을 명시적으로 전
156
159
  | app publish | app.publish | app_publish | id, expectedRevision, visibility | giz.write | app.publish |
157
160
  | app open | app.open | app_open | id | giz.read | app.open |
158
161
  | 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 |
162
+ | plugin list | plugin.catalog | plugin_catalog | - | giz.read | plugin.catalog |
163
+ | plugin search | plugin.catalog | plugin_catalog | - | giz.read | plugin.catalog |
164
+ | plugin inspect | plugin.catalog | plugin_catalog | - | giz.read | plugin.catalog |
165
+ | plugin install | plugin.install | plugin_install | - | giz.execute | plugin.install |
166
+ | plugin update | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
167
+ | plugin enable | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
168
+ | plugin disable | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
169
+ | plugin remove | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
170
+ | plugin publish | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
171
+ | plugin unpublish | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
172
+ | connection list | capability.status | capability_status | - | giz.read | capability.status |
164
173
  | agent start | agent.start | agent_start | agentId, prompt | giz.execute | external only |
165
174
  | agent status | agent.status | agent_status | turnId | giz.read | agent.status |
166
175
  | agent cancel | agent.cancel | agent_cancel | turnId | giz.execute | agent.cancel |
167
176
  <!-- cli-surface:end -->
177
+
178
+ ## Support
179
+
180
+ Questions and bug reports: [support@giz.ai](mailto:support@giz.ai) or the Support Forum at [giz.ai](https://www.giz.ai).
181
+
182
+ ## License
183
+
184
+ Proprietary. Copyright Giz Inc. All rights reserved. Use of the service is governed by the [Terms of Use](https://www.giz.ai/terms-of-use).
@@ -33,18 +33,18 @@ export async function computerIdentity(rejectedToken) {
33
33
  res.setEncoding('utf8')
34
34
  res.on('data', chunk => {
35
35
  body += chunk
36
- if (body.length > 16_384) req.destroy(new Error('컴퓨터 신원 응답이 너무 큽니다.'))
36
+ if (body.length > 16_384) req.destroy(new Error('The Computer identity response is too large.'))
37
37
  })
38
38
  res.on('error', reject)
39
39
  res.on('end', () => {
40
40
  try {
41
41
  const value = JSON.parse(body)
42
- if (res.statusCode !== 200) throw new Error(value.error_description || value.message || '컴퓨터 신원을 사용할 수 없습니다.')
42
+ if (res.statusCode !== 200) throw new Error(value.error_description || value.message || 'The Computer identity is unavailable.')
43
43
  const url = new URL(value.baseUrl)
44
44
  if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password ||
45
45
  url.pathname !== '/' || url.search || url.hash || !value.access_token ||
46
46
  value.token_type !== 'Bearer' || Number(value.expires_in) <= 0 || Number(value.expires_in) > 900) {
47
- throw new Error('컴퓨터 신원 응답이 올바르지 않습니다.')
47
+ throw new Error('The Computer identity response is invalid.')
48
48
  }
49
49
  cached = { access_token: value.access_token, baseUrl: url.origin,
50
50
  expires_at: Date.now() + Number(value.expires_in) * 1000 }
@@ -56,20 +56,20 @@ export async function computerIdentity(rejectedToken) {
56
56
  if (error.code !== 'ENOENT') return reject(error)
57
57
  try {
58
58
  await lstat(socketPath)
59
- reject(new Error('컴퓨터 신원 서비스에 연결할 수 없습니다. 잠시 후 다시 시도해 주세요.'))
59
+ reject(new Error('Cannot reach the Computer identity service. Try again shortly.'))
60
60
  } catch (lookupError) {
61
61
  if (lookupError.code !== 'ENOENT') return reject(lookupError)
62
62
  try {
63
63
  if (await managedRevision() !== undefined) {
64
- reject(new Error('컴퓨터 신원을 확인할 수 없습니다. 잠시 후 다시 시도하거나 소유자에게 문의해 주세요.'))
64
+ reject(new Error('Cannot verify the Computer identity. Try again shortly or contact the owner.'))
65
65
  } else resolve(undefined)
66
66
  } catch (error) { reject(error) }
67
67
  }
68
68
  })
69
- req.setTimeout(10_000, () => req.destroy(new Error('컴퓨터 신원 응답 시간이 초과되었습니다.')))
69
+ req.setTimeout(10_000, () => req.destroy(new Error('The Computer identity response timed out.')))
70
70
  req.end()
71
71
  })).catch(error => {
72
- error.hint = '컴퓨터 상태를 확인하고 다시 시도해 주세요. 차단되었다면 소유자 또는 조직 관리자에게 문의해 주세요.'
72
+ error.hint = 'Check that the Computer is running and try again; if it is blocked, contact the owner or an organization admin.'
73
73
  throw error
74
74
  }).finally(() => { pending = undefined })
75
75
  return pending
package/cli/giz.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import { computerIdentity, managedRevision } from './computer-identity.mjs'
3
+ import { checkForUpdate, explicitUpdate, installSource, maybeNotify } from './update.mjs'
3
4
  import { createHash, randomBytes } from 'node:crypto'
4
5
  import { openAsBlob } from 'node:fs'
5
6
  import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
@@ -133,7 +134,7 @@ async function credentials(baseUrl, rejectedToken) {
133
134
  if (!config?.access_token) {
134
135
  const computer = await computerIdentity(rejectedToken)
135
136
  if (!computer) throw new CliError('Not authenticated.', 'Run: giz auth login')
136
- if (computer.baseUrl !== baseUrl) throw new CliError('컴퓨터 신원은 다른 서버에서 사용할 수 없습니다.', '사용자 계정으로 giz auth login을 실행해 주세요.')
137
+ if (computer.baseUrl !== baseUrl) throw new CliError('A Computer identity cannot be used with another server.', 'Run giz auth login with your user account.')
137
138
  return computer
138
139
  }
139
140
  if (String(config.baseUrl).replace(/\/+$/, '') !== baseUrl) {
@@ -190,10 +191,10 @@ async function jsonResponse(response, baseUrl) {
190
191
  if (response.status === 404 && (payload?.code === 'EXTERNAL_TOOL_UNAVAILABLE' ||
191
192
  (new URL(response.url).pathname.startsWith('/api/tools/') && /^Unknown Giz tool: /.test(message)))) {
192
193
  const compatibility = payload?.code === 'EXTERNAL_TOOL_UNAVAILABLE'
193
- ? message : '서버 버전은 확인할 수 없습니다(구버전 서버).'
194
+ ? message : 'The server version is unknown (older server).'
194
195
  throw new CliError(
195
- `이 서버는 요청한 명령을 지원하지 않습니다. CLI ${CLI_VERSION}. ${compatibility}`,
196
- `서버 주소(${baseUrl})와 지원 버전을 확인해 주세요. 지원하는 서버는 --base-url로 선택할 수 있습니다.`,
196
+ `This server does not support the requested command. CLI ${CLI_VERSION}. ${compatibility}`,
197
+ `Check the server address (${baseUrl}) and its supported version; choose a supporting server with --base-url.`,
197
198
  response.status
198
199
  )
199
200
  }
@@ -342,8 +343,9 @@ async function waitForCallback(expectedState) {
342
343
  }
343
344
 
344
345
  function openBrowser(url) {
345
- const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open'
346
- const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url]
346
+ // Not `cmd /c start`: cmd splits the URL at its first `&`, so Windows sign-in lost client_id and every later parameter.
347
+ const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'rundll32' : 'xdg-open'
348
+ const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url]
347
349
  const child = spawn(command, args, { detached: true, stdio: 'ignore' })
348
350
  child.on('error', () => {})
349
351
  child.unref()
@@ -404,11 +406,11 @@ async function applyUploads(baseUrl, input, values = []) {
404
406
  function appRevision(options) {
405
407
  const raw = options['expected-revision']
406
408
  if (raw === undefined || raw === '') {
407
- throw new CliError('개정 번호가 필요합니다.', 'giz app list에서 확인한 revision을 --expected-revision으로 전달해 주세요.')
409
+ throw new CliError('A revision is required.', 'Pass the revision from giz app list as --expected-revision.')
408
410
  }
409
411
  const revision = Number(raw)
410
412
  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을 확인해 주세요.')
413
+ throw new CliError('The revision must be a safe integer of 1 or more.', 'Check the current revision with giz app list.')
412
414
  }
413
415
  return revision
414
416
  }
@@ -446,6 +448,15 @@ async function serveMcp(baseUrl) {
446
448
 
447
449
  async function runCommand(baseUrl, positionals, options) {
448
450
  const [command, subcommand, ...rest] = positionals
451
+ if (command === '__update-check') {
452
+ await checkForUpdate({ file: options.cache, url: options.registry })
453
+ return undefined
454
+ }
455
+ if (command === 'update' && options.help === true) return { kind: 'help', value: help('update') }
456
+ if (command === 'update' && !subcommand) {
457
+ const result = await explicitUpdate({ version: CLI_VERSION, source: installSource() })
458
+ return { kind: 'message', value: result.message }
459
+ }
449
460
  if (options.version === true) {
450
461
  const revision = await managedRevision()
451
462
  return { kind: 'message', value: CLI_VERSION + (revision ? ` (region ${revision.trim()})` : '') }
@@ -649,6 +660,11 @@ async function runCommand(baseUrl, positionals, options) {
649
660
  if (command === 'table' && subcommand === 'action') {
650
661
  const view = required(rest[0], 'TABLE', 'Run: giz table action TABLE ACTION [--id ID] --input JSON')
651
662
  const action = required(rest[1], 'ACTION', 'Run: giz table get TABLE to see its actions')
663
+ // Namespaced table tools use their canonical external contract and table ownership checks.
664
+ if (action.startsWith('table.')) {
665
+ if (options.id !== undefined) throw new CliError('Table tools do not take --id.', 'Put row IDs in --input.')
666
+ return { kind: 'result', value: await invoke(baseUrl, action, { ...jsonInput(options.input), table: view }) }
667
+ }
652
668
  const id = options.id === undefined ? undefined : String(options.id).includes(',') ? String(options.id).split(',') : String(options.id)
653
669
  return {
654
670
  kind: 'result',
@@ -727,7 +743,7 @@ async function runCommand(baseUrl, positionals, options) {
727
743
  throw new CliError(`Unknown app command "${subcommand || ''}".`, 'Run: giz app --help')
728
744
  }
729
745
 
730
- if (command === 'capability') {
746
+ if (command === 'connection') {
731
747
  if (subcommand === 'list') {
732
748
  return {
733
749
  kind: 'result',
@@ -736,44 +752,65 @@ async function runCommand(baseUrl, positionals, options) {
736
752
  })
737
753
  }
738
754
  }
739
- if (subcommand === 'search') {
740
- const kind = required(rest[0], 'KIND', 'Run: giz capability search skill|mcp QUERY')
741
- const query = required(rest.slice(1).join(' '), 'QUERY', 'Run: giz capability search skill|mcp QUERY')
755
+ throw new CliError(`Unknown connection command "${subcommand || ''}".`, 'Run: giz connection --help')
756
+ }
757
+
758
+ if (command === 'plugin') {
759
+ const computer = options.computer ? { computer: options.computer } : {}
760
+ if (subcommand === 'list' || subcommand === 'search') {
761
+ const query = subcommand === 'search'
762
+ ? required(rest.join(' '), 'QUERY', 'Run: giz plugin search QUERY [--origin catalog|skill|mcp]')
763
+ : undefined
742
764
  return {
743
765
  kind: 'result',
744
- value: await invoke(baseUrl, 'capability.catalog', {
745
- kind,
746
- query,
747
- ...(options.limit ? { limit: Number(options.limit) } : {})
766
+ value: await invoke(baseUrl, 'plugin.catalog', {
767
+ ...(query ? { query } : { origin: 'installed' }),
768
+ ...(query && options.origin ? { origin: options.origin } : {}),
769
+ ...(options.limit ? { limit: Number(options.limit) } : {}),
770
+ ...computer
748
771
  })
749
772
  }
750
773
  }
774
+ if (subcommand === 'inspect') {
775
+ const drivePath = required(rest[0], 'PATH', 'Run: giz plugin inspect DRIVE_PATH')
776
+ return { kind: 'result', value: await invoke(baseUrl, 'plugin.catalog', { path: drivePath, ...computer }) }
777
+ }
751
778
  if (subcommand === 'install') {
752
- const kind = required(rest[0], 'KIND', 'Run: giz capability install skill|mcp ID --query QUERY')
753
- const id = required(rest[1], 'ID', 'Run: giz capability install skill|mcp ID --query QUERY')
754
- const query = required(options.query, 'QUERY', 'Repeat the catalog query with --query QUERY.')
779
+ const source = options.path
780
+ ? { path: options.path }
781
+ : { id: required(rest[0], 'ID', 'Run: giz plugin install ID --query QUERY, or giz plugin install --path DRIVE_PATH') }
755
782
  return {
756
783
  kind: 'result',
757
- value: await invoke(baseUrl, 'capability.install', {
758
- kind,
759
- id,
760
- query,
761
- ...(options.computer ? { computer: options.computer } : {})
784
+ value: await invoke(baseUrl, 'plugin.install', {
785
+ ...source,
786
+ ...(options.query ? { query: options.query } : {}),
787
+ ...computer
762
788
  })
763
789
  }
764
790
  }
765
- if (subcommand === 'update' || subcommand === 'remove') {
766
- const id = required(rest[0], 'ID', `Run: giz capability ${subcommand} ID`)
791
+ if (['update', 'enable', 'disable', 'remove'].includes(subcommand)) {
792
+ const name = required(rest[0], 'NAME', `Run: giz plugin ${subcommand} NAME`)
793
+ return { kind: 'result', value: await invoke(baseUrl, 'plugin.manage', { operation: subcommand, name, ...computer }) }
794
+ }
795
+ if (subcommand === 'publish') {
796
+ const drivePath = required(rest[0], 'PATH', 'Run: giz plugin publish DRIVE_PATH --visibility organization|public')
797
+ const visibility = required(options.visibility, 'VISIBILITY', 'Add --visibility organization|public.')
767
798
  return {
768
799
  kind: 'result',
769
- value: await invoke(baseUrl, 'capability.manage', {
770
- id,
771
- operation: subcommand,
772
- ...(options.computer ? { computer: options.computer } : {})
800
+ value: await invoke(baseUrl, 'plugin.manage', {
801
+ operation: 'publish',
802
+ path: drivePath,
803
+ visibility,
804
+ ...(options.organization ? { organizationId: options.organization } : {}),
805
+ ...computer
773
806
  })
774
807
  }
775
808
  }
776
- throw new CliError(`Unknown capability command "${subcommand || ''}".`, 'Run: giz capability --help')
809
+ if (subcommand === 'unpublish') {
810
+ const releaseId = required(rest[0], 'RELEASE', 'Run: giz plugin unpublish RELEASE')
811
+ return { kind: 'result', value: await invoke(baseUrl, 'plugin.manage', { operation: 'unpublish', releaseId }) }
812
+ }
813
+ throw new CliError(`Unknown plugin command "${subcommand || ''}".`, 'Run: giz plugin --help')
777
814
  }
778
815
 
779
816
  if (command === 'agent') {
@@ -864,7 +901,7 @@ function print(kind, value, json, output = process.stdout) {
864
901
  return
865
902
  }
866
903
  if (kind === 'auth' && value.identity?.type === 'computer') {
867
- output.write(`컴퓨터 신원으로 로그인됨 (소유자 ${value.email || value.name})\n`)
904
+ output.write(`Signed in as a Computer identity (owner ${value.email || value.name})\n`)
868
905
  return
869
906
  }
870
907
  if (kind === 'models') {
@@ -955,7 +992,7 @@ Generation returns a job immediately; use giz job wait JOB_ID for its result.`
955
992
 
956
993
  TABLE is a table's Drive path (for example Sales/Customers.view.ts) or a business object name from giz table list
957
994
  (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.
995
+ actions. Namespaced table actions (such as table.updateSchema) use the table tool contract; an object without a write changes only through its actions. Use --org to work in an organization.
959
996
  read input: {"select":["name"],"filters":[{"field":"status","operator":"eq","value":"open"}],"limit":50}
960
997
  write input: {"create":[{"name":"Acme"}],"update":[{"id":"ROW_ID","status":"done"}],"remove":["ROW_ID"]}
961
998
  One write is one change people can undo in the table. At most 200 rows per write.`
@@ -970,11 +1007,19 @@ One write is one change people can undo in the table. At most 200 rows per write
970
1007
  giz app publish APP_ID private|workspace|public --expected-revision REVISION [--computer COMPUTER]
971
1008
  giz app open APP_ID [--computer COMPUTER]
972
1009
  giz app delete APP_ID --expected-revision REVISION [--computer COMPUTER]`
973
- if (command === 'capability') return `Usage
974
- giz capability list [--computer COMPUTER]
975
- giz capability search skill|mcp QUERY
976
- giz capability install skill|mcp ID --query QUERY [--computer COMPUTER]
977
- giz capability update|remove ID [--computer COMPUTER]`
1010
+ if (command === 'plugin') return `Usage
1011
+ giz plugin list [--computer COMPUTER]
1012
+ giz plugin search QUERY [--origin catalog|skill|mcp] [--limit N]
1013
+ giz plugin inspect DRIVE_PATH
1014
+ giz plugin install ID --query QUERY [--computer COMPUTER]
1015
+ giz plugin install --path DRIVE_PATH [--computer COMPUTER]
1016
+ giz plugin update|enable|disable|remove NAME [--computer COMPUTER]
1017
+ giz plugin publish DRIVE_PATH --visibility organization|public [--organization ORG]
1018
+ giz plugin unpublish RELEASE
1019
+
1020
+ A plugin (Agent Plugins 1.0) bundles skills, MCP servers and Agents; it is what you install, turn off and share.`
1021
+ if (command === 'connection') return `Usage
1022
+ giz connection list [--computer COMPUTER]`
978
1023
  if (command === 'agent') return `Usage
979
1024
  giz agent start [--computer COMPUTER] --input JSON
980
1025
  giz agent status TURN [--wait]
@@ -987,6 +1032,7 @@ One write is one change people can undo in the table. At most 200 rows per write
987
1032
 
988
1033
  stdio (default) shares giz auth login and refreshes tokens automatically.
989
1034
  http uses the client's own OAuth login. Install the chosen client first.`
1035
+ if (command === 'update') return `Usage\n giz update\n\nUpdates @gizai/cli with its owning package manager.`
990
1036
  if (command === 'auth') return `Usage
991
1037
  giz auth login [--device]
992
1038
  giz auth status
@@ -995,6 +1041,7 @@ http uses the client's own OAuth login. Install the chosen client first.`
995
1041
 
996
1042
  Usage
997
1043
  giz auth login [--device]
1044
+ giz update
998
1045
  giz api-key list|create|revoke
999
1046
  giz chat MODEL "your prompt"
1000
1047
  giz model list [--type TYPE] [--search TEXT]
@@ -1009,7 +1056,8 @@ Usage
1009
1056
  giz upload LOCAL_PATH
1010
1057
  giz computer list|get|start|stop|exec
1011
1058
  giz app list|create|save|run|publish|open|delete
1012
- giz capability list|search|install|update|remove
1059
+ giz plugin list|search|inspect|install|update|enable|disable|remove|publish|unpublish
1060
+ giz connection list
1013
1061
  giz agent start|status|cancel
1014
1062
  giz engine run claude "instruction" [--dir path]
1015
1063
  giz mcp url|serve|check|add
@@ -1029,9 +1077,17 @@ Docs: https://www.giz.ai/mcp/`
1029
1077
 
1030
1078
  async function main() {
1031
1079
  const { positionals, options } = parse(process.argv.slice(2))
1080
+ if (positionals[0] === '__update-check') {
1081
+ await runCommand('', positionals, options)
1082
+ return
1083
+ }
1084
+ const source = installSource()
1032
1085
  const saved = await readConfig()
1086
+ const updateMode = ['notify', 'off'].includes(saved?.updates?.mode) ? saved.updates.mode : 'notify'
1087
+ void maybeNotify({ version: CLI_VERSION, source, mode: updateMode, env: { ...process.env, GIZ_JSON: options.json === true ? '1' : process.env.GIZ_JSON } }).catch(() => {})
1033
1088
  const requiresAuth = positionals.length && !options.help && !options.version &&
1034
- !(positionals[0] === 'auth' && ['login', 'logout'].includes(positionals[1]))
1089
+ !(positionals[0] === 'auth' && ['login', 'logout'].includes(positionals[1])) &&
1090
+ !['update', '__update-check'].includes(positionals[0])
1035
1091
  const computer = requiresAuth && !process.env.GIZ_TOKEN && !saved?.access_token ? await computerIdentity() : undefined
1036
1092
  const baseUrl = String(options['base-url'] || process.env.GIZ_URL || saved?.baseUrl || computer?.baseUrl || DEFAULT_URL).replace(/\/+$/, '')
1037
1093
  if (typeof options.org === 'string') process.env.GIZ_ORGANIZATION = options.org
@@ -0,0 +1,81 @@
1
+ // The Agent runtime's stdio MCP client. It runs in the person's Computer as
2
+ // `node --input-type=module -e <this source> <request JSON> <server argv...>`: it starts the server, initializes it,
3
+ // sends one request (tools/list follows every cursor) and prints the result as one JSON line. The server's stderr is
4
+ // shown only when the call fails. It replaced `npx @modelcontextprotocol/inspector@0.4.1 --cli`, a version without a
5
+ // CLI mode that opened its web UI and hung every plugin call until it was canceled (10/6).
6
+ import { spawn } from 'node:child_process';
7
+
8
+ const [requestJson, command, ...args] = process.argv.slice(1);
9
+ const request = JSON.parse(requestJson);
10
+ const child = spawn(command, args, { stdio: ['pipe', 'pipe', 'pipe'] });
11
+ let stderr = '';
12
+ child.stderr.on('data', (chunk) => { stderr = (stderr + chunk).slice(-8000); });
13
+
14
+ function fail(message) {
15
+ process.stderr.write(`${message}\n${stderr}`);
16
+ child.kill('SIGKILL');
17
+ process.exit(1);
18
+ }
19
+ child.on('error', (error) => fail(`MCP server could not start: ${error.message}`));
20
+ child.on('exit', (code, signal) => fail(`MCP server exited (${signal || code}) before it answered.`));
21
+
22
+ const send = (message) => child.stdin.write(`${JSON.stringify({ jsonrpc: '2.0', ...message })}\n`);
23
+ const pending = new Map();
24
+ let nextId = 1;
25
+ let buffer = '';
26
+ child.stdout.setEncoding('utf8');
27
+ child.stdout.on('data', (chunk) => {
28
+ buffer += chunk;
29
+ for (let newline = buffer.indexOf('\n'); newline >= 0; newline = buffer.indexOf('\n')) {
30
+ const line = buffer.slice(0, newline).trim();
31
+ buffer = buffer.slice(newline + 1);
32
+ let message;
33
+ // Like the MCP SDK, a line that is not JSON-RPC (a stray log) is skipped rather than ending the call.
34
+ try { message = JSON.parse(line); } catch { continue; }
35
+ if (message?.method && message.id !== undefined) {
36
+ send(message.method === 'ping' ? { id: message.id, result: {} }
37
+ : { id: message.id, error: { code: -32601, message: `${message.method} is not supported by this client.` } });
38
+ continue;
39
+ }
40
+ const waiter = pending.get(message?.id);
41
+ if (!waiter) continue;
42
+ pending.delete(message.id);
43
+ if (message.error) fail(`MCP ${waiter.method} failed: ${message.error.message || JSON.stringify(message.error)}`);
44
+ else waiter.resolve(message.result);
45
+ }
46
+ });
47
+
48
+ function call(method, params) {
49
+ return new Promise((resolve) => {
50
+ const id = nextId++;
51
+ pending.set(id, { method, resolve });
52
+ send({ id, method, ...(params === undefined ? {} : { params }) });
53
+ });
54
+ }
55
+
56
+ await call('initialize', { protocolVersion: '2025-06-18', capabilities: {}, clientInfo: { name: 'giz-agent', version: '1' } });
57
+ send({ method: 'notifications/initialized' });
58
+ let result;
59
+ if (request.method === 'tools/list') {
60
+ const tools = [];
61
+ const cursors = new Set();
62
+ let cursor;
63
+ for (;;) {
64
+ const page = await call('tools/list', cursor === undefined ? undefined : { cursor });
65
+ if (!Array.isArray(page?.tools)) fail('MCP server returned an invalid tool catalog.');
66
+ tools.push(...page.tools);
67
+ if (page?.nextCursor == null) break;
68
+ if (cursors.has(page.nextCursor) || cursors.size >= 32) fail('MCP server did not finish its tool catalog.');
69
+ cursors.add(page.nextCursor);
70
+ cursor = page.nextCursor;
71
+ }
72
+ result = { tools };
73
+ } else if (request.method === 'tools/call') {
74
+ result = await call('tools/call', { name: request.name, arguments: request.arguments ?? {} });
75
+ } else {
76
+ fail(`Unsupported MCP request ${request.method}.`);
77
+ }
78
+ child.removeAllListeners('exit');
79
+ process.stdout.write(`${JSON.stringify(result)}\n`);
80
+ child.kill();
81
+ process.exit(0);
@@ -0,0 +1,321 @@
1
+ // media.capture inside the person's Computer: records a browser scenario at 2x device pixels, logs every pointer target,
2
+ // typing span and wait, shortens long waits, and writes an editable HyperFrames take (camera that follows the action, a
3
+ // redrawn cursor, click and typing feedback, speed chips). The request JSON file (its path is the one argument)
4
+ // is deleted once read: its sign-in steps carry the person's secrets. Result JSON on stdout.
5
+ // Runs with giz-media-node (video-tools runtime): playwright-core and HYPERFRAMES_BROWSER_PATH come from that runtime.
6
+ import { createRequire } from 'node:module';
7
+ import { execFileSync } from 'node:child_process';
8
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+
11
+ const request = JSON.parse(fs.readFileSync(process.argv[1], 'utf8'));
12
+ fs.rmSync(process.argv[1], { force: true });
13
+ const { chromium } = createRequire(path.join(process.cwd(), 'noop.js'))('playwright-core');
14
+ const out = request.dir;
15
+ const [VW, VH] = request.viewport;
16
+ const [CW, CH] = request.canvas;
17
+ const FPS = 30;
18
+ const SHORTEN_AFTER = 2.5; // A wait longer than this plays at a labelled speed instead of real time.
19
+ const SHORTENED = 1.2;
20
+ fs.mkdirSync(path.join(out, 'frames'), { recursive: true });
21
+
22
+ const browser = await chromium.launch({ executablePath: process.env.HYPERFRAMES_BROWSER_PATH, args: ['--force-device-scale-factor=2'] });
23
+ const context = await browser.newContext({ viewport: { width: VW, height: VH }, deviceScaleFactor: 2, locale: request.locale,
24
+ timezoneId: request.timezone });
25
+ const page = await context.newPage();
26
+ const events = [];
27
+ let t0 = 0;
28
+ const now = () => (Date.now() - t0) / 1000;
29
+ const round = (value) => Math.round(value * 1000) / 1000;
30
+ const log = (event) => { if (t0) events.push({ t: round(now()), ...event }); };
31
+
32
+ function locate(target) {
33
+ const text = String(target || '');
34
+ if (text.startsWith('css:')) return page.locator(text.slice(4)).first();
35
+ if (text.startsWith('label:')) return page.getByLabel(text.slice(6)).locator('visible=true').first();
36
+ if (text.startsWith('placeholder:')) return page.getByPlaceholder(text.slice(12)).locator('visible=true').first();
37
+ if (text.startsWith('role:')) {
38
+ const [, role, ...name] = text.split(':');
39
+ return page.getByRole(role, { name: name.join(':') || undefined }).locator('visible=true').first();
40
+ }
41
+ // Visible text: an exact match first, so "저장" does not pick "저장된 보기".
42
+ const exact = page.getByText(text, { exact: true }).locator('visible=true');
43
+ return exact.or(page.getByText(text).locator('visible=true')).first();
44
+ }
45
+
46
+ async function box(locator) {
47
+ const b = await locator.boundingBox().catch(() => null);
48
+ return b && { x: round(b.x), y: round(b.y), w: round(b.width), h: round(b.height) };
49
+ }
50
+
51
+ // A real pointer path, so hover states appear the way a person causes them.
52
+ async function point(step) {
53
+ const locator = locate(step.target);
54
+ await locator.waitFor({ state: 'visible', timeout: step.timeoutMs ?? 15000 });
55
+ await locator.scrollIntoViewIfNeeded().catch(() => {});
56
+ const b = await box(locator);
57
+ if (!b) throw new Error(`${step.target} has no visible box`);
58
+ const x = b.x + b.w / 2, y = b.y + b.h / 2;
59
+ await page.mouse.move(x, y, { steps: 18 });
60
+ return { locator, b, x, y };
61
+ }
62
+
63
+ const label = (step) => step.label || (step.secret ? '••••' : step.target || step.value || step.action);
64
+
65
+ async function run(step) {
66
+ const timeout = step.timeoutMs ?? 15000;
67
+ switch (step.action) {
68
+ case 'open': await page.goto(new URL(step.value, page.url() === 'about:blank' ? request.url : page.url()).href, { waitUntil: 'domcontentloaded' }); log({ action: 'open', label: label(step) }); break;
69
+ case 'click': case 'doubleClick': case 'rightClick': case 'hover': {
70
+ const { b, x, y } = await point(step);
71
+ log({ action: step.action, label: label(step), box: b, x, y });
72
+ if (step.action === 'click') await page.mouse.click(x, y);
73
+ if (step.action === 'doubleClick') await page.mouse.dblclick(x, y);
74
+ if (step.action === 'rightClick') await page.mouse.click(x, y, { button: 'right' });
75
+ break;
76
+ }
77
+ case 'type': case 'fill': {
78
+ const { locator, b, x, y } = await point(step);
79
+ log({ action: 'click', label: label(step), box: b, x, y });
80
+ await page.mouse.click(x, y);
81
+ if (step.action === 'fill') await locator.fill('');
82
+ const from = now();
83
+ await locator.pressSequentially(String(step.value ?? ''), { delay: 45 });
84
+ log({ action: 'typing', label: label(step), box: await box(locator), from: round(from) });
85
+ break;
86
+ }
87
+ case 'press': await page.keyboard.press(step.value); log({ action: 'press', label: step.value }); break;
88
+ case 'upload': {
89
+ const files = String(step.value || '').split('\n').filter(Boolean).map((file) => path.resolve('/home/user', file));
90
+ await locate(step.target).setInputFiles(files, { timeout });
91
+ log({ action: 'upload', label: step.label || files.map((file) => path.basename(file)).join(', ') });
92
+ break;
93
+ }
94
+ case 'waitFor': {
95
+ const from = now();
96
+ await locate(step.target).waitFor({ state: 'visible', timeout: step.timeoutMs ?? 180000 });
97
+ log({ action: 'wait', label: label(step), from: round(from) });
98
+ break;
99
+ }
100
+ case 'scroll': {
101
+ const locator = locate(step.target);
102
+ await locator.waitFor({ state: 'attached', timeout });
103
+ await locator.evaluate((element) => element.scrollIntoView({ block: 'center', behavior: 'smooth' }));
104
+ await page.waitForTimeout(900);
105
+ log({ action: 'scroll', label: label(step), box: await box(locator) });
106
+ break;
107
+ }
108
+ case 'focus': {
109
+ const locator = locate(step.target);
110
+ await locator.waitFor({ state: 'visible', timeout });
111
+ log({ action: 'focus', label: label(step), box: await box(locator) });
112
+ await page.waitForTimeout(step.ms ?? 1500);
113
+ break;
114
+ }
115
+ case 'pause': await page.waitForTimeout(step.ms ?? 1000); break;
116
+ case 'mark': log({ action: 'mark', label: label(step) }); break;
117
+ default: throw new Error(`Unknown capture action: ${step.action}`);
118
+ }
119
+ }
120
+
121
+ const frames = [];
122
+ let cdp;
123
+ let failure;
124
+ let failedStep;
125
+ try {
126
+ await page.goto(request.url, { waitUntil: 'domcontentloaded' });
127
+ await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {});
128
+ for (const [index, step] of request.setup.entries()) {
129
+ failedStep = { phase: 'setup', index };
130
+ await run(step);
131
+ }
132
+ // Only the scenario is recorded; signing in and preparing data stay off camera.
133
+ cdp = await context.newCDPSession(page);
134
+ cdp.on('Page.screencastFrame', async (frame) => {
135
+ const file = `frames/${String(frames.length).padStart(6, '0')}.jpg`;
136
+ fs.writeFileSync(path.join(out, file), Buffer.from(frame.data, 'base64'));
137
+ frames.push([frame.metadata.timestamp, file]);
138
+ await cdp.send('Page.screencastFrameAck', { sessionId: frame.sessionId }).catch(() => {});
139
+ });
140
+ t0 = Date.now();
141
+ await cdp.send('Page.startScreencast', { format: 'jpeg', quality: 92, maxWidth: VW * 2, maxHeight: VH * 2 });
142
+ await page.waitForTimeout(800);
143
+ for (const [index, step] of request.steps.entries()) {
144
+ failedStep = { phase: 'steps', index };
145
+ await run(step);
146
+ }
147
+ await page.waitForTimeout(1200);
148
+ failedStep = undefined;
149
+ } catch (error) {
150
+ failure = String(error?.message || error).split('\n')[0];
151
+ await page.screenshot({ path: path.join(out, 'failed.png') }).catch(() => {});
152
+ } finally {
153
+ await cdp?.send('Page.stopScreencast').catch(() => {});
154
+ }
155
+ const end = now();
156
+ await context.close();
157
+ await browser.close();
158
+ if (failure) {
159
+ fs.rmSync(path.join(out, 'frames'), { recursive: true, force: true });
160
+ console.log(JSON.stringify({ failed: true, failure, step: failedStep, screenshot: 'failed.png' }));
161
+ process.exit(0);
162
+ }
163
+
164
+ // Long waits are shortened: a piecewise-linear clock maps recorded time to take time, and the frames, events and speed
165
+ // chips all read that one map, so the cursor and camera stay on the action.
166
+ const spans = events.filter((event) => event.action === 'wait' && event.t - event.from > SHORTEN_AFTER)
167
+ .map((event) => ({ from: event.from, to: event.t, rate: Math.min(16, (event.t - event.from) / SHORTENED) }));
168
+ const clock = (t) => {
169
+ let shift = 0;
170
+ for (const span of spans) {
171
+ if (t <= span.from) break;
172
+ const inside = Math.min(t, span.to) - span.from;
173
+ shift += inside - inside / span.rate;
174
+ }
175
+ return round(t - shift);
176
+ };
177
+ const duration = clock(end);
178
+ const start = frames[0][0];
179
+ const list = frames.map(([stamp, file], index) => {
180
+ const at = clock(stamp - start);
181
+ const next = index + 1 < frames.length ? clock(frames[index + 1][0] - start) : duration;
182
+ return `file '${file}'\nduration ${Math.max(0.001, next - at).toFixed(3)}`;
183
+ });
184
+ fs.writeFileSync(path.join(out, 'frames.txt'), `ffconcat version 1.0\n${list.join('\n')}\nfile '${frames.at(-1)[1]}'\n`);
185
+ // A keyframe every second: HyperFrames seeks the recording frame by frame and freezes on sparse keyframes.
186
+ // Threads and lookahead are fixed: ffmpeg sizes them by the cores it sees, and a 2 GB Computer that saw the host's cores
187
+ // was OOM-killed at 1.4 GB on a 2880x1620 take (prod 10/8); fixed, it stays near 0.55 GB on any core count.
188
+ try {
189
+ execFileSync('ffmpeg', ['-v', 'error', '-y', '-threads', '2', '-filter_threads', '1', '-f', 'concat', '-safe', '0', '-i', path.join(out, 'frames.txt'),
190
+ '-vf', `fps=${FPS},scale=trunc(iw/2)*2:trunc(ih/2)*2,format=yuv420p`, '-c:v', 'libx264', '-crf', '16', '-preset', 'medium', '-g', String(FPS),
191
+ '-threads', '2', '-rc-lookahead', '10', '-movflags', '+faststart', path.join(out, 'screen.mp4')]);
192
+ } catch (error) {
193
+ // The job reports an out-of-memory kill as such (137), not as a stack the Agent would answer by lowering quality.
194
+ if (error.signal === 'SIGKILL') { console.error('The screen recording encoder ran out of Computer memory.'); process.exit(137); }
195
+ throw error;
196
+ }
197
+ fs.rmSync(path.join(out, 'frames'), { recursive: true, force: true });
198
+ fs.rmSync(path.join(out, 'frames.txt'), { force: true });
199
+ const timeline = events.map((event) => ({ ...event, t: clock(event.t), ...(event.from !== undefined ? { from: clock(event.from) } : {}) }));
200
+ const shortened = spans.map((span) => ({ from: clock(span.from), to: clock(span.to), rate: Math.round(span.rate * 10) / 10 }));
201
+ fs.writeFileSync(path.join(out, 'events.json'), JSON.stringify({ viewport: [VW, VH], scale: 2, duration, shortened, events: timeline }, null, 1));
202
+ fs.writeFileSync(path.join(out, 'take.html'), composition(timeline, shortened));
203
+ console.log(JSON.stringify({ duration, shortened, events: timeline.map(({ t, action, label: name, from }) => ({ t, action, label: name, ...(from !== undefined ? { from } : {}) })) }));
204
+
205
+ // The take composition: the screen fills the canvas, the camera frames each cluster of actions and returns to the whole
206
+ // screen between them, and the cursor keeps its size while the camera zooms because it lives inside the camera.
207
+ function composition(timeline, shortened) {
208
+ const scale = Math.min(CW / VW, CH / VH);
209
+ const SW = VW * scale, SH = VH * scale, SX = (CW - SW) / 2, SY = (CH - SH) / 2;
210
+ const maxZoom = request.camera === false ? 1 : request.maxZoom;
211
+ const id = request.name;
212
+ const tl = [];
213
+ const html = [];
214
+ const frame = (b) => {
215
+ if (!b || maxZoom <= 1) return { s: 1, x: 0, y: 0 };
216
+ const pad = 1.3;
217
+ const s = Math.max(1, Math.min(maxZoom, SW / (b.w * scale * pad), SH / (b.h * scale * pad)));
218
+ const cx = (b.x + b.w / 2) * scale, cy = (b.y + b.h / 2) * scale;
219
+ const x = Math.min(0, Math.max(SW - SW * s, SW / 2 - cx * s));
220
+ const y = Math.min(0, Math.max(SH - SH * s, SH / 2 - cy * s));
221
+ return { s: round(s), x: Math.round(x), y: Math.round(y) };
222
+ };
223
+ const union = (a, b) => {
224
+ const x = Math.min(a.x, b.x), y = Math.min(a.y, b.y);
225
+ return { x, y, w: Math.max(a.x + a.w, b.x + b.w) - x, h: Math.max(a.y + a.h, b.y + b.h) - y };
226
+ };
227
+ // Actions close in time and space share one framing; a page change or a shortened wait ends it.
228
+ const clusters = [];
229
+ for (const event of timeline) {
230
+ const last = clusters.at(-1);
231
+ if (event.action === 'open' || event.action === 'wait') { if (last) last.closed = true; continue; }
232
+ if (!event.box) continue;
233
+ const begins = event.from ?? event.t;
234
+ const ends = event.action === 'focus' ? event.t + 1.5 : event.t;
235
+ if (last && !last.closed && begins - last.end < 3 && frame(union(last.box, event.box)).s >= 1.2) {
236
+ last.box = union(last.box, event.box);
237
+ last.end = Math.max(last.end, ends);
238
+ } else clusters.push({ start: begins, end: ends, box: event.box });
239
+ }
240
+ // A framing too wide to zoom is a full-screen shot, so the camera always lands on what the next action shows.
241
+ const moves = [];
242
+ const wide = { s: 1, x: 0, y: 0 };
243
+ let camera = wide, free = 0;
244
+ for (const [index, cluster] of clusters.entries()) {
245
+ const framed = frame(cluster.box);
246
+ const target = framed.s < 1.15 ? wide : framed;
247
+ if (target.s !== camera.s || target.x !== camera.x || target.y !== camera.y) {
248
+ const at = Math.max(free, cluster.start - 0.8);
249
+ moves.push({ at: round(at), d: round(Math.min(0.8, Math.max(0.3, cluster.start - at))), ...target });
250
+ camera = target;
251
+ }
252
+ const hold = cluster.end + 1.4;
253
+ free = hold;
254
+ const next = clusters[index + 1];
255
+ if (camera !== wide && (!next || next.start - 0.8 - hold > 1.6)) {
256
+ moves.push({ at: round(Math.min(hold, duration - 0.9)), d: 0.9, ...wide });
257
+ camera = wide;
258
+ free = hold + 0.9;
259
+ }
260
+ }
261
+ for (const move of moves) {
262
+ tl.push(`tl.to("#${id}-cam",{scale:${move.s},x:${move.x},y:${move.y},duration:${move.d},ease:"power2.inOut"},${move.at});`);
263
+ tl.push(`tl.to("#${id}-cursor .scale",{scale:${round(1 / move.s)},duration:${move.d},ease:"power2.inOut"},${move.at});`);
264
+ }
265
+ const pointers = timeline.filter((event) => ['click', 'doubleClick', 'rightClick', 'hover'].includes(event.action) && event.x !== undefined);
266
+ if (pointers.length) {
267
+ const first = pointers[0];
268
+ tl.push(`tl.set("#${id}-cursor",{x:${Math.round(Math.min(VW - 40, first.x + 240) * scale)},y:${Math.round(Math.min(VH - 40, first.y + 160) * scale)}},0);`);
269
+ let previousAt = 0;
270
+ for (const event of pointers) {
271
+ const move = round(Math.max(0.25, Math.min(0.7, event.t - previousAt - 0.1)));
272
+ tl.push(`tl.to("#${id}-cursor",{x:${Math.round(event.x * scale)},y:${Math.round(event.y * scale)},duration:${move},ease:"power2.inOut"},${round(Math.max(0, event.t - move))});`);
273
+ if (event.action !== 'hover') {
274
+ tl.push(`tl.to("#${id}-cursor svg",{scale:0.82,duration:0.08,yoyo:true,repeat:1,transformOrigin:"6px 3px"},${event.t});`);
275
+ tl.push(`tl.fromTo("#${id}-cursor .ring",{scale:0.2,opacity:0.9},{scale:1.6,opacity:0,duration:0.5,ease:"power2.out",immediateRender:false},${event.t});`);
276
+ }
277
+ previousAt = event.t;
278
+ }
279
+ }
280
+ for (const [index, event] of timeline.filter((item) => item.action === 'typing' && item.box).entries()) {
281
+ const b = event.box;
282
+ html.push(`<div class="typing" id="${id}-type${index}" style="left:${Math.round(b.x * scale - 5)}px;top:${Math.round(b.y * scale - 5)}px;width:${Math.round(b.w * scale + 10)}px;height:${Math.round(b.h * scale + 10)}px"></div>`);
283
+ tl.push(`tl.fromTo("#${id}-type${index}",{opacity:0},{opacity:1,duration:0.2},${event.from});`);
284
+ tl.push(`tl.to("#${id}-type${index}",{opacity:0,duration:0.3},${round(event.t + 0.6)});`);
285
+ }
286
+ const chips = shortened.map((span, index) => {
287
+ tl.push(`tl.fromTo("#${id}-speed${index}",{opacity:0,scale:0.9},{opacity:1,scale:1,duration:0.2},${span.from});`);
288
+ tl.push(`tl.to("#${id}-speed${index}",{opacity:0,duration:0.2},${round(Math.max(span.from + 0.2, span.to - 0.2))});`);
289
+ return `<div class="speed" id="${id}-speed${index}">▶▶ ${span.rate}×</div>`;
290
+ });
291
+ return `<!doctype html>
292
+ <html><head><meta charset="UTF-8">
293
+ <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
294
+ <style>
295
+ #${id}{position:relative;width:${CW}px;height:${CH}px;overflow:hidden;background:#0b0c10;}
296
+ #${id} .screen{position:absolute;left:${SX}px;top:${SY}px;width:${SW}px;height:${SH}px;overflow:hidden;}
297
+ #${id}-cam{position:absolute;inset:0;transform-origin:0 0;}
298
+ #${id}-cam video{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;}
299
+ #${id} .typing{position:absolute;border-radius:10px;border:3px solid #6463cc;box-shadow:0 0 0 6px rgba(100,99,204,.18);opacity:0;}
300
+ #${id}-cursor{position:absolute;left:0;top:0;width:0;height:0;z-index:5;}
301
+ #${id}-cursor .scale{position:absolute;left:-6px;top:-3px;transform-origin:6px 3px;filter:drop-shadow(0 3px 6px rgba(0,0,0,.35));}
302
+ #${id}-cursor .ring{position:absolute;left:-26px;top:-29px;width:64px;height:64px;border-radius:50%;border:3px solid #6463cc;opacity:0;}
303
+ #${id} .speed{position:absolute;right:${Math.round(SX + 28)}px;top:${Math.round(SY + 24)}px;z-index:6;background:rgba(12,14,30,.86);color:#fff;
304
+ font:600 24px/1 system-ui,sans-serif;padding:12px 20px;border-radius:999px;opacity:0;}
305
+ </style></head>
306
+ <body><div id="${id}" data-composition-id="${id}" data-start="0" data-width="${CW}" data-height="${CH}" data-duration="${duration}">
307
+ <div class="screen"><div id="${id}-cam">
308
+ <video id="${id}-screen" src="screen.mp4" muted playsinline data-start="0" data-duration="${duration}"></video>
309
+ ${html.join('\n')}
310
+ <div id="${id}-cursor"${pointers.length ? '' : ' style="display:none"'}><div class="scale"><div class="ring"></div><svg viewBox="0 0 32 32" width="${Math.round(40 * scale / 1.2)}" height="${Math.round(40 * scale / 1.2)}"><path d="M6 3 L6 25 L11.5 19.8 L15.4 28.6 L19.2 27 L15.4 18.4 L23 18.2 Z" fill="#111" stroke="#fff" stroke-width="2" stroke-linejoin="round"/></svg></div></div>
311
+ </div></div>
312
+ ${chips.join('\n')}
313
+ </div>
314
+ <script>
315
+ const tl = gsap.timeline({ paused: true });
316
+ ${tl.join('\n')}
317
+ window.__timelines = window.__timelines || {};
318
+ window.__timelines["${id}"] = tl;
319
+ </script></body></html>
320
+ `;
321
+ }
package/cli/update.mjs ADDED
@@ -0,0 +1,150 @@
1
+ import { randomBytes } from 'node:crypto'
2
+ import { mkdir, open, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawn, spawnSync } from 'node:child_process'
6
+ import { fileURLToPath } from 'node:url'
7
+
8
+ export const UPDATE_INTERVAL = 24 * 60 * 60 * 1000
9
+ export const UPDATE_URL = 'https://registry.npmjs.org/@gizai%2fcli/latest'
10
+ export const UPDATE_COMMAND = 'npm install -g @gizai/cli'
11
+ const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
12
+ const cachePath = path.join(process.env.XDG_CACHE_HOME || path.join(os.homedir(), '.cache'), 'giz', 'cli-update.json')
13
+
14
+ export function installSource(root = packageRoot) {
15
+ const resolved = path.resolve(root)
16
+ const parts = resolved.split(path.sep)
17
+ const nodeModules = parts.lastIndexOf('node_modules')
18
+ if (nodeModules < 0) return { type: 'source' }
19
+ const normalized = resolved.toLowerCase()
20
+ if (normalized.includes(`${path.sep}.pnpm${path.sep}`) || /[\\/]pnpm[\\/]global[\\/]/.test(normalized)) return { type: 'pnpm', executable: 'pnpm', args: ['add', '-g', '@gizai/cli'], command: 'pnpm add -g @gizai/cli' }
21
+ if (normalized.includes(`${path.sep}.bun${path.sep}`)) return { type: 'bun', executable: 'bun', args: ['add', '-g', '@gizai/cli'], command: 'bun add -g @gizai/cli' }
22
+ if (normalized.includes(`${path.sep}.yarn${path.sep}`) || normalized.includes(`${path.sep}yarn${path.sep}global${path.sep}`)) return { type: 'yarn', executable: 'yarn', args: ['global', 'add', '@gizai/cli'], command: 'yarn global add @gizai/cli' }
23
+ return { type: 'npm', executable: 'npm', args: ['install', '-g', '@gizai/cli'], command: UPDATE_COMMAND }
24
+ }
25
+
26
+ export function stableVersion(value) {
27
+ const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(String(value || ''))
28
+ if (!match) return undefined
29
+ const prerelease = match[4]?.split('.')
30
+ if (prerelease?.some((part) => !part || (/^\d+$/.test(part) && part.length > 1 && part.startsWith('0')))) return undefined
31
+ return { major: match[1], minor: match[2], patch: match[3], prerelease }
32
+ }
33
+
34
+ export function compareVersions(left, right) {
35
+ const a = stableVersion(left)
36
+ const b = stableVersion(right)
37
+ if (!a || !b) return undefined
38
+ for (const key of ['major', 'minor', 'patch']) {
39
+ if (a[key] === b[key]) continue
40
+ return a[key].length === b[key].length ? (a[key] < b[key] ? -1 : 1) : (a[key].length < b[key].length ? -1 : 1)
41
+ }
42
+ if (!a.prerelease && !b.prerelease) return 0
43
+ if (!a.prerelease) return 1
44
+ if (!b.prerelease) return -1
45
+ for (let index = 0; index < Math.max(a.prerelease.length, b.prerelease.length); index += 1) {
46
+ if (a.prerelease[index] === undefined) return -1
47
+ if (b.prerelease[index] === undefined) return 1
48
+ if (a.prerelease[index] === b.prerelease[index]) continue
49
+ const an = /^\d+$/.test(a.prerelease[index])
50
+ const bn = /^\d+$/.test(b.prerelease[index])
51
+ if (an && bn) {
52
+ const left = a.prerelease[index]
53
+ const right = b.prerelease[index]
54
+ return left.length === right.length ? (left < right ? -1 : 1) : (left.length < right.length ? -1 : 1)
55
+ }
56
+ if (an !== bn) return an ? -1 : 1
57
+ return a.prerelease[index] < b.prerelease[index] ? -1 : 1
58
+ }
59
+ return 0
60
+ }
61
+
62
+ export async function readUpdateCache(file = cachePath) {
63
+ try {
64
+ const cache = JSON.parse(await readFile(file, 'utf8'))
65
+ if (!Number.isFinite(cache.checkedAt) || typeof cache.latest !== 'string' || !stableVersion(cache.latest)) return undefined
66
+ return cache
67
+ } catch { return undefined }
68
+ }
69
+
70
+ async function atomicCache(file, value) {
71
+ await mkdir(path.dirname(file), { recursive: true, mode: 0o700 })
72
+ const temporary = `${file}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`
73
+ try {
74
+ await writeFile(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600, flag: 'wx' })
75
+ await rename(temporary, file)
76
+ } finally { await unlink(temporary).catch(() => {}) }
77
+ }
78
+
79
+ export async function latestStableVersion(fetcher = fetch, url = UPDATE_URL) {
80
+ const response = await fetcher(url, { signal: AbortSignal.timeout(5000), headers: { accept: 'application/json', 'npm-command': 'view' } })
81
+ if (!response.ok) throw new Error(`Registry HTTP ${response.status}`)
82
+ const version = (await response.json()).version
83
+ const parsed = stableVersion(version)
84
+ if (!parsed || parsed.prerelease) throw new Error('Registry returned no stable version')
85
+ return version
86
+ }
87
+
88
+ export async function checkForUpdate({ file = cachePath, fetcher = fetch, now = Date.now, url = UPDATE_URL } = {}) {
89
+ const lockFile = `${file}.lock`
90
+ let createdLock = false
91
+ try { await mkdir(path.dirname(file), { recursive: true, mode: 0o700 }) } catch { return undefined }
92
+ let lockHandle
93
+ try { lockHandle = await open(lockFile, 'wx', 0o600); createdLock = true }
94
+ catch (error) {
95
+ if (error.code === 'EEXIST') {
96
+ try { if (now() - (await stat(lockFile)).mtimeMs > 5 * 60_000) { await unlink(lockFile); return await checkForUpdate({ file, fetcher, now, url }) } } catch {}
97
+ }
98
+ return undefined
99
+ }
100
+ try {
101
+ const cached = await readUpdateCache(file)
102
+ if (cached && now() - cached.checkedAt < UPDATE_INTERVAL) return cached
103
+ let latest
104
+ try {
105
+ latest = await latestStableVersion(fetcher, url)
106
+ } catch { return cached }
107
+ if (!latest) return undefined
108
+ const result = { checkedAt: now(), latest }
109
+ await atomicCache(file, result)
110
+ return result
111
+ } finally {
112
+ await lockHandle.close().catch(() => {})
113
+ if (createdLock) await unlink(lockFile).catch(() => {})
114
+ }
115
+ }
116
+
117
+ export async function explicitUpdate({ version, source = installSource(), fetcher = fetch, run = spawnSync }) {
118
+ if (source.type === 'source') throw new Error('This Giz CLI is running from a source checkout; update it with your source control workflow.')
119
+ const latest = await latestStableVersion(fetcher)
120
+ const comparison = compareVersions(latest, version)
121
+ if (comparison === undefined) throw new Error('The installed CLI version is invalid; update it with your package manager.')
122
+ if (comparison <= 0) return { latest, updated: false, message: `Giz CLI ${version} is already up to date.` }
123
+ if (source.type !== 'npm') return { latest, updated: false, message: `Update available: ${version} → ${latest}\n${source.command}` }
124
+ const result = run(source.executable, source.args, { encoding: 'utf8', stdio: 'inherit' })
125
+ if (result.error || result.status !== 0) throw new Error(`${source.executable} update failed (${result.error?.code || result.signal || result.status}). Run: ${source.command}`)
126
+ return { latest, updated: true, message: `Giz CLI updated to ${latest}.` }
127
+ }
128
+
129
+ export function startBackgroundCheck({ file = cachePath, fetcherUrl = UPDATE_URL, entry = path.join(path.dirname(fileURLToPath(import.meta.url)), 'giz.mjs'), spawnProcess = spawn } = {}) {
130
+ try {
131
+ const child = spawnProcess(process.execPath, [entry, '__update-check', '--cache', file, '--registry', fetcherUrl], {
132
+ detached: true, stdio: 'ignore', env: { ...process.env, GIZ_UPDATE_CHECK_BACKGROUND: '1' }
133
+ })
134
+ child.on?.('error', () => {})
135
+ child.unref()
136
+ return true
137
+ } catch { return false }
138
+ }
139
+
140
+ export async function maybeNotify({ version, source = installSource(), mode = 'notify', env = process.env,
141
+ stderr = process.stderr, stdout = process.stdout, cacheFile = cachePath, now = Date.now,
142
+ spawnCheck = startBackgroundCheck } = {}) {
143
+ if (source.type === 'source' || mode === 'off' || env.GIZ_NO_UPDATE_CHECK === '1') return
144
+ const cache = await readUpdateCache(cacheFile)
145
+ if (!cache || cache.checkedAt <= 0 || now() - cache.checkedAt >= UPDATE_INTERVAL) spawnCheck({ file: cacheFile })
146
+ if (env.CI || !stderr.isTTY || stdout.isTTY !== true || env.GIZ_JSON === '1') return
147
+ if (cache && compareVersions(cache.latest, version) > 0) {
148
+ stderr.write(`Update available: ${version} → ${cache.latest}\n${source.command}\n`)
149
+ }
150
+ }
package/mcp.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/mcp.schema.json",
3
+ "mcpServers": {
4
+ "giz": { "type": "streamable-http", "url": "https://www.giz.ai/api/mcp" }
5
+ }
6
+ }
package/package.json CHANGED
@@ -1,7 +1,31 @@
1
1
  {
2
2
  "name": "@gizai/cli",
3
- "version": "1.3.0",
4
- "description": "Giz CLI, MCP installer, OpenAI-compatible models, Drive, and Computer",
3
+ "version": "2.0.0",
4
+ "description": "The official command-line interface for giz.ai: run AI models, Drive, Computers and Apps, and connect Claude Code, Codex and other agents over MCP.",
5
+ "keywords": [
6
+ "giz",
7
+ "giz.ai",
8
+ "cli",
9
+ "mcp",
10
+ "model-context-protocol",
11
+ "ai",
12
+ "llm",
13
+ "agent",
14
+ "claude-code",
15
+ "codex",
16
+ "image-generation",
17
+ "video-generation"
18
+ ],
19
+ "homepage": "https://www.giz.ai/mcp/",
20
+ "bugs": {
21
+ "email": "support@giz.ai",
22
+ "url": "https://www.giz.ai/mcp/"
23
+ },
24
+ "author": {
25
+ "name": "Giz Inc.",
26
+ "url": "https://www.giz.ai"
27
+ },
28
+ "license": "UNLICENSED",
5
29
  "type": "module",
6
30
  "bin": {
7
31
  "giz": "cli/giz.mjs"
@@ -9,6 +33,8 @@
9
33
  "files": [
10
34
  "cli",
11
35
  "skills",
36
+ "plugin.json",
37
+ "mcp.json",
12
38
  "README.md"
13
39
  ],
14
40
  "engines": {
@@ -18,17 +44,6 @@
18
44
  "@modelcontextprotocol/sdk": "1.29.0",
19
45
  "proper-lockfile": "4.1.2"
20
46
  },
21
- "keywords": [
22
- "giz",
23
- "mcp",
24
- "ai",
25
- "image",
26
- "video",
27
- "audio",
28
- "agent"
29
- ],
30
- "license": "UNLICENSED",
31
- "homepage": "https://www.giz.ai/mcp/",
32
47
  "repository": {
33
48
  "type": "git",
34
49
  "url": "git+https://github.com/GizAI/giz.git",
package/plugin.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
+ "name": "giz",
4
+ "version": "2.0.0",
5
+ "description": "Run Giz models, media generation, Drive, tables, Apps and Computers from any agent: the giz skill plus the Giz MCP server.",
6
+ "author": { "name": "Giz", "url": "https://www.giz.ai" },
7
+ "homepage": "https://www.giz.ai/mcp/",
8
+ "license": "UNLICENSED",
9
+ "keywords": ["giz", "mcp", "media", "drive", "computer"]
10
+ }
@@ -64,17 +64,17 @@ Pass `--json` for machine output. Use `GIZ_TOKEN` only in ephemeral automation.
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
66
 
67
- App 변경은 `giz app list`에서 확인한 `revision`을 명시적으로 전달합니다.
68
- `run`, `publish`, `delete`에는 `--expected-revision REVISION`이 필요합니다.
69
- `create`/`save`의 `--input`에는 `id`와 `expectedRevision`을 넣습니다(새 앱은 0).
70
- 목록을 다시 읽어 개정 번호를 자동 대체하지 않으므로, 검토 뒤 다른 변경이 생기면 서버가 충돌을 거부합니다.
67
+ App changes pass the `revision` read from `giz app list` explicitly.
68
+ `run`, `publish` and `delete` require `--expected-revision REVISION`.
69
+ The `--input` of `create` and `save` carries `id` and `expectedRevision` (0 for a new App).
70
+ The CLI never re-reads the list to substitute a revision, so the server rejects the call if another change landed after your review.
71
71
 
72
- 아래 표는 CLI 디스패처와 도구 item에서 생성합니다. 갱신 명령:
72
+ The table below is generated from the CLI dispatcher and the tool descriptors. To refresh it, run:
73
73
  `GIZ_UPDATE_CLI_SURFACE=1 pnpm --filter giz-app-giz test --runInBand --runTestsByPath server/external/cli-surface.spec.ts`.
74
- 일반 CI 실행은 문서를 쓰지 않고 표의 차이를 실패로 보고합니다.
74
+ A normal CI run does not write the document; it fails on any difference in the table.
75
75
 
76
76
  <!-- cli-surface:start -->
77
- | CLI 명령 | 도구 ID | MCP 이름 | 필수 입력 | scope | Agent 발견 |
77
+ | Command | Tool ID | MCP tool | Required input | Scope | Agent discovery |
78
78
  | --- | --- | --- | --- | --- | --- |
79
79
  | model list | model.list | model_list | - | giz.read | model.list |
80
80
  | model get | model.get | model_get | model | giz.read | model.get |
@@ -114,11 +114,17 @@ App 변경은 `giz app list`에서 확인한 `revision`을 명시적으로 전
114
114
  | app publish | app.publish | app_publish | id, expectedRevision, visibility | giz.write | app.publish |
115
115
  | app open | app.open | app_open | id | giz.read | app.open |
116
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 |
117
+ | plugin list | plugin.catalog | plugin_catalog | - | giz.read | plugin.catalog |
118
+ | plugin search | plugin.catalog | plugin_catalog | - | giz.read | plugin.catalog |
119
+ | plugin inspect | plugin.catalog | plugin_catalog | - | giz.read | plugin.catalog |
120
+ | plugin install | plugin.install | plugin_install | - | giz.execute | plugin.install |
121
+ | plugin update | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
122
+ | plugin enable | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
123
+ | plugin disable | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
124
+ | plugin remove | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
125
+ | plugin publish | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
126
+ | plugin unpublish | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
127
+ | connection list | capability.status | capability_status | - | giz.read | capability.status |
122
128
  | agent start | agent.start | agent_start | agentId, prompt | giz.execute | external only |
123
129
  | agent status | agent.status | agent_status | turnId | giz.read | agent.status |
124
130
  | agent cancel | agent.cancel | agent_cancel | turnId | giz.execute | agent.cancel |
@@ -14,6 +14,6 @@
14
14
  | Discover and control Computers | `computer_list`, `computer_start`, `computer_stop` |
15
15
  | Run a Computer command | `giz_computer_exec` |
16
16
  | Find, inspect, or interact with an existing terminal | `terminal_list`, `terminal_observe`, `terminal_send` |
17
- | Inspect or transform media | `media_probe`, `media_process` |
17
+ | Record, inspect or transform media | `media_capture`, `media_probe`, `media_process` |
18
18
 
19
19
  Always prefer the live `tools/list` result over this map. Streamable HTTP MCP is `/api/mcp`; ChatGPT OpenAPI is `/api/openapi.json`; the authenticated HTTP manifest is `/api/tools`. `giz_computer_exec` accepts an exact Computer name, slug, or ID as `computer`, argv `command`, optional `stdin`, `cwd`, and `deadlineMs`; do not send `timeoutMs`, `yieldMs`, `sessionId`, or shell command strings. Omit `computer` for the default Computer and use `computer_list` only when the target is unknown or ambiguous.