@gizai/cli 1.3.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +97 -80
- package/cli/computer-identity.mjs +7 -7
- package/cli/giz.mjs +101 -41
- package/cli/mcp-stdio-client.mjs +81 -0
- package/cli/media-capture.mjs +321 -0
- package/cli/update.mjs +150 -0
- package/mcp.json +6 -0
- package/package.json +28 -13
- package/plugin.json +10 -0
- package/skills/giz/SKILL.md +18 -12
- package/skills/giz/references/operations.md +1 -1
package/README.md
CHANGED
|
@@ -1,15 +1,33 @@
|
|
|
1
1
|
# Giz CLI
|
|
2
2
|
|
|
3
|
-
|
|
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
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
49
|
+
giz mcp add codex
|
|
50
|
+
giz mcp check --json # verifies the MCP handshake and lists tool names
|
|
29
51
|
```
|
|
30
52
|
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
57
|
+
## Drive, Computers and Apps
|
|
42
58
|
|
|
43
59
|
```bash
|
|
44
|
-
|
|
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
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
75
|
+
## Plugins
|
|
54
76
|
|
|
55
77
|
```bash
|
|
56
|
-
giz skill
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
94
|
+
## Configuration
|
|
78
95
|
|
|
79
|
-
|
|
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
|
-
|
|
102
|
+
The update check is cached for up to 24 hours and prints only in interactive terminals.
|
|
82
103
|
|
|
83
|
-
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
|
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
|
-
|
|
|
160
|
-
|
|
|
161
|
-
|
|
|
162
|
-
|
|
|
163
|
-
|
|
|
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,7 +1,8 @@
|
|
|
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
|
-
import { openAsBlob } from 'node:fs'
|
|
5
|
+
import { openAsBlob, statSync } from 'node:fs'
|
|
5
6
|
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
|
6
7
|
import { createServer } from 'node:http'
|
|
7
8
|
import { once } from 'node:events'
|
|
@@ -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('
|
|
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
|
-
|
|
196
|
-
|
|
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
|
}
|
|
@@ -228,6 +229,10 @@ async function api(baseUrl, pathname, data, options = {}) {
|
|
|
228
229
|
|
|
229
230
|
async function uploadMedia(baseUrl, localFile) {
|
|
230
231
|
const filePath = path.resolve(String(localFile))
|
|
232
|
+
// openAsBlob reports a missing or unreadable path only as "Unable to open file as blob".
|
|
233
|
+
let isFile = false
|
|
234
|
+
try { isFile = statSync(filePath).isFile() } catch {}
|
|
235
|
+
if (!isFile) throw new CliError(`Cannot read the file to upload: ${filePath}`, 'Check the path; it is resolved from the current directory.')
|
|
231
236
|
const body = new FormData()
|
|
232
237
|
body.append('file', await openAsBlob(filePath), path.basename(filePath))
|
|
233
238
|
const response = await authenticatedFetch(baseUrl, new URL('/api/storage/media', baseUrl), {
|
|
@@ -342,8 +347,9 @@ async function waitForCallback(expectedState) {
|
|
|
342
347
|
}
|
|
343
348
|
|
|
344
349
|
function openBrowser(url) {
|
|
345
|
-
|
|
346
|
-
const
|
|
350
|
+
// Not `cmd /c start`: cmd splits the URL at its first `&`, so Windows sign-in lost client_id and every later parameter.
|
|
351
|
+
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'rundll32' : 'xdg-open'
|
|
352
|
+
const args = process.platform === 'win32' ? ['url.dll,FileProtocolHandler', url] : [url]
|
|
347
353
|
const child = spawn(command, args, { detached: true, stdio: 'ignore' })
|
|
348
354
|
child.on('error', () => {})
|
|
349
355
|
child.unref()
|
|
@@ -404,11 +410,11 @@ async function applyUploads(baseUrl, input, values = []) {
|
|
|
404
410
|
function appRevision(options) {
|
|
405
411
|
const raw = options['expected-revision']
|
|
406
412
|
if (raw === undefined || raw === '') {
|
|
407
|
-
throw new CliError('
|
|
413
|
+
throw new CliError('A revision is required.', 'Pass the revision from giz app list as --expected-revision.')
|
|
408
414
|
}
|
|
409
415
|
const revision = Number(raw)
|
|
410
416
|
if (typeof raw !== 'string' || !/^\d+$/.test(raw) || !Number.isSafeInteger(revision) || revision < 1 || revision > 9_007_199_254_740_990) {
|
|
411
|
-
throw new CliError('
|
|
417
|
+
throw new CliError('The revision must be a safe integer of 1 or more.', 'Check the current revision with giz app list.')
|
|
412
418
|
}
|
|
413
419
|
return revision
|
|
414
420
|
}
|
|
@@ -446,6 +452,15 @@ async function serveMcp(baseUrl) {
|
|
|
446
452
|
|
|
447
453
|
async function runCommand(baseUrl, positionals, options) {
|
|
448
454
|
const [command, subcommand, ...rest] = positionals
|
|
455
|
+
if (command === '__update-check') {
|
|
456
|
+
await checkForUpdate({ file: options.cache, url: options.registry })
|
|
457
|
+
return undefined
|
|
458
|
+
}
|
|
459
|
+
if (command === 'update' && options.help === true) return { kind: 'help', value: help('update') }
|
|
460
|
+
if (command === 'update' && !subcommand) {
|
|
461
|
+
const result = await explicitUpdate({ version: CLI_VERSION, source: installSource() })
|
|
462
|
+
return { kind: 'message', value: result.message }
|
|
463
|
+
}
|
|
449
464
|
if (options.version === true) {
|
|
450
465
|
const revision = await managedRevision()
|
|
451
466
|
return { kind: 'message', value: CLI_VERSION + (revision ? ` (region ${revision.trim()})` : '') }
|
|
@@ -649,6 +664,11 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
649
664
|
if (command === 'table' && subcommand === 'action') {
|
|
650
665
|
const view = required(rest[0], 'TABLE', 'Run: giz table action TABLE ACTION [--id ID] --input JSON')
|
|
651
666
|
const action = required(rest[1], 'ACTION', 'Run: giz table get TABLE to see its actions')
|
|
667
|
+
// Namespaced table tools use their canonical external contract and table ownership checks.
|
|
668
|
+
if (action.startsWith('table.')) {
|
|
669
|
+
if (options.id !== undefined) throw new CliError('Table tools do not take --id.', 'Put row IDs in --input.')
|
|
670
|
+
return { kind: 'result', value: await invoke(baseUrl, action, { ...jsonInput(options.input), table: view }) }
|
|
671
|
+
}
|
|
652
672
|
const id = options.id === undefined ? undefined : String(options.id).includes(',') ? String(options.id).split(',') : String(options.id)
|
|
653
673
|
return {
|
|
654
674
|
kind: 'result',
|
|
@@ -727,7 +747,7 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
727
747
|
throw new CliError(`Unknown app command "${subcommand || ''}".`, 'Run: giz app --help')
|
|
728
748
|
}
|
|
729
749
|
|
|
730
|
-
if (command === '
|
|
750
|
+
if (command === 'connection') {
|
|
731
751
|
if (subcommand === 'list') {
|
|
732
752
|
return {
|
|
733
753
|
kind: 'result',
|
|
@@ -736,44 +756,65 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
736
756
|
})
|
|
737
757
|
}
|
|
738
758
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
759
|
+
throw new CliError(`Unknown connection command "${subcommand || ''}".`, 'Run: giz connection --help')
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
if (command === 'plugin') {
|
|
763
|
+
const computer = options.computer ? { computer: options.computer } : {}
|
|
764
|
+
if (subcommand === 'list' || subcommand === 'search') {
|
|
765
|
+
const query = subcommand === 'search'
|
|
766
|
+
? required(rest.join(' '), 'QUERY', 'Run: giz plugin search QUERY [--origin catalog|skill|mcp]')
|
|
767
|
+
: undefined
|
|
742
768
|
return {
|
|
743
769
|
kind: 'result',
|
|
744
|
-
value: await invoke(baseUrl, '
|
|
745
|
-
|
|
746
|
-
query,
|
|
747
|
-
...(options.limit ? { limit: Number(options.limit) } : {})
|
|
770
|
+
value: await invoke(baseUrl, 'plugin.catalog', {
|
|
771
|
+
...(query ? { query } : { origin: 'installed' }),
|
|
772
|
+
...(query && options.origin ? { origin: options.origin } : {}),
|
|
773
|
+
...(options.limit ? { limit: Number(options.limit) } : {}),
|
|
774
|
+
...computer
|
|
748
775
|
})
|
|
749
776
|
}
|
|
750
777
|
}
|
|
778
|
+
if (subcommand === 'inspect') {
|
|
779
|
+
const drivePath = required(rest[0], 'PATH', 'Run: giz plugin inspect DRIVE_PATH')
|
|
780
|
+
return { kind: 'result', value: await invoke(baseUrl, 'plugin.catalog', { path: drivePath, ...computer }) }
|
|
781
|
+
}
|
|
751
782
|
if (subcommand === 'install') {
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
783
|
+
const source = options.path
|
|
784
|
+
? { path: options.path }
|
|
785
|
+
: { id: required(rest[0], 'ID', 'Run: giz plugin install ID --query QUERY, or giz plugin install --path DRIVE_PATH') }
|
|
755
786
|
return {
|
|
756
787
|
kind: 'result',
|
|
757
|
-
value: await invoke(baseUrl, '
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
...(options.computer ? { computer: options.computer } : {})
|
|
788
|
+
value: await invoke(baseUrl, 'plugin.install', {
|
|
789
|
+
...source,
|
|
790
|
+
...(options.query ? { query: options.query } : {}),
|
|
791
|
+
...computer
|
|
762
792
|
})
|
|
763
793
|
}
|
|
764
794
|
}
|
|
765
|
-
if (
|
|
766
|
-
const
|
|
795
|
+
if (['update', 'enable', 'disable', 'remove'].includes(subcommand)) {
|
|
796
|
+
const name = required(rest[0], 'NAME', `Run: giz plugin ${subcommand} NAME`)
|
|
797
|
+
return { kind: 'result', value: await invoke(baseUrl, 'plugin.manage', { operation: subcommand, name, ...computer }) }
|
|
798
|
+
}
|
|
799
|
+
if (subcommand === 'publish') {
|
|
800
|
+
const drivePath = required(rest[0], 'PATH', 'Run: giz plugin publish DRIVE_PATH --visibility organization|public')
|
|
801
|
+
const visibility = required(options.visibility, 'VISIBILITY', 'Add --visibility organization|public.')
|
|
767
802
|
return {
|
|
768
803
|
kind: 'result',
|
|
769
|
-
value: await invoke(baseUrl, '
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
804
|
+
value: await invoke(baseUrl, 'plugin.manage', {
|
|
805
|
+
operation: 'publish',
|
|
806
|
+
path: drivePath,
|
|
807
|
+
visibility,
|
|
808
|
+
...(options.organization ? { organizationId: options.organization } : {}),
|
|
809
|
+
...computer
|
|
773
810
|
})
|
|
774
811
|
}
|
|
775
812
|
}
|
|
776
|
-
|
|
813
|
+
if (subcommand === 'unpublish') {
|
|
814
|
+
const releaseId = required(rest[0], 'RELEASE', 'Run: giz plugin unpublish RELEASE')
|
|
815
|
+
return { kind: 'result', value: await invoke(baseUrl, 'plugin.manage', { operation: 'unpublish', releaseId }) }
|
|
816
|
+
}
|
|
817
|
+
throw new CliError(`Unknown plugin command "${subcommand || ''}".`, 'Run: giz plugin --help')
|
|
777
818
|
}
|
|
778
819
|
|
|
779
820
|
if (command === 'agent') {
|
|
@@ -864,7 +905,7 @@ function print(kind, value, json, output = process.stdout) {
|
|
|
864
905
|
return
|
|
865
906
|
}
|
|
866
907
|
if (kind === 'auth' && value.identity?.type === 'computer') {
|
|
867
|
-
output.write(
|
|
908
|
+
output.write(`Signed in as a Computer identity (owner ${value.email || value.name})\n`)
|
|
868
909
|
return
|
|
869
910
|
}
|
|
870
911
|
if (kind === 'models') {
|
|
@@ -955,7 +996,7 @@ Generation returns a job immediately; use giz job wait JOB_ID for its result.`
|
|
|
955
996
|
|
|
956
997
|
TABLE is a table's Drive path (for example Sales/Customers.view.ts) or a business object name from giz table list
|
|
957
998
|
(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.
|
|
999
|
+
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
1000
|
read input: {"select":["name"],"filters":[{"field":"status","operator":"eq","value":"open"}],"limit":50}
|
|
960
1001
|
write input: {"create":[{"name":"Acme"}],"update":[{"id":"ROW_ID","status":"done"}],"remove":["ROW_ID"]}
|
|
961
1002
|
One write is one change people can undo in the table. At most 200 rows per write.`
|
|
@@ -970,11 +1011,19 @@ One write is one change people can undo in the table. At most 200 rows per write
|
|
|
970
1011
|
giz app publish APP_ID private|workspace|public --expected-revision REVISION [--computer COMPUTER]
|
|
971
1012
|
giz app open APP_ID [--computer COMPUTER]
|
|
972
1013
|
giz app delete APP_ID --expected-revision REVISION [--computer COMPUTER]`
|
|
973
|
-
if (command === '
|
|
974
|
-
giz
|
|
975
|
-
giz
|
|
976
|
-
giz
|
|
977
|
-
giz
|
|
1014
|
+
if (command === 'plugin') return `Usage
|
|
1015
|
+
giz plugin list [--computer COMPUTER]
|
|
1016
|
+
giz plugin search QUERY [--origin catalog|skill|mcp] [--limit N]
|
|
1017
|
+
giz plugin inspect DRIVE_PATH
|
|
1018
|
+
giz plugin install ID --query QUERY [--computer COMPUTER]
|
|
1019
|
+
giz plugin install --path DRIVE_PATH [--computer COMPUTER]
|
|
1020
|
+
giz plugin update|enable|disable|remove NAME [--computer COMPUTER]
|
|
1021
|
+
giz plugin publish DRIVE_PATH --visibility organization|public [--organization ORG]
|
|
1022
|
+
giz plugin unpublish RELEASE
|
|
1023
|
+
|
|
1024
|
+
A plugin (Agent Plugins 1.0) bundles skills, MCP servers and Agents; it is what you install, turn off and share.`
|
|
1025
|
+
if (command === 'connection') return `Usage
|
|
1026
|
+
giz connection list [--computer COMPUTER]`
|
|
978
1027
|
if (command === 'agent') return `Usage
|
|
979
1028
|
giz agent start [--computer COMPUTER] --input JSON
|
|
980
1029
|
giz agent status TURN [--wait]
|
|
@@ -987,6 +1036,7 @@ One write is one change people can undo in the table. At most 200 rows per write
|
|
|
987
1036
|
|
|
988
1037
|
stdio (default) shares giz auth login and refreshes tokens automatically.
|
|
989
1038
|
http uses the client's own OAuth login. Install the chosen client first.`
|
|
1039
|
+
if (command === 'update') return `Usage\n giz update\n\nUpdates @gizai/cli with its owning package manager.`
|
|
990
1040
|
if (command === 'auth') return `Usage
|
|
991
1041
|
giz auth login [--device]
|
|
992
1042
|
giz auth status
|
|
@@ -995,6 +1045,7 @@ http uses the client's own OAuth login. Install the chosen client first.`
|
|
|
995
1045
|
|
|
996
1046
|
Usage
|
|
997
1047
|
giz auth login [--device]
|
|
1048
|
+
giz update
|
|
998
1049
|
giz api-key list|create|revoke
|
|
999
1050
|
giz chat MODEL "your prompt"
|
|
1000
1051
|
giz model list [--type TYPE] [--search TEXT]
|
|
@@ -1009,7 +1060,8 @@ Usage
|
|
|
1009
1060
|
giz upload LOCAL_PATH
|
|
1010
1061
|
giz computer list|get|start|stop|exec
|
|
1011
1062
|
giz app list|create|save|run|publish|open|delete
|
|
1012
|
-
giz
|
|
1063
|
+
giz plugin list|search|inspect|install|update|enable|disable|remove|publish|unpublish
|
|
1064
|
+
giz connection list
|
|
1013
1065
|
giz agent start|status|cancel
|
|
1014
1066
|
giz engine run claude "instruction" [--dir path]
|
|
1015
1067
|
giz mcp url|serve|check|add
|
|
@@ -1029,9 +1081,17 @@ Docs: https://www.giz.ai/mcp/`
|
|
|
1029
1081
|
|
|
1030
1082
|
async function main() {
|
|
1031
1083
|
const { positionals, options } = parse(process.argv.slice(2))
|
|
1084
|
+
if (positionals[0] === '__update-check') {
|
|
1085
|
+
await runCommand('', positionals, options)
|
|
1086
|
+
return
|
|
1087
|
+
}
|
|
1088
|
+
const source = installSource()
|
|
1032
1089
|
const saved = await readConfig()
|
|
1090
|
+
const updateMode = ['notify', 'off'].includes(saved?.updates?.mode) ? saved.updates.mode : 'notify'
|
|
1091
|
+
void maybeNotify({ version: CLI_VERSION, source, mode: updateMode, env: { ...process.env, GIZ_JSON: options.json === true ? '1' : process.env.GIZ_JSON } }).catch(() => {})
|
|
1033
1092
|
const requiresAuth = positionals.length && !options.help && !options.version &&
|
|
1034
|
-
!(positionals[0] === 'auth' && ['login', 'logout'].includes(positionals[1]))
|
|
1093
|
+
!(positionals[0] === 'auth' && ['login', 'logout'].includes(positionals[1])) &&
|
|
1094
|
+
!['update', '__update-check'].includes(positionals[0])
|
|
1035
1095
|
const computer = requiresAuth && !process.env.GIZ_TOKEN && !saved?.access_token ? await computerIdentity() : undefined
|
|
1036
1096
|
const baseUrl = String(options['base-url'] || process.env.GIZ_URL || saved?.baseUrl || computer?.baseUrl || DEFAULT_URL).replace(/\/+$/, '')
|
|
1037
1097
|
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
package/package.json
CHANGED
|
@@ -1,7 +1,31 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gizai/cli",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "2.0.1",
|
|
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.1",
|
|
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
|
+
}
|
package/skills/giz/SKILL.md
CHANGED
|
@@ -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
|
|
68
|
-
`run`, `publish
|
|
69
|
-
`create
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
|
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
|
-
|
|
|
118
|
-
|
|
|
119
|
-
|
|
|
120
|
-
|
|
|
121
|
-
|
|
|
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
|
-
|
|
|
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.
|