@gizai/cli 2.2.0 → 2.2.2
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/LICENSE +21 -0
- package/cli/giz.mjs +53 -8
- package/cli/gizlang-commands.json +210 -0
- package/cli/toolchain.mjs +34 -26
- package/cli/update.mjs +4 -4
- package/package.json +5 -4
- package/plugin.json +1 -1
- package/skills/giz/SKILL.md +1 -3
- package/cli/codex-relay.mjs +0 -46
- package/cli/mcp-stdio-client.mjs +0 -81
- package/cli/media-capture.mjs +0 -321
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Giz Systems Inc. and the Gizlang contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/cli/giz.mjs
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { computerIdentity, managedRevision } from './computer-identity.mjs'
|
|
3
3
|
import { checkForUpdate, explicitUpdate, installSource, maybeNotify } from './update.mjs'
|
|
4
|
-
import { runToolchain, toolchainCommand } from './toolchain.mjs'
|
|
4
|
+
import { GROUPS, runToolchain, toolchainCommand, toolchainRoot, toolchainVersion } from './toolchain.mjs'
|
|
5
5
|
import { createHash, randomBytes } from 'node:crypto'
|
|
6
|
-
import { openAsBlob, statSync } from 'node:fs'
|
|
6
|
+
import { existsSync, openAsBlob, rmSync, statSync } from 'node:fs'
|
|
7
7
|
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
|
8
|
+
import { createInterface } from 'node:readline/promises'
|
|
8
9
|
import { createServer } from 'node:http'
|
|
9
10
|
import { once } from 'node:events'
|
|
10
11
|
import { createRequire } from 'node:module'
|
|
@@ -457,6 +458,7 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
457
458
|
await checkForUpdate({ file: options.cache, url: options.registry })
|
|
458
459
|
return undefined
|
|
459
460
|
}
|
|
461
|
+
if (command === 'uninstall') return { kind: 'message', value: options.help === true ? help('uninstall') : await uninstall(options) }
|
|
460
462
|
if (command === 'update' && options.help === true) return { kind: 'help', value: help('update') }
|
|
461
463
|
if (command === 'update' && !subcommand) {
|
|
462
464
|
const result = await explicitUpdate({ version: CLI_VERSION, source: installSource() })
|
|
@@ -464,14 +466,15 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
464
466
|
}
|
|
465
467
|
if (options.version === true) {
|
|
466
468
|
const revision = await managedRevision()
|
|
467
|
-
|
|
469
|
+
const toolchain = toolchainVersion() ?? `not installed (${toolchainRoot()})`
|
|
470
|
+
return { kind: 'message', value: `@gizai/cli ${CLI_VERSION}${revision ? ` (region ${revision.trim()})` : ''}\nGizlang toolchain: ${toolchain}` }
|
|
468
471
|
}
|
|
469
472
|
if (!command || command === 'help' || options.help === true) {
|
|
470
473
|
// A script that asks for help on a command that does not exist must fail, not read the overview as success.
|
|
471
474
|
if (command && !gizCommand(command)) {
|
|
472
475
|
throw new CliError(`Unknown command "${command}".`, 'Run: giz --help')
|
|
473
476
|
}
|
|
474
|
-
return { kind: 'help', value: help(command) }
|
|
477
|
+
return { kind: 'help', value: help(command, options.all === true) }
|
|
475
478
|
}
|
|
476
479
|
if (command === 'engine' && subcommand === 'run') {
|
|
477
480
|
if (rest[0] !== 'claude') throw new CliError('Only Claude Code is an engine connection.')
|
|
@@ -938,7 +941,13 @@ function print(kind, value, json, output = process.stdout) {
|
|
|
938
941
|
output.write(`${JSON.stringify(value, null, 2)}\n`)
|
|
939
942
|
}
|
|
940
943
|
|
|
941
|
-
function help(command) {
|
|
944
|
+
function help(command, all = false) {
|
|
945
|
+
if (command === 'uninstall') return `Usage
|
|
946
|
+
giz uninstall [--yes] [--credentials]
|
|
947
|
+
|
|
948
|
+
Removes the Gizlang toolchain (${toolchainRoot()}; GIZ_INSTALL names another folder) after a confirmation.
|
|
949
|
+
--yes skips the confirmation. --credentials also removes your giz.ai login (${configPath}).
|
|
950
|
+
It prints the command that removes @gizai/cli itself.`
|
|
942
951
|
if (command === 'engine') return `Usage
|
|
943
952
|
giz engine run claude "instruction" [--dir path]
|
|
944
953
|
|
|
@@ -1047,6 +1056,7 @@ http uses the client's own OAuth login. Install the chosen client first.`
|
|
|
1047
1056
|
Usage
|
|
1048
1057
|
giz auth login [--device]
|
|
1049
1058
|
giz update
|
|
1059
|
+
giz uninstall [--yes] [--credentials]
|
|
1050
1060
|
giz api-key list|create|revoke
|
|
1051
1061
|
giz chat MODEL "your prompt"
|
|
1052
1062
|
giz model list [--type TYPE] [--search TEXT]
|
|
@@ -1071,8 +1081,8 @@ Usage
|
|
|
1071
1081
|
|
|
1072
1082
|
Use giz COMMAND --help for parameters and examples.
|
|
1073
1083
|
|
|
1074
|
-
Gizlang (the toolchain is installed on first use, with its size shown)
|
|
1075
|
-
|
|
1084
|
+
Gizlang (giz COMMAND; the toolchain is installed on first use, with its size shown; gizdb runs the database server)
|
|
1085
|
+
${gizlangCommands(all)}
|
|
1076
1086
|
Docs: https://gizstack.com/docs
|
|
1077
1087
|
|
|
1078
1088
|
Global options
|
|
@@ -1084,6 +1094,41 @@ Global options
|
|
|
1084
1094
|
Docs: https://www.giz.ai/mcp/`
|
|
1085
1095
|
}
|
|
1086
1096
|
|
|
1097
|
+
// The toolchain's commands by group, as its own help shows them, from the generated table (toolchain.mjs GROUPS), so the
|
|
1098
|
+
// list needs no download. Its Repository group serves a Gizlang checkout and shows only with --all. No line starts with
|
|
1099
|
+
// " giz ": gizCommand reads those as giz.ai's.
|
|
1100
|
+
function gizlangCommands(all) {
|
|
1101
|
+
return GROUPS.filter((group) => all || group.name !== 'Repository').map((group) => ` ${group.name}:\n` +
|
|
1102
|
+
group.commands.map((command) => ` ${command.synopsis.padEnd(30)} ${command.summary}`).join('\n')).join('\n')
|
|
1103
|
+
}
|
|
1104
|
+
|
|
1105
|
+
// Removes the toolchain's folder and nothing else: a folder without the toolchain's program is refused.
|
|
1106
|
+
async function uninstall(options) {
|
|
1107
|
+
const root = toolchainRoot()
|
|
1108
|
+
const after = `\nTo remove @gizai/cli itself, run: ${installSource().remove ?? 'npm uninstall -g @gizai/cli'}`
|
|
1109
|
+
const removed = []
|
|
1110
|
+
if (existsSync(root)) {
|
|
1111
|
+
if (!existsSync(path.join(root, 'bin', process.platform === 'win32' ? 'giz.exe' : 'giz'))) {
|
|
1112
|
+
throw new CliError(`${root} does not hold the Gizlang toolchain (no bin/giz); nothing was removed.`)
|
|
1113
|
+
}
|
|
1114
|
+
if (options.yes !== true) {
|
|
1115
|
+
if (!process.stdin.isTTY) throw new CliError(`Removing ${root} needs a confirmation.`, 'Run: giz uninstall --yes')
|
|
1116
|
+
const ask = createInterface({ input: process.stdin, output: process.stderr })
|
|
1117
|
+
const login = options.credentials === true ? ` and your giz.ai login (${configPath})` : ''
|
|
1118
|
+
const answer = await ask.question(`Remove the Gizlang toolchain at ${root}${login}? [y/N] `)
|
|
1119
|
+
ask.close()
|
|
1120
|
+
if (!/^y(es)?$/i.test(answer.trim())) return `Nothing was removed.${after}`
|
|
1121
|
+
}
|
|
1122
|
+
rmSync(root, { recursive: true, force: true })
|
|
1123
|
+
removed.push(root)
|
|
1124
|
+
}
|
|
1125
|
+
if (options.credentials === true && existsSync(configPath)) {
|
|
1126
|
+
rmSync(configPath)
|
|
1127
|
+
removed.push(configPath)
|
|
1128
|
+
}
|
|
1129
|
+
return (removed.length ? `Removed ${removed.join(' and ')}.` : `The Gizlang toolchain is not installed (${root}).`) + after
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1087
1132
|
// giz.ai's commands are the ones its overview lists; every other command is the Gizlang toolchain's (toolchain.mjs).
|
|
1088
1133
|
function gizCommand(name) {
|
|
1089
1134
|
return ['help', 'models', '__update-check'].includes(name) ||
|
|
@@ -1108,7 +1153,7 @@ async function main() {
|
|
|
1108
1153
|
void maybeNotify({ version: CLI_VERSION, source, mode: updateMode, env: { ...process.env, GIZ_JSON: options.json === true ? '1' : process.env.GIZ_JSON } }).catch(() => {})
|
|
1109
1154
|
const requiresAuth = positionals.length && !options.help && !options.version &&
|
|
1110
1155
|
!(positionals[0] === 'auth' && ['login', 'logout'].includes(positionals[1])) &&
|
|
1111
|
-
!['update', '__update-check'].includes(positionals[0])
|
|
1156
|
+
!['help', 'update', 'uninstall', '__update-check'].includes(positionals[0])
|
|
1112
1157
|
const computer = requiresAuth && !process.env.GIZ_TOKEN && !saved?.access_token ? await computerIdentity() : undefined
|
|
1113
1158
|
const baseUrl = String(options['base-url'] || process.env.GIZ_URL || saved?.baseUrl || computer?.baseUrl || DEFAULT_URL).replace(/\/+$/, '')
|
|
1114
1159
|
if (typeof options.org === 'string') process.env.GIZ_ORGANIZATION = options.org
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schema": "gizai.gizlang-commands.v1",
|
|
3
|
+
"groups": [
|
|
4
|
+
{
|
|
5
|
+
"name": "Daily work",
|
|
6
|
+
"commands": [
|
|
7
|
+
{
|
|
8
|
+
"name": "init",
|
|
9
|
+
"synopsis": "init [FLAG...] [DIR]",
|
|
10
|
+
"summary": "Create a Hello project: `main.giz`, its test `main.test.giz` and a `package.json` that names the entry, or with `--template NAME` a copy of an example app."
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"name": "run",
|
|
14
|
+
"synopsis": "run [FLAG...] FILE [ARGUMENT...]",
|
|
15
|
+
"summary": "Build FILE (cached) and run it with the arguments."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "check",
|
|
19
|
+
"synopsis": "check [FLAG...] FILE...",
|
|
20
|
+
"summary": "Check each FILE (`.giz`, `.gizgraph` or `package.json`)."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "build",
|
|
24
|
+
"synopsis": "build [FLAG...] FILE",
|
|
25
|
+
"summary": "Compile FILE to a program for this CPU, and print its path."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "test",
|
|
29
|
+
"synopsis": "test [FLAG...] [FILE... | FOLDER | FILTER]",
|
|
30
|
+
"summary": "Run the tests of each FILE: a test function `testName()`, or vitest's `test(\"name\", () => ...)` with `expect`."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "live",
|
|
34
|
+
"synopsis": "live [FLAG...] FILE",
|
|
35
|
+
"summary": "Run FILE, and replace its code while it runs each time you save a source in its folder."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "doc",
|
|
39
|
+
"synopsis": "doc [FLAG...] [NAME]",
|
|
40
|
+
"summary": "Show the documentation of NAME: a module (`gizlang:json`), a declaration (`Json.get`) or a command (`check`)."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "repl",
|
|
44
|
+
"synopsis": "repl",
|
|
45
|
+
"summary": "Start the read-eval-print loop."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "fmt",
|
|
49
|
+
"synopsis": "fmt [FLAG...] FILE...",
|
|
50
|
+
"summary": "Lay out each FILE by its brackets and spacing rules; a line over 120 characters is cut at its statements, blocks, items and lists."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "help",
|
|
54
|
+
"synopsis": "help [--all | COMMAND]",
|
|
55
|
+
"summary": "Print the commands by group, or the help of COMMAND."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "version",
|
|
59
|
+
"synopsis": "version",
|
|
60
|
+
"summary": "Print the identity of the toolchain."
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"name": "Code and packages",
|
|
66
|
+
"commands": [
|
|
67
|
+
{
|
|
68
|
+
"name": "python",
|
|
69
|
+
"synopsis": "python [OPTION...] (-c CODE | -m MODULE | FILE | -) [ARGUMENT...]",
|
|
70
|
+
"summary": "Run Python as `python3` does, on the VM: CPython's options are taken (most have no effect)."
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"name": "wasm",
|
|
74
|
+
"synopsis": "wasm FILE [ARGUMENT...]",
|
|
75
|
+
"summary": "Build FILE as a wasm64 module and run it in `runtime/wasm.mjs`, which gives it only the console."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "outline",
|
|
79
|
+
"synopsis": "outline [FLAG...] FILE",
|
|
80
|
+
"summary": "Print the declarations of FILE, one a line: where it is, its signature and effects, and its doc summary under it."
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "context",
|
|
84
|
+
"synopsis": "context FILE WORD...",
|
|
85
|
+
"summary": "Print the declarations that mention a WORD in full, and the first lines of their neighbours."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "project",
|
|
89
|
+
"synopsis": "project FILE",
|
|
90
|
+
"summary": "Print the canonical source of FILE, with the sugar spelled out."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "edit",
|
|
94
|
+
"synopsis": "edit FILE",
|
|
95
|
+
"summary": "Apply the JSON edit request on stdin to FILE, if the result checks."
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "lsp",
|
|
99
|
+
"synopsis": "lsp",
|
|
100
|
+
"summary": "Start the language server on stdin and stdout."
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "bind",
|
|
104
|
+
"synopsis": "bind HEADER LIBRARY NAME...",
|
|
105
|
+
"summary": "Print `declare` lines for the C functions NAME of HEADER in LIBRARY."
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "codec",
|
|
109
|
+
"synopsis": "codec FILE TYPE...",
|
|
110
|
+
"summary": "Print JSON functions for the record types TYPE of FILE."
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "i18n",
|
|
114
|
+
"synopsis": "i18n [FLAG...] DIR",
|
|
115
|
+
"summary": "Bring the catalogs `locales/TAG.json` of the `.ghtml` files under DIR up to date: add each message a catalog lacks with an empty translation (shown in English until translated), remove the keys no file shows, and print how much of each catalog is translated."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "pin",
|
|
119
|
+
"synopsis": "pin DIR",
|
|
120
|
+
"summary": "Write the sha256 pins in `DIR/package.json` again, dependencies first."
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"name": "get",
|
|
124
|
+
"synopsis": "get [FLAG...] DIR [NAME@VERSION... npm:NAME@SPEC...]",
|
|
125
|
+
"summary": "Raise the requirements of the package in DIR, vendor the minimal version selection, run `npm install` (without its scripts) and pin the result."
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"name": "capabilities",
|
|
129
|
+
"synopsis": "capabilities",
|
|
130
|
+
"summary": "Write the operations of this command line and the exact identity of the running toolchain."
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"name": "completion",
|
|
134
|
+
"synopsis": "completion bash",
|
|
135
|
+
"summary": "Print a bash completion script."
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"name": "vscode",
|
|
139
|
+
"synopsis": "vscode",
|
|
140
|
+
"summary": "Install the Gizlang extension of this toolchain into VS Code."
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "licenses",
|
|
144
|
+
"synopsis": "licenses FILE",
|
|
145
|
+
"summary": "Print the third-party notices for the native link inputs of FILE."
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"name": "doctor",
|
|
149
|
+
"synopsis": "doctor",
|
|
150
|
+
"summary": "Check what this machine gives Gizlang: the system and its glibc, the toolchain, `giz` on PATH, free disk space, each GPU with its memory and driver, the models downloaded and the release host."
|
|
151
|
+
}
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
"name": "Deploy and cloud",
|
|
156
|
+
"commands": [
|
|
157
|
+
{
|
|
158
|
+
"name": "deploy",
|
|
159
|
+
"synopsis": "deploy [FLAG...] [FILE...]",
|
|
160
|
+
"summary": "Without `--live`, send FILE, else the folder's `app.giz` (and `app.test.giz`), to GizStack with your terminal token (`GIZ_TOKEN`): it checks and tests the change and publishes it into the running app at NAME.giz.sh, or refuses it and the old version keeps serving."
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
"name": "ai",
|
|
164
|
+
"synopsis": "ai [FLAG...] models | pull NAME | chat NAME [PROMPT...] | image PROMPT... | serve NAME... | worker NAME | gate URL CASES DIR | registry",
|
|
165
|
+
"summary": "Run AI models on this computer by name: `models` lists the catalog (the models of every family in ai/models); any Hugging Face repository (`owner/name`) of a family gizai:llm reads is a chat model too."
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"name": "api",
|
|
169
|
+
"synopsis": "api [FLAG...] [NAME [FIELD=VALUE...]]",
|
|
170
|
+
"summary": "Run the GizStack operation NAME with your terminal token (`GIZ_TOKEN`), as the console, MCP and agents do, and print its result as JSON; a refusal prints why and exits 1."
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "ssh-proxy",
|
|
174
|
+
"synopsis": "ssh-proxy COMPUTER [PORT]",
|
|
175
|
+
"summary": "Reach a running Giz Computer of your team through GizStack with your terminal token (`GIZ_TOKEN`): standard input and output are copied through a one-use tunnel to PORT (22 by default) of the Computer, with no public address."
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"name": "publish",
|
|
179
|
+
"synopsis": "publish DIR [NAME@VERSION]",
|
|
180
|
+
"summary": "Publish the package in DIR under the name and version of its manifest."
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "public-key",
|
|
184
|
+
"synopsis": "public-key SEED",
|
|
185
|
+
"summary": "Print the hexadecimal Ed25519 public key of the 32-byte SEED file (`GIZLANG_SIGNING_KEY`), for `GIZLANG_TRUSTED_KEYS`."
|
|
186
|
+
}
|
|
187
|
+
]
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
"name": "Repository",
|
|
191
|
+
"commands": [
|
|
192
|
+
{
|
|
193
|
+
"name": "storage",
|
|
194
|
+
"synopsis": "storage [FLAG...] OPERATION [OPERAND...]",
|
|
195
|
+
"summary": "Manage a storage range: buckets, objects (`ls`, `cp`, `rm`, `stat`, `presign`), keys, usage, a FUSE mount and block volumes."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
"name": "bench",
|
|
199
|
+
"synopsis": "bench [FLAG...] list [SUITE] | run|show|compare|record ID...",
|
|
200
|
+
"summary": "Run the benchmark suites of this toolchain, all or in part."
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"name": "work",
|
|
204
|
+
"synopsis": "work [FLAG...] OPERATION [OPERAND...]",
|
|
205
|
+
"summary": "Manage the work items of the git repository here: `list`, `show`, `open`, `drop`, `claim`, `release`, `claims`, `age`, `burndown`, `check`."
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
]
|
|
210
|
+
}
|
package/cli/toolchain.mjs
CHANGED
|
@@ -1,20 +1,19 @@
|
|
|
1
1
|
import { spawnSync } from 'node:child_process'
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
2
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
3
|
import os from 'node:os'
|
|
4
4
|
import path from 'node:path'
|
|
5
|
-
import { createInterface } from 'node:readline/promises'
|
|
6
5
|
|
|
7
6
|
// The Gizlang toolchain (GizStack: giz init, run, build, test, deploy, … and gizdb) is a native program per OS. This
|
|
8
|
-
// package carries none of it: the first command that needs it installs it with gizstack.com's
|
|
9
|
-
// this OS, which names the download size, checks the release signature and every file,
|
|
10
|
-
// project pin or the active one). Later runs execute the installed program at once.
|
|
7
|
+
// package carries none of it: the first command that needs it installs it at once, with no question, by gizstack.com's
|
|
8
|
+
// own install script for this OS, which names the download size first, checks the release signature and every file,
|
|
9
|
+
// and selects the version (a project pin or the active one). Later runs execute the installed program at once.
|
|
11
10
|
const SITE = 'https://gizstack.com'
|
|
12
|
-
// The toolchain's public commands (
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
11
|
+
// The toolchain's public commands by help group, written from its command table (tools/cli/commands.giz) by
|
|
12
|
+
// tools/cli/npm/commands.giz. Only these, or a program file, install it: an agent that asks for help on a command that
|
|
13
|
+
// does not exist must get an error, not a 200 MB download. Once the toolchain is installed, every command giz.ai does not
|
|
14
|
+
// know goes to it, and it owns the error.
|
|
15
|
+
export const GROUPS = JSON.parse(readFileSync(new URL('./gizlang-commands.json', import.meta.url), 'utf8')).groups
|
|
16
|
+
const COMMANDS = new Set(GROUPS.flatMap((group) => group.commands.map((command) => command.name)))
|
|
18
17
|
const windows = process.platform === 'win32'
|
|
19
18
|
|
|
20
19
|
export function toolchainRoot(env = process.env) {
|
|
@@ -30,26 +29,36 @@ function installed(root, program) {
|
|
|
30
29
|
return path.join(root, 'bin', program + (windows ? '.exe' : ''))
|
|
31
30
|
}
|
|
32
31
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return !/^n/i.test(answer.trim())
|
|
40
|
-
} finally { prompt.close() }
|
|
32
|
+
/** The installed toolchain's identity (`giz version`), or undefined when it is not installed. It never installs it. */
|
|
33
|
+
export function toolchainVersion() {
|
|
34
|
+
const file = installed(toolchainRoot(), 'giz')
|
|
35
|
+
if (!existsSync(file)) return undefined
|
|
36
|
+
const ran = spawnSync(file, ['version'], { encoding: 'utf8' })
|
|
37
|
+
return ran.status === 0 ? ran.stdout.trim() : undefined
|
|
41
38
|
}
|
|
42
39
|
|
|
43
40
|
async function install(root) {
|
|
44
41
|
const response = await fetch(`${SITE}/${windows ? 'install.ps1' : 'install'}`)
|
|
45
42
|
if (!response.ok) throw new Error(`cannot download the install script: HTTP ${response.status} from ${SITE}`)
|
|
46
43
|
const script = await response.text()
|
|
47
|
-
// The script's progress goes to stderr: stdout stays the command's own.
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
// The script's progress goes to stderr: stdout stays the command's own. PATH is this package's: the script changes none.
|
|
45
|
+
const env = { ...process.env, GIZ_INSTALL: root, NO_MODIFY_PATH: '1' }
|
|
46
|
+
if (!windows) {
|
|
47
|
+
const ran = spawnSync('sh', ['-s', '--', '--no-modify-path'], { input: script, stdio: ['pipe', process.stderr, 'inherit'], env })
|
|
48
|
+
if (ran.error) throw new Error(`cannot run the install script: ${ran.error.message}`)
|
|
49
|
+
return ran.status === 0
|
|
50
|
+
}
|
|
51
|
+
// PowerShell runs a file whole; from stdin it runs line by line and breaks the script's blocks.
|
|
52
|
+
const folder = mkdtempSync(path.join(os.tmpdir(), 'giz-install-'))
|
|
53
|
+
try {
|
|
54
|
+
const file = path.join(folder, 'install.ps1')
|
|
55
|
+
writeFileSync(file, script)
|
|
56
|
+
const ran = spawnSync('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', file], { stdio: ['ignore', process.stderr, 'inherit'], env })
|
|
57
|
+
if (ran.error) throw new Error(`cannot run the install script: ${ran.error.message}`)
|
|
58
|
+
return ran.status === 0
|
|
59
|
+
} finally {
|
|
60
|
+
rmSync(folder, { recursive: true, force: true })
|
|
61
|
+
}
|
|
53
62
|
}
|
|
54
63
|
|
|
55
64
|
/** Runs PROGRAM (giz or gizdb) of the toolchain with ARGS, installing the toolchain first when it is missing. */
|
|
@@ -57,7 +66,6 @@ export async function runToolchain(program, args) {
|
|
|
57
66
|
const root = toolchainRoot()
|
|
58
67
|
const file = installed(root, program)
|
|
59
68
|
if (!existsSync(file)) {
|
|
60
|
-
if (!await confirmed(args[0] || program)) { process.exitCode = 1; return }
|
|
61
69
|
if (!await install(root) || !existsSync(file)) {
|
|
62
70
|
process.stderr.write(`The Gizlang toolchain is not installed, so giz ${args[0] || program} cannot run here. Supported platforms: ${SITE}/download\n`)
|
|
63
71
|
process.exitCode = 1
|
package/cli/update.mjs
CHANGED
|
@@ -17,10 +17,10 @@ export function installSource(root = packageRoot) {
|
|
|
17
17
|
const nodeModules = parts.lastIndexOf('node_modules')
|
|
18
18
|
if (nodeModules < 0) return { type: 'source' }
|
|
19
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 }
|
|
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', remove: 'pnpm remove -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', remove: 'bun remove -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', remove: 'yarn global remove @gizai/cli' }
|
|
23
|
+
return { type: 'npm', executable: 'npm', args: ['install', '-g', '@gizai/cli'], command: UPDATE_COMMAND, remove: 'npm uninstall -g @gizai/cli' }
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
export function stableVersion(value) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gizai/cli",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.2",
|
|
4
4
|
"description": "The official command-line interface for giz.ai: run AI models, Drive, Computers and Apps, connect Claude Code, Codex and other agents over MCP, and run Gizlang (giz init, run, deploy), installed on first use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"giz",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"name": "Giz Inc.",
|
|
26
26
|
"url": "https://www.giz.ai"
|
|
27
27
|
},
|
|
28
|
-
"license": "
|
|
28
|
+
"license": "MIT",
|
|
29
29
|
"type": "module",
|
|
30
30
|
"bin": {
|
|
31
31
|
"giz": "cli/giz.mjs",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"files": [
|
|
35
35
|
"cli",
|
|
36
36
|
"skills",
|
|
37
|
+
"LICENSE",
|
|
37
38
|
"plugin.json",
|
|
38
39
|
"mcp.json",
|
|
39
40
|
"README.md"
|
|
@@ -47,8 +48,8 @@
|
|
|
47
48
|
},
|
|
48
49
|
"repository": {
|
|
49
50
|
"type": "git",
|
|
50
|
-
"url": "git+https://github.com/gizai/
|
|
51
|
-
"directory": "
|
|
51
|
+
"url": "git+https://github.com/gizai/gizlang.git",
|
|
52
|
+
"directory": "tools/cli/npm"
|
|
52
53
|
},
|
|
53
54
|
"publishConfig": {
|
|
54
55
|
"access": "public"
|
package/plugin.json
CHANGED
|
@@ -5,6 +5,6 @@
|
|
|
5
5
|
"description": "Run Giz models, media generation, Drive, tables, Apps and Computers from any agent: the giz skill plus the Giz MCP server.",
|
|
6
6
|
"author": { "name": "Giz", "url": "https://www.giz.ai" },
|
|
7
7
|
"homepage": "https://www.giz.ai/mcp/",
|
|
8
|
-
"license": "
|
|
8
|
+
"license": "MIT",
|
|
9
9
|
"keywords": ["giz", "mcp", "media", "drive", "computer"]
|
|
10
10
|
}
|
package/skills/giz/SKILL.md
CHANGED
|
@@ -69,9 +69,7 @@ App changes pass the `revision` read from `giz app list` explicitly.
|
|
|
69
69
|
The `--input` of `create` and `save` carries `id` and `expectedRevision` (0 for a new App).
|
|
70
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
|
-
The table below is generated from the CLI dispatcher and the tool descriptors.
|
|
73
|
-
`GIZ_UPDATE_CLI_SURFACE=1 pnpm --filter giz-app-giz test --runInBand --runTestsByPath server/external/cli-surface.spec.ts`.
|
|
74
|
-
A normal CI run does not write the document; it fails on any difference in the table.
|
|
72
|
+
The table below is generated from the CLI dispatcher and the tool descriptors of the giz.ai server, which checks it.
|
|
75
73
|
|
|
76
74
|
<!-- cli-surface:start -->
|
|
77
75
|
| Command | Tool ID | MCP tool | Required input | Scope | Agent discovery |
|
package/cli/codex-relay.mjs
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
// Runs ONLY inside the user's Computer. stdout is the authenticated gateway wire,
|
|
2
|
-
// never credentials or provider error bodies. No listener or separate credential store.
|
|
3
|
-
import { readFile } from 'node:fs/promises'
|
|
4
|
-
import { homedir } from 'node:os'
|
|
5
|
-
import { join } from 'node:path'
|
|
6
|
-
import { execFileSync } from 'node:child_process'
|
|
7
|
-
|
|
8
|
-
const emit = value => process.stdout.write(JSON.stringify(value) + '\n')
|
|
9
|
-
try {
|
|
10
|
-
const auth = JSON.parse(await readFile(join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'auth.json'), 'utf8'))
|
|
11
|
-
const token = auth.tokens?.access_token
|
|
12
|
-
const account = auth.tokens?.account_id
|
|
13
|
-
if (auth.OPENAI_API_KEY || !token || !account) {
|
|
14
|
-
emit({ status: 401 }); process.exitCode = 1
|
|
15
|
-
} else {
|
|
16
|
-
const headers = { authorization: `Bearer ${token}`, 'chatgpt-account-id': account, 'content-type': 'application/json', originator: 'codex_cli_rs' }
|
|
17
|
-
const mode = process.argv[2]
|
|
18
|
-
if (mode === 'models') {
|
|
19
|
-
const version = execFileSync('codex', ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().split(' ').at(-1)
|
|
20
|
-
const response = await fetch(`https://chatgpt.com/backend-api/codex/models?client_version=${encodeURIComponent(version)}`, { headers, signal: AbortSignal.timeout(30000) })
|
|
21
|
-
if (!response.ok) emit({ status: response.status })
|
|
22
|
-
else {
|
|
23
|
-
const payload = await response.json()
|
|
24
|
-
emit({ status: 200, models: (payload.models || []).filter(model => model.visibility !== 'hide').map(model => ({ id: model.slug, label: model.display_name || model.slug })).filter(model => typeof model.id === 'string' && model.id.length <= 256) })
|
|
25
|
-
}
|
|
26
|
-
} else if (mode === 'responses') {
|
|
27
|
-
let input = ''
|
|
28
|
-
for await (const chunk of process.stdin) input += chunk
|
|
29
|
-
const body = JSON.parse(input)
|
|
30
|
-
body.store = false
|
|
31
|
-
body.stream = true
|
|
32
|
-
body.instructions ??= ''
|
|
33
|
-
delete body.max_output_tokens
|
|
34
|
-
delete body.temperature
|
|
35
|
-
delete body.top_p
|
|
36
|
-
const response = await fetch('https://chatgpt.com/backend-api/codex/responses', { method: 'POST', headers, body: JSON.stringify(body), signal: AbortSignal.timeout(300000) })
|
|
37
|
-
emit({ status: response.status })
|
|
38
|
-
if (response.ok) {
|
|
39
|
-
for await (const chunk of response.body) emit({ chunk: Buffer.from(chunk).toString('base64') })
|
|
40
|
-
emit({ done: true })
|
|
41
|
-
}
|
|
42
|
-
} else emit({ status: 400 })
|
|
43
|
-
}
|
|
44
|
-
} catch {
|
|
45
|
-
emit({ status: 503 }); process.exitCode = 1
|
|
46
|
-
}
|
package/cli/mcp-stdio-client.mjs
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
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);
|
package/cli/media-capture.mjs
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
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
|
-
}
|