@gizai/cli 2.1.2 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +190 -9
- package/cli/codex-relay.mjs +46 -0
- package/cli/computer-identity.mjs +76 -0
- package/cli/engine.mjs +58 -0
- package/cli/giz.mjs +1129 -0
- package/cli/gizdb.mjs +8 -0
- package/cli/mcp-stdio-client.mjs +81 -0
- package/cli/media-capture.mjs +321 -0
- package/cli/toolchain.mjs +59 -0
- package/cli/update.mjs +150 -0
- package/mcp.json +6 -0
- package/package.json +48 -13
- package/plugin.json +10 -0
- package/skills/giz/SKILL.md +131 -0
- package/skills/giz/agents/openai.yaml +4 -0
- package/skills/giz/references/operations.md +19 -0
- package/skills/giz/references/slides.md +95 -0
- package/LICENSE +0 -21
- package/bin/giz.js +0 -103
- package/bin/gizdb.js +0 -5
- package/bin/install.sh +0 -260
package/README.md
CHANGED
|
@@ -1,15 +1,196 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Giz CLI
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The official command-line interface for [giz.ai](https://www.giz.ai). Run Giz text, image, video and audio models, manage Drive files, tables, Apps and Computers, and connect Claude Code, Codex and other AI agents to your Giz account, all from a terminal.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[Documentation](https://www.giz.ai/mcp/) · [giz.ai](https://www.giz.ai) · [Sign up](https://www.giz.ai)
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Requires Node.js 20.12 or later and a [giz.ai](https://www.giz.ai) account.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
6
12
|
npm install -g @gizai/cli
|
|
7
|
-
|
|
8
|
-
|
|
13
|
+
giz --version
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Update later with `giz update` (or `npm install -g @gizai/cli`).
|
|
17
|
+
|
|
18
|
+
## Quick start
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
giz auth login # opens your browser, one-time sign-in
|
|
22
|
+
giz chat gpt-6-luna "Say hello in Korean" # chat with a text model
|
|
23
|
+
giz generate image "A paper observatory at blue hour"
|
|
24
|
+
giz credits # check your balance
|
|
25
|
+
giz --help # every command
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Gizlang
|
|
29
|
+
|
|
30
|
+
The same `giz` runs [Gizlang](https://gizstack.com/docs) and GizStack. The native toolchain is not part of this
|
|
31
|
+
package: the first Gizlang command installs it for your OS into `~/.giz` (`GIZ_INSTALL`), with its size shown first.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
giz init hello && cd hello # installs the toolchain once, then creates the project
|
|
35
|
+
giz run main.giz
|
|
36
|
+
giz deploy # signs in with the same giz auth login
|
|
37
|
+
gizdb serve --dir ~/gizdb-data --port 5432
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Run any model
|
|
41
|
+
|
|
42
|
+
Discover a model's live inputs, upload local files, and run it:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
giz model list --type video-generation
|
|
46
|
+
giz model get ltx2-distilled
|
|
47
|
+
giz model run ltx2-distilled \
|
|
48
|
+
--input '{"prompt":"A slow camera push through morning fog"}' \
|
|
49
|
+
--upload image=./first-frame.png
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Media jobs run independently. Track them with `giz job get JOB` or block with `giz job wait JOB` (exit status 1 if the job failed or was cancelled). Add `--json` to any command for machine-readable output on stdout; progress goes to stderr.
|
|
53
|
+
|
|
54
|
+
## Use Giz from Claude Code, Codex and other agents
|
|
55
|
+
|
|
56
|
+
Register the Giz MCP server with your agent (install the client first):
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
giz auth login
|
|
60
|
+
giz mcp add claude
|
|
61
|
+
giz mcp add codex
|
|
62
|
+
giz mcp check --json # verifies the MCP handshake and lists tool names
|
|
9
63
|
```
|
|
10
64
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
65
|
+
This registers `giz mcp serve` over stdio. It forwards Giz's live MCP protocol and shares the CLI's credentials, so no token is copied into the client's configuration. For clients that run their own OAuth session, use `giz mcp add claude --transport http` (then sign in with `/mcp`) or `giz mcp add codex --transport http`. The remote endpoint is `https://www.giz.ai/api/mcp`; it supports OAuth discovery and scoped API keys.
|
|
66
|
+
|
|
67
|
+
Install the Giz agent skill with `giz skill install`. The package root is also an [Agent Plugins](https://agent-plugins.org) 1.0 plugin (`plugin.json`, the `giz` skill and `mcp.json`), so a client that installs plugins gets the skill and the MCP server together.
|
|
68
|
+
|
|
69
|
+
## Drive, Computers and Apps
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
giz drive ls
|
|
73
|
+
giz drive read /notes/todo.md
|
|
74
|
+
giz computer list
|
|
75
|
+
giz computer exec --deadline-ms 120000 -- ls -la # up to 15 minutes; the remote exit code is preserved
|
|
76
|
+
giz app list
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
App changes are explicit: pass the `revision` shown by `giz app list` as `--expected-revision REVISION` to `run`, `publish` and `delete`, and as `expectedRevision` in the `--input` of `create` and `save` (use 0 for a new App). A concurrent change is rejected by the server instead of being overwritten.
|
|
80
|
+
|
|
81
|
+
Free Computers have a runtime limit; check your plan before starting long workflows. Signing in does not extend that runtime.
|
|
82
|
+
|
|
83
|
+
### Inside a Giz Computer
|
|
84
|
+
|
|
85
|
+
`giz` is already installed in every Giz Computer, and no login is needed: the Computer signs in as itself and can reach only its own Drive, model discovery and inference, and jobs it started. API keys, billing, organization administration, destructive operations and external writes need a separate user sign-in with `giz auth login`. `giz auth status` shows who you are.
|
|
86
|
+
|
|
87
|
+
## Plugins
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
giz plugin search QUERY [--origin skill|mcp]
|
|
91
|
+
giz plugin install ID --query QUERY
|
|
92
|
+
giz plugin list
|
|
93
|
+
giz connection list # connected apps
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Also available: `plugin inspect`, `update`, `remove`, `enable`, `disable`, `publish` and `unpublish`.
|
|
97
|
+
|
|
98
|
+
## Authentication and security
|
|
99
|
+
|
|
100
|
+
- `giz auth login` uses browser OAuth with a loopback callback; the CLI never sees or stores your password.
|
|
101
|
+
- Credentials are stored as JSON in `~/.config/giz/credentials.json` with mode `0600` (owner only), written atomically. `GIZ_CONFIG` overrides the path. `giz auth logout` revokes the token and removes the file.
|
|
102
|
+
- `giz auth status --json` shows the granted scopes; a missing scope error names the permission it needs. After updating from 1.0.1, sign in again to grant `giz.inference`.
|
|
103
|
+
- Create a scoped API key with `giz api-key create "My app"` (the secret is shown once). Set `GIZ_TOKEN` only for short-lived automation.
|
|
104
|
+
- An OAuth access token copied by hand expires after one hour. Prefer `giz mcp add`, which refreshes it for you.
|
|
105
|
+
|
|
106
|
+
## Configuration
|
|
107
|
+
|
|
108
|
+
| Variable | Purpose |
|
|
109
|
+
| --- | --- |
|
|
110
|
+
| `GIZ_TOKEN` | Use this API key or token instead of the stored login |
|
|
111
|
+
| `GIZ_CONFIG` | Path of the credentials file |
|
|
112
|
+
| `GIZ_NO_UPDATE_CHECK=1` | Turn off the update notice (or set `updates.mode` to `off` in `credentials.json`) |
|
|
113
|
+
|
|
114
|
+
The update check is cached for up to 24 hours and prints only in interactive terminals.
|
|
115
|
+
|
|
116
|
+
## Upgrading from 1.x
|
|
117
|
+
|
|
118
|
+
Version 2.0 removes `giz capability` with no alias. Skills, MCP servers and Agents now install as one plugin.
|
|
119
|
+
|
|
120
|
+
| 1.x | 2.0 |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| `giz capability list` | `giz connection list` (connected apps), `giz plugin list` (installed plugins) |
|
|
123
|
+
| `giz capability search skill\|mcp QUERY` | `giz plugin search QUERY [--origin skill\|mcp]` |
|
|
124
|
+
| `giz capability install skill\|mcp ID --query QUERY` | `giz plugin install ID --query QUERY` |
|
|
125
|
+
| `giz capability update\|remove ID` | `giz plugin update\|remove NAME` |
|
|
126
|
+
|
|
127
|
+
New in 2.0: `plugin inspect`, `install --path`, `enable`, `disable`, `publish` and `unpublish`. A Giz server that supports plugins reports 2.0.0 as its minimum CLI, so 1.x prints an update warning there.
|
|
128
|
+
|
|
129
|
+
## Command reference
|
|
130
|
+
|
|
131
|
+
Generated from the CLI dispatcher and the Giz tool descriptors.
|
|
132
|
+
|
|
133
|
+
<!-- cli-surface:start -->
|
|
134
|
+
| Command | Tool ID | MCP tool | Required input | Scope | Agent discovery |
|
|
135
|
+
| --- | --- | --- | --- | --- | --- |
|
|
136
|
+
| model list | model.list | model_list | - | giz.read | model.list |
|
|
137
|
+
| model get | model.get | model_get | model | giz.read | model.get |
|
|
138
|
+
| model run | model.run | model_run | model, input | giz.inference | model.run |
|
|
139
|
+
| generate image | generateImage | generateImage | prompt | giz.write | generateImage |
|
|
140
|
+
| generate video | generateVideo | generateVideo | prompt | giz.write | generateVideo |
|
|
141
|
+
| generate music | generateMusic | generateMusic | prompt | giz.write | generateMusic |
|
|
142
|
+
| generate talking-head | generateTalkingHead | generateTalkingHead | prompt, references | giz.write | generateTalkingHead |
|
|
143
|
+
| generate audio | generateAudio | generateAudio | purpose | giz.write | generateAudio |
|
|
144
|
+
| job list | job.list | job_list | - | giz.read | server job |
|
|
145
|
+
| job get | job.get | job_get | job | giz.read | server job |
|
|
146
|
+
| job wait | job.get | job_get | job | giz.read | server job |
|
|
147
|
+
| job cancel | job.cancel | job_cancel | job | giz.destructive | server job |
|
|
148
|
+
| credits | account.credits | account_credits | - | giz.read | account.credits |
|
|
149
|
+
| drive ls | drive.list | drive_list | - | giz.read | drive.list |
|
|
150
|
+
| drive list | drive.list | drive_list | - | giz.read | drive.list |
|
|
151
|
+
| drive search | drive.search | drive_search | query | giz.read | drive.search |
|
|
152
|
+
| drive read | drive.readFile | drive_readFile | path | giz.read | drive.readFile |
|
|
153
|
+
| drive write | drive.writeFile | drive_writeFile | path, content | giz.write | drive.writeFile |
|
|
154
|
+
| drive mkdir | drive.makeDirectory | drive_makeDirectory | path | giz.write | drive.makeDirectory |
|
|
155
|
+
| drive move | drive.movePath | drive_movePath | from, to | giz.write | drive.movePath |
|
|
156
|
+
| drive delete | drive.deletePath | drive_deletePath | path | giz.destructive | drive.deletePath |
|
|
157
|
+
| table list | workspace.objects | workspace_objects | - | giz.read | workspace.objects |
|
|
158
|
+
| table get | workspace.objects | workspace_objects | - | giz.read | workspace.objects |
|
|
159
|
+
| table read | table.readRows | table_readRows | table | giz.read | table.readRows |
|
|
160
|
+
| table write | table.writeRows | table_writeRows | table | giz.write | table.writeRows |
|
|
161
|
+
| table action | workspace.executeAction | workspace_executeAction | view, action | giz.execute | workspace.executeAction |
|
|
162
|
+
| computer list | computer.list | computer_list | - | giz.read | computer.list |
|
|
163
|
+
| computer get | computer.get | computer_get | - | giz.read | external only |
|
|
164
|
+
| computer start | computer.start | computer_start | - | giz.execute | external only |
|
|
165
|
+
| computer stop | computer.stop | computer_stop | - | giz.destructive | external only |
|
|
166
|
+
| computer exec | computer.exec | giz_computer_exec | - | giz.execute | computer.exec |
|
|
167
|
+
| app list | app.list | app_list | - | giz.read | app.list |
|
|
168
|
+
| app create | app.save | app_save | id, expectedRevision | giz.execute | app.save |
|
|
169
|
+
| app save | app.save | app_save | id, expectedRevision | giz.execute | app.save |
|
|
170
|
+
| app run | app.run | app_run | id, expectedRevision | giz.execute | app.run |
|
|
171
|
+
| app publish | app.publish | app_publish | id, expectedRevision, visibility | giz.write | app.publish |
|
|
172
|
+
| app open | app.open | app_open | id | giz.read | app.open |
|
|
173
|
+
| app delete | app.delete | app_delete | id, expectedRevision | giz.destructive | app.delete |
|
|
174
|
+
| plugin list | plugin.catalog | plugin_catalog | - | giz.read | plugin.catalog |
|
|
175
|
+
| plugin search | plugin.catalog | plugin_catalog | - | giz.read | plugin.catalog |
|
|
176
|
+
| plugin inspect | plugin.catalog | plugin_catalog | - | giz.read | plugin.catalog |
|
|
177
|
+
| plugin install | plugin.install | plugin_install | - | giz.execute | plugin.install |
|
|
178
|
+
| plugin update | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
|
|
179
|
+
| plugin enable | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
|
|
180
|
+
| plugin disable | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
|
|
181
|
+
| plugin remove | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
|
|
182
|
+
| plugin publish | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
|
|
183
|
+
| plugin unpublish | plugin.manage | plugin_manage | operation | giz.destructive | plugin.manage |
|
|
184
|
+
| connection list | capability.status | capability_status | - | giz.read | capability.status |
|
|
185
|
+
| agent start | agent.start | agent_start | agentId, prompt | giz.execute | external only |
|
|
186
|
+
| agent status | agent.status | agent_status | turnId | giz.read | agent.status |
|
|
187
|
+
| agent cancel | agent.cancel | agent_cancel | turnId | giz.execute | agent.cancel |
|
|
188
|
+
<!-- cli-surface:end -->
|
|
189
|
+
|
|
190
|
+
## Support
|
|
191
|
+
|
|
192
|
+
Questions and bug reports: [support@giz.ai](mailto:support@giz.ai) or the Support Forum at [giz.ai](https://www.giz.ai).
|
|
193
|
+
|
|
194
|
+
## License
|
|
14
195
|
|
|
15
|
-
|
|
196
|
+
Proprietary. Copyright Giz Inc. All rights reserved. Use of the service is governed by the [Terms of Use](https://www.giz.ai/terms-of-use).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Runs ONLY inside the user's Computer. stdout is the authenticated gateway wire,
|
|
2
|
+
// never credentials or provider error bodies. No listener or separate credential store.
|
|
3
|
+
import { readFile } from 'node:fs/promises'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { join } from 'node:path'
|
|
6
|
+
import { execFileSync } from 'node:child_process'
|
|
7
|
+
|
|
8
|
+
const emit = value => process.stdout.write(JSON.stringify(value) + '\n')
|
|
9
|
+
try {
|
|
10
|
+
const auth = JSON.parse(await readFile(join(process.env.CODEX_HOME || join(homedir(), '.codex'), 'auth.json'), 'utf8'))
|
|
11
|
+
const token = auth.tokens?.access_token
|
|
12
|
+
const account = auth.tokens?.account_id
|
|
13
|
+
if (auth.OPENAI_API_KEY || !token || !account) {
|
|
14
|
+
emit({ status: 401 }); process.exitCode = 1
|
|
15
|
+
} else {
|
|
16
|
+
const headers = { authorization: `Bearer ${token}`, 'chatgpt-account-id': account, 'content-type': 'application/json', originator: 'codex_cli_rs' }
|
|
17
|
+
const mode = process.argv[2]
|
|
18
|
+
if (mode === 'models') {
|
|
19
|
+
const version = execFileSync('codex', ['--version'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim().split(' ').at(-1)
|
|
20
|
+
const response = await fetch(`https://chatgpt.com/backend-api/codex/models?client_version=${encodeURIComponent(version)}`, { headers, signal: AbortSignal.timeout(30000) })
|
|
21
|
+
if (!response.ok) emit({ status: response.status })
|
|
22
|
+
else {
|
|
23
|
+
const payload = await response.json()
|
|
24
|
+
emit({ status: 200, models: (payload.models || []).filter(model => model.visibility !== 'hide').map(model => ({ id: model.slug, label: model.display_name || model.slug })).filter(model => typeof model.id === 'string' && model.id.length <= 256) })
|
|
25
|
+
}
|
|
26
|
+
} else if (mode === 'responses') {
|
|
27
|
+
let input = ''
|
|
28
|
+
for await (const chunk of process.stdin) input += chunk
|
|
29
|
+
const body = JSON.parse(input)
|
|
30
|
+
body.store = false
|
|
31
|
+
body.stream = true
|
|
32
|
+
body.instructions ??= ''
|
|
33
|
+
delete body.max_output_tokens
|
|
34
|
+
delete body.temperature
|
|
35
|
+
delete body.top_p
|
|
36
|
+
const response = await fetch('https://chatgpt.com/backend-api/codex/responses', { method: 'POST', headers, body: JSON.stringify(body), signal: AbortSignal.timeout(300000) })
|
|
37
|
+
emit({ status: response.status })
|
|
38
|
+
if (response.ok) {
|
|
39
|
+
for await (const chunk of response.body) emit({ chunk: Buffer.from(chunk).toString('base64') })
|
|
40
|
+
emit({ done: true })
|
|
41
|
+
}
|
|
42
|
+
} else emit({ status: 400 })
|
|
43
|
+
}
|
|
44
|
+
} catch {
|
|
45
|
+
emit({ status: 503 }); process.exitCode = 1
|
|
46
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { lstat, readFile } from 'node:fs/promises'
|
|
2
|
+
import { request } from 'node:http'
|
|
3
|
+
|
|
4
|
+
export const IDENTITY_SOCKET = '/opt/giz/identity/identity.sock'
|
|
5
|
+
|
|
6
|
+
// One-release bridge: the managed CLI overlay can arrive before the Region gateway migration.
|
|
7
|
+
export async function resolveIdentitySocket() {
|
|
8
|
+
try {
|
|
9
|
+
await lstat(IDENTITY_SOCKET)
|
|
10
|
+
return IDENTITY_SOCKET
|
|
11
|
+
} catch (error) {
|
|
12
|
+
if (error.code !== 'ENOENT') throw error
|
|
13
|
+
return '/run/giz/identity.sock'
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
let cached
|
|
17
|
+
let pending
|
|
18
|
+
|
|
19
|
+
// The release overlay owns this stamp; outside npm installations do not have it.
|
|
20
|
+
export async function managedRevision() {
|
|
21
|
+
return readFile(new URL('../REVISION', import.meta.url), 'utf8').catch(error => {
|
|
22
|
+
if (error.code !== 'ENOENT') throw error
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The CLI never accepts a socket path from options, environment, or request data.
|
|
27
|
+
export async function computerIdentity(rejectedToken) {
|
|
28
|
+
if (cached?.access_token !== rejectedToken && cached?.expires_at > Date.now() + 60_000) return cached
|
|
29
|
+
pending ??= resolveIdentitySocket().then(socketPath => new Promise((resolve, reject) => {
|
|
30
|
+
const req = request({ socketPath, path: '/token', method: 'POST',
|
|
31
|
+
headers: { 'content-length': '0' } }, res => {
|
|
32
|
+
let body = ''
|
|
33
|
+
res.setEncoding('utf8')
|
|
34
|
+
res.on('data', chunk => {
|
|
35
|
+
body += chunk
|
|
36
|
+
if (body.length > 16_384) req.destroy(new Error('The Computer identity response is too large.'))
|
|
37
|
+
})
|
|
38
|
+
res.on('error', reject)
|
|
39
|
+
res.on('end', () => {
|
|
40
|
+
try {
|
|
41
|
+
const value = JSON.parse(body)
|
|
42
|
+
if (res.statusCode !== 200) throw new Error(value.error_description || value.message || 'The Computer identity is unavailable.')
|
|
43
|
+
const url = new URL(value.baseUrl)
|
|
44
|
+
if (!['http:', 'https:'].includes(url.protocol) || url.username || url.password ||
|
|
45
|
+
url.pathname !== '/' || url.search || url.hash || !value.access_token ||
|
|
46
|
+
value.token_type !== 'Bearer' || Number(value.expires_in) <= 0 || Number(value.expires_in) > 900) {
|
|
47
|
+
throw new Error('The Computer identity response is invalid.')
|
|
48
|
+
}
|
|
49
|
+
cached = { access_token: value.access_token, baseUrl: url.origin,
|
|
50
|
+
expires_at: Date.now() + Number(value.expires_in) * 1000 }
|
|
51
|
+
resolve(cached)
|
|
52
|
+
} catch (error) { reject(error) }
|
|
53
|
+
})
|
|
54
|
+
})
|
|
55
|
+
req.on('error', async error => {
|
|
56
|
+
if (error.code !== 'ENOENT') return reject(error)
|
|
57
|
+
try {
|
|
58
|
+
await lstat(socketPath)
|
|
59
|
+
reject(new Error('Cannot reach the Computer identity service. Try again shortly.'))
|
|
60
|
+
} catch (lookupError) {
|
|
61
|
+
if (lookupError.code !== 'ENOENT') return reject(lookupError)
|
|
62
|
+
try {
|
|
63
|
+
if (await managedRevision() !== undefined) {
|
|
64
|
+
reject(new Error('Cannot verify the Computer identity. Try again shortly or contact the owner.'))
|
|
65
|
+
} else resolve(undefined)
|
|
66
|
+
} catch (error) { reject(error) }
|
|
67
|
+
}
|
|
68
|
+
})
|
|
69
|
+
req.setTimeout(10_000, () => req.destroy(new Error('The Computer identity response timed out.')))
|
|
70
|
+
req.end()
|
|
71
|
+
})).catch(error => {
|
|
72
|
+
error.hint = 'Check that the Computer is running and try again; if it is blocked, contact the owner or an organization admin.'
|
|
73
|
+
throw error
|
|
74
|
+
}).finally(() => { pending = undefined })
|
|
75
|
+
return pending
|
|
76
|
+
}
|
package/cli/engine.mjs
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { spawn, execFileSync } from 'node:child_process'
|
|
2
|
+
import { mkdir } from 'node:fs/promises'
|
|
3
|
+
import { randomUUID } from 'node:crypto'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
/** Local Computer execution: official binary, existing permissions, isolated reviewable worktree. */
|
|
7
|
+
export async function runEngine(engine, instruction, directory = process.cwd(), progress = () => {}) {
|
|
8
|
+
if (engine !== 'claude' || typeof instruction !== 'string' || !instruction.trim()) throw new Error('Run: giz engine run claude "instruction" [--dir path]')
|
|
9
|
+
const git = (args, cwd = directory) => execFileSync('git', args, { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], maxBuffer: 8 * 1024 * 1024 })
|
|
10
|
+
const env = { ...process.env }
|
|
11
|
+
for (const key of ['ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'ANTHROPIC_BASE_URL', 'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY']) delete env[key]
|
|
12
|
+
const status = spawn('claude', ['auth', 'status'], { env, stdio: ['ignore', 'pipe', 'ignore'] })
|
|
13
|
+
let login = ''
|
|
14
|
+
status.stdout.on('data', chunk => { login += chunk })
|
|
15
|
+
await new Promise((resolve, reject) => { status.once('error', reject); status.once('close', code => code === 0 ? resolve() : reject(new Error('Log in to Claude Code in Connections.'))) })
|
|
16
|
+
const account = JSON.parse(login)
|
|
17
|
+
if (!account.loggedIn || account.authMethod !== 'claude.ai') throw new Error('A Claude subscription login is required.')
|
|
18
|
+
const root = git(['rev-parse', '--show-toplevel']).trim()
|
|
19
|
+
const common = path.resolve(root, git(['rev-parse', '--git-common-dir']).trim())
|
|
20
|
+
const worktree = path.join(common, 'giz-engine-worktrees', randomUUID())
|
|
21
|
+
await mkdir(path.dirname(worktree), { recursive: true })
|
|
22
|
+
git(['worktree', 'add', '--detach', worktree, 'HEAD'])
|
|
23
|
+
progress({ status: 'running', engine, worktree })
|
|
24
|
+
const child = spawn('claude', ['-p', instruction, '--output-format', 'stream-json', '--verbose', '--permission-mode', 'acceptEdits'],
|
|
25
|
+
{ cwd: worktree, env, detached: true, stdio: ['ignore', 'pipe', 'pipe'] })
|
|
26
|
+
let output = '', summary = '', stopped = false
|
|
27
|
+
const stop = () => { stopped = true; try { process.kill(-child.pid, 'SIGTERM') } catch {} }
|
|
28
|
+
process.once('SIGTERM', stop); process.once('SIGINT', stop)
|
|
29
|
+
child.stdout.on('data', chunk => {
|
|
30
|
+
output += chunk
|
|
31
|
+
let newline
|
|
32
|
+
while ((newline = output.indexOf('\n')) >= 0) {
|
|
33
|
+
const line = output.slice(0, newline); output = output.slice(newline + 1)
|
|
34
|
+
try {
|
|
35
|
+
const event = JSON.parse(line)
|
|
36
|
+
if (event.type === 'result') summary = String(event.result || '')
|
|
37
|
+
// Do not pass raw tool inputs, prompts or authorization data into the task card.
|
|
38
|
+
progress({ status: 'running', event: event.type, engine, worktree })
|
|
39
|
+
} catch {}
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
child.stderr.resume()
|
|
43
|
+
let exitCode
|
|
44
|
+
try {
|
|
45
|
+
exitCode = await new Promise((resolve, reject) => { child.once('error', reject); child.once('close', code => resolve(code)) })
|
|
46
|
+
} finally { process.removeListener('SIGTERM', stop); process.removeListener('SIGINT', stop) }
|
|
47
|
+
const tracked = git(['diff', '--name-only', 'HEAD'], worktree).trim().split('\n').filter(Boolean)
|
|
48
|
+
const created = git(['ls-files', '--others', '--exclude-standard'], worktree).trim().split('\n').filter(Boolean)
|
|
49
|
+
let diff = git(['diff', '--binary', 'HEAD'], worktree)
|
|
50
|
+
for (const file of created) {
|
|
51
|
+
try { diff += git(['diff', '--no-index', '--binary', '--', '/dev/null', file], worktree) }
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.status !== 1) throw error
|
|
54
|
+
diff += String(error.stdout)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return { status: stopped ? 'canceled' : exitCode === 0 ? 'completed' : 'failed', engine, exitCode, worktree, files: [...new Set([...tracked, ...created])], diff, summary }
|
|
58
|
+
}
|