@gravity-rail/cli 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli-utils.js +0 -1
- package/dist/{errors-4MAJGD3C.js → errors-SMY7LZJB.js} +1 -2
- package/dist/gr.js +39866 -4982
- package/dist/gr.js.map +1 -1
- package/dist/lib.d.ts +19 -12
- package/dist/lib.js +6495 -4706
- package/dist/lib.js.map +1 -1
- package/dist/register-special-commands.d.ts +4 -0
- package/dist/{chunk-QVG4L6BE.js → register-special-commands.js} +1708 -51
- package/dist/register-special-commands.js.map +1 -0
- package/package.json +9 -3
- package/dist/account-verify-phone-4LRJZT5I.js +0 -36
- package/dist/account-verify-phone-4LRJZT5I.js.map +0 -1
- package/dist/chunk-3EO4COHB.js +0 -14202
- package/dist/chunk-3EO4COHB.js.map +0 -1
- package/dist/chunk-424PT5DM.js +0 -23
- package/dist/chunk-QVG4L6BE.js.map +0 -1
- package/dist/chunk-XRBVAFCB.js +0 -47
- package/dist/chunk-XRBVAFCB.js.map +0 -1
- package/dist/concierge-ZOG7YFWQ.js +0 -6
- package/dist/concierge-ZOG7YFWQ.js.map +0 -1
- package/dist/errors-4MAJGD3C.js.map +0 -1
- package/dist/errors-DJILV5RW.js +0 -4
- package/dist/errors-DJILV5RW.js.map +0 -1
- /package/dist/{chunk-424PT5DM.js.map → errors-SMY7LZJB.js.map} +0 -0
|
@@ -1,39 +1,1103 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
// src/cli-utils/config.ts
|
|
12
|
+
import { existsSync, readFileSync } from "fs";
|
|
13
|
+
import { dirname, join, resolve } from "path";
|
|
14
|
+
var CALLBACK_PORT, CALLBACK_PATH, REDIRECT_URI;
|
|
15
|
+
var init_config = __esm({
|
|
16
|
+
"src/cli-utils/config.ts"() {
|
|
17
|
+
"use strict";
|
|
18
|
+
CALLBACK_PORT = 31337;
|
|
19
|
+
CALLBACK_PATH = "/oauth/callback";
|
|
20
|
+
REDIRECT_URI = `http://localhost:${CALLBACK_PORT}${CALLBACK_PATH}`;
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// src/cli-utils/auth.ts
|
|
25
|
+
import { createServer } from "http";
|
|
26
|
+
import { createHash, randomBytes } from "crypto";
|
|
27
|
+
import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, mkdirSync, chmodSync, unlinkSync } from "fs";
|
|
28
|
+
import { join as join2 } from "path";
|
|
29
|
+
import { exec } from "child_process";
|
|
30
|
+
function stderr(msg) {
|
|
31
|
+
process.stderr.write(msg + "\n");
|
|
32
|
+
}
|
|
33
|
+
function isOAuthError(body, errorCode) {
|
|
34
|
+
try {
|
|
35
|
+
const parsed = JSON.parse(body);
|
|
36
|
+
if (parsed.error === errorCode) return true;
|
|
37
|
+
} catch {
|
|
38
|
+
}
|
|
39
|
+
return body.includes(errorCode);
|
|
40
|
+
}
|
|
41
|
+
function validateTokenResponse(tokenData) {
|
|
42
|
+
if (!tokenData.access_token || typeof tokenData.access_token !== "string") {
|
|
43
|
+
throw new Error("Token response missing access_token");
|
|
44
|
+
}
|
|
45
|
+
if (tokenData.token_type && String(tokenData.token_type).toLowerCase() !== "bearer") {
|
|
46
|
+
throw new Error(`Unsupported token type: ${tokenData.token_type}`);
|
|
47
|
+
}
|
|
48
|
+
let expiresIn = 86400;
|
|
49
|
+
if (tokenData.expires_in != null) {
|
|
50
|
+
const raw = Number(tokenData.expires_in);
|
|
51
|
+
if (!Number.isFinite(raw) || raw < 60 || raw > 31536e3) {
|
|
52
|
+
stderr(` Warning: unusual expires_in value (${tokenData.expires_in}), using default`);
|
|
53
|
+
} else {
|
|
54
|
+
expiresIn = raw;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
accessToken: tokenData.access_token,
|
|
59
|
+
refreshToken: typeof tokenData.refresh_token === "string" ? tokenData.refresh_token : void 0,
|
|
60
|
+
expiresIn
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function generatePKCE() {
|
|
64
|
+
const codeVerifier = randomBytes(64).toString("base64url").slice(0, 128);
|
|
65
|
+
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
|
|
66
|
+
return { codeVerifier, codeChallenge };
|
|
67
|
+
}
|
|
68
|
+
function openBrowser(url) {
|
|
69
|
+
const platform = process.platform;
|
|
70
|
+
let cmd;
|
|
71
|
+
if (platform === "darwin") {
|
|
72
|
+
cmd = `open "${url}"`;
|
|
73
|
+
} else if (platform === "win32") {
|
|
74
|
+
cmd = `start "" "${url}"`;
|
|
75
|
+
} else {
|
|
76
|
+
cmd = `xdg-open "${url}"`;
|
|
77
|
+
}
|
|
78
|
+
exec(cmd, (err) => {
|
|
79
|
+
if (err) {
|
|
80
|
+
stderr(" Could not open browser automatically.");
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
function startCallbackServer() {
|
|
85
|
+
return new Promise((resolve2, reject) => {
|
|
86
|
+
let authCode = null;
|
|
87
|
+
let error = null;
|
|
88
|
+
let receivedState = null;
|
|
89
|
+
const server = createServer((req, res) => {
|
|
90
|
+
const url = new URL(req.url, `http://localhost:${CALLBACK_PORT}`);
|
|
91
|
+
if (url.pathname !== CALLBACK_PATH) {
|
|
92
|
+
res.writeHead(404);
|
|
93
|
+
res.end("Not found");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (url.searchParams.has("error")) {
|
|
97
|
+
error = url.searchParams.get("error");
|
|
98
|
+
const errorDesc = url.searchParams.get("error_description") || "Unknown error";
|
|
99
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
100
|
+
res.end(`
|
|
101
|
+
<html>
|
|
102
|
+
<head><title>Authorization Failed</title></head>
|
|
103
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
104
|
+
<h1 style="color: #dc2626;">Authorization Failed</h1>
|
|
105
|
+
<p>${errorDesc}</p>
|
|
106
|
+
<p>You can close this window.</p>
|
|
107
|
+
</body>
|
|
108
|
+
</html>
|
|
109
|
+
`);
|
|
110
|
+
server.close();
|
|
111
|
+
reject(new Error(`Authorization failed: ${error}`));
|
|
112
|
+
} else if (url.searchParams.has("code")) {
|
|
113
|
+
authCode = url.searchParams.get("code");
|
|
114
|
+
receivedState = url.searchParams.get("state");
|
|
115
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
116
|
+
res.end(`
|
|
117
|
+
<html>
|
|
118
|
+
<head><title>Authorization Successful</title></head>
|
|
119
|
+
<body style="font-family: system-ui; text-align: center; padding: 50px;">
|
|
120
|
+
<h1 style="color: #16a34a;">Authorization Successful!</h1>
|
|
121
|
+
<p>You can close this window and return to the terminal.</p>
|
|
122
|
+
<script>setTimeout(() => window.close(), 2000);</script>
|
|
123
|
+
</body>
|
|
124
|
+
</html>
|
|
125
|
+
`);
|
|
126
|
+
server.close();
|
|
127
|
+
resolve2({ code: authCode, state: receivedState });
|
|
128
|
+
} else {
|
|
129
|
+
res.writeHead(400);
|
|
130
|
+
res.end("Missing code parameter");
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
server.listen(CALLBACK_PORT, "localhost", () => {
|
|
134
|
+
});
|
|
135
|
+
const timeout = setTimeout(
|
|
136
|
+
() => {
|
|
137
|
+
if (!authCode && !error) {
|
|
138
|
+
server.close();
|
|
139
|
+
reject(new Error("Authorization timeout - no response received"));
|
|
140
|
+
}
|
|
141
|
+
},
|
|
142
|
+
5 * 60 * 1e3
|
|
143
|
+
);
|
|
144
|
+
timeout.unref();
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
async function dynamicClientRegistration(envConfig, clientName = "Gravity CLI") {
|
|
148
|
+
stderr(" Registering OAuth client...");
|
|
149
|
+
const registrationUrl = `${envConfig.apiUrl}/api/v2/oauth/register`;
|
|
150
|
+
const registrationData = {
|
|
151
|
+
client_name: clientName,
|
|
152
|
+
redirect_uris: [REDIRECT_URI],
|
|
153
|
+
grant_types: ["authorization_code", "refresh_token"],
|
|
154
|
+
response_types: ["code"],
|
|
155
|
+
token_endpoint_auth_method: "client_secret_basic",
|
|
156
|
+
scope: "workspace:admin"
|
|
157
|
+
};
|
|
158
|
+
const response = await fetch(registrationUrl, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "Content-Type": "application/json" },
|
|
161
|
+
body: JSON.stringify(registrationData)
|
|
162
|
+
});
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
throw new Error(`Client registration failed: ${response.status} - ${await response.text()}`);
|
|
165
|
+
}
|
|
166
|
+
return await response.json();
|
|
167
|
+
}
|
|
168
|
+
async function performOAuthFlow(options) {
|
|
169
|
+
const { env, envConfig, credentialsManager, clientName = "Gravity CLI" } = options;
|
|
170
|
+
let hasRetried = false;
|
|
171
|
+
while (true) {
|
|
172
|
+
let creds = credentialsManager.load(env);
|
|
173
|
+
if (!creds || !creds.clientId) {
|
|
174
|
+
const registration = await dynamicClientRegistration(envConfig, clientName);
|
|
175
|
+
creds = {
|
|
176
|
+
clientId: registration.client_id,
|
|
177
|
+
clientSecret: registration.client_secret
|
|
178
|
+
};
|
|
179
|
+
credentialsManager.save(env, creds);
|
|
180
|
+
} else {
|
|
181
|
+
stderr(" Using cached client credentials");
|
|
182
|
+
}
|
|
183
|
+
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
184
|
+
const state = randomBytes(32).toString("base64url");
|
|
185
|
+
const authParams = new URLSearchParams({
|
|
186
|
+
response_type: "code",
|
|
187
|
+
client_id: creds.clientId,
|
|
188
|
+
redirect_uri: REDIRECT_URI,
|
|
189
|
+
scope: "workspace:admin",
|
|
190
|
+
state,
|
|
191
|
+
code_challenge: codeChallenge,
|
|
192
|
+
code_challenge_method: "S256"
|
|
193
|
+
});
|
|
194
|
+
const authUrl = `${envConfig.apiUrl}/api/v2/oauth/authorize?${authParams}`;
|
|
195
|
+
const callbackPromise = startCallbackServer();
|
|
196
|
+
stderr("\n Opening browser for authorization...");
|
|
197
|
+
stderr(` If browser doesn't open, visit:
|
|
198
|
+
${authUrl}
|
|
199
|
+
`);
|
|
200
|
+
openBrowser(authUrl);
|
|
201
|
+
stderr(" Waiting for authorization (timeout: 5 minutes)...");
|
|
202
|
+
let code;
|
|
203
|
+
let receivedState;
|
|
204
|
+
try {
|
|
205
|
+
({ code, state: receivedState } = await callbackPromise);
|
|
206
|
+
} catch (err) {
|
|
207
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
208
|
+
if (!hasRetried && isOAuthError(msg, "invalid_client")) {
|
|
209
|
+
stderr(" Cached client credentials expired, re-registering...");
|
|
210
|
+
credentialsManager.clear(env);
|
|
211
|
+
hasRetried = true;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
throw err;
|
|
215
|
+
}
|
|
216
|
+
if (receivedState !== state) {
|
|
217
|
+
throw new Error("State mismatch - possible CSRF attack");
|
|
218
|
+
}
|
|
219
|
+
stderr(" Authorization code received!");
|
|
220
|
+
stderr(" Exchanging code for tokens...");
|
|
221
|
+
const tokenUrl = `${envConfig.apiUrl}/api/v2/oauth/token`;
|
|
222
|
+
const authHeader = Buffer.from(`${creds.clientId}:${creds.clientSecret}`).toString("base64");
|
|
223
|
+
const tokenResponse = await fetch(tokenUrl, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: {
|
|
226
|
+
Authorization: `Basic ${authHeader}`,
|
|
227
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
228
|
+
},
|
|
229
|
+
body: new URLSearchParams({
|
|
230
|
+
grant_type: "authorization_code",
|
|
231
|
+
code,
|
|
232
|
+
redirect_uri: REDIRECT_URI,
|
|
233
|
+
code_verifier: codeVerifier
|
|
234
|
+
})
|
|
235
|
+
});
|
|
236
|
+
if (!tokenResponse.ok) {
|
|
237
|
+
const body = await tokenResponse.text();
|
|
238
|
+
if (!hasRetried && isOAuthError(body, "invalid_client")) {
|
|
239
|
+
stderr(" Cached client credentials expired, re-registering...");
|
|
240
|
+
credentialsManager.clear(env);
|
|
241
|
+
hasRetried = true;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
throw new Error(`Token exchange failed: ${tokenResponse.status} - ${body}`);
|
|
245
|
+
}
|
|
246
|
+
const tokenData = await tokenResponse.json();
|
|
247
|
+
const { accessToken, refreshToken, expiresIn } = validateTokenResponse(tokenData);
|
|
248
|
+
creds.accessToken = accessToken;
|
|
249
|
+
creds.refreshToken = refreshToken ?? creds.refreshToken;
|
|
250
|
+
creds.expiresAt = new Date(Date.now() + expiresIn * 1e3).toISOString();
|
|
251
|
+
credentialsManager.save(env, creds);
|
|
252
|
+
stderr(" Authentication successful!");
|
|
253
|
+
return { accessToken, credentials: creds };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
var init_auth = __esm({
|
|
257
|
+
"src/cli-utils/auth.ts"() {
|
|
258
|
+
"use strict";
|
|
259
|
+
init_config();
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// src/cli-utils/ui.ts
|
|
264
|
+
import * as readline from "readline";
|
|
265
|
+
async function selectFromList(items, options = {}) {
|
|
266
|
+
const {
|
|
267
|
+
title = "Select an item",
|
|
268
|
+
labelFn = (item) => String(item.name || item.title || item),
|
|
269
|
+
detailFn = null,
|
|
270
|
+
env = null,
|
|
271
|
+
emptyMessage = "No items found.",
|
|
272
|
+
allowNew = false,
|
|
273
|
+
newLabel = "Create new..."
|
|
274
|
+
} = options;
|
|
275
|
+
if ((!items || items.length === 0) && !allowNew) {
|
|
276
|
+
console.log(`
|
|
277
|
+
${emptyMessage}`);
|
|
278
|
+
return { item: null, isNew: false };
|
|
279
|
+
}
|
|
280
|
+
const displayItems = allowNew ? [NEW_ITEM_MARKER, ...items] : items;
|
|
281
|
+
return new Promise((resolve2) => {
|
|
282
|
+
let selectedIndex = allowNew ? 1 : 0;
|
|
283
|
+
let filterText = "";
|
|
284
|
+
const getFilteredItems = () => {
|
|
285
|
+
if (!filterText) return displayItems;
|
|
286
|
+
const lower = filterText.toLowerCase();
|
|
287
|
+
return displayItems.filter((item) => {
|
|
288
|
+
if (item === NEW_ITEM_MARKER) return true;
|
|
289
|
+
const label = labelFn(item);
|
|
290
|
+
return label.toLowerCase().includes(lower);
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
const render = () => {
|
|
294
|
+
const filtered = getFilteredItems();
|
|
295
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
296
|
+
console.log("=".repeat(60));
|
|
297
|
+
console.log(`${title} (\u2191/\u2193 to move, Enter to select, Esc to cancel)`);
|
|
298
|
+
console.log("=".repeat(60));
|
|
299
|
+
if (env) {
|
|
300
|
+
console.log(`Environment: ${env}`);
|
|
301
|
+
}
|
|
302
|
+
console.log(`Filter: ${filterText}${filterText ? "" : "(type to filter)"}`);
|
|
303
|
+
console.log("");
|
|
304
|
+
if (filtered.length === 0) {
|
|
305
|
+
console.log(` ${emptyMessage}`);
|
|
306
|
+
console.log("");
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
const maxVisible = 15;
|
|
310
|
+
let startIdx = Math.max(0, selectedIndex - Math.floor(maxVisible / 2));
|
|
311
|
+
let endIdx = Math.min(filtered.length, startIdx + maxVisible);
|
|
312
|
+
if (endIdx - startIdx < maxVisible && startIdx > 0) {
|
|
313
|
+
startIdx = Math.max(0, endIdx - maxVisible);
|
|
314
|
+
}
|
|
315
|
+
if (startIdx > 0) {
|
|
316
|
+
console.log(` ... ${startIdx} more above ...`);
|
|
317
|
+
}
|
|
318
|
+
for (let i = startIdx; i < endIdx; i++) {
|
|
319
|
+
const item = filtered[i];
|
|
320
|
+
const prefix = i === selectedIndex ? "> " : " ";
|
|
321
|
+
let line;
|
|
322
|
+
if (item === NEW_ITEM_MARKER) {
|
|
323
|
+
line = `\x1B[32m${newLabel}\x1B[0m`;
|
|
324
|
+
} else {
|
|
325
|
+
const typedItem = item;
|
|
326
|
+
line = labelFn(typedItem);
|
|
327
|
+
if (detailFn) {
|
|
328
|
+
const detail = detailFn(typedItem);
|
|
329
|
+
if (detail) {
|
|
330
|
+
line = `${line} \x1B[90m${detail}\x1B[0m`;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (i === selectedIndex) {
|
|
335
|
+
console.log(`\x1B[7m${prefix}${line}\x1B[0m`);
|
|
336
|
+
} else {
|
|
337
|
+
console.log(`${prefix}${line}`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (endIdx < filtered.length) {
|
|
341
|
+
console.log(` ... ${filtered.length - endIdx} more below ...`);
|
|
342
|
+
}
|
|
343
|
+
console.log("");
|
|
344
|
+
console.log(` ${filtered.length} item(s)`);
|
|
345
|
+
};
|
|
346
|
+
readline.emitKeypressEvents(process.stdin);
|
|
347
|
+
if (process.stdin.isTTY) {
|
|
348
|
+
process.stdin.setRawMode(true);
|
|
349
|
+
}
|
|
350
|
+
render();
|
|
351
|
+
const handleKeypress = (str, key) => {
|
|
352
|
+
const filtered = getFilteredItems();
|
|
353
|
+
if (key.name === "up") {
|
|
354
|
+
selectedIndex = Math.max(0, selectedIndex - 1);
|
|
355
|
+
render();
|
|
356
|
+
} else if (key.name === "down") {
|
|
357
|
+
selectedIndex = Math.min(filtered.length - 1, selectedIndex + 1);
|
|
358
|
+
render();
|
|
359
|
+
} else if (key.name === "return") {
|
|
360
|
+
cleanup();
|
|
361
|
+
if (filtered.length > 0 && selectedIndex < filtered.length) {
|
|
362
|
+
const selected = filtered[selectedIndex];
|
|
363
|
+
if (selected === NEW_ITEM_MARKER) {
|
|
364
|
+
resolve2({ item: null, isNew: true });
|
|
365
|
+
} else {
|
|
366
|
+
resolve2({ item: selected, isNew: false });
|
|
367
|
+
}
|
|
368
|
+
} else {
|
|
369
|
+
resolve2({ item: null, isNew: false });
|
|
370
|
+
}
|
|
371
|
+
} else if (key.name === "escape" || key.ctrl && key.name === "c") {
|
|
372
|
+
cleanup();
|
|
373
|
+
resolve2({ item: null, isNew: false });
|
|
374
|
+
} else if (key.name === "backspace") {
|
|
375
|
+
filterText = filterText.slice(0, -1);
|
|
376
|
+
selectedIndex = allowNew ? 0 : 0;
|
|
377
|
+
render();
|
|
378
|
+
} else if (str && str.length === 1 && !key.ctrl && !key.meta) {
|
|
379
|
+
filterText += str;
|
|
380
|
+
selectedIndex = allowNew ? 0 : 0;
|
|
381
|
+
render();
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
const cleanup = () => {
|
|
385
|
+
process.stdin.removeListener("keypress", handleKeypress);
|
|
386
|
+
if (process.stdin.isTTY) {
|
|
387
|
+
process.stdin.setRawMode(false);
|
|
388
|
+
}
|
|
389
|
+
process.stdin.pause();
|
|
390
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
391
|
+
};
|
|
392
|
+
process.stdin.on("keypress", handleKeypress);
|
|
393
|
+
process.stdin.resume();
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
function printTable(items, columns, options = {}) {
|
|
397
|
+
if (!items || items.length === 0) {
|
|
398
|
+
console.log("\nNo items found.");
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (options.title) {
|
|
402
|
+
console.log(`
|
|
403
|
+
=== ${options.title} ===`);
|
|
404
|
+
}
|
|
405
|
+
if (options.env) {
|
|
406
|
+
console.log(`Environment: ${options.env}`);
|
|
407
|
+
}
|
|
408
|
+
console.log("");
|
|
409
|
+
const widths = columns.map((col) => {
|
|
410
|
+
const maxContent = Math.max(...items.map((item) => String(col.getValue(item) ?? "").length));
|
|
411
|
+
const width = Math.max(col.header.length, maxContent);
|
|
412
|
+
return col.maxWidth ? Math.min(width, col.maxWidth) : width;
|
|
413
|
+
});
|
|
414
|
+
const headerLine = columns.map((col, i) => col.header.padEnd(widths[i])).join(" ");
|
|
415
|
+
console.log(headerLine);
|
|
416
|
+
console.log(widths.map((w) => "-".repeat(w)).join(" "));
|
|
417
|
+
for (const item of items) {
|
|
418
|
+
const row = columns.map((col, i) => {
|
|
419
|
+
const value = String(col.getValue(item) ?? "");
|
|
420
|
+
const width = widths[i];
|
|
421
|
+
return value.slice(0, width).padEnd(width);
|
|
422
|
+
}).join(" ");
|
|
423
|
+
console.log(row);
|
|
424
|
+
}
|
|
425
|
+
console.log("");
|
|
426
|
+
console.log(`Total: ${items.length} item(s)`);
|
|
427
|
+
}
|
|
428
|
+
function formatTimestamp(isoString) {
|
|
429
|
+
if (!isoString) return "";
|
|
430
|
+
const date = new Date(isoString);
|
|
431
|
+
const now = /* @__PURE__ */ new Date();
|
|
432
|
+
const diff = now.getTime() - date.getTime();
|
|
433
|
+
if (diff < 6e4) return "just now";
|
|
434
|
+
if (diff < 36e5) return `${Math.floor(diff / 6e4)}m ago`;
|
|
435
|
+
if (diff < 864e5) return `${Math.floor(diff / 36e5)}h ago`;
|
|
436
|
+
if (diff < 6048e5) return `${Math.floor(diff / 864e5)}d ago`;
|
|
437
|
+
return date.toLocaleDateString();
|
|
438
|
+
}
|
|
439
|
+
function wrapText(text, width = 70) {
|
|
440
|
+
const words = text.split(" ");
|
|
441
|
+
const lines = [];
|
|
442
|
+
let currentLine = "";
|
|
443
|
+
for (const word of words) {
|
|
444
|
+
if (currentLine.length + word.length + 1 <= width) {
|
|
445
|
+
currentLine += (currentLine ? " " : "") + word;
|
|
446
|
+
} else {
|
|
447
|
+
if (currentLine) lines.push(currentLine);
|
|
448
|
+
currentLine = word;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (currentLine) lines.push(currentLine);
|
|
452
|
+
return lines;
|
|
453
|
+
}
|
|
454
|
+
function printMessage(content, role, indent = " ") {
|
|
455
|
+
const roleColors = {
|
|
456
|
+
user: "\x1B[34m",
|
|
457
|
+
// Blue
|
|
458
|
+
assistant: "\x1B[32m",
|
|
459
|
+
// Green
|
|
460
|
+
system: "\x1B[90m"
|
|
461
|
+
// Gray
|
|
462
|
+
};
|
|
463
|
+
const resetColor = "\x1B[0m";
|
|
464
|
+
const color = roleColors[role] || resetColor;
|
|
465
|
+
const label = role === "user" ? "You" : role === "assistant" ? "Assistant" : "System";
|
|
466
|
+
console.log(`${indent}${color}[${label}]${resetColor}`);
|
|
467
|
+
const lines = content.split("\n");
|
|
468
|
+
for (const line of lines) {
|
|
469
|
+
const wrapped = wrapText(line, 70);
|
|
470
|
+
for (const wrappedLine of wrapped) {
|
|
471
|
+
console.log(`${indent} ${wrappedLine}`);
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
console.log("");
|
|
475
|
+
}
|
|
476
|
+
function clearScreen() {
|
|
477
|
+
process.stdout.write("\x1B[2J\x1B[H");
|
|
478
|
+
}
|
|
479
|
+
var NEW_ITEM_MARKER;
|
|
480
|
+
var init_ui = __esm({
|
|
481
|
+
"src/cli-utils/ui.ts"() {
|
|
482
|
+
"use strict";
|
|
483
|
+
NEW_ITEM_MARKER = Symbol("NEW_ITEM");
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// src/cli-utils/agui.ts
|
|
488
|
+
function isTextContentEvent(event) {
|
|
489
|
+
const type = event.type.toLowerCase();
|
|
490
|
+
return type === "text_message_content";
|
|
491
|
+
}
|
|
492
|
+
function isErrorEvent(event) {
|
|
493
|
+
const type = event.type.toLowerCase();
|
|
494
|
+
return type === "run_error";
|
|
495
|
+
}
|
|
496
|
+
function getTextDelta(event) {
|
|
497
|
+
if (isTextContentEvent(event) && event.delta) {
|
|
498
|
+
return event.delta;
|
|
499
|
+
}
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
function getErrorMessage(event) {
|
|
503
|
+
if (isErrorEvent(event)) {
|
|
504
|
+
return event.message || event.code || "Unknown error";
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
var AGUIClient;
|
|
509
|
+
var init_agui = __esm({
|
|
510
|
+
"src/cli-utils/agui.ts"() {
|
|
511
|
+
"use strict";
|
|
512
|
+
AGUIClient = class {
|
|
513
|
+
endpointUrl;
|
|
514
|
+
baseUrl;
|
|
515
|
+
workspaceUuid;
|
|
516
|
+
chatId;
|
|
517
|
+
accessToken;
|
|
518
|
+
debug;
|
|
519
|
+
model;
|
|
520
|
+
appContext;
|
|
521
|
+
constructor(options) {
|
|
522
|
+
this.baseUrl = options.baseUrl;
|
|
523
|
+
this.workspaceUuid = options.workspaceUuid ?? "";
|
|
524
|
+
this.chatId = options.chatId ?? 0;
|
|
525
|
+
if (!options.endpointUrl && (!options.workspaceUuid || options.chatId === void 0)) {
|
|
526
|
+
throw new Error("AGUIClient requires workspaceUuid and chatId, or endpointUrl");
|
|
527
|
+
}
|
|
528
|
+
this.endpointUrl = options.endpointUrl ?? `${options.baseUrl}/api/v2/w/${options.workspaceUuid}/web-chat/${options.chatId}/chat/agui`;
|
|
529
|
+
this.accessToken = options.accessToken;
|
|
530
|
+
this.debug = options.debug ?? false;
|
|
531
|
+
this.model = "model" in options ? options.model : "manager";
|
|
532
|
+
this.appContext = options.appContext;
|
|
533
|
+
}
|
|
534
|
+
inferenceStatusUrl() {
|
|
535
|
+
if (!this.workspaceUuid || this.chatId <= 0) return null;
|
|
536
|
+
return `${this.baseUrl}/api/v2/w/${this.workspaceUuid}/chats/${this.chatId}/inference/status`;
|
|
537
|
+
}
|
|
538
|
+
async waitForInferenceInactive(maxWaitMs = 6e4) {
|
|
539
|
+
const url = this.inferenceStatusUrl();
|
|
540
|
+
if (!url) return false;
|
|
541
|
+
const deadline = Date.now() + maxWaitMs;
|
|
542
|
+
while (Date.now() < deadline) {
|
|
543
|
+
try {
|
|
544
|
+
const res = await fetch(url, {
|
|
545
|
+
headers: { Authorization: `Bearer ${this.accessToken}` }
|
|
546
|
+
});
|
|
547
|
+
if (res.ok) {
|
|
548
|
+
const body = await res.json();
|
|
549
|
+
if (!body.active) return true;
|
|
550
|
+
}
|
|
551
|
+
} catch (err) {
|
|
552
|
+
if (this.debug) {
|
|
553
|
+
console.log(
|
|
554
|
+
`[DEBUG] waitForInferenceInactive fetch failed: ${err instanceof Error ? err.message : String(err)}`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
559
|
+
}
|
|
560
|
+
return false;
|
|
561
|
+
}
|
|
562
|
+
/**
|
|
563
|
+
* Send messages and stream the response
|
|
564
|
+
*/
|
|
565
|
+
async sendMessage(messages, onEvent, options) {
|
|
566
|
+
return this.streamRequest({ messages }, onEvent, options);
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Stream an AG-UI request (initial messages or HITL resume).
|
|
570
|
+
*/
|
|
571
|
+
async streamRequest(request, onEvent, options) {
|
|
572
|
+
const body = JSON.stringify({
|
|
573
|
+
...this.model ? { model: this.model } : {},
|
|
574
|
+
messages: request.messages ?? [],
|
|
575
|
+
...this.appContext ? { appContext: this.appContext } : {},
|
|
576
|
+
...request.resumeValue !== void 0 ? { resumeValue: request.resumeValue } : {},
|
|
577
|
+
...request.resumeInterruptId ? { resumeInterruptId: request.resumeInterruptId } : {},
|
|
578
|
+
...options?.autoApprove ? { context: { autoApprove: true } } : {}
|
|
579
|
+
});
|
|
580
|
+
if (this.debug) {
|
|
581
|
+
console.log(`
|
|
582
|
+
[DEBUG] POST ${this.endpointUrl}`);
|
|
583
|
+
console.log(
|
|
584
|
+
`[DEBUG] Body: ${request.resumeValue !== void 0 ? "resume" : `${(request.messages ?? []).length} message(s)`}`
|
|
585
|
+
);
|
|
586
|
+
}
|
|
587
|
+
await this.postStream(body, onEvent, options);
|
|
588
|
+
}
|
|
589
|
+
async postStream(body, onEvent, options, conflictRetries = 0) {
|
|
590
|
+
const response = await fetch(this.endpointUrl, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: {
|
|
593
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
594
|
+
"Content-Type": "application/json",
|
|
595
|
+
Accept: "text/event-stream"
|
|
596
|
+
},
|
|
597
|
+
body
|
|
598
|
+
});
|
|
599
|
+
if (this.debug) {
|
|
600
|
+
console.log(`[DEBUG] Response status: ${response.status}`);
|
|
601
|
+
}
|
|
602
|
+
if (response.status === 409 && conflictRetries < 3) {
|
|
603
|
+
const ready = await this.waitForInferenceInactive();
|
|
604
|
+
if (ready) {
|
|
605
|
+
await this.postStream(body, onEvent, options, conflictRetries + 1);
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
if (!response.ok) {
|
|
610
|
+
const errorText = await response.text().catch(() => "");
|
|
611
|
+
if (this.debug) {
|
|
612
|
+
console.log(`[DEBUG] Error response: ${errorText}`);
|
|
613
|
+
}
|
|
614
|
+
throw new Error(
|
|
615
|
+
`Chat request failed: ${response.status}${errorText ? `: ${errorText.slice(0, 200)}` : ""}`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
const reader = response.body.getReader();
|
|
619
|
+
const decoder = new TextDecoder();
|
|
620
|
+
let buffer = "";
|
|
621
|
+
while (true) {
|
|
622
|
+
const { done, value } = await reader.read();
|
|
623
|
+
if (done) break;
|
|
624
|
+
buffer += decoder.decode(value, { stream: true });
|
|
625
|
+
const lines = buffer.split("\n");
|
|
626
|
+
buffer = lines.pop() || "";
|
|
627
|
+
for (const line of lines) {
|
|
628
|
+
if (line.startsWith("data: ")) {
|
|
629
|
+
const data = line.slice(6);
|
|
630
|
+
if (data === "[DONE]") {
|
|
631
|
+
onEvent({ type: "done" });
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
const event = JSON.parse(data);
|
|
636
|
+
onEvent(event);
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
// src/cli-utils/index.ts
|
|
648
|
+
var init_cli_utils = __esm({
|
|
649
|
+
"src/cli-utils/index.ts"() {
|
|
650
|
+
"use strict";
|
|
651
|
+
init_config();
|
|
652
|
+
init_auth();
|
|
653
|
+
init_ui();
|
|
654
|
+
init_agui();
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
|
|
658
|
+
// src/lib/errors.ts
|
|
659
|
+
var CLIError;
|
|
660
|
+
var init_errors = __esm({
|
|
661
|
+
"src/lib/errors.ts"() {
|
|
662
|
+
"use strict";
|
|
663
|
+
CLIError = class extends Error {
|
|
664
|
+
constructor(message, exitCode = 1) {
|
|
665
|
+
super(message);
|
|
666
|
+
this.exitCode = exitCode;
|
|
667
|
+
this.name = "CLIError";
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
// src/lib/pagination.ts
|
|
674
|
+
function isListResult(data) {
|
|
675
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) return false;
|
|
676
|
+
const obj = data;
|
|
677
|
+
return Array.isArray(obj.items) && ("supportsAll" in obj || "meta" in obj && (obj.meta === void 0 || typeof obj.meta === "object"));
|
|
678
|
+
}
|
|
679
|
+
function isPagePaginatedResponse(data) {
|
|
680
|
+
if (!data || typeof data !== "object") return false;
|
|
681
|
+
const obj = data;
|
|
682
|
+
if (!Array.isArray(obj.items) || typeof obj.page !== "number") return false;
|
|
683
|
+
if (typeof obj.totalPages === "number") return true;
|
|
684
|
+
if (typeof obj.total === "number" && typeof obj.pageSize === "number") return true;
|
|
685
|
+
return false;
|
|
686
|
+
}
|
|
687
|
+
function isCursorPaginatedResponse(data) {
|
|
688
|
+
if (!data || typeof data !== "object") return false;
|
|
689
|
+
const obj = data;
|
|
690
|
+
return Array.isArray(obj.items) && typeof obj.hasMore === "boolean";
|
|
691
|
+
}
|
|
692
|
+
function extractPaginationMeta(data) {
|
|
693
|
+
if (isPagePaginatedResponse(data)) {
|
|
694
|
+
const totalPages = data.totalPages ?? (data.pageSize ? Math.ceil(data.total / data.pageSize) : void 0);
|
|
695
|
+
return {
|
|
696
|
+
total: data.total,
|
|
697
|
+
page: data.page,
|
|
698
|
+
totalPages
|
|
699
|
+
};
|
|
700
|
+
}
|
|
701
|
+
if (isCursorPaginatedResponse(data)) {
|
|
702
|
+
return {
|
|
703
|
+
hasMore: data.hasMore,
|
|
704
|
+
nextCursor: data.nextCursor
|
|
705
|
+
};
|
|
706
|
+
}
|
|
707
|
+
return void 0;
|
|
708
|
+
}
|
|
709
|
+
function formatPaginationWarning(meta, supportsAll = true) {
|
|
710
|
+
if (!meta) return void 0;
|
|
711
|
+
const allHint = supportsAll ? " Use --all to fetch all pages." : "";
|
|
712
|
+
if (meta.totalPages !== void 0 && meta.totalPages > 1) {
|
|
713
|
+
const pageNum = meta.page ?? 1;
|
|
714
|
+
return `Showing page ${pageNum} of ${meta.totalPages} (${meta.total} total items).${allHint}`;
|
|
715
|
+
}
|
|
716
|
+
if (meta.hasMore === true) {
|
|
717
|
+
return `More results available.${allHint}`;
|
|
718
|
+
}
|
|
719
|
+
return void 0;
|
|
720
|
+
}
|
|
721
|
+
function unwrapItems(data) {
|
|
722
|
+
if (Array.isArray(data)) return data;
|
|
723
|
+
if (data && typeof data === "object" && "items" in data && Array.isArray(data.items)) {
|
|
724
|
+
return data.items;
|
|
725
|
+
}
|
|
726
|
+
return [data];
|
|
727
|
+
}
|
|
728
|
+
var init_pagination = __esm({
|
|
729
|
+
"src/lib/pagination.ts"() {
|
|
730
|
+
"use strict";
|
|
731
|
+
init_errors();
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// src/lib/output.ts
|
|
736
|
+
function outputData(data, opts, columns, paginationMeta, supportsAll) {
|
|
737
|
+
let items;
|
|
738
|
+
let meta;
|
|
739
|
+
let allSupported;
|
|
740
|
+
if (isListResult(data)) {
|
|
741
|
+
items = data.items;
|
|
742
|
+
meta = data.meta;
|
|
743
|
+
allSupported = data.supportsAll ?? false;
|
|
744
|
+
} else {
|
|
745
|
+
items = unwrapItems(data);
|
|
746
|
+
meta = paginationMeta ?? extractPaginationMeta(data);
|
|
747
|
+
allSupported = supportsAll ?? false;
|
|
748
|
+
}
|
|
749
|
+
const warning = formatPaginationWarning(meta, allSupported);
|
|
750
|
+
if (warning) {
|
|
751
|
+
process.stderr.write(warning + "\n");
|
|
752
|
+
}
|
|
753
|
+
switch (opts.outputFormat) {
|
|
754
|
+
case "json":
|
|
755
|
+
process.stdout.write(JSON.stringify(items, null, 2) + "\n");
|
|
756
|
+
break;
|
|
757
|
+
case "jsonl":
|
|
758
|
+
for (const item of items) {
|
|
759
|
+
process.stdout.write(JSON.stringify(item) + "\n");
|
|
760
|
+
}
|
|
761
|
+
break;
|
|
762
|
+
case "table":
|
|
763
|
+
if (columns) {
|
|
764
|
+
printTable(items, columns);
|
|
765
|
+
} else {
|
|
766
|
+
process.stdout.write(JSON.stringify(items, null, 2) + "\n");
|
|
767
|
+
}
|
|
768
|
+
break;
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
function outputSingle(data, opts, columns) {
|
|
772
|
+
switch (opts.outputFormat) {
|
|
773
|
+
case "json":
|
|
774
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
775
|
+
break;
|
|
776
|
+
case "jsonl":
|
|
777
|
+
process.stdout.write(JSON.stringify(data) + "\n");
|
|
778
|
+
break;
|
|
779
|
+
case "table":
|
|
780
|
+
if (columns) {
|
|
781
|
+
printTable([data], columns);
|
|
782
|
+
} else {
|
|
783
|
+
process.stdout.write(JSON.stringify(data, null, 2) + "\n");
|
|
784
|
+
}
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
function stderr2(msg) {
|
|
789
|
+
process.stderr.write(msg + "\n");
|
|
790
|
+
}
|
|
791
|
+
var init_output = __esm({
|
|
792
|
+
"src/lib/output.ts"() {
|
|
793
|
+
"use strict";
|
|
794
|
+
init_cli_utils();
|
|
795
|
+
init_pagination();
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
// src/lib/realm.ts
|
|
800
|
+
import { execSync } from "child_process";
|
|
801
|
+
import { existsSync as existsSync3, mkdirSync as mkdirSync2, readFileSync as readFileSync3, statSync, writeFileSync as writeFileSync2 } from "fs";
|
|
802
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
803
|
+
var CACHE_MAX_AGE_MS;
|
|
804
|
+
var init_realm = __esm({
|
|
805
|
+
"src/lib/realm.ts"() {
|
|
806
|
+
"use strict";
|
|
807
|
+
CACHE_MAX_AGE_MS = 60 * 60 * 1e3;
|
|
808
|
+
}
|
|
809
|
+
});
|
|
810
|
+
|
|
811
|
+
// src/lib/org-auth.ts
|
|
812
|
+
import { execFile as execFile2 } from "child_process";
|
|
813
|
+
var ORG_AUTH_TIMEOUT_MS;
|
|
814
|
+
var init_org_auth = __esm({
|
|
815
|
+
"src/lib/org-auth.ts"() {
|
|
816
|
+
"use strict";
|
|
817
|
+
init_output();
|
|
818
|
+
ORG_AUTH_TIMEOUT_MS = 5 * 60 * 1e3;
|
|
819
|
+
}
|
|
820
|
+
});
|
|
821
|
+
|
|
822
|
+
// src/lib/pinned-fetch.ts
|
|
823
|
+
import { Agent, fetch as undiciFetch } from "undici";
|
|
824
|
+
var init_pinned_fetch = __esm({
|
|
825
|
+
"src/lib/pinned-fetch.ts"() {
|
|
826
|
+
"use strict";
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
// src/lib/context.ts
|
|
831
|
+
import { GravityRailClient } from "@gravity-rail/sdk";
|
|
832
|
+
import { homedir } from "os";
|
|
833
|
+
function requireAuth(ctx) {
|
|
834
|
+
if (!("apiClient" in ctx)) {
|
|
835
|
+
throw new Error("Command handler accessed apiClient without declaring requiresAuth");
|
|
836
|
+
}
|
|
837
|
+
return ctx;
|
|
838
|
+
}
|
|
839
|
+
var init_context = __esm({
|
|
840
|
+
"src/lib/context.ts"() {
|
|
841
|
+
"use strict";
|
|
842
|
+
init_cli_utils();
|
|
843
|
+
init_output();
|
|
844
|
+
init_realm();
|
|
845
|
+
init_errors();
|
|
846
|
+
init_org_auth();
|
|
847
|
+
init_pinned_fetch();
|
|
848
|
+
}
|
|
849
|
+
});
|
|
850
|
+
|
|
851
|
+
// src/lib/concierge-api.ts
|
|
852
|
+
function baseUrl(ctx) {
|
|
853
|
+
return `${ctx.envConfig.apiUrl}/api/v2/concierge`;
|
|
854
|
+
}
|
|
855
|
+
function authHeaders(ctx) {
|
|
856
|
+
return {
|
|
857
|
+
Authorization: `Bearer ${ctx.accessToken}`,
|
|
858
|
+
"Content-Type": "application/json"
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
function toCamelChat(raw) {
|
|
862
|
+
return {
|
|
863
|
+
id: raw.id,
|
|
864
|
+
uuid: raw.uuid,
|
|
865
|
+
chatType: raw.chat_type,
|
|
866
|
+
title: raw.title,
|
|
867
|
+
threadId: raw.thread_id,
|
|
868
|
+
createdAt: raw.created_at,
|
|
869
|
+
updatedAt: raw.updated_at
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
function toCamelMessage(raw) {
|
|
873
|
+
return {
|
|
874
|
+
id: raw.id,
|
|
875
|
+
role: raw.role,
|
|
876
|
+
content: raw.content ?? "",
|
|
877
|
+
createdAt: raw.createdAt ?? raw.created_at ?? null,
|
|
878
|
+
toolCalls: raw.toolCalls ?? raw.tool_calls ?? null,
|
|
879
|
+
toolCallId: raw.toolCallId ?? raw.tool_call_id ?? null
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
async function listConciergeChats(ctx) {
|
|
883
|
+
const res = await fetch(`${baseUrl(ctx)}/chats`, { headers: authHeaders(ctx) });
|
|
884
|
+
if (!res.ok) throw new Error(`Failed to list concierge chats: ${res.status}`);
|
|
885
|
+
const data = await res.json();
|
|
886
|
+
return data.map(toCamelChat);
|
|
887
|
+
}
|
|
888
|
+
async function createConciergeChat(ctx, title) {
|
|
889
|
+
const res = await fetch(`${baseUrl(ctx)}/chats`, {
|
|
890
|
+
method: "POST",
|
|
891
|
+
headers: authHeaders(ctx),
|
|
892
|
+
body: JSON.stringify({ title: title ?? null })
|
|
893
|
+
});
|
|
894
|
+
if (!res.ok) throw new Error(`Failed to create concierge chat: ${res.status}`);
|
|
895
|
+
return toCamelChat(await res.json());
|
|
896
|
+
}
|
|
897
|
+
async function getConciergeChat(ctx, chatId) {
|
|
898
|
+
const res = await fetch(`${baseUrl(ctx)}/chats/${chatId}`, { headers: authHeaders(ctx) });
|
|
899
|
+
if (!res.ok) throw new Error(`Failed to get concierge chat ${chatId}: ${res.status}`);
|
|
900
|
+
return toCamelChat(await res.json());
|
|
901
|
+
}
|
|
902
|
+
async function originateConciergeCall(ctx, kind = "concierge_setup") {
|
|
903
|
+
const res = await fetch(`${baseUrl(ctx)}/calls`, {
|
|
904
|
+
method: "POST",
|
|
905
|
+
headers: authHeaders(ctx),
|
|
906
|
+
body: JSON.stringify({ kind })
|
|
907
|
+
});
|
|
908
|
+
if (res.status === 412) {
|
|
909
|
+
throw new Error("Your account phone is not verified. Run `gr account verify-phone` first.");
|
|
910
|
+
}
|
|
911
|
+
if (!res.ok) {
|
|
912
|
+
const detail = await res.text();
|
|
913
|
+
throw new Error(`Failed to start concierge call: ${res.status} ${detail}`);
|
|
914
|
+
}
|
|
915
|
+
return await res.json();
|
|
916
|
+
}
|
|
917
|
+
async function originateConciergeSms(ctx, kind = "concierge_setup") {
|
|
918
|
+
const res = await fetch(`${baseUrl(ctx)}/sms`, {
|
|
919
|
+
method: "POST",
|
|
920
|
+
headers: authHeaders(ctx),
|
|
921
|
+
body: JSON.stringify({ kind })
|
|
922
|
+
});
|
|
923
|
+
if (res.status === 412) {
|
|
924
|
+
throw new Error("Your account phone is not verified. Run `gr account verify-phone` first.");
|
|
925
|
+
}
|
|
926
|
+
if (!res.ok) {
|
|
927
|
+
const detail = await res.text();
|
|
928
|
+
throw new Error(`Failed to start concierge SMS: ${res.status} ${detail}`);
|
|
929
|
+
}
|
|
930
|
+
return await res.json();
|
|
931
|
+
}
|
|
932
|
+
async function startAccountPhoneVerify(ctx, phone) {
|
|
933
|
+
const res = await fetch(`${ctx.envConfig.apiUrl}/api/v2/account/phone/verify/start`, {
|
|
934
|
+
method: "POST",
|
|
935
|
+
headers: authHeaders(ctx),
|
|
936
|
+
body: JSON.stringify({ phone })
|
|
937
|
+
});
|
|
938
|
+
if (!res.ok) {
|
|
939
|
+
const detail = await res.text();
|
|
940
|
+
throw new Error(`Failed to send verification code: ${res.status} ${detail}`);
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
async function confirmAccountPhoneVerify(ctx, code) {
|
|
944
|
+
const res = await fetch(`${ctx.envConfig.apiUrl}/api/v2/account/phone/verify/confirm`, {
|
|
945
|
+
method: "POST",
|
|
946
|
+
headers: authHeaders(ctx),
|
|
947
|
+
body: JSON.stringify({ code })
|
|
948
|
+
});
|
|
949
|
+
if (!res.ok) {
|
|
950
|
+
const detail = await res.text();
|
|
951
|
+
throw new Error(`Failed to confirm verification code: ${res.status} ${detail}`);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
async function getConciergeChatMessages(ctx, chatId) {
|
|
955
|
+
const res = await fetch(`${baseUrl(ctx)}/chats/${chatId}/messages`, {
|
|
956
|
+
headers: authHeaders(ctx)
|
|
957
|
+
});
|
|
958
|
+
if (!res.ok)
|
|
959
|
+
throw new Error(`Failed to get concierge messages for chat ${chatId}: ${res.status}`);
|
|
960
|
+
const data = await res.json();
|
|
961
|
+
return data.map(toCamelMessage);
|
|
962
|
+
}
|
|
963
|
+
var init_concierge_api = __esm({
|
|
964
|
+
"src/lib/concierge-api.ts"() {
|
|
965
|
+
"use strict";
|
|
966
|
+
}
|
|
967
|
+
});
|
|
968
|
+
|
|
969
|
+
// src/commands/account-verify-phone.ts
|
|
970
|
+
var account_verify_phone_exports = {};
|
|
971
|
+
__export(account_verify_phone_exports, {
|
|
972
|
+
accountVerifyPhoneCommand: () => accountVerifyPhoneCommand
|
|
973
|
+
});
|
|
974
|
+
import * as readline6 from "readline/promises";
|
|
975
|
+
import { stdin as input, stdout as output } from "process";
|
|
976
|
+
async function accountVerifyPhoneCommand(hctx) {
|
|
977
|
+
const ctx = requireAuth(hctx.ctx);
|
|
978
|
+
const phoneArg = typeof hctx.args.phone === "string" ? hctx.args.phone : void 0;
|
|
979
|
+
process.stderr.write(`${TCPA_NOTICE}
|
|
980
|
+
`);
|
|
981
|
+
const phone = phoneArg ?? await promptLine("Phone number (E.164, e.g. +15551234567): ");
|
|
982
|
+
if (!phone.trim()) {
|
|
983
|
+
throw new Error("Phone number is required");
|
|
984
|
+
}
|
|
985
|
+
await startAccountPhoneVerify(ctx, phone.trim());
|
|
986
|
+
process.stderr.write("Verification code sent via SMS.\n");
|
|
987
|
+
const code = await promptLine("Enter 6-digit code: ");
|
|
988
|
+
await confirmAccountPhoneVerify(ctx, code.trim());
|
|
989
|
+
process.stderr.write(
|
|
990
|
+
"Phone verified. You can use `gr concierge chat --call` or `gr concierge chat --sms`.\n"
|
|
991
|
+
);
|
|
992
|
+
}
|
|
993
|
+
async function promptLine(question) {
|
|
994
|
+
const rl = readline6.createInterface({ input, output });
|
|
995
|
+
try {
|
|
996
|
+
return await rl.question(question) ?? "";
|
|
997
|
+
} finally {
|
|
998
|
+
rl.close();
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
var TCPA_NOTICE;
|
|
1002
|
+
var init_account_verify_phone = __esm({
|
|
1003
|
+
"src/commands/account-verify-phone.ts"() {
|
|
1004
|
+
"use strict";
|
|
1005
|
+
init_concierge_api();
|
|
1006
|
+
init_context();
|
|
1007
|
+
TCPA_NOTICE = "By verifying you agree to receive calls and SMS from Gravity Rail at this number.";
|
|
1008
|
+
}
|
|
1009
|
+
});
|
|
11
1010
|
|
|
1011
|
+
// src/commands/auth-browser.ts
|
|
1012
|
+
init_output();
|
|
1013
|
+
import { execFile } from "child_process";
|
|
1014
|
+
async function authBrowserCommand(ctx, args) {
|
|
1015
|
+
const noOpen = Boolean(args["no-open"]);
|
|
1016
|
+
const account = await ctx.apiClient.getUserAccount();
|
|
1017
|
+
if (!account.email) {
|
|
1018
|
+
process.stderr.write("Error: No email associated with current account\n");
|
|
1019
|
+
process.exit(1);
|
|
1020
|
+
}
|
|
1021
|
+
const response = await ctx.apiClient.requestMagicLink(account.email);
|
|
1022
|
+
if (!response.success || !response.magicLinkUrl) {
|
|
1023
|
+
process.stderr.write("Error: Failed to create magic link\n");
|
|
1024
|
+
process.exit(1);
|
|
1025
|
+
}
|
|
1026
|
+
const url = response.magicLinkUrl;
|
|
1027
|
+
if (noOpen) {
|
|
1028
|
+
process.stdout.write(url + "\n");
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
stderr2(`Opening browser for ${account.email} (${ctx.env})...`);
|
|
1032
|
+
openUrl(url);
|
|
1033
|
+
stderr2("Browser opened. Link expires in 15 minutes.");
|
|
1034
|
+
}
|
|
1035
|
+
function openUrl(url) {
|
|
1036
|
+
const platform = process.platform;
|
|
1037
|
+
let command;
|
|
1038
|
+
let args;
|
|
1039
|
+
if (platform === "darwin") {
|
|
1040
|
+
command = "open";
|
|
1041
|
+
args = [url];
|
|
1042
|
+
} else if (platform === "win32") {
|
|
1043
|
+
command = "cmd.exe";
|
|
1044
|
+
args = ["/c", "start", "", url];
|
|
1045
|
+
} else {
|
|
1046
|
+
command = "xdg-open";
|
|
1047
|
+
args = [url];
|
|
1048
|
+
}
|
|
1049
|
+
execFile(command, args, (err) => {
|
|
1050
|
+
if (err) {
|
|
1051
|
+
process.stderr.write("Warning: Could not open browser automatically\n");
|
|
1052
|
+
}
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// src/commands/manage.ts
|
|
1057
|
+
init_cli_utils();
|
|
1058
|
+
init_errors();
|
|
1059
|
+
init_context();
|
|
1060
|
+
|
|
1061
|
+
// src/lib/cli-client-context.ts
|
|
1062
|
+
import path from "path";
|
|
12
1063
|
function buildCliConciergeAppContext() {
|
|
13
|
-
const shell = process.env.SHELL ?
|
|
1064
|
+
const shell = process.env.SHELL ? path.basename(process.env.SHELL) : void 0;
|
|
14
1065
|
return {
|
|
15
1066
|
clientSurface: "cli",
|
|
16
1067
|
channel: "text",
|
|
17
1068
|
cliContext: {
|
|
18
1069
|
shell,
|
|
19
1070
|
os: process.platform,
|
|
20
|
-
cwdBasename:
|
|
1071
|
+
cwdBasename: path.basename(process.cwd())
|
|
21
1072
|
}
|
|
22
1073
|
};
|
|
23
1074
|
}
|
|
1075
|
+
|
|
1076
|
+
// src/lib/chat-session.ts
|
|
1077
|
+
init_cli_utils();
|
|
1078
|
+
|
|
1079
|
+
// src/lib/concierge-agui-loop.ts
|
|
1080
|
+
init_agui();
|
|
1081
|
+
|
|
1082
|
+
// src/lib/cli-hitl.ts
|
|
1083
|
+
import readline3 from "readline";
|
|
1084
|
+
|
|
1085
|
+
// src/lib/local-files.ts
|
|
1086
|
+
import fs from "fs";
|
|
1087
|
+
import path2 from "path";
|
|
24
1088
|
var DEFAULT_MAX_READ_BYTES = 5e4;
|
|
25
1089
|
var ABSOLUTE_MAX_READ_BYTES = 1e6;
|
|
26
1090
|
function resolveSafePath(root, relativePath) {
|
|
27
|
-
const rootResolved =
|
|
28
|
-
const target =
|
|
29
|
-
if (target !== rootResolved && !target.startsWith(rootResolved +
|
|
1091
|
+
const rootResolved = path2.resolve(root);
|
|
1092
|
+
const target = path2.resolve(rootResolved, relativePath || ".");
|
|
1093
|
+
if (target !== rootResolved && !target.startsWith(rootResolved + path2.sep)) {
|
|
30
1094
|
return null;
|
|
31
1095
|
}
|
|
32
1096
|
let cursor = rootResolved;
|
|
33
|
-
const rest =
|
|
1097
|
+
const rest = path2.relative(rootResolved, target);
|
|
34
1098
|
if (rest && rest !== ".") {
|
|
35
|
-
for (const segment of rest.split(
|
|
36
|
-
cursor =
|
|
1099
|
+
for (const segment of rest.split(path2.sep)) {
|
|
1100
|
+
cursor = path2.join(cursor, segment);
|
|
37
1101
|
try {
|
|
38
1102
|
const lstat = fs.lstatSync(cursor);
|
|
39
1103
|
if (lstat.isSymbolicLink()) {
|
|
@@ -94,7 +1158,7 @@ function executeCliLocalFileAction(root, interrupt, opts) {
|
|
|
94
1158
|
if (interrupt.content === void 0) {
|
|
95
1159
|
return { ok: false, error: "Missing content for write_file" };
|
|
96
1160
|
}
|
|
97
|
-
fs.mkdirSync(
|
|
1161
|
+
fs.mkdirSync(path2.dirname(safe), { recursive: true });
|
|
98
1162
|
fs.writeFileSync(safe, interrupt.content, "utf8");
|
|
99
1163
|
return { ok: true, content: "written" };
|
|
100
1164
|
}
|
|
@@ -120,6 +1184,12 @@ function isCliLocalFileInterrupt(value) {
|
|
|
120
1184
|
const v = value;
|
|
121
1185
|
return v.type === "cli_local_file" && typeof v.action === "string" && typeof v.path === "string";
|
|
122
1186
|
}
|
|
1187
|
+
|
|
1188
|
+
// src/lib/trusted-dirs.ts
|
|
1189
|
+
import fs2 from "fs";
|
|
1190
|
+
import os from "os";
|
|
1191
|
+
import path3 from "path";
|
|
1192
|
+
import readline2 from "readline";
|
|
123
1193
|
var STORE_VERSION = 1;
|
|
124
1194
|
function storePath() {
|
|
125
1195
|
const base = process.env.XDG_CONFIG_HOME ?? (process.platform === "win32" ? path3.join(process.env.APPDATA ?? os.homedir(), "gravityrail") : path3.join(os.homedir(), ".config", "gravityrail"));
|
|
@@ -128,7 +1198,7 @@ function storePath() {
|
|
|
128
1198
|
function loadTrustedDirsStore() {
|
|
129
1199
|
const file = storePath();
|
|
130
1200
|
try {
|
|
131
|
-
const raw = JSON.parse(
|
|
1201
|
+
const raw = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
132
1202
|
if (raw?.version === STORE_VERSION && Array.isArray(raw.always)) {
|
|
133
1203
|
return { version: STORE_VERSION, always: raw.always.map((p) => path3.resolve(p)) };
|
|
134
1204
|
}
|
|
@@ -139,8 +1209,8 @@ function loadTrustedDirsStore() {
|
|
|
139
1209
|
function saveTrustedDirsStore(store) {
|
|
140
1210
|
const file = storePath();
|
|
141
1211
|
const dir = path3.dirname(file);
|
|
142
|
-
|
|
143
|
-
|
|
1212
|
+
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1213
|
+
fs2.writeFileSync(file, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
144
1214
|
}
|
|
145
1215
|
function isDirectoryTrusted(dirPath, opts) {
|
|
146
1216
|
if (opts?.trustCwdFlag) return true;
|
|
@@ -149,8 +1219,8 @@ function isDirectoryTrusted(dirPath, opts) {
|
|
|
149
1219
|
return store.always.includes(resolved);
|
|
150
1220
|
}
|
|
151
1221
|
async function promptTrustInteractive(dirPath) {
|
|
152
|
-
const rl =
|
|
153
|
-
const question = (q) => new Promise((
|
|
1222
|
+
const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
|
|
1223
|
+
const question = (q) => new Promise((resolve2) => rl.question(q, resolve2));
|
|
154
1224
|
process.stderr.write(
|
|
155
1225
|
`
|
|
156
1226
|
Gravity Rail may read files in:
|
|
@@ -270,9 +1340,9 @@ ${prompt}
|
|
|
270
1340
|
resumeInterruptId: typeof payload.interrupt_id === "string" ? payload.interrupt_id : void 0
|
|
271
1341
|
};
|
|
272
1342
|
}
|
|
273
|
-
const rl =
|
|
274
|
-
const answer = await new Promise((
|
|
275
|
-
rl.question("Your choice (value): ",
|
|
1343
|
+
const rl = readline3.createInterface({ input: process.stdin, output: process.stderr });
|
|
1344
|
+
const answer = await new Promise((resolve2) => {
|
|
1345
|
+
rl.question("Your choice (value): ", resolve2);
|
|
276
1346
|
});
|
|
277
1347
|
rl.close();
|
|
278
1348
|
return {
|
|
@@ -330,6 +1400,13 @@ async function runConciergeAguiLoop(client, initialMessages, onEvent, opts) {
|
|
|
330
1400
|
}
|
|
331
1401
|
return { streamText, hasToolCalls };
|
|
332
1402
|
}
|
|
1403
|
+
|
|
1404
|
+
// src/lib/chat-session.ts
|
|
1405
|
+
init_errors();
|
|
1406
|
+
import * as readline5 from "readline";
|
|
1407
|
+
|
|
1408
|
+
// src/lib/realtime-client.ts
|
|
1409
|
+
import { EventEmitter } from "events";
|
|
333
1410
|
var RealtimeClient = class extends EventEmitter {
|
|
334
1411
|
ws = null;
|
|
335
1412
|
connected = false;
|
|
@@ -349,7 +1426,7 @@ var RealtimeClient = class extends EventEmitter {
|
|
|
349
1426
|
new Error("WebSocket not available. Node.js 22+ is required for realtime mode.")
|
|
350
1427
|
);
|
|
351
1428
|
}
|
|
352
|
-
return new Promise((
|
|
1429
|
+
return new Promise((resolve2, reject) => {
|
|
353
1430
|
const params = new URLSearchParams();
|
|
354
1431
|
if (this.options.vadMode) {
|
|
355
1432
|
params.append("vad", this.options.vadMode);
|
|
@@ -404,7 +1481,7 @@ var RealtimeClient = class extends EventEmitter {
|
|
|
404
1481
|
}, 1e4);
|
|
405
1482
|
this.once("connected", () => {
|
|
406
1483
|
clearTimeout(timeout);
|
|
407
|
-
|
|
1484
|
+
resolve2();
|
|
408
1485
|
});
|
|
409
1486
|
this.once("error", () => {
|
|
410
1487
|
clearTimeout(timeout);
|
|
@@ -578,6 +1655,9 @@ var RealtimeClient = class extends EventEmitter {
|
|
|
578
1655
|
}
|
|
579
1656
|
}
|
|
580
1657
|
};
|
|
1658
|
+
|
|
1659
|
+
// src/lib/chat-tui.ts
|
|
1660
|
+
import { createWriteStream } from "fs";
|
|
581
1661
|
function isToolCall(item) {
|
|
582
1662
|
return "kind" in item && item.kind === "tool_call";
|
|
583
1663
|
}
|
|
@@ -1352,11 +2432,16 @@ function buildDebugSummary(_direction, msg) {
|
|
|
1352
2432
|
}
|
|
1353
2433
|
return type;
|
|
1354
2434
|
}
|
|
2435
|
+
|
|
2436
|
+
// src/lib/audio.ts
|
|
2437
|
+
import { spawn, execSync as execSync2, spawnSync } from "child_process";
|
|
2438
|
+
import { EventEmitter as EventEmitter2 } from "events";
|
|
2439
|
+
import * as readline4 from "readline";
|
|
1355
2440
|
var SAMPLE_RATE = 24e3;
|
|
1356
2441
|
var MIC_CHUNK_BYTES = 4800;
|
|
1357
2442
|
function checkSoxAvailable() {
|
|
1358
2443
|
try {
|
|
1359
|
-
|
|
2444
|
+
execSync2("which sox", { stdio: "pipe" });
|
|
1360
2445
|
return true;
|
|
1361
2446
|
} catch {
|
|
1362
2447
|
return false;
|
|
@@ -1369,16 +2454,16 @@ async function listOutputDevices() {
|
|
|
1369
2454
|
return listDevices("play");
|
|
1370
2455
|
}
|
|
1371
2456
|
function listDevices(command) {
|
|
1372
|
-
return new Promise((
|
|
2457
|
+
return new Promise((resolve2) => {
|
|
1373
2458
|
try {
|
|
1374
2459
|
const proc = spawnSync(command, ["--list-devices"], {
|
|
1375
2460
|
encoding: "utf-8",
|
|
1376
2461
|
stdio: ["ignore", "pipe", "pipe"],
|
|
1377
2462
|
timeout: 5e3
|
|
1378
2463
|
});
|
|
1379
|
-
const
|
|
2464
|
+
const output2 = (proc.stdout ?? "") + (proc.stderr ?? "");
|
|
1380
2465
|
const devices = [];
|
|
1381
|
-
const lines =
|
|
2466
|
+
const lines = output2.split("\n");
|
|
1382
2467
|
for (const line of lines) {
|
|
1383
2468
|
const match = line.match(/(?:Input|Output)\s+Device\s+(\d+)\s*:\s*(.+)/i);
|
|
1384
2469
|
if (match && match[1] && match[2]) {
|
|
@@ -1393,9 +2478,9 @@ function listDevices(command) {
|
|
|
1393
2478
|
if (devices.length === 0) {
|
|
1394
2479
|
devices.push({ id: "default", name: "System Default", isDefault: true });
|
|
1395
2480
|
}
|
|
1396
|
-
|
|
2481
|
+
resolve2(devices);
|
|
1397
2482
|
} catch {
|
|
1398
|
-
|
|
2483
|
+
resolve2([{ id: "default", name: "System Default", isDefault: true }]);
|
|
1399
2484
|
}
|
|
1400
2485
|
});
|
|
1401
2486
|
}
|
|
@@ -1403,7 +2488,7 @@ async function selectDevice(devices, label) {
|
|
|
1403
2488
|
if (devices.length <= 1) {
|
|
1404
2489
|
return devices[0];
|
|
1405
2490
|
}
|
|
1406
|
-
const rl =
|
|
2491
|
+
const rl = readline4.createInterface({
|
|
1407
2492
|
input: process.stdin,
|
|
1408
2493
|
output: process.stderr,
|
|
1409
2494
|
terminal: true
|
|
@@ -1417,14 +2502,14 @@ async function selectDevice(devices, label) {
|
|
|
1417
2502
|
process.stderr.write(` ${i + 1}. ${dev.name}${marker}
|
|
1418
2503
|
`);
|
|
1419
2504
|
}
|
|
1420
|
-
return new Promise((
|
|
2505
|
+
return new Promise((resolve2) => {
|
|
1421
2506
|
const defaultIdx = devices.findIndex((d) => d.isDefault);
|
|
1422
2507
|
const defaultNum = defaultIdx >= 0 ? defaultIdx + 1 : 1;
|
|
1423
2508
|
rl.question(` Select [${defaultNum}]: `, (answer) => {
|
|
1424
2509
|
rl.close();
|
|
1425
2510
|
const num = answer.trim() ? parseInt(answer.trim(), 10) : defaultNum;
|
|
1426
2511
|
const idx = Math.max(0, Math.min(devices.length - 1, num - 1));
|
|
1427
|
-
|
|
2512
|
+
resolve2(devices[idx]);
|
|
1428
2513
|
});
|
|
1429
2514
|
});
|
|
1430
2515
|
}
|
|
@@ -1495,7 +2580,7 @@ var MicCapture = class {
|
|
|
1495
2580
|
this.buffer = Buffer.alloc(0);
|
|
1496
2581
|
}
|
|
1497
2582
|
};
|
|
1498
|
-
var AudioPlayer = class extends
|
|
2583
|
+
var AudioPlayer = class extends EventEmitter2 {
|
|
1499
2584
|
process = null;
|
|
1500
2585
|
deviceId;
|
|
1501
2586
|
// When true, we own the SoX process's stdin and are actively streaming to it.
|
|
@@ -1722,6 +2807,7 @@ async function singleTurnChatSession(session, message, debug, autoApprove = fals
|
|
|
1722
2807
|
{
|
|
1723
2808
|
autoApprove,
|
|
1724
2809
|
allowWrite: allowWrite || !!session.allowWrite,
|
|
2810
|
+
debug,
|
|
1725
2811
|
trustCwd: session.trustCwd,
|
|
1726
2812
|
trustDir: session.trustDir,
|
|
1727
2813
|
nonInteractive: true
|
|
@@ -1797,11 +2883,11 @@ async function interactiveChatSession(session, debug, autoApprove, allowWrite) {
|
|
|
1797
2883
|
conversationMessages.push({ role: msg.role, content: text });
|
|
1798
2884
|
}
|
|
1799
2885
|
}
|
|
1800
|
-
const rl =
|
|
1801
|
-
return new Promise((
|
|
2886
|
+
const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
|
|
2887
|
+
return new Promise((resolve2) => {
|
|
1802
2888
|
const prompt = () => {
|
|
1803
|
-
rl.question("\x1B[34mYou:\x1B[0m ", async (
|
|
1804
|
-
const trimmed =
|
|
2889
|
+
rl.question("\x1B[34mYou:\x1B[0m ", async (input2) => {
|
|
2890
|
+
const trimmed = input2.trim();
|
|
1805
2891
|
if (!trimmed) {
|
|
1806
2892
|
prompt();
|
|
1807
2893
|
return;
|
|
@@ -1856,7 +2942,7 @@ async function interactiveChatSession(session, debug, autoApprove, allowWrite) {
|
|
|
1856
2942
|
};
|
|
1857
2943
|
rl.on("close", () => {
|
|
1858
2944
|
console.log("\nGoodbye!");
|
|
1859
|
-
|
|
2945
|
+
resolve2();
|
|
1860
2946
|
});
|
|
1861
2947
|
prompt();
|
|
1862
2948
|
});
|
|
@@ -1953,8 +3039,8 @@ async function realtimeChatSession(session, vad, debug) {
|
|
|
1953
3039
|
});
|
|
1954
3040
|
tui.loadHistory(history);
|
|
1955
3041
|
tui.start();
|
|
1956
|
-
await new Promise((
|
|
1957
|
-
client.once("closed",
|
|
3042
|
+
await new Promise((resolve2) => {
|
|
3043
|
+
client.once("closed", resolve2);
|
|
1958
3044
|
});
|
|
1959
3045
|
}
|
|
1960
3046
|
async function voiceChatSession(session, vad, args, debug) {
|
|
@@ -2071,8 +3157,8 @@ Connecting to ${session.label.toLowerCase()}...
|
|
|
2071
3157
|
tui.loadHistory(history);
|
|
2072
3158
|
tui.start();
|
|
2073
3159
|
mic.start(inputDeviceId);
|
|
2074
|
-
await new Promise((
|
|
2075
|
-
client.once("closed",
|
|
3160
|
+
await new Promise((resolve2) => {
|
|
3161
|
+
client.once("closed", resolve2);
|
|
2076
3162
|
});
|
|
2077
3163
|
}
|
|
2078
3164
|
async function listAudioDevices() {
|
|
@@ -2095,7 +3181,148 @@ async function listAudioDevices() {
|
|
|
2095
3181
|
}
|
|
2096
3182
|
}
|
|
2097
3183
|
|
|
3184
|
+
// src/commands/manage.ts
|
|
3185
|
+
async function manageCommand(hctx) {
|
|
3186
|
+
const ctx = requireAuth(hctx.ctx);
|
|
3187
|
+
const { workspace, args } = hctx;
|
|
3188
|
+
const asMember = typeof args["as-member"] === "string" ? args["as-member"] : void 0;
|
|
3189
|
+
const chatIdArg = typeof args["chat-id"] === "string" ? args["chat-id"] : void 0;
|
|
3190
|
+
const message = typeof args.message === "string" ? args.message : void 0;
|
|
3191
|
+
const chat = await getOrCreateChat(ctx, workspace, chatIdArg, asMember, "manager", !!message);
|
|
3192
|
+
const session = buildWorkspaceSession(ctx, workspace, chat, "Workspace Manager", {
|
|
3193
|
+
appContext: buildCliConciergeAppContext(),
|
|
3194
|
+
allowWrite: true,
|
|
3195
|
+
trustCwd: !!args["trust-cwd"],
|
|
3196
|
+
trustDir: typeof args["trust-dir"] === "string" ? args["trust-dir"] : void 0
|
|
3197
|
+
});
|
|
3198
|
+
process.stderr.write(`Chat #${session.chatId}
|
|
3199
|
+
`);
|
|
3200
|
+
await runChatSession(session, args);
|
|
3201
|
+
process.stderr.write(`Chat #${session.chatId}
|
|
3202
|
+
`);
|
|
3203
|
+
}
|
|
3204
|
+
async function chatCommand(hctx) {
|
|
3205
|
+
const ctx = requireAuth(hctx.ctx);
|
|
3206
|
+
const { workspace, args } = hctx;
|
|
3207
|
+
const chatIdArg = typeof args["chat-id"] === "string" ? args["chat-id"] : void 0;
|
|
3208
|
+
const workflowId = typeof args.workflow === "string" ? args.workflow : void 0;
|
|
3209
|
+
const memberId = typeof args.member === "string" ? args.member : void 0;
|
|
3210
|
+
let chat;
|
|
3211
|
+
if (chatIdArg) {
|
|
3212
|
+
const id = Number(chatIdArg);
|
|
3213
|
+
if (isNaN(id)) throw new CLIError("--chat-id must be a number");
|
|
3214
|
+
chat = await ctx.apiClient.getChat(workspace, id);
|
|
3215
|
+
} else if (workflowId) {
|
|
3216
|
+
chat = await createAssignmentChat(ctx.apiClient, workspace, Number(workflowId), memberId);
|
|
3217
|
+
} else if (memberId) {
|
|
3218
|
+
chat = await createMemberChat(ctx.apiClient, workspace, memberId);
|
|
3219
|
+
} else {
|
|
3220
|
+
throw new CLIError(
|
|
3221
|
+
'Specify --workflow <id>, --member <id>, or --chat-id <id>.\nUse "gr manage" for workspace manager chats.'
|
|
3222
|
+
);
|
|
3223
|
+
}
|
|
3224
|
+
const session = buildWorkspaceSession(ctx, workspace, chat, "Chat");
|
|
3225
|
+
await runChatSession(session, args);
|
|
3226
|
+
}
|
|
3227
|
+
function buildWorkspaceSession(ctx, workspace, chat, label, extra) {
|
|
3228
|
+
const wsBase = ctx.envConfig.apiUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
|
|
3229
|
+
return {
|
|
3230
|
+
ctx,
|
|
3231
|
+
workspaceUuid: workspace,
|
|
3232
|
+
chatId: chat.id,
|
|
3233
|
+
chatUuid: chat.uuid,
|
|
3234
|
+
chatTitle: chat.title || chat.uuid,
|
|
3235
|
+
aguiEndpointUrl: `${ctx.envConfig.apiUrl}/api/v2/w/${workspace}/web-chat/${chat.id}/chat/agui`,
|
|
3236
|
+
wsUrl: `${wsBase}/api/v2/w/${workspace}/realtime/stream-ws`,
|
|
3237
|
+
loadMessages: () => ctx.apiClient.getChatMessages(workspace, chat.id, 50).catch(() => []),
|
|
3238
|
+
label,
|
|
3239
|
+
model: "manager",
|
|
3240
|
+
...extra
|
|
3241
|
+
};
|
|
3242
|
+
}
|
|
3243
|
+
async function getOrCreateChat(ctx, workspace, chatIdArg, asMemberId, chatType, skipSelection = false) {
|
|
3244
|
+
if (chatIdArg) {
|
|
3245
|
+
const chatId = Number(chatIdArg);
|
|
3246
|
+
if (isNaN(chatId)) throw new CLIError("--chat-id must be a number");
|
|
3247
|
+
return ctx.apiClient.getChat(workspace, chatId);
|
|
3248
|
+
}
|
|
3249
|
+
if (skipSelection) {
|
|
3250
|
+
return createManagerChat(ctx.apiClient, workspace, asMemberId);
|
|
3251
|
+
}
|
|
3252
|
+
const chatsResponse = await ctx.apiClient.getChats(workspace, {
|
|
3253
|
+
chatType,
|
|
3254
|
+
sortBy: "updatedAt",
|
|
3255
|
+
sortOrder: "desc",
|
|
3256
|
+
pageSize: 20
|
|
3257
|
+
});
|
|
3258
|
+
const chats = chatsResponse.items || [];
|
|
3259
|
+
const { item: selectedChat, isNew } = await selectFromList(chats, {
|
|
3260
|
+
title: "Select a chat or create a new one",
|
|
3261
|
+
labelFn: (chat) => chat.title || `Chat ${chat.uuid.slice(0, 8)}...`,
|
|
3262
|
+
detailFn: (chat) => {
|
|
3263
|
+
const time = formatTimestamp(chat.updatedAt);
|
|
3264
|
+
const msgCount = chat.messageCount || 0;
|
|
3265
|
+
return `${msgCount} msg${msgCount !== 1 ? "s" : ""} - ${time}`;
|
|
3266
|
+
},
|
|
3267
|
+
emptyMessage: "No existing chats.",
|
|
3268
|
+
allowNew: true,
|
|
3269
|
+
newLabel: "+ Start new chat"
|
|
3270
|
+
});
|
|
3271
|
+
if (!selectedChat && !isNew) {
|
|
3272
|
+
throw new CLIError("No chat selected.");
|
|
3273
|
+
}
|
|
3274
|
+
if (isNew) {
|
|
3275
|
+
return createManagerChat(ctx.apiClient, workspace, asMemberId);
|
|
3276
|
+
}
|
|
3277
|
+
return selectedChat;
|
|
3278
|
+
}
|
|
3279
|
+
async function createManagerChat(apiClient, workspace, asMemberId) {
|
|
3280
|
+
const data = {
|
|
3281
|
+
chatType: "manager",
|
|
3282
|
+
channel: "web-chat"
|
|
3283
|
+
};
|
|
3284
|
+
if (asMemberId) {
|
|
3285
|
+
data.memberId = Number(asMemberId);
|
|
3286
|
+
}
|
|
3287
|
+
const chat = await apiClient.createChat(workspace, data);
|
|
3288
|
+
process.stderr.write(`Created chat #${chat.id}
|
|
3289
|
+
`);
|
|
3290
|
+
return chat;
|
|
3291
|
+
}
|
|
3292
|
+
async function createAssignmentChat(apiClient, workspace, workflowId, asMemberId) {
|
|
3293
|
+
if (isNaN(workflowId)) throw new CLIError("--workflow must be a number");
|
|
3294
|
+
const data = {
|
|
3295
|
+
chatType: "assignment",
|
|
3296
|
+
channel: "web-chat",
|
|
3297
|
+
workflowId
|
|
3298
|
+
};
|
|
3299
|
+
if (asMemberId) {
|
|
3300
|
+
data.memberId = Number(asMemberId);
|
|
3301
|
+
}
|
|
3302
|
+
const chat = await apiClient.createChat(workspace, data);
|
|
3303
|
+
process.stderr.write(`Created assignment chat #${chat.id} (workflow ${workflowId})
|
|
3304
|
+
`);
|
|
3305
|
+
return chat;
|
|
3306
|
+
}
|
|
3307
|
+
async function createMemberChat(apiClient, workspace, memberId) {
|
|
3308
|
+
const id = Number(memberId);
|
|
3309
|
+
if (isNaN(id)) throw new CLIError("--member must be a number");
|
|
3310
|
+
const data = {
|
|
3311
|
+
chatType: "member",
|
|
3312
|
+
channel: "web-chat",
|
|
3313
|
+
memberId: id
|
|
3314
|
+
};
|
|
3315
|
+
const chat = await apiClient.createChat(workspace, data);
|
|
3316
|
+
process.stderr.write(`Created DM chat #${chat.id} (member ${memberId})
|
|
3317
|
+
`);
|
|
3318
|
+
return chat;
|
|
3319
|
+
}
|
|
3320
|
+
|
|
2098
3321
|
// src/commands/concierge.ts
|
|
3322
|
+
init_cli_utils();
|
|
3323
|
+
init_concierge_api();
|
|
3324
|
+
init_context();
|
|
3325
|
+
init_errors();
|
|
2099
3326
|
async function conciergeOutboundCommand(hctx) {
|
|
2100
3327
|
const ctx = requireAuth(hctx.ctx);
|
|
2101
3328
|
const { args } = hctx;
|
|
@@ -2194,6 +3421,436 @@ async function getOrCreateConciergeChat(ctx, chatIdArg, skipSelection) {
|
|
|
2194
3421
|
return selectedChat;
|
|
2195
3422
|
}
|
|
2196
3423
|
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
3424
|
+
// src/commands/dev.ts
|
|
3425
|
+
init_errors();
|
|
3426
|
+
init_output();
|
|
3427
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync3, readFileSync as readFileSync4 } from "fs";
|
|
3428
|
+
import { execSync as execSync3 } from "child_process";
|
|
3429
|
+
import { join as join4 } from "path";
|
|
3430
|
+
async function devImportLocalCommand(ctx, workspace) {
|
|
3431
|
+
stderr2(`Exporting workspace ${workspace}...`);
|
|
3432
|
+
const exportData = await ctx.apiClient.exportWorkspace(workspace);
|
|
3433
|
+
stderr2("Export successful!");
|
|
3434
|
+
stderr2(` Workflows: ${(exportData.workflows || []).length}`);
|
|
3435
|
+
stderr2(` Tasks: ${(exportData.tasks || []).length}`);
|
|
3436
|
+
stderr2(` Data Types: ${(exportData.dataTypes || []).length}`);
|
|
3437
|
+
stderr2(` Sites: ${(exportData.sites || []).length}`);
|
|
3438
|
+
const slug = workspace.slice(0, 8);
|
|
3439
|
+
const importCacheDir = join4(ctx.credentialsManager.getCacheDir(), "imports");
|
|
3440
|
+
if (!existsSync4(importCacheDir)) {
|
|
3441
|
+
mkdirSync3(importCacheDir, { recursive: true });
|
|
3442
|
+
}
|
|
3443
|
+
const filename = `ws-${slug}-${workspace}.json`;
|
|
3444
|
+
const filepath = join4(importCacheDir, filename);
|
|
3445
|
+
writeFileSync3(filepath, JSON.stringify(exportData, null, 2));
|
|
3446
|
+
stderr2(`Saved to: ${filepath}`);
|
|
3447
|
+
stderr2("Running dev:restore...");
|
|
3448
|
+
const rootDir = findProjectRoot();
|
|
3449
|
+
const relativePath = filepath.replace(rootDir + "/", "");
|
|
3450
|
+
try {
|
|
3451
|
+
const result = execSync3(`yarn dev:restore ${relativePath}`, {
|
|
3452
|
+
cwd: rootDir,
|
|
3453
|
+
stdio: "pipe",
|
|
3454
|
+
encoding: "utf-8"
|
|
3455
|
+
});
|
|
3456
|
+
process.stderr.write(result);
|
|
3457
|
+
const failed = result.includes("Failed/Skipped: ") && !result.includes("Failed/Skipped: 0") || result.includes("Successfully restored: 0");
|
|
3458
|
+
if (failed) throw new CLIError("Import failed");
|
|
3459
|
+
} catch (error) {
|
|
3460
|
+
if (error instanceof CLIError) throw error;
|
|
3461
|
+
const execError = error;
|
|
3462
|
+
if (execError.stdout) process.stderr.write(execError.stdout);
|
|
3463
|
+
if (execError.stderr) process.stderr.write(execError.stderr);
|
|
3464
|
+
throw new CLIError("dev:restore failed");
|
|
3465
|
+
}
|
|
3466
|
+
stderr2(`Import complete: ${workspace}`);
|
|
3467
|
+
}
|
|
3468
|
+
function findProjectRoot() {
|
|
3469
|
+
let dir = process.cwd();
|
|
3470
|
+
while (dir !== "/") {
|
|
3471
|
+
const pkgPath = join4(dir, "package.json");
|
|
3472
|
+
if (existsSync4(pkgPath)) {
|
|
3473
|
+
try {
|
|
3474
|
+
const pkg = JSON.parse(readFileSync4(pkgPath, "utf-8"));
|
|
3475
|
+
if (pkg.workspaces) return dir;
|
|
3476
|
+
} catch {
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
dir = join4(dir, "..");
|
|
3480
|
+
}
|
|
3481
|
+
return process.cwd();
|
|
3482
|
+
}
|
|
3483
|
+
|
|
3484
|
+
// src/commands/get-started.ts
|
|
3485
|
+
init_context();
|
|
3486
|
+
var WEB_SETUP = "https://app.gravityrail.com/setup";
|
|
3487
|
+
var WEB_GET_STARTED = "https://app.gravityrail.com/get-started";
|
|
3488
|
+
var HUMAN_GUIDE = `Gravity Rail \u2014 getting started
|
|
3489
|
+
|
|
3490
|
+
1. Authenticate: gr login --env prod
|
|
3491
|
+
Confirm: gr whoami
|
|
3492
|
+
|
|
3493
|
+
2. Recommended path \u2014 concierge (no workspace required):
|
|
3494
|
+
gr concierge chat --allow-write
|
|
3495
|
+
The concierge can create your org (auto-created on first login), list products,
|
|
3496
|
+
start Stripe checkout, and create your first workspace.
|
|
3497
|
+
|
|
3498
|
+
3. Verify workspaces: gr workspaces list
|
|
3499
|
+
|
|
3500
|
+
4. Web wizards (human-friendly): ${WEB_SETUP} and ${WEB_GET_STARTED}
|
|
3501
|
+
|
|
3502
|
+
Headless boundary: Stripe checkout needs a human in a browser \u2014 the concierge returns
|
|
3503
|
+
a checkout URL; an agent should hand that link to the user, not loop on payment.
|
|
3504
|
+
|
|
3505
|
+
Agent-readable steps: gr get-started --format llm
|
|
3506
|
+
`;
|
|
3507
|
+
var LLM_GUIDE = `# Gravity Rail CLI bootstrap (agent instructions)
|
|
3508
|
+
|
|
3509
|
+
## Preconditions
|
|
3510
|
+
- Install: \`npm install -g @gravity-rail/cli\` or \`npx @gravity-rail/cli\`
|
|
3511
|
+
- Authenticate: \`gr login --env prod\` (interactive OAuth). Verify with \`gr whoami\`.
|
|
3512
|
+
- **Monorepo dev:** from gravity-team with Docker dev up, use \`yarn gr <cmd> --realm superadmin\` (reads \`dev-realms.json\`; no browser). Rebuild: \`yarn workspace @gravity-rail/gr-cli build\`.
|
|
3513
|
+
|
|
3514
|
+
## Important facts
|
|
3515
|
+
- **Agents/CI:** do not leave stdin open on read-only commands; use \`GRAVITY_RAIL_API_KEY\` or redirect stdin from /dev/null. Commands only read stdin when \`--data\`/\`--file\` is passed.
|
|
3516
|
+
- Use \`--env dev|staging|prod\` (default prod) so credentials match the target; \`gr whoami\` shows the active env.
|
|
3517
|
+
- After login, an **organization is created automatically** \u2014 do not create a duplicate org unless \`gr organizations list\` is empty.
|
|
3518
|
+
- **Workspace is required** for workflow/agent/task commands (\`-w <workspace-uuid>\`).
|
|
3519
|
+
- **Concierge does not require \`-w\`** \u2014 use it for first-run setup.
|
|
3520
|
+
|
|
3521
|
+
## Recommended path (concierge-driven)
|
|
3522
|
+
\`\`\`bash
|
|
3523
|
+
gr concierge chat --allow-write --message "Help me set up my first Gravity Rail workspace"
|
|
3524
|
+
\`\`\`
|
|
3525
|
+
Flags for unattended turns:
|
|
3526
|
+
- \`--message\` / \`-m\`: single-turn (non-interactive)
|
|
3527
|
+
- \`--auto-approve\`: skip HITL tool approval where policy allows
|
|
3528
|
+
- \`--allow-write\`: required for create/update tools
|
|
3529
|
+
|
|
3530
|
+
The concierge can: list/create orgs, list org/workspace products, create Stripe checkout sessions, create workspace specs, and \`create_workspace\`.
|
|
3531
|
+
|
|
3532
|
+
## Manual CLI path (if not using concierge)
|
|
3533
|
+
1. \`gr whoami\` \u2014 confirm account
|
|
3534
|
+
2. \`gr organizations list\` \u2014 note org slug/UUID
|
|
3535
|
+
3. \`gr concierge chat\` OR web: ${WEB_GET_STARTED}
|
|
3536
|
+
4. Product purchase: concierge returns Stripe checkout URL \u2192 **human must complete in browser**
|
|
3537
|
+
5. \`gr workspaces list\` \u2014 pick workspace UUID for \`-w\`
|
|
3538
|
+
|
|
3539
|
+
## Support (account-level, no workspace)
|
|
3540
|
+
\`\`\`bash
|
|
3541
|
+
gr support create --type question --title "..." --description "..." --allow-write
|
|
3542
|
+
gr support list
|
|
3543
|
+
\`\`\`
|
|
3544
|
+
|
|
3545
|
+
## Web fallbacks
|
|
3546
|
+
- Concierge / setup: ${WEB_SETUP}
|
|
3547
|
+
- Wizard: ${WEB_GET_STARTED}
|
|
3548
|
+
|
|
3549
|
+
## Headless limits (do not retry forever)
|
|
3550
|
+
- Stripe payment / checkout: requires human browser
|
|
3551
|
+
- OAuth login: requires human browser
|
|
3552
|
+
- Directory trust for future local-file tools: use \`--trust-cwd\` only after user confirms files are safe
|
|
3553
|
+
|
|
3554
|
+
## Local project files (CLI only)
|
|
3555
|
+
After directory trust + \`--allow-write\`, the concierge can use \`read_local_file\`, \`list_local_dir\`, \`write_local_file\` (client-executed via HITL).
|
|
3556
|
+
|
|
3557
|
+
## Verify success
|
|
3558
|
+
\`gr workspaces list\` then \`gr workflows list -w <uuid>\` or \`gr agents list -w <uuid>\`
|
|
3559
|
+
|
|
3560
|
+
## Local phone / voice QA (monorepo dev)
|
|
3561
|
+
\`\`\`bash
|
|
3562
|
+
yarn dev:twilio-ngrok --start-ngrok --url gravityrail.ngrok.io dev.<you> -w acme-demo
|
|
3563
|
+
yarn gr phone setup
|
|
3564
|
+
yarn gr account verify-phone --phone +1\u2026 --allow-write --realm superadmin
|
|
3565
|
+
yarn gr concierge chat --call --allow-write --realm superadmin
|
|
3566
|
+
yarn gr phone outbound --realm acme-demo/acme-admin --workflow-id 1 --allow-write
|
|
3567
|
+
\`\`\`
|
|
3568
|
+
See \`.claude/skills/phone-voice-qa/SKILL.md\` and \`./scripts/e2e-voice-phone-matrix.sh\`.
|
|
3569
|
+
`;
|
|
3570
|
+
async function getStartedCommand(hctx) {
|
|
3571
|
+
const format = hctx.args.format ?? hctx.args.agent;
|
|
3572
|
+
const isLlm = format === "llm" || format === "agent" || hctx.args["format"] === "llm" || !!hctx.args.agent;
|
|
3573
|
+
if (!isLlm) {
|
|
3574
|
+
try {
|
|
3575
|
+
requireAuth(hctx.ctx);
|
|
3576
|
+
} catch {
|
|
3577
|
+
}
|
|
3578
|
+
process.stdout.write(HUMAN_GUIDE);
|
|
3579
|
+
return;
|
|
3580
|
+
}
|
|
3581
|
+
process.stdout.write(LLM_GUIDE);
|
|
3582
|
+
}
|
|
3583
|
+
|
|
3584
|
+
// src/commands/login.ts
|
|
3585
|
+
init_cli_utils();
|
|
3586
|
+
init_output();
|
|
3587
|
+
import { GravityRailClient as GravityRailClient2 } from "@gravity-rail/sdk";
|
|
3588
|
+
async function loginCommand(ctx) {
|
|
3589
|
+
const { accessToken } = await performOAuthFlow({
|
|
3590
|
+
env: ctx.env,
|
|
3591
|
+
envConfig: ctx.envConfig,
|
|
3592
|
+
credentialsManager: ctx.credentialsManager
|
|
3593
|
+
});
|
|
3594
|
+
const apiClient = new GravityRailClient2(accessToken, ctx.envConfig.apiUrl);
|
|
3595
|
+
const account = await apiClient.getUserAccount();
|
|
3596
|
+
stderr2(`Logged in as ${account.name} (${account.email})`);
|
|
3597
|
+
}
|
|
3598
|
+
async function logoutCommand(ctx) {
|
|
3599
|
+
ctx.credentialsManager.clear(ctx.env);
|
|
3600
|
+
stderr2(`Credentials cleared for ${ctx.env}`);
|
|
3601
|
+
}
|
|
3602
|
+
|
|
3603
|
+
// src/commands/organizations.ts
|
|
3604
|
+
init_output();
|
|
3605
|
+
var COLUMNS = [
|
|
3606
|
+
{ header: "NAME", getValue: (o) => o.organizationName || "", maxWidth: 35 },
|
|
3607
|
+
{ header: "SLUG", getValue: (o) => o.organizationSlug || "", maxWidth: 25 },
|
|
3608
|
+
{ header: "UUID", getValue: (o) => o.organizationUuid },
|
|
3609
|
+
{ header: "ROLE", getValue: (o) => o.role || "" }
|
|
3610
|
+
];
|
|
3611
|
+
async function organizationsListCommand(ctx) {
|
|
3612
|
+
const organizations = await ctx.apiClient.getMyOrganizations();
|
|
3613
|
+
outputData(organizations, ctx, COLUMNS);
|
|
3614
|
+
}
|
|
3615
|
+
|
|
3616
|
+
// src/commands/whoami.ts
|
|
3617
|
+
init_output();
|
|
3618
|
+
var COLUMNS2 = [
|
|
3619
|
+
{ header: "NAME", getValue: (a) => a.name || "", maxWidth: 30 },
|
|
3620
|
+
{ header: "EMAIL", getValue: (a) => a.email || "", maxWidth: 40 },
|
|
3621
|
+
{ header: "UUID", getValue: (a) => a.uuid },
|
|
3622
|
+
{ header: "ENV", getValue: () => "" },
|
|
3623
|
+
{ header: "SUPERUSER", getValue: (a) => a.isSuperuser ? "yes" : "no" }
|
|
3624
|
+
];
|
|
3625
|
+
async function whoamiCommand(ctx) {
|
|
3626
|
+
const account = await ctx.apiClient.getUserAccount();
|
|
3627
|
+
const columns = COLUMNS2.map(
|
|
3628
|
+
(col) => col.header === "ENV" ? { ...col, getValue: () => ctx.env } : col
|
|
3629
|
+
);
|
|
3630
|
+
outputSingle(account, ctx, columns);
|
|
3631
|
+
}
|
|
3632
|
+
|
|
3633
|
+
// src/register-special-commands.ts
|
|
3634
|
+
init_context();
|
|
3635
|
+
function registerSpecialCommands(registry) {
|
|
3636
|
+
registry.registerCommand({
|
|
3637
|
+
path: ["login"],
|
|
3638
|
+
description: "Authenticate via OAuth",
|
|
3639
|
+
requiresAuth: false,
|
|
3640
|
+
handler: async ({ ctx }) => {
|
|
3641
|
+
await loginCommand(ctx);
|
|
3642
|
+
}
|
|
3643
|
+
});
|
|
3644
|
+
registry.registerCommand({
|
|
3645
|
+
path: ["logout"],
|
|
3646
|
+
description: "Clear cached credentials",
|
|
3647
|
+
requiresAuth: false,
|
|
3648
|
+
handler: async ({ ctx }) => {
|
|
3649
|
+
await logoutCommand(ctx);
|
|
3650
|
+
}
|
|
3651
|
+
});
|
|
3652
|
+
registry.registerCommand({
|
|
3653
|
+
path: ["auth", "browser"],
|
|
3654
|
+
description: "Open an authenticated browser session using CLI credentials",
|
|
3655
|
+
args: [
|
|
3656
|
+
{ name: "no-open", description: "Print the URL instead of opening browser", isFlag: true }
|
|
3657
|
+
],
|
|
3658
|
+
handler: async ({ ctx, args }) => {
|
|
3659
|
+
await authBrowserCommand(requireAuth(ctx), args);
|
|
3660
|
+
}
|
|
3661
|
+
});
|
|
3662
|
+
registry.registerCommand({
|
|
3663
|
+
path: ["manage"],
|
|
3664
|
+
description: "Interactive workspace manager chat (text, realtime, or voice)",
|
|
3665
|
+
requiresWorkspace: true,
|
|
3666
|
+
mutates: true,
|
|
3667
|
+
args: [
|
|
3668
|
+
{ name: "realtime", description: "Use WebSocket realtime connection", isFlag: true },
|
|
3669
|
+
{ name: "voice", description: "Enable voice mode (microphone + speaker)", isFlag: true },
|
|
3670
|
+
{ name: "message", alias: "m", description: "Single-turn: send one message and exit" },
|
|
3671
|
+
{ name: "as-member", description: "Chat as a specific member (by numeric ID)" },
|
|
3672
|
+
{ name: "chat-id", description: "Resume an existing chat by numeric ID" },
|
|
3673
|
+
{ name: "vad", description: "Voice activity detection: server (default) or client" },
|
|
3674
|
+
{ name: "list-devices", description: "List audio devices and exit", isFlag: true },
|
|
3675
|
+
{ name: "input-device", description: "Microphone device name or ID" },
|
|
3676
|
+
{ name: "output-device", description: "Speaker device name or ID" },
|
|
3677
|
+
{ name: "debug", description: "Enable debug logging", isFlag: true },
|
|
3678
|
+
{
|
|
3679
|
+
name: "auto-approve",
|
|
3680
|
+
description: "Auto-approve all tool calls (skip HITL)",
|
|
3681
|
+
isFlag: true
|
|
3682
|
+
},
|
|
3683
|
+
{
|
|
3684
|
+
name: "trust-cwd",
|
|
3685
|
+
description: "Trust files in the current working directory (non-interactive)",
|
|
3686
|
+
isFlag: true
|
|
3687
|
+
},
|
|
3688
|
+
{
|
|
3689
|
+
name: "trust-dir",
|
|
3690
|
+
description: "Trust files in this directory (non-interactive; absolute path)"
|
|
3691
|
+
}
|
|
3692
|
+
],
|
|
3693
|
+
handler: async (hctx) => {
|
|
3694
|
+
await manageCommand(hctx);
|
|
3695
|
+
}
|
|
3696
|
+
});
|
|
3697
|
+
registry.registerCommand({
|
|
3698
|
+
path: ["chat"],
|
|
3699
|
+
description: "Chat with a workflow, member, or existing chat (text, realtime, or voice)",
|
|
3700
|
+
requiresWorkspace: true,
|
|
3701
|
+
mutates: true,
|
|
3702
|
+
args: [
|
|
3703
|
+
{ name: "workflow", description: "Start assignment chat with workflow (by numeric ID)" },
|
|
3704
|
+
{ name: "member", description: "Start DM with a member (by numeric ID)" },
|
|
3705
|
+
{ name: "chat-id", description: "Resume an existing chat by numeric ID" },
|
|
3706
|
+
{ name: "realtime", description: "Use WebSocket realtime connection", isFlag: true },
|
|
3707
|
+
{ name: "voice", description: "Enable voice mode (microphone + speaker)", isFlag: true },
|
|
3708
|
+
{ name: "message", alias: "m", description: "Single-turn: send one message and exit" },
|
|
3709
|
+
{ name: "vad", description: "Voice activity detection: server (default) or client" },
|
|
3710
|
+
{ name: "list-devices", description: "List audio devices and exit", isFlag: true },
|
|
3711
|
+
{ name: "input-device", description: "Microphone device name or ID" },
|
|
3712
|
+
{ name: "output-device", description: "Speaker device name or ID" },
|
|
3713
|
+
{ name: "debug", description: "Enable debug logging", isFlag: true },
|
|
3714
|
+
{
|
|
3715
|
+
name: "auto-approve",
|
|
3716
|
+
description: "Auto-approve all tool calls (skip HITL)",
|
|
3717
|
+
isFlag: true
|
|
3718
|
+
}
|
|
3719
|
+
],
|
|
3720
|
+
handler: async (hctx) => {
|
|
3721
|
+
await chatCommand(hctx);
|
|
3722
|
+
}
|
|
3723
|
+
});
|
|
3724
|
+
registry.registerCommand({
|
|
3725
|
+
path: ["get-started"],
|
|
3726
|
+
description: "Bootstrap instructions for new users and LLM agents",
|
|
3727
|
+
requiresAuth: false,
|
|
3728
|
+
args: [
|
|
3729
|
+
{
|
|
3730
|
+
name: "format",
|
|
3731
|
+
description: "Output format: llm (agent-readable) or default (human)"
|
|
3732
|
+
},
|
|
3733
|
+
{
|
|
3734
|
+
name: "agent",
|
|
3735
|
+
description: "Alias for --format llm",
|
|
3736
|
+
isFlag: true
|
|
3737
|
+
}
|
|
3738
|
+
],
|
|
3739
|
+
handler: async (hctx) => {
|
|
3740
|
+
await getStartedCommand(hctx);
|
|
3741
|
+
}
|
|
3742
|
+
});
|
|
3743
|
+
registry.registerCommand({
|
|
3744
|
+
path: ["setup"],
|
|
3745
|
+
description: "Alias for get-started \u2014 bootstrap instructions",
|
|
3746
|
+
requiresAuth: false,
|
|
3747
|
+
args: [
|
|
3748
|
+
{ name: "format", description: "Output format: llm or default" },
|
|
3749
|
+
{ name: "agent", description: "Alias for --format llm", isFlag: true }
|
|
3750
|
+
],
|
|
3751
|
+
handler: async (hctx) => {
|
|
3752
|
+
await getStartedCommand(hctx);
|
|
3753
|
+
}
|
|
3754
|
+
});
|
|
3755
|
+
registry.registerCommand({
|
|
3756
|
+
path: ["concierge"],
|
|
3757
|
+
description: "Call or SMS your verified phone via the Concierge (server-side)",
|
|
3758
|
+
requiresWorkspace: false,
|
|
3759
|
+
mutates: true,
|
|
3760
|
+
args: [
|
|
3761
|
+
{ name: "call", description: "Ring your verified account phone", isFlag: true },
|
|
3762
|
+
{ name: "sms", description: "Start an SMS thread on your verified phone", isFlag: true }
|
|
3763
|
+
],
|
|
3764
|
+
handler: async (hctx) => {
|
|
3765
|
+
await conciergeOutboundCommand(hctx);
|
|
3766
|
+
}
|
|
3767
|
+
});
|
|
3768
|
+
registry.registerCommand({
|
|
3769
|
+
path: ["account", "verify-phone"],
|
|
3770
|
+
description: "Verify your account phone for concierge chat --call / --sms",
|
|
3771
|
+
requiresWorkspace: false,
|
|
3772
|
+
mutates: true,
|
|
3773
|
+
args: [{ name: "phone", description: "E.164 phone number (optional; prompts if omitted)" }],
|
|
3774
|
+
handler: async (hctx) => {
|
|
3775
|
+
const { accountVerifyPhoneCommand: accountVerifyPhoneCommand2 } = await Promise.resolve().then(() => (init_account_verify_phone(), account_verify_phone_exports));
|
|
3776
|
+
await accountVerifyPhoneCommand2(hctx);
|
|
3777
|
+
}
|
|
3778
|
+
});
|
|
3779
|
+
registry.registerCommand({
|
|
3780
|
+
path: ["concierge", "chat"],
|
|
3781
|
+
description: "Interactive concierge chat (text, realtime, or voice)",
|
|
3782
|
+
requiresWorkspace: false,
|
|
3783
|
+
mutates: true,
|
|
3784
|
+
args: [
|
|
3785
|
+
{ name: "realtime", description: "Use WebSocket realtime connection", isFlag: true },
|
|
3786
|
+
{ name: "voice", description: "Enable voice mode (microphone + speaker)", isFlag: true },
|
|
3787
|
+
{
|
|
3788
|
+
name: "call",
|
|
3789
|
+
description: "Ring your verified account phone (server-side)",
|
|
3790
|
+
isFlag: true
|
|
3791
|
+
},
|
|
3792
|
+
{
|
|
3793
|
+
name: "sms",
|
|
3794
|
+
description: "SMS your verified account phone (server-side)",
|
|
3795
|
+
isFlag: true
|
|
3796
|
+
},
|
|
3797
|
+
{ name: "message", alias: "m", description: "Single-turn: send one message and exit" },
|
|
3798
|
+
{ name: "chat-id", description: "Resume an existing chat by numeric ID" },
|
|
3799
|
+
{ name: "vad", description: "Voice activity detection: server (default) or client" },
|
|
3800
|
+
{ name: "list-devices", description: "List audio devices and exit", isFlag: true },
|
|
3801
|
+
{ name: "input-device", description: "Microphone device name or ID" },
|
|
3802
|
+
{ name: "output-device", description: "Speaker device name or ID" },
|
|
3803
|
+
{ name: "debug", description: "Enable debug logging", isFlag: true },
|
|
3804
|
+
{
|
|
3805
|
+
name: "auto-approve",
|
|
3806
|
+
description: "Auto-approve all tool calls (skip HITL)",
|
|
3807
|
+
isFlag: true
|
|
3808
|
+
},
|
|
3809
|
+
{
|
|
3810
|
+
name: "trust-cwd",
|
|
3811
|
+
description: "Trust files in the current working directory (non-interactive)",
|
|
3812
|
+
isFlag: true
|
|
3813
|
+
},
|
|
3814
|
+
{
|
|
3815
|
+
name: "trust-dir",
|
|
3816
|
+
description: "Trust files in this directory (non-interactive; absolute path)"
|
|
3817
|
+
}
|
|
3818
|
+
],
|
|
3819
|
+
handler: async (hctx) => {
|
|
3820
|
+
await conciergeCommand(hctx);
|
|
3821
|
+
}
|
|
3822
|
+
});
|
|
3823
|
+
registry.registerCommand({
|
|
3824
|
+
path: ["dev", "import-local"],
|
|
3825
|
+
description: "Import remote workspace locally",
|
|
3826
|
+
requiresWorkspace: true,
|
|
3827
|
+
handler: async ({ ctx, workspace }) => {
|
|
3828
|
+
await devImportLocalCommand(requireAuth(ctx), workspace);
|
|
3829
|
+
}
|
|
3830
|
+
});
|
|
3831
|
+
registry.registerCommand({
|
|
3832
|
+
path: ["whoami"],
|
|
3833
|
+
description: "Show current user info",
|
|
3834
|
+
handler: async ({ ctx }) => {
|
|
3835
|
+
await whoamiCommand(requireAuth(ctx));
|
|
3836
|
+
}
|
|
3837
|
+
});
|
|
3838
|
+
registry.registerCommand({
|
|
3839
|
+
path: ["organizations", "list"],
|
|
3840
|
+
description: "List organizations",
|
|
3841
|
+
handler: async ({ ctx }) => {
|
|
3842
|
+
await organizationsListCommand(requireAuth(ctx));
|
|
3843
|
+
}
|
|
3844
|
+
});
|
|
3845
|
+
registry.registerCommand({
|
|
3846
|
+
path: ["orgs", "list"],
|
|
3847
|
+
description: "List organizations",
|
|
3848
|
+
handler: async ({ ctx }) => {
|
|
3849
|
+
await organizationsListCommand(requireAuth(ctx));
|
|
3850
|
+
}
|
|
3851
|
+
});
|
|
3852
|
+
}
|
|
3853
|
+
export {
|
|
3854
|
+
registerSpecialCommands
|
|
3855
|
+
};
|
|
3856
|
+
//# sourceMappingURL=register-special-commands.js.map
|