@gizai/cli 2.2.1 → 2.2.3
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 +202 -0
- package/README.md +1 -1
- package/cli/computer-identity.mjs +3 -0
- package/cli/engine.mjs +3 -0
- package/cli/giz.mjs +56 -8
- package/cli/gizdb.mjs +3 -0
- package/cli/gizlang-commands.json +215 -0
- package/cli/toolchain.mjs +37 -13
- package/cli/update.mjs +7 -4
- package/package.json +6 -5
- package/plugin.json +1 -1
- package/skills/giz/SKILL.md +1 -3
- package/skills/giz/references/slides.md +1 -1
- 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,202 @@
|
|
|
1
|
+
|
|
2
|
+
Apache License
|
|
3
|
+
Version 2.0, January 2004
|
|
4
|
+
http://www.apache.org/licenses/
|
|
5
|
+
|
|
6
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
7
|
+
|
|
8
|
+
1. Definitions.
|
|
9
|
+
|
|
10
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
11
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
12
|
+
|
|
13
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
14
|
+
the copyright owner that is granting the License.
|
|
15
|
+
|
|
16
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
17
|
+
other entities that control, are controlled by, or are under common
|
|
18
|
+
control with that entity. For the purposes of this definition,
|
|
19
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
20
|
+
direction or management of such entity, whether by contract or
|
|
21
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
22
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
23
|
+
|
|
24
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
25
|
+
exercising permissions granted by this License.
|
|
26
|
+
|
|
27
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
28
|
+
including but not limited to software source code, documentation
|
|
29
|
+
source, and configuration files.
|
|
30
|
+
|
|
31
|
+
"Object" form shall mean any form resulting from mechanical
|
|
32
|
+
transformation or translation of a Source form, including but
|
|
33
|
+
not limited to compiled object code, generated documentation,
|
|
34
|
+
and conversions to other media types.
|
|
35
|
+
|
|
36
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
37
|
+
Object form, made available under the License, as indicated by a
|
|
38
|
+
copyright notice that is included in or attached to the work
|
|
39
|
+
(an example is provided in the Appendix below).
|
|
40
|
+
|
|
41
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
42
|
+
form, that is based on (or derived from) the Work and for which the
|
|
43
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
44
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
45
|
+
of this License, Derivative Works shall not include works that remain
|
|
46
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
47
|
+
the Work and Derivative Works thereof.
|
|
48
|
+
|
|
49
|
+
"Contribution" shall mean any work of authorship, including
|
|
50
|
+
the original version of the Work and any modifications or additions
|
|
51
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
52
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
53
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
54
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
55
|
+
means any form of electronic, verbal, or written communication sent
|
|
56
|
+
to the Licensor or its representatives, including but not limited to
|
|
57
|
+
communication on electronic mailing lists, source code control systems,
|
|
58
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
59
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
60
|
+
excluding communication that is conspicuously marked or otherwise
|
|
61
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
62
|
+
|
|
63
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
64
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
65
|
+
subsequently incorporated within the Work.
|
|
66
|
+
|
|
67
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
68
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
69
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
70
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
71
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
72
|
+
Work and such Derivative Works in Source or Object form.
|
|
73
|
+
|
|
74
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
75
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
76
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
77
|
+
(except as stated in this section) patent license to make, have made,
|
|
78
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
79
|
+
where such license applies only to those patent claims licensable
|
|
80
|
+
by such Contributor that are necessarily infringed by their
|
|
81
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
82
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
83
|
+
institute patent litigation against any entity (including a
|
|
84
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
85
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
86
|
+
or contributory patent infringement, then any patent licenses
|
|
87
|
+
granted to You under this License for that Work shall terminate
|
|
88
|
+
as of the date such litigation is filed.
|
|
89
|
+
|
|
90
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
91
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
92
|
+
modifications, and in Source or Object form, provided that You
|
|
93
|
+
meet the following conditions:
|
|
94
|
+
|
|
95
|
+
(a) You must give any other recipients of the Work or
|
|
96
|
+
Derivative Works a copy of this License; and
|
|
97
|
+
|
|
98
|
+
(b) You must cause any modified files to carry prominent notices
|
|
99
|
+
stating that You changed the files; and
|
|
100
|
+
|
|
101
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
102
|
+
that You distribute, all copyright, patent, trademark, and
|
|
103
|
+
attribution notices from the Source form of the Work,
|
|
104
|
+
excluding those notices that do not pertain to any part of
|
|
105
|
+
the Derivative Works; and
|
|
106
|
+
|
|
107
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
108
|
+
distribution, then any Derivative Works that You distribute must
|
|
109
|
+
include a readable copy of the attribution notices contained
|
|
110
|
+
within such NOTICE file, excluding those notices that do not
|
|
111
|
+
pertain to any part of the Derivative Works, in at least one
|
|
112
|
+
of the following places: within a NOTICE text file distributed
|
|
113
|
+
as part of the Derivative Works; within the Source form or
|
|
114
|
+
documentation, if provided along with the Derivative Works; or,
|
|
115
|
+
within a display generated by the Derivative Works, if and
|
|
116
|
+
wherever such third-party notices normally appear. The contents
|
|
117
|
+
of the NOTICE file are for informational purposes only and
|
|
118
|
+
do not modify the License. You may add Your own attribution
|
|
119
|
+
notices within Derivative Works that You distribute, alongside
|
|
120
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
121
|
+
that such additional attribution notices cannot be construed
|
|
122
|
+
as modifying the License.
|
|
123
|
+
|
|
124
|
+
You may add Your own copyright statement to Your modifications and
|
|
125
|
+
may provide additional or different license terms and conditions
|
|
126
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
127
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
128
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
129
|
+
the conditions stated in this License.
|
|
130
|
+
|
|
131
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
132
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
133
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
134
|
+
this License, without any additional terms or conditions.
|
|
135
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
136
|
+
the terms of any separate license agreement you may have executed
|
|
137
|
+
with Licensor regarding such Contributions.
|
|
138
|
+
|
|
139
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
140
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
141
|
+
except as required for reasonable and customary use in describing the
|
|
142
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
143
|
+
|
|
144
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
145
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
146
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
147
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
148
|
+
implied, including, without limitation, any warranties or conditions
|
|
149
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
150
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
151
|
+
appropriateness of using or redistributing the Work and assume any
|
|
152
|
+
risks associated with Your exercise of permissions under this License.
|
|
153
|
+
|
|
154
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
155
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
156
|
+
unless required by applicable law (such as deliberate and grossly
|
|
157
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
158
|
+
liable to You for damages, including any direct, indirect, special,
|
|
159
|
+
incidental, or consequential damages of any character arising as a
|
|
160
|
+
result of this License or out of the use or inability to use the
|
|
161
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
162
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
163
|
+
other commercial damages or losses), even if such Contributor
|
|
164
|
+
has been advised of the possibility of such damages.
|
|
165
|
+
|
|
166
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
167
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
168
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
169
|
+
or other liability obligations and/or rights consistent with this
|
|
170
|
+
License. However, in accepting such obligations, You may act only
|
|
171
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
172
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
173
|
+
defend, and hold each Contributor harmless for any liability
|
|
174
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
175
|
+
of your accepting any such warranty or additional liability.
|
|
176
|
+
|
|
177
|
+
END OF TERMS AND CONDITIONS
|
|
178
|
+
|
|
179
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
180
|
+
|
|
181
|
+
To apply the Apache License to your work, attach the following
|
|
182
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
183
|
+
replaced with your own identifying information. (Don't include
|
|
184
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
185
|
+
comment syntax for the file format. We also recommend that a
|
|
186
|
+
file or class name and description of purpose be included on the
|
|
187
|
+
same "printed page" as the copyright notice for easier
|
|
188
|
+
identification within third-party archives.
|
|
189
|
+
|
|
190
|
+
Copyright [yyyy] [name of copyright owner]
|
|
191
|
+
|
|
192
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
193
|
+
you may not use this file except in compliance with the License.
|
|
194
|
+
You may obtain a copy of the License at
|
|
195
|
+
|
|
196
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
197
|
+
|
|
198
|
+
Unless required by applicable law or agreed to in writing, software
|
|
199
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
200
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
201
|
+
See the License for the specific language governing permissions and
|
|
202
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -193,4 +193,4 @@ Questions and bug reports: [support@giz.ai](mailto:support@giz.ai) or the Suppor
|
|
|
193
193
|
|
|
194
194
|
## License
|
|
195
195
|
|
|
196
|
-
|
|
196
|
+
Apache-2.0 ([LICENSE](LICENSE)). Copyright 2026 Giz Inc. Use of the giz.ai and GizStack services is governed by the [Terms of Use](https://www.giz.ai/terms-of-use).
|
package/cli/engine.mjs
CHANGED
package/cli/giz.mjs
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// SPDX-FileCopyrightText: 2026 Giz Inc.
|
|
3
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
|
|
2
5
|
import { computerIdentity, managedRevision } from './computer-identity.mjs'
|
|
3
6
|
import { checkForUpdate, explicitUpdate, installSource, maybeNotify } from './update.mjs'
|
|
4
|
-
import { runToolchain, toolchainCommand } from './toolchain.mjs'
|
|
7
|
+
import { GROUPS, runToolchain, toolchainCommand, toolchainRoot, toolchainVersion } from './toolchain.mjs'
|
|
5
8
|
import { createHash, randomBytes } from 'node:crypto'
|
|
6
|
-
import { openAsBlob, statSync } from 'node:fs'
|
|
9
|
+
import { existsSync, openAsBlob, rmSync, statSync } from 'node:fs'
|
|
7
10
|
import { mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises'
|
|
11
|
+
import { createInterface } from 'node:readline/promises'
|
|
8
12
|
import { createServer } from 'node:http'
|
|
9
13
|
import { once } from 'node:events'
|
|
10
14
|
import { createRequire } from 'node:module'
|
|
@@ -457,6 +461,7 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
457
461
|
await checkForUpdate({ file: options.cache, url: options.registry })
|
|
458
462
|
return undefined
|
|
459
463
|
}
|
|
464
|
+
if (command === 'uninstall') return { kind: 'message', value: options.help === true ? help('uninstall') : await uninstall(options) }
|
|
460
465
|
if (command === 'update' && options.help === true) return { kind: 'help', value: help('update') }
|
|
461
466
|
if (command === 'update' && !subcommand) {
|
|
462
467
|
const result = await explicitUpdate({ version: CLI_VERSION, source: installSource() })
|
|
@@ -464,14 +469,15 @@ async function runCommand(baseUrl, positionals, options) {
|
|
|
464
469
|
}
|
|
465
470
|
if (options.version === true) {
|
|
466
471
|
const revision = await managedRevision()
|
|
467
|
-
|
|
472
|
+
const toolchain = toolchainVersion() ?? `not installed (${toolchainRoot()})`
|
|
473
|
+
return { kind: 'message', value: `@gizai/cli ${CLI_VERSION}${revision ? ` (region ${revision.trim()})` : ''}\nGizlang toolchain: ${toolchain}` }
|
|
468
474
|
}
|
|
469
475
|
if (!command || command === 'help' || options.help === true) {
|
|
470
476
|
// A script that asks for help on a command that does not exist must fail, not read the overview as success.
|
|
471
477
|
if (command && !gizCommand(command)) {
|
|
472
478
|
throw new CliError(`Unknown command "${command}".`, 'Run: giz --help')
|
|
473
479
|
}
|
|
474
|
-
return { kind: 'help', value: help(command) }
|
|
480
|
+
return { kind: 'help', value: help(command, options.all === true) }
|
|
475
481
|
}
|
|
476
482
|
if (command === 'engine' && subcommand === 'run') {
|
|
477
483
|
if (rest[0] !== 'claude') throw new CliError('Only Claude Code is an engine connection.')
|
|
@@ -938,7 +944,13 @@ function print(kind, value, json, output = process.stdout) {
|
|
|
938
944
|
output.write(`${JSON.stringify(value, null, 2)}\n`)
|
|
939
945
|
}
|
|
940
946
|
|
|
941
|
-
function help(command) {
|
|
947
|
+
function help(command, all = false) {
|
|
948
|
+
if (command === 'uninstall') return `Usage
|
|
949
|
+
giz uninstall [--yes] [--credentials]
|
|
950
|
+
|
|
951
|
+
Removes the Gizlang toolchain (${toolchainRoot()}; GIZ_INSTALL names another folder) after a confirmation.
|
|
952
|
+
--yes skips the confirmation. --credentials also removes your giz.ai login (${configPath}).
|
|
953
|
+
It prints the command that removes @gizai/cli itself.`
|
|
942
954
|
if (command === 'engine') return `Usage
|
|
943
955
|
giz engine run claude "instruction" [--dir path]
|
|
944
956
|
|
|
@@ -1047,6 +1059,7 @@ http uses the client's own OAuth login. Install the chosen client first.`
|
|
|
1047
1059
|
Usage
|
|
1048
1060
|
giz auth login [--device]
|
|
1049
1061
|
giz update
|
|
1062
|
+
giz uninstall [--yes] [--credentials]
|
|
1050
1063
|
giz api-key list|create|revoke
|
|
1051
1064
|
giz chat MODEL "your prompt"
|
|
1052
1065
|
giz model list [--type TYPE] [--search TEXT]
|
|
@@ -1071,8 +1084,8 @@ Usage
|
|
|
1071
1084
|
|
|
1072
1085
|
Use giz COMMAND --help for parameters and examples.
|
|
1073
1086
|
|
|
1074
|
-
Gizlang (the toolchain is installed on first use, with its size shown)
|
|
1075
|
-
|
|
1087
|
+
Gizlang (giz COMMAND; the toolchain is installed on first use, with its size shown; gizdb runs the database server)
|
|
1088
|
+
${gizlangCommands(all)}
|
|
1076
1089
|
Docs: https://gizstack.com/docs
|
|
1077
1090
|
|
|
1078
1091
|
Global options
|
|
@@ -1084,6 +1097,41 @@ Global options
|
|
|
1084
1097
|
Docs: https://www.giz.ai/mcp/`
|
|
1085
1098
|
}
|
|
1086
1099
|
|
|
1100
|
+
// The toolchain's commands by group, as its own help shows them, from the generated table (toolchain.mjs GROUPS), so the
|
|
1101
|
+
// list needs no download. Its Repository group serves a Gizlang checkout and shows only with --all. No line starts with
|
|
1102
|
+
// " giz ": gizCommand reads those as giz.ai's.
|
|
1103
|
+
function gizlangCommands(all) {
|
|
1104
|
+
return GROUPS.filter((group) => all || group.name !== 'Repository').map((group) => ` ${group.name}:\n` +
|
|
1105
|
+
group.commands.map((command) => ` ${command.synopsis.padEnd(30)} ${command.summary}`).join('\n')).join('\n')
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Removes the toolchain's folder and nothing else: a folder without the toolchain's program is refused.
|
|
1109
|
+
async function uninstall(options) {
|
|
1110
|
+
const root = toolchainRoot()
|
|
1111
|
+
const after = `\nTo remove @gizai/cli itself, run: ${installSource().remove ?? 'npm uninstall -g @gizai/cli'}`
|
|
1112
|
+
const removed = []
|
|
1113
|
+
if (existsSync(root)) {
|
|
1114
|
+
if (!existsSync(path.join(root, 'bin', process.platform === 'win32' ? 'giz.exe' : 'giz'))) {
|
|
1115
|
+
throw new CliError(`${root} does not hold the Gizlang toolchain (no bin/giz); nothing was removed.`)
|
|
1116
|
+
}
|
|
1117
|
+
if (options.yes !== true) {
|
|
1118
|
+
if (!process.stdin.isTTY) throw new CliError(`Removing ${root} needs a confirmation.`, 'Run: giz uninstall --yes')
|
|
1119
|
+
const ask = createInterface({ input: process.stdin, output: process.stderr })
|
|
1120
|
+
const login = options.credentials === true ? ` and your giz.ai login (${configPath})` : ''
|
|
1121
|
+
const answer = await ask.question(`Remove the Gizlang toolchain at ${root}${login}? [y/N] `)
|
|
1122
|
+
ask.close()
|
|
1123
|
+
if (!/^y(es)?$/i.test(answer.trim())) return `Nothing was removed.${after}`
|
|
1124
|
+
}
|
|
1125
|
+
rmSync(root, { recursive: true, force: true })
|
|
1126
|
+
removed.push(root)
|
|
1127
|
+
}
|
|
1128
|
+
if (options.credentials === true && existsSync(configPath)) {
|
|
1129
|
+
rmSync(configPath)
|
|
1130
|
+
removed.push(configPath)
|
|
1131
|
+
}
|
|
1132
|
+
return (removed.length ? `Removed ${removed.join(' and ')}.` : `The Gizlang toolchain is not installed (${root}).`) + after
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1087
1135
|
// giz.ai's commands are the ones its overview lists; every other command is the Gizlang toolchain's (toolchain.mjs).
|
|
1088
1136
|
function gizCommand(name) {
|
|
1089
1137
|
return ['help', 'models', '__update-check'].includes(name) ||
|
|
@@ -1108,7 +1156,7 @@ async function main() {
|
|
|
1108
1156
|
void maybeNotify({ version: CLI_VERSION, source, mode: updateMode, env: { ...process.env, GIZ_JSON: options.json === true ? '1' : process.env.GIZ_JSON } }).catch(() => {})
|
|
1109
1157
|
const requiresAuth = positionals.length && !options.help && !options.version &&
|
|
1110
1158
|
!(positionals[0] === 'auth' && ['login', 'logout'].includes(positionals[1])) &&
|
|
1111
|
-
!['update', '__update-check'].includes(positionals[0])
|
|
1159
|
+
!['help', 'update', 'uninstall', '__update-check'].includes(positionals[0])
|
|
1112
1160
|
const computer = requiresAuth && !process.env.GIZ_TOKEN && !saved?.access_token ? await computerIdentity() : undefined
|
|
1113
1161
|
const baseUrl = String(options['base-url'] || process.env.GIZ_URL || saved?.baseUrl || computer?.baseUrl || DEFAULT_URL).replace(/\/+$/, '')
|
|
1114
1162
|
if (typeof options.org === 'string') process.env.GIZ_ORGANIZATION = options.org
|
package/cli/gizdb.mjs
CHANGED
|
@@ -0,0 +1,215 @@
|
|
|
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": "desk",
|
|
14
|
+
"synopsis": "desk",
|
|
15
|
+
"summary": "Run GizDesk, the first-class AI agent workspace (the repository's desk/ app, or the shipped desk template)."
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"name": "run",
|
|
19
|
+
"synopsis": "run [FLAG...] FILE [ARGUMENT...]",
|
|
20
|
+
"summary": "Build FILE (cached) and run it with the arguments."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "check",
|
|
24
|
+
"synopsis": "check [FLAG...] FILE...",
|
|
25
|
+
"summary": "Check each FILE (`.giz`, `.gizgraph` or `package.json`)."
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"name": "build",
|
|
29
|
+
"synopsis": "build [FLAG...] FILE",
|
|
30
|
+
"summary": "Compile FILE to a program for this CPU, and print its path."
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "test",
|
|
34
|
+
"synopsis": "test [FLAG...] [FILE... | FOLDER | FILTER]",
|
|
35
|
+
"summary": "Run the tests of each FILE: vitest's `test(\"name\", () => ...)` with `expect`."
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"name": "live",
|
|
39
|
+
"synopsis": "live [FLAG...] FILE",
|
|
40
|
+
"summary": "Run FILE, and replace its code while it runs each time you save a source in its folder."
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"name": "doc",
|
|
44
|
+
"synopsis": "doc [FLAG...] [NAME]",
|
|
45
|
+
"summary": "Show the documentation of NAME: a module (`gizlang:json`), a declaration (`Json.get`) or a command (`check`)."
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"name": "repl",
|
|
49
|
+
"synopsis": "repl",
|
|
50
|
+
"summary": "Start the read-eval-print loop."
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "fmt",
|
|
54
|
+
"synopsis": "fmt [FLAG...] FILE...",
|
|
55
|
+
"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."
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
"name": "help",
|
|
59
|
+
"synopsis": "help [--all | COMMAND]",
|
|
60
|
+
"summary": "Print the commands by group, or the help of COMMAND."
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"name": "version",
|
|
64
|
+
"synopsis": "version",
|
|
65
|
+
"summary": "Print the identity of the toolchain."
|
|
66
|
+
}
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
"name": "Code and packages",
|
|
71
|
+
"commands": [
|
|
72
|
+
{
|
|
73
|
+
"name": "python",
|
|
74
|
+
"synopsis": "python [OPTION...] (-c CODE | -m MODULE | FILE | -) [ARGUMENT...]",
|
|
75
|
+
"summary": "Run Python as `python3` does, on the VM: CPython's options are taken (most have no effect)."
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "wasm",
|
|
79
|
+
"synopsis": "wasm FILE [ARGUMENT...]",
|
|
80
|
+
"summary": "Build FILE as a wasm64 module and run it in `runtime/wasm.mjs`, which gives it only the console."
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
"name": "outline",
|
|
84
|
+
"synopsis": "outline [FLAG...] FILE",
|
|
85
|
+
"summary": "Print the declarations of FILE, one a line: where it is, its signature and effects, and its doc summary under it."
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"name": "context",
|
|
89
|
+
"synopsis": "context FILE WORD...",
|
|
90
|
+
"summary": "Print the declarations that mention a WORD in full, and the first lines of their neighbours."
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
"name": "project",
|
|
94
|
+
"synopsis": "project FILE",
|
|
95
|
+
"summary": "Print the canonical source of FILE, with the sugar spelled out."
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"name": "edit",
|
|
99
|
+
"synopsis": "edit FILE",
|
|
100
|
+
"summary": "Apply the JSON edit request on stdin to FILE, if the result checks."
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"name": "lsp",
|
|
104
|
+
"synopsis": "lsp",
|
|
105
|
+
"summary": "Start the language server on stdin and stdout."
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
"name": "bind",
|
|
109
|
+
"synopsis": "bind HEADER LIBRARY NAME...",
|
|
110
|
+
"summary": "Print `declare` lines for the C functions NAME of HEADER in LIBRARY."
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
"name": "codec",
|
|
114
|
+
"synopsis": "codec FILE TYPE...",
|
|
115
|
+
"summary": "Print JSON functions for the record types TYPE of FILE."
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
"name": "i18n",
|
|
119
|
+
"synopsis": "i18n [FLAG...] DIR",
|
|
120
|
+
"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."
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
"name": "pin",
|
|
124
|
+
"synopsis": "pin DIR",
|
|
125
|
+
"summary": "Write the sha256 pins in `DIR/package.json` again, dependencies first."
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
"name": "get",
|
|
129
|
+
"synopsis": "get [FLAG...] DIR [NAME@VERSION... npm:NAME@SPEC...]",
|
|
130
|
+
"summary": "Raise the requirements of the package in DIR, vendor the minimal version selection, run `npm install` (without its scripts) and pin the result."
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
"name": "capabilities",
|
|
134
|
+
"synopsis": "capabilities",
|
|
135
|
+
"summary": "Write the operations of this command line and the exact identity of the running toolchain."
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
"name": "completion",
|
|
139
|
+
"synopsis": "completion bash",
|
|
140
|
+
"summary": "Print a bash completion script."
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "vscode",
|
|
144
|
+
"synopsis": "vscode",
|
|
145
|
+
"summary": "Install the Gizlang extension of this toolchain into VS Code."
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
"name": "licenses",
|
|
149
|
+
"synopsis": "licenses FILE",
|
|
150
|
+
"summary": "Print the third-party notices for the native link inputs of FILE."
|
|
151
|
+
},
|
|
152
|
+
{
|
|
153
|
+
"name": "doctor",
|
|
154
|
+
"synopsis": "doctor",
|
|
155
|
+
"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."
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
"name": "Deploy and cloud",
|
|
161
|
+
"commands": [
|
|
162
|
+
{
|
|
163
|
+
"name": "deploy",
|
|
164
|
+
"synopsis": "deploy [FLAG...] [FILE...]",
|
|
165
|
+
"summary": "Without `--live`, send the folder (a project of `giz init`, without `data/`, hidden files and what `.gitignore` names) to GizStack with your Giz sign-in or terminal token (`GIZ_TOKEN`): each file goes up once by its SHA-256, the app's Giz Computer builds it (`giz test`, then `giz build` of the entry of `package.json`) and runs it with `PORT` and `DATABASE_URL`."
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
"name": "ai",
|
|
169
|
+
"synopsis": "ai [FLAG...] models | pull NAME | chat NAME [PROMPT...] | image PROMPT... | serve NAME... | worker NAME | gate URL CASES DIR | registry",
|
|
170
|
+
"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."
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
"name": "api",
|
|
174
|
+
"synopsis": "api [FLAG...] [NAME [FIELD=VALUE...]]",
|
|
175
|
+
"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."
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
"name": "ssh-proxy",
|
|
179
|
+
"synopsis": "ssh-proxy COMPUTER [PORT]",
|
|
180
|
+
"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."
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
"name": "publish",
|
|
184
|
+
"synopsis": "publish DIR [NAME@VERSION]",
|
|
185
|
+
"summary": "Publish the package in DIR under the name and version of its manifest."
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
"name": "public-key",
|
|
189
|
+
"synopsis": "public-key SEED",
|
|
190
|
+
"summary": "Print the hexadecimal Ed25519 public key of the 32-byte SEED file (`GIZLANG_SIGNING_KEY`), for `GIZLANG_TRUSTED_KEYS`."
|
|
191
|
+
}
|
|
192
|
+
]
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"name": "Repository",
|
|
196
|
+
"commands": [
|
|
197
|
+
{
|
|
198
|
+
"name": "storage",
|
|
199
|
+
"synopsis": "storage [FLAG...] OPERATION [OPERAND...]",
|
|
200
|
+
"summary": "Manage a storage range: buckets, objects (`ls`, `cp`, `rm`, `stat`, `presign`), keys, usage, a FUSE mount and block volumes."
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
"name": "bench",
|
|
204
|
+
"synopsis": "bench [FLAG...] list [SUITE] | run|show|compare|record ID...",
|
|
205
|
+
"summary": "Run the benchmark suites of this toolchain, all or in part."
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
"name": "work",
|
|
209
|
+
"synopsis": "work [FLAG...] OPERATION [OPERAND...]",
|
|
210
|
+
"summary": "Manage the work items of the git repository here: `list`, `show`, `open`, `drop`, `claim`, `release`, `claims`, `age`, `burndown`, `check`."
|
|
211
|
+
}
|
|
212
|
+
]
|
|
213
|
+
}
|
|
214
|
+
]
|
|
215
|
+
}
|
package/cli/toolchain.mjs
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Giz Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
1
4
|
import { spawnSync } from 'node:child_process'
|
|
2
|
-
import { existsSync } from 'node:fs'
|
|
5
|
+
import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
|
|
3
6
|
import os from 'node:os'
|
|
4
7
|
import path from 'node:path'
|
|
5
8
|
|
|
@@ -8,12 +11,12 @@ import path from 'node:path'
|
|
|
8
11
|
// own install script for this OS, which names the download size first, checks the release signature and every file,
|
|
9
12
|
// and selects the version (a project pin or the active one). Later runs execute the installed program at once.
|
|
10
13
|
const SITE = 'https://gizstack.com'
|
|
11
|
-
// The toolchain's public commands (
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
14
|
+
// The toolchain's public commands by help group, written from its command table (tools/cli/commands.giz) by
|
|
15
|
+
// tools/cli/npm/commands.giz. Only these, or a program file, install it: an agent that asks for help on a command that
|
|
16
|
+
// does not exist must get an error, not a 200 MB download. Once the toolchain is installed, every command giz.ai does not
|
|
17
|
+
// know goes to it, and it owns the error.
|
|
18
|
+
export const GROUPS = JSON.parse(readFileSync(new URL('./gizlang-commands.json', import.meta.url), 'utf8')).groups
|
|
19
|
+
const COMMANDS = new Set(GROUPS.flatMap((group) => group.commands.map((command) => command.name)))
|
|
17
20
|
const windows = process.platform === 'win32'
|
|
18
21
|
|
|
19
22
|
export function toolchainRoot(env = process.env) {
|
|
@@ -29,16 +32,37 @@ function installed(root, program) {
|
|
|
29
32
|
return path.join(root, 'bin', program + (windows ? '.exe' : ''))
|
|
30
33
|
}
|
|
31
34
|
|
|
35
|
+
/** The installed toolchain's identity (`giz version`), or undefined when it is not installed. It never installs it. */
|
|
36
|
+
export function toolchainVersion() {
|
|
37
|
+
const file = installed(toolchainRoot(), 'giz')
|
|
38
|
+
if (!existsSync(file)) return undefined
|
|
39
|
+
const ran = spawnSync(file, ['version'], { encoding: 'utf8' })
|
|
40
|
+
return ran.status === 0 ? ran.stdout.trim() : undefined
|
|
41
|
+
}
|
|
42
|
+
|
|
32
43
|
async function install(root) {
|
|
33
44
|
const response = await fetch(`${SITE}/${windows ? 'install.ps1' : 'install'}`)
|
|
34
45
|
if (!response.ok) throw new Error(`cannot download the install script: HTTP ${response.status} from ${SITE}`)
|
|
35
46
|
const script = await response.text()
|
|
36
|
-
// The script's progress goes to stderr: stdout stays the command's own.
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
// The script's progress goes to stderr: stdout stays the command's own. PATH is this package's: the script changes none.
|
|
48
|
+
// NO_NEXT: the command the person typed runs next, so the script names no other.
|
|
49
|
+
const env = { ...process.env, GIZ_INSTALL: root, NO_MODIFY_PATH: '1', NO_NEXT: '1' }
|
|
50
|
+
if (!windows) {
|
|
51
|
+
const ran = spawnSync('sh', ['-s', '--', '--no-modify-path'], { input: script, stdio: ['pipe', process.stderr, 'inherit'], env })
|
|
52
|
+
if (ran.error) throw new Error(`cannot run the install script: ${ran.error.message}`)
|
|
53
|
+
return ran.status === 0
|
|
54
|
+
}
|
|
55
|
+
// PowerShell runs a file whole; from stdin it runs line by line and breaks the script's blocks.
|
|
56
|
+
const folder = mkdtempSync(path.join(os.tmpdir(), 'giz-install-'))
|
|
57
|
+
try {
|
|
58
|
+
const file = path.join(folder, 'install.ps1')
|
|
59
|
+
writeFileSync(file, script)
|
|
60
|
+
const ran = spawnSync('powershell', ['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', file], { stdio: ['ignore', process.stderr, 'inherit'], env })
|
|
61
|
+
if (ran.error) throw new Error(`cannot run the install script: ${ran.error.message}`)
|
|
62
|
+
return ran.status === 0
|
|
63
|
+
} finally {
|
|
64
|
+
rmSync(folder, { recursive: true, force: true })
|
|
65
|
+
}
|
|
42
66
|
}
|
|
43
67
|
|
|
44
68
|
/** Runs PROGRAM (giz or gizdb) of the toolchain with ARGS, installing the toolchain first when it is missing. */
|
package/cli/update.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Giz Inc.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
|
|
1
4
|
import { randomBytes } from 'node:crypto'
|
|
2
5
|
import { mkdir, open, readFile, rename, stat, unlink, writeFile } from 'node:fs/promises'
|
|
3
6
|
import os from 'node:os'
|
|
@@ -17,10 +20,10 @@ export function installSource(root = packageRoot) {
|
|
|
17
20
|
const nodeModules = parts.lastIndexOf('node_modules')
|
|
18
21
|
if (nodeModules < 0) return { type: 'source' }
|
|
19
22
|
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 }
|
|
23
|
+
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' }
|
|
24
|
+
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' }
|
|
25
|
+
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' }
|
|
26
|
+
return { type: 'npm', executable: 'npm', args: ['install', '-g', '@gizai/cli'], command: UPDATE_COMMAND, remove: 'npm uninstall -g @gizai/cli' }
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
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.3",
|
|
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": "Apache-2.0",
|
|
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"
|
|
@@ -42,13 +43,13 @@
|
|
|
42
43
|
"node": ">=20.12"
|
|
43
44
|
},
|
|
44
45
|
"dependencies": {
|
|
45
|
-
"@modelcontextprotocol/sdk": "1.
|
|
46
|
+
"@modelcontextprotocol/sdk": "1.32.2",
|
|
46
47
|
"proper-lockfile": "4.1.2"
|
|
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": "Apache-2.0",
|
|
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 |
|
|
@@ -67,7 +67,7 @@ SVG editor and PowerPoint use different text renderers.
|
|
|
67
67
|
```json
|
|
68
68
|
{
|
|
69
69
|
"path": "Proposals/inventory.svg",
|
|
70
|
-
"content": "<svg xmlns=\"http://www.w3.org/2000/svg\" id=\"deck\" viewBox=\"0 0 1920 1080\"><svg data-slide=\"true\" id=\"slide-1\" viewBox=\"0 0 1920 1080\"><title>Inventory</title><rect width=\"1920\" height=\"1080\" fill=\"#FFFFFF\"/><image id=\"hero\" x=\"1000\" y=\"100\" width=\"800\" height=\"600\"/><g id=\"value\"><rect x=\"80\" y=\"160\" width=\"840\" height=\"320\" rx=\"16\" fill=\"#EEF4FA\"/><text id=\"headline\" x=\"120\" y=\"250\" font-family=\"
|
|
70
|
+
"content": "<svg xmlns=\"http://www.w3.org/2000/svg\" id=\"deck\" viewBox=\"0 0 1920 1080\"><svg data-slide=\"true\" id=\"slide-1\" viewBox=\"0 0 1920 1080\"><title>Inventory</title><rect width=\"1920\" height=\"1080\" fill=\"#FFFFFF\"/><image id=\"hero\" x=\"1000\" y=\"100\" width=\"800\" height=\"600\"/><g id=\"value\"><rect x=\"80\" y=\"160\" width=\"840\" height=\"320\" rx=\"16\" fill=\"#EEF4FA\"/><text id=\"headline\" x=\"120\" y=\"250\" font-family=\"sans-serif\" font-size=\"48\" fill=\"#123456\">Inventory Overview</text></g></svg></svg>",
|
|
71
71
|
"assets": {
|
|
72
72
|
"hero": { "path": "Proposals/artwork.png", "crop": { "left": 620, "top": 70, "width": 800, "height": 600 } }
|
|
73
73
|
}
|
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
|
-
}
|