@gravity-rail/cli 0.7.2 → 0.8.0
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 +27 -0
- package/dist/account-verify-phone-TC73QIIE.js +41 -0
- package/dist/account-verify-phone-TC73QIIE.js.map +1 -0
- package/dist/{chunk-OPWCTOHP.js → chunk-7VZ4JLAJ.js} +11 -1
- package/dist/chunk-7VZ4JLAJ.js.map +1 -0
- package/dist/chunk-L5YCPUAM.js +761 -0
- package/dist/chunk-L5YCPUAM.js.map +1 -0
- package/dist/{errors-SMY7LZJB.js → errors-VK4QO3QZ.js} +2 -2
- package/dist/gr.js +2699 -668
- package/dist/gr.js.map +1 -1
- package/dist/lib.d.ts +5 -2
- package/dist/lib.js +1869 -1290
- package/dist/lib.js.map +1 -1
- package/package.json +5 -8
- package/dist/chunk-OPWCTOHP.js.map +0 -1
- package/dist/register-special-commands.d.ts +0 -4
- package/dist/register-special-commands.js +0 -3856
- package/dist/register-special-commands.js.map +0 -1
- /package/dist/{errors-SMY7LZJB.js.map → errors-VK4QO3QZ.js.map} +0 -0
|
@@ -1,3856 +0,0 @@
|
|
|
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
|
-
});
|
|
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";
|
|
1063
|
-
function buildCliConciergeAppContext() {
|
|
1064
|
-
const shell = process.env.SHELL ? path.basename(process.env.SHELL) : void 0;
|
|
1065
|
-
return {
|
|
1066
|
-
clientSurface: "cli",
|
|
1067
|
-
channel: "text",
|
|
1068
|
-
cliContext: {
|
|
1069
|
-
shell,
|
|
1070
|
-
os: process.platform,
|
|
1071
|
-
cwdBasename: path.basename(process.cwd())
|
|
1072
|
-
}
|
|
1073
|
-
};
|
|
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";
|
|
1088
|
-
var DEFAULT_MAX_READ_BYTES = 5e4;
|
|
1089
|
-
var ABSOLUTE_MAX_READ_BYTES = 1e6;
|
|
1090
|
-
function resolveSafePath(root, relativePath) {
|
|
1091
|
-
const rootResolved = path2.resolve(root);
|
|
1092
|
-
const target = path2.resolve(rootResolved, relativePath || ".");
|
|
1093
|
-
if (target !== rootResolved && !target.startsWith(rootResolved + path2.sep)) {
|
|
1094
|
-
return null;
|
|
1095
|
-
}
|
|
1096
|
-
let cursor = rootResolved;
|
|
1097
|
-
const rest = path2.relative(rootResolved, target);
|
|
1098
|
-
if (rest && rest !== ".") {
|
|
1099
|
-
for (const segment of rest.split(path2.sep)) {
|
|
1100
|
-
cursor = path2.join(cursor, segment);
|
|
1101
|
-
try {
|
|
1102
|
-
const lstat = fs.lstatSync(cursor);
|
|
1103
|
-
if (lstat.isSymbolicLink()) {
|
|
1104
|
-
return null;
|
|
1105
|
-
}
|
|
1106
|
-
} catch {
|
|
1107
|
-
break;
|
|
1108
|
-
}
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
return target;
|
|
1112
|
-
}
|
|
1113
|
-
function executeCliLocalFileAction(root, interrupt, opts) {
|
|
1114
|
-
const safe = resolveSafePath(root, interrupt.path);
|
|
1115
|
-
if (!safe) {
|
|
1116
|
-
return { ok: false, error: "Path escapes trusted directory" };
|
|
1117
|
-
}
|
|
1118
|
-
try {
|
|
1119
|
-
switch (interrupt.action) {
|
|
1120
|
-
case "read_file": {
|
|
1121
|
-
if (!fs.existsSync(safe)) {
|
|
1122
|
-
return { ok: false, error: "File not found" };
|
|
1123
|
-
}
|
|
1124
|
-
const stat = fs.lstatSync(safe);
|
|
1125
|
-
if (stat.isSymbolicLink()) {
|
|
1126
|
-
return { ok: false, error: "Path escapes trusted directory" };
|
|
1127
|
-
}
|
|
1128
|
-
if (!stat.isFile()) {
|
|
1129
|
-
return { ok: false, error: "Not a file" };
|
|
1130
|
-
}
|
|
1131
|
-
const requested = Number.isFinite(interrupt.max_bytes) ? Math.max(1, Math.min(Number(interrupt.max_bytes), ABSOLUTE_MAX_READ_BYTES)) : DEFAULT_MAX_READ_BYTES;
|
|
1132
|
-
if (stat.size > requested) {
|
|
1133
|
-
const buf = fs.readFileSync(safe, { encoding: "utf8" });
|
|
1134
|
-
return {
|
|
1135
|
-
ok: true,
|
|
1136
|
-
content: buf.slice(0, requested) + `
|
|
1137
|
-
|
|
1138
|
-
[truncated at ${requested} bytes]`
|
|
1139
|
-
};
|
|
1140
|
-
}
|
|
1141
|
-
return { ok: true, content: fs.readFileSync(safe, "utf8") };
|
|
1142
|
-
}
|
|
1143
|
-
case "list_dir": {
|
|
1144
|
-
if (!fs.existsSync(safe)) {
|
|
1145
|
-
return { ok: false, error: "Directory not found" };
|
|
1146
|
-
}
|
|
1147
|
-
const entries = fs.readdirSync(safe, { withFileTypes: true });
|
|
1148
|
-
const lines = entries.slice(0, 200).map((e) => e.isDirectory() ? `${e.name}/` : e.name);
|
|
1149
|
-
if (entries.length > 200) {
|
|
1150
|
-
lines.push(`... and ${entries.length - 200} more`);
|
|
1151
|
-
}
|
|
1152
|
-
return { ok: true, content: lines.join("\n") };
|
|
1153
|
-
}
|
|
1154
|
-
case "write_file": {
|
|
1155
|
-
if (!opts.allowWrite) {
|
|
1156
|
-
return { ok: false, error: "Write requires --allow-write" };
|
|
1157
|
-
}
|
|
1158
|
-
if (interrupt.content === void 0) {
|
|
1159
|
-
return { ok: false, error: "Missing content for write_file" };
|
|
1160
|
-
}
|
|
1161
|
-
fs.mkdirSync(path2.dirname(safe), { recursive: true });
|
|
1162
|
-
fs.writeFileSync(safe, interrupt.content, "utf8");
|
|
1163
|
-
return { ok: true, content: "written" };
|
|
1164
|
-
}
|
|
1165
|
-
default:
|
|
1166
|
-
return { ok: false, error: `Unknown action: ${interrupt.action}` };
|
|
1167
|
-
}
|
|
1168
|
-
} catch (e) {
|
|
1169
|
-
return { ok: false, error: sanitizeLocalFileError(e) };
|
|
1170
|
-
}
|
|
1171
|
-
}
|
|
1172
|
-
function sanitizeLocalFileError(e) {
|
|
1173
|
-
if (e && typeof e === "object" && "code" in e) {
|
|
1174
|
-
const code = e.code;
|
|
1175
|
-
if (code === "ENOENT") return "File or directory not found";
|
|
1176
|
-
if (code === "EACCES" || code === "EPERM") return "Permission denied";
|
|
1177
|
-
if (code === "EISDIR") return "Not a file";
|
|
1178
|
-
if (code === "ENOTDIR") return "Not a directory";
|
|
1179
|
-
}
|
|
1180
|
-
return "Local file operation failed";
|
|
1181
|
-
}
|
|
1182
|
-
function isCliLocalFileInterrupt(value) {
|
|
1183
|
-
if (typeof value !== "object" || value === null) return false;
|
|
1184
|
-
const v = value;
|
|
1185
|
-
return v.type === "cli_local_file" && typeof v.action === "string" && typeof v.path === "string";
|
|
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";
|
|
1193
|
-
var STORE_VERSION = 1;
|
|
1194
|
-
function storePath() {
|
|
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"));
|
|
1196
|
-
return path3.join(base, "trusted-dirs.json");
|
|
1197
|
-
}
|
|
1198
|
-
function loadTrustedDirsStore() {
|
|
1199
|
-
const file = storePath();
|
|
1200
|
-
try {
|
|
1201
|
-
const raw = JSON.parse(fs2.readFileSync(file, "utf8"));
|
|
1202
|
-
if (raw?.version === STORE_VERSION && Array.isArray(raw.always)) {
|
|
1203
|
-
return { version: STORE_VERSION, always: raw.always.map((p) => path3.resolve(p)) };
|
|
1204
|
-
}
|
|
1205
|
-
} catch {
|
|
1206
|
-
}
|
|
1207
|
-
return { version: STORE_VERSION, always: [] };
|
|
1208
|
-
}
|
|
1209
|
-
function saveTrustedDirsStore(store) {
|
|
1210
|
-
const file = storePath();
|
|
1211
|
-
const dir = path3.dirname(file);
|
|
1212
|
-
fs2.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
1213
|
-
fs2.writeFileSync(file, JSON.stringify(store, null, 2) + "\n", { mode: 384 });
|
|
1214
|
-
}
|
|
1215
|
-
function isDirectoryTrusted(dirPath, opts) {
|
|
1216
|
-
if (opts?.trustCwdFlag) return true;
|
|
1217
|
-
const resolved = path3.resolve(dirPath);
|
|
1218
|
-
const store = loadTrustedDirsStore();
|
|
1219
|
-
return store.always.includes(resolved);
|
|
1220
|
-
}
|
|
1221
|
-
async function promptTrustInteractive(dirPath) {
|
|
1222
|
-
const rl = readline2.createInterface({ input: process.stdin, output: process.stderr });
|
|
1223
|
-
const question = (q) => new Promise((resolve2) => rl.question(q, resolve2));
|
|
1224
|
-
process.stderr.write(
|
|
1225
|
-
`
|
|
1226
|
-
Gravity Rail may read files in:
|
|
1227
|
-
${dirPath}
|
|
1228
|
-
|
|
1229
|
-
File contents can influence the assistant. Do you trust the files in this directory?
|
|
1230
|
-
[1] Trust once (this session)
|
|
1231
|
-
[2] Trust always (remember for this path)
|
|
1232
|
-
[3] No \u2014 do not read local files
|
|
1233
|
-
|
|
1234
|
-
Choice [1/2/3] (default 3): `
|
|
1235
|
-
);
|
|
1236
|
-
const answer = (await question("")).trim() || "3";
|
|
1237
|
-
rl.close();
|
|
1238
|
-
if (answer === "1") return "once";
|
|
1239
|
-
if (answer === "2") return "always";
|
|
1240
|
-
return "denied";
|
|
1241
|
-
}
|
|
1242
|
-
var sessionTrusted = /* @__PURE__ */ new Set();
|
|
1243
|
-
async function resolveDirectoryTrust(opts) {
|
|
1244
|
-
const dir = path3.resolve(opts.trustDir ?? opts.cwd ?? process.cwd());
|
|
1245
|
-
if (isDirectoryTrusted(dir, { trustCwdFlag: opts.trustCwd })) return "trusted";
|
|
1246
|
-
if (sessionTrusted.has(dir)) return "trusted";
|
|
1247
|
-
if (opts.trustDir && path3.resolve(opts.trustDir) === dir) {
|
|
1248
|
-
sessionTrusted.add(dir);
|
|
1249
|
-
return "trusted";
|
|
1250
|
-
}
|
|
1251
|
-
if (opts.trustCwd) {
|
|
1252
|
-
sessionTrusted.add(dir);
|
|
1253
|
-
return "trusted";
|
|
1254
|
-
}
|
|
1255
|
-
if (!process.stdin.isTTY || opts.nonInteractive) {
|
|
1256
|
-
return "denied";
|
|
1257
|
-
}
|
|
1258
|
-
const decision = await promptTrustInteractive(dir);
|
|
1259
|
-
if (decision === "denied") {
|
|
1260
|
-
return "denied";
|
|
1261
|
-
}
|
|
1262
|
-
if (decision === "always") {
|
|
1263
|
-
const store = loadTrustedDirsStore();
|
|
1264
|
-
if (!store.always.includes(dir)) {
|
|
1265
|
-
store.always.push(dir);
|
|
1266
|
-
saveTrustedDirsStore(store);
|
|
1267
|
-
}
|
|
1268
|
-
}
|
|
1269
|
-
sessionTrusted.add(dir);
|
|
1270
|
-
return "trusted";
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
// src/lib/cli-hitl.ts
|
|
1274
|
-
function extractHumanInputRequest(event) {
|
|
1275
|
-
const type = String(event.type ?? "").toUpperCase();
|
|
1276
|
-
if (type === "CUSTOM" && event.name === "human_input_request") {
|
|
1277
|
-
return event.value;
|
|
1278
|
-
}
|
|
1279
|
-
if (event.name === "human_input_request") {
|
|
1280
|
-
return event.value;
|
|
1281
|
-
}
|
|
1282
|
-
return null;
|
|
1283
|
-
}
|
|
1284
|
-
async function handleCliHitlInterrupt(value, opts) {
|
|
1285
|
-
if (!value || typeof value !== "object") return null;
|
|
1286
|
-
const payload = value;
|
|
1287
|
-
if (isCliLocalFileInterrupt(payload)) {
|
|
1288
|
-
const interrupt = payload;
|
|
1289
|
-
if (typeof payload.interrupt_id === "string") {
|
|
1290
|
-
interrupt.interrupt_id = payload.interrupt_id;
|
|
1291
|
-
}
|
|
1292
|
-
return handleCliLocalFile(interrupt, opts);
|
|
1293
|
-
}
|
|
1294
|
-
if (payload.type === "present_choice") {
|
|
1295
|
-
return handlePresentChoiceTerminal(payload);
|
|
1296
|
-
}
|
|
1297
|
-
return null;
|
|
1298
|
-
}
|
|
1299
|
-
async function handleCliLocalFile(interrupt, opts) {
|
|
1300
|
-
const trust = await resolveDirectoryTrust({
|
|
1301
|
-
cwd: process.cwd(),
|
|
1302
|
-
trustCwd: opts.trustCwd,
|
|
1303
|
-
trustDir: opts.trustDir,
|
|
1304
|
-
// Interactive sessions: prompt on first local-file use (review feedback).
|
|
1305
|
-
nonInteractive: opts.nonInteractive ?? process.stdin.isTTY !== true
|
|
1306
|
-
});
|
|
1307
|
-
if (trust !== "trusted") {
|
|
1308
|
-
return {
|
|
1309
|
-
resumeValue: JSON.stringify({
|
|
1310
|
-
ok: false,
|
|
1311
|
-
error: "Directory not trusted for local file access. Re-run with --trust-cwd or --trust-dir <path>, or approve trust interactively when prompted."
|
|
1312
|
-
}),
|
|
1313
|
-
resumeInterruptId: interrupt.interrupt_id
|
|
1314
|
-
};
|
|
1315
|
-
}
|
|
1316
|
-
process.stderr.write(`
|
|
1317
|
-
[local ${interrupt.action}] ${interrupt.path}
|
|
1318
|
-
`);
|
|
1319
|
-
const result = executeCliLocalFileAction(process.cwd(), interrupt, {
|
|
1320
|
-
allowWrite: opts.allowWrite
|
|
1321
|
-
});
|
|
1322
|
-
return {
|
|
1323
|
-
resumeValue: JSON.stringify(result),
|
|
1324
|
-
resumeInterruptId: interrupt.interrupt_id
|
|
1325
|
-
};
|
|
1326
|
-
}
|
|
1327
|
-
async function handlePresentChoiceTerminal(payload) {
|
|
1328
|
-
const prompt = typeof payload.user_prompt === "string" && payload.user_prompt || typeof payload.description === "string" && payload.description || "Choose an option";
|
|
1329
|
-
const markup = typeof payload.openui_markup === "string" ? payload.openui_markup : "";
|
|
1330
|
-
process.stderr.write(`
|
|
1331
|
-
${prompt}
|
|
1332
|
-
`);
|
|
1333
|
-
if (markup) {
|
|
1334
|
-
process.stderr.write(`${markup.slice(0, 2e3)}
|
|
1335
|
-
`);
|
|
1336
|
-
}
|
|
1337
|
-
if (!process.stdin.isTTY) {
|
|
1338
|
-
return {
|
|
1339
|
-
resumeValue: "",
|
|
1340
|
-
resumeInterruptId: typeof payload.interrupt_id === "string" ? payload.interrupt_id : void 0
|
|
1341
|
-
};
|
|
1342
|
-
}
|
|
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);
|
|
1346
|
-
});
|
|
1347
|
-
rl.close();
|
|
1348
|
-
return {
|
|
1349
|
-
resumeValue: answer.trim(),
|
|
1350
|
-
resumeInterruptId: typeof payload.interrupt_id === "string" ? payload.interrupt_id : void 0
|
|
1351
|
-
};
|
|
1352
|
-
}
|
|
1353
|
-
|
|
1354
|
-
// src/lib/concierge-agui-loop.ts
|
|
1355
|
-
var MAX_HITL_ROUNDS = 25;
|
|
1356
|
-
async function runConciergeAguiLoop(client, initialMessages, onEvent, opts) {
|
|
1357
|
-
let streamText = "";
|
|
1358
|
-
let hasToolCalls = false;
|
|
1359
|
-
let resume;
|
|
1360
|
-
for (let round = 0; round < MAX_HITL_ROUNDS; round++) {
|
|
1361
|
-
let hitlPayload = null;
|
|
1362
|
-
await client.streamRequest(
|
|
1363
|
-
resume ? {
|
|
1364
|
-
messages: [],
|
|
1365
|
-
resumeValue: resume.resumeValue,
|
|
1366
|
-
resumeInterruptId: resume.resumeInterruptId
|
|
1367
|
-
} : { messages: initialMessages },
|
|
1368
|
-
(event) => {
|
|
1369
|
-
onEvent(event);
|
|
1370
|
-
const delta = getTextDelta(event);
|
|
1371
|
-
if (delta) streamText += delta;
|
|
1372
|
-
const type = event.type?.toLowerCase() ?? "";
|
|
1373
|
-
if (type === "tool_call_start") hasToolCalls = true;
|
|
1374
|
-
const errorMsg = getErrorMessage(event);
|
|
1375
|
-
if (errorMsg) process.stderr.write(`\x1B[31mError: ${errorMsg}\x1B[0m
|
|
1376
|
-
`);
|
|
1377
|
-
const hitl = extractHumanInputRequest(event);
|
|
1378
|
-
if (hitl) hitlPayload = hitl;
|
|
1379
|
-
},
|
|
1380
|
-
{ autoApprove: opts.autoApprove }
|
|
1381
|
-
);
|
|
1382
|
-
if (!hitlPayload) {
|
|
1383
|
-
break;
|
|
1384
|
-
}
|
|
1385
|
-
const next = await handleCliHitlInterrupt(hitlPayload, {
|
|
1386
|
-
allowWrite: opts.allowWrite,
|
|
1387
|
-
autoApprove: opts.autoApprove,
|
|
1388
|
-
trustCwd: opts.trustCwd,
|
|
1389
|
-
trustDir: opts.trustDir,
|
|
1390
|
-
nonInteractive: opts.nonInteractive
|
|
1391
|
-
});
|
|
1392
|
-
if (!next) {
|
|
1393
|
-
process.stderr.write(
|
|
1394
|
-
" [HITL] Unsupported interrupt in CLI \u2014 open app.gravityrail.com/setup to continue.\n"
|
|
1395
|
-
);
|
|
1396
|
-
break;
|
|
1397
|
-
}
|
|
1398
|
-
resume = next;
|
|
1399
|
-
streamText = "";
|
|
1400
|
-
}
|
|
1401
|
-
return { streamText, hasToolCalls };
|
|
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";
|
|
1410
|
-
var RealtimeClient = class extends EventEmitter {
|
|
1411
|
-
ws = null;
|
|
1412
|
-
connected = false;
|
|
1413
|
-
options;
|
|
1414
|
-
constructor(options) {
|
|
1415
|
-
super();
|
|
1416
|
-
this.options = {
|
|
1417
|
-
vadMode: "server",
|
|
1418
|
-
debug: false,
|
|
1419
|
-
...options
|
|
1420
|
-
};
|
|
1421
|
-
}
|
|
1422
|
-
/** Connect to the realtime WebSocket endpoint */
|
|
1423
|
-
connect() {
|
|
1424
|
-
if (typeof globalThis.WebSocket === "undefined") {
|
|
1425
|
-
return Promise.reject(
|
|
1426
|
-
new Error("WebSocket not available. Node.js 22+ is required for realtime mode.")
|
|
1427
|
-
);
|
|
1428
|
-
}
|
|
1429
|
-
return new Promise((resolve2, reject) => {
|
|
1430
|
-
const params = new URLSearchParams();
|
|
1431
|
-
if (this.options.vadMode) {
|
|
1432
|
-
params.append("vad", this.options.vadMode);
|
|
1433
|
-
}
|
|
1434
|
-
const url = params.toString() ? `${this.options.url}?${params.toString()}` : this.options.url;
|
|
1435
|
-
if (this.options.debug) {
|
|
1436
|
-
process.stderr.write(`[debug] Connecting to ${url}
|
|
1437
|
-
`);
|
|
1438
|
-
}
|
|
1439
|
-
this.ws = new WebSocket(url);
|
|
1440
|
-
this.ws.onerror = (event) => {
|
|
1441
|
-
const msg = event instanceof ErrorEvent ? event.message : "WebSocket connection failed";
|
|
1442
|
-
if (!this.connected) {
|
|
1443
|
-
reject(new Error(msg));
|
|
1444
|
-
} else {
|
|
1445
|
-
this.emit("error", new Error(msg));
|
|
1446
|
-
}
|
|
1447
|
-
};
|
|
1448
|
-
this.ws.onclose = (event) => {
|
|
1449
|
-
const reason = event.code === 1006 ? "Connection lost" : event.reason || "Closed";
|
|
1450
|
-
this.connected = false;
|
|
1451
|
-
this.emit("closed", { reason, code: event.code });
|
|
1452
|
-
};
|
|
1453
|
-
this.ws.onmessage = (event) => {
|
|
1454
|
-
try {
|
|
1455
|
-
const data = typeof event.data === "string" ? event.data : event.data.toString();
|
|
1456
|
-
const parsed = JSON.parse(data);
|
|
1457
|
-
this.emit("ws_recv", parsed);
|
|
1458
|
-
this.handleEvent(parsed);
|
|
1459
|
-
} catch {
|
|
1460
|
-
if (this.options.debug) {
|
|
1461
|
-
process.stderr.write(`[debug] Failed to parse message
|
|
1462
|
-
`);
|
|
1463
|
-
}
|
|
1464
|
-
}
|
|
1465
|
-
};
|
|
1466
|
-
this.ws.onopen = () => {
|
|
1467
|
-
if (this.options.debug) {
|
|
1468
|
-
process.stderr.write("[debug] WebSocket connected, sending login\n");
|
|
1469
|
-
}
|
|
1470
|
-
this.sendCustom("voice.login", {
|
|
1471
|
-
token: this.options.token,
|
|
1472
|
-
chat_id: this.options.chatId,
|
|
1473
|
-
vad: this.options.vadMode
|
|
1474
|
-
});
|
|
1475
|
-
};
|
|
1476
|
-
const timeout = setTimeout(() => {
|
|
1477
|
-
if (!this.connected) {
|
|
1478
|
-
this.close();
|
|
1479
|
-
reject(new Error("Connection timeout - no login response within 10s"));
|
|
1480
|
-
}
|
|
1481
|
-
}, 1e4);
|
|
1482
|
-
this.once("connected", () => {
|
|
1483
|
-
clearTimeout(timeout);
|
|
1484
|
-
resolve2();
|
|
1485
|
-
});
|
|
1486
|
-
this.once("error", () => {
|
|
1487
|
-
clearTimeout(timeout);
|
|
1488
|
-
});
|
|
1489
|
-
});
|
|
1490
|
-
}
|
|
1491
|
-
handleEvent(event) {
|
|
1492
|
-
switch (event.type) {
|
|
1493
|
-
case "CUSTOM":
|
|
1494
|
-
this.handleCustomEvent(event.name || "", event.value);
|
|
1495
|
-
break;
|
|
1496
|
-
case "TEXT_MESSAGE_START":
|
|
1497
|
-
this.emit("text_start", { messageId: event.messageId || "" });
|
|
1498
|
-
break;
|
|
1499
|
-
case "TEXT_MESSAGE_CONTENT":
|
|
1500
|
-
this.emit("text_delta", {
|
|
1501
|
-
messageId: event.messageId || "",
|
|
1502
|
-
delta: event.delta || ""
|
|
1503
|
-
});
|
|
1504
|
-
break;
|
|
1505
|
-
case "TEXT_MESSAGE_END":
|
|
1506
|
-
this.emit("text_end", { messageId: event.messageId || "" });
|
|
1507
|
-
break;
|
|
1508
|
-
case "TOOL_CALL_START":
|
|
1509
|
-
this.emit("tool_call_start", {
|
|
1510
|
-
toolCallId: event.toolCallId || "",
|
|
1511
|
-
toolCallName: event.toolCallName || ""
|
|
1512
|
-
});
|
|
1513
|
-
break;
|
|
1514
|
-
case "TOOL_CALL_ARGS":
|
|
1515
|
-
this.emit("tool_call_args", {
|
|
1516
|
-
toolCallId: event.toolCallId || "",
|
|
1517
|
-
delta: event.delta || ""
|
|
1518
|
-
});
|
|
1519
|
-
break;
|
|
1520
|
-
case "TOOL_CALL_END":
|
|
1521
|
-
this.emit("tool_call_end", { toolCallId: event.toolCallId || "" });
|
|
1522
|
-
break;
|
|
1523
|
-
case "TOOL_CALL_RESULT":
|
|
1524
|
-
this.emit("tool_call_result", {
|
|
1525
|
-
toolCallId: event.toolCallId || "",
|
|
1526
|
-
result: event.result
|
|
1527
|
-
});
|
|
1528
|
-
break;
|
|
1529
|
-
case "RUN_STARTED":
|
|
1530
|
-
this.emit("run_started", { runId: event.runId });
|
|
1531
|
-
break;
|
|
1532
|
-
case "RUN_FINISHED":
|
|
1533
|
-
this.emit("run_finished", { runId: event.runId });
|
|
1534
|
-
break;
|
|
1535
|
-
case "RUN_ERROR":
|
|
1536
|
-
this.emit("run_error", {
|
|
1537
|
-
message: event.message || "Unknown error",
|
|
1538
|
-
code: event.code
|
|
1539
|
-
});
|
|
1540
|
-
break;
|
|
1541
|
-
default:
|
|
1542
|
-
if (this.options.debug) {
|
|
1543
|
-
process.stderr.write(`[debug] Unknown event type: ${event.type}
|
|
1544
|
-
`);
|
|
1545
|
-
}
|
|
1546
|
-
}
|
|
1547
|
-
}
|
|
1548
|
-
handleCustomEvent(name, value) {
|
|
1549
|
-
switch (name) {
|
|
1550
|
-
case "voice.login_success":
|
|
1551
|
-
this.connected = true;
|
|
1552
|
-
this.emit("connected", {
|
|
1553
|
-
chatId: value?.chat_id,
|
|
1554
|
-
memberId: value?.member_id,
|
|
1555
|
-
assistantId: value?.assistant_id
|
|
1556
|
-
});
|
|
1557
|
-
break;
|
|
1558
|
-
case "voice.audio":
|
|
1559
|
-
if (value?.data) {
|
|
1560
|
-
const pcm16 = Buffer.from(value.data, "base64");
|
|
1561
|
-
this.emit("audio", { pcm16, messageId: value.message_id });
|
|
1562
|
-
}
|
|
1563
|
-
break;
|
|
1564
|
-
case "voice.audio_begin":
|
|
1565
|
-
this.emit("audio_begin", { messageId: value?.message_id });
|
|
1566
|
-
break;
|
|
1567
|
-
case "voice.audio_end":
|
|
1568
|
-
this.emit("audio_end", { messageId: value?.message_id });
|
|
1569
|
-
break;
|
|
1570
|
-
case "voice.interrupt":
|
|
1571
|
-
this.emit("interrupt", { messageId: value?.message_id });
|
|
1572
|
-
break;
|
|
1573
|
-
case "voice.response_message":
|
|
1574
|
-
if (value) {
|
|
1575
|
-
this.emit("response_message", value);
|
|
1576
|
-
}
|
|
1577
|
-
break;
|
|
1578
|
-
case "voice.transcription":
|
|
1579
|
-
if (value) {
|
|
1580
|
-
this.emit("transcription", { text: value.text, role: value.role });
|
|
1581
|
-
}
|
|
1582
|
-
break;
|
|
1583
|
-
case "voice.truncate":
|
|
1584
|
-
if (value) {
|
|
1585
|
-
this.emit("truncate", {
|
|
1586
|
-
messageId: value.message_id,
|
|
1587
|
-
content: value.content,
|
|
1588
|
-
originalContent: value.original_content,
|
|
1589
|
-
truncationReason: value.truncation_reason || "barge_in",
|
|
1590
|
-
truncatedAtMs: value.audio_end_ms
|
|
1591
|
-
});
|
|
1592
|
-
}
|
|
1593
|
-
break;
|
|
1594
|
-
case "app.data_update":
|
|
1595
|
-
case "app.navigation":
|
|
1596
|
-
case "app.notification":
|
|
1597
|
-
break;
|
|
1598
|
-
default:
|
|
1599
|
-
if (this.options.debug) {
|
|
1600
|
-
process.stderr.write(`[debug] Unknown CUSTOM event: ${name}
|
|
1601
|
-
`);
|
|
1602
|
-
}
|
|
1603
|
-
}
|
|
1604
|
-
}
|
|
1605
|
-
sendCustom(name, value) {
|
|
1606
|
-
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1607
|
-
return;
|
|
1608
|
-
}
|
|
1609
|
-
const event = { type: "CUSTOM", name };
|
|
1610
|
-
if (value !== void 0) {
|
|
1611
|
-
event.value = value;
|
|
1612
|
-
}
|
|
1613
|
-
this.emit("ws_send", event);
|
|
1614
|
-
this.ws.send(JSON.stringify(event));
|
|
1615
|
-
}
|
|
1616
|
-
/** Send raw PCM16 audio data (will be base64-encoded) */
|
|
1617
|
-
sendAudio(pcm16) {
|
|
1618
|
-
this.sendCustom("voice.audio", {
|
|
1619
|
-
data: pcm16.toString("base64"),
|
|
1620
|
-
format: "pcm16",
|
|
1621
|
-
sample_rate: 24e3
|
|
1622
|
-
});
|
|
1623
|
-
}
|
|
1624
|
-
/** Signal the start of an audio utterance */
|
|
1625
|
-
sendAudioBegin(messageId) {
|
|
1626
|
-
this.sendCustom("voice.audio_begin", { message_id: messageId });
|
|
1627
|
-
}
|
|
1628
|
-
/** Signal the end of an audio utterance */
|
|
1629
|
-
sendAudioEnd() {
|
|
1630
|
-
this.sendCustom("voice.audio_end");
|
|
1631
|
-
}
|
|
1632
|
-
/** Send a text message (triggers AI inference) */
|
|
1633
|
-
sendTextMessage(content) {
|
|
1634
|
-
this.sendCustom("voice.user_message", {
|
|
1635
|
-
message: { role: "user", content }
|
|
1636
|
-
});
|
|
1637
|
-
}
|
|
1638
|
-
/** Send interrupt signal with playback position */
|
|
1639
|
-
sendInterrupt(audioEndMs, messageId) {
|
|
1640
|
-
this.sendCustom("voice.interrupt", {
|
|
1641
|
-
audio_end_ms: audioEndMs,
|
|
1642
|
-
message_id: messageId
|
|
1643
|
-
});
|
|
1644
|
-
}
|
|
1645
|
-
/** Check if the client is connected */
|
|
1646
|
-
isConnected() {
|
|
1647
|
-
return this.connected && this.ws?.readyState === WebSocket.OPEN;
|
|
1648
|
-
}
|
|
1649
|
-
/** Close the connection */
|
|
1650
|
-
close() {
|
|
1651
|
-
this.connected = false;
|
|
1652
|
-
if (this.ws) {
|
|
1653
|
-
this.ws.close();
|
|
1654
|
-
this.ws = null;
|
|
1655
|
-
}
|
|
1656
|
-
}
|
|
1657
|
-
};
|
|
1658
|
-
|
|
1659
|
-
// src/lib/chat-tui.ts
|
|
1660
|
-
import { createWriteStream } from "fs";
|
|
1661
|
-
function isToolCall(item) {
|
|
1662
|
-
return "kind" in item && item.kind === "tool_call";
|
|
1663
|
-
}
|
|
1664
|
-
function isToolResult(item) {
|
|
1665
|
-
return "kind" in item && item.kind === "tool_result";
|
|
1666
|
-
}
|
|
1667
|
-
function getTruncationLabel(reason, truncatedAtMs) {
|
|
1668
|
-
const timeStr = truncatedAtMs ? ` at ${(truncatedAtMs / 1e3).toFixed(1)}s` : "";
|
|
1669
|
-
switch (reason) {
|
|
1670
|
-
case "barge_in":
|
|
1671
|
-
return `interrupted: user spoke${timeStr}`;
|
|
1672
|
-
case "client_interrupt":
|
|
1673
|
-
return `interrupted${timeStr}`;
|
|
1674
|
-
case "user_cancel":
|
|
1675
|
-
return `cancelled by user${timeStr}`;
|
|
1676
|
-
case "error":
|
|
1677
|
-
return `error${timeStr}`;
|
|
1678
|
-
default:
|
|
1679
|
-
return `interrupted${timeStr}`;
|
|
1680
|
-
}
|
|
1681
|
-
}
|
|
1682
|
-
function getExtraContent(content, originalContent) {
|
|
1683
|
-
if (!content || !originalContent) return null;
|
|
1684
|
-
const stripped = content.replace(/—interrupted$/, "");
|
|
1685
|
-
if (originalContent.startsWith(stripped)) {
|
|
1686
|
-
const extra = originalContent.slice(stripped.length);
|
|
1687
|
-
return extra.length > 0 ? extra : null;
|
|
1688
|
-
}
|
|
1689
|
-
return originalContent !== content ? originalContent : null;
|
|
1690
|
-
}
|
|
1691
|
-
var CSI = "\x1B[";
|
|
1692
|
-
var HIDE_CURSOR = `${CSI}?25l`;
|
|
1693
|
-
var SHOW_CURSOR = `${CSI}?25h`;
|
|
1694
|
-
var ALT_SCREEN_ON = `${CSI}?1049h`;
|
|
1695
|
-
var ALT_SCREEN_OFF = `${CSI}?1049l`;
|
|
1696
|
-
var CLEAR_EOL = `${CSI}K`;
|
|
1697
|
-
var RESET = `${CSI}0m`;
|
|
1698
|
-
var MOUSE_ON = `${CSI}?1000h${CSI}?1006h`;
|
|
1699
|
-
var MOUSE_OFF = `${CSI}?1000l${CSI}?1006l`;
|
|
1700
|
-
var BLUE = `${CSI}34m`;
|
|
1701
|
-
var GREEN = `${CSI}32m`;
|
|
1702
|
-
var GRAY = `${CSI}90m`;
|
|
1703
|
-
var YELLOW = `${CSI}33m`;
|
|
1704
|
-
var ORANGE = `${CSI}38;5;208m`;
|
|
1705
|
-
var LIGHT_BLUE = `${CSI}38;5;117m`;
|
|
1706
|
-
var DIM = `${CSI}2m`;
|
|
1707
|
-
var DIM_ITALIC_GRAY = `${CSI}2;3;90m`;
|
|
1708
|
-
function moveTo(row, col) {
|
|
1709
|
-
return `${CSI}${row};${col}H`;
|
|
1710
|
-
}
|
|
1711
|
-
var ChatTUI = class {
|
|
1712
|
-
items = [];
|
|
1713
|
-
inputBuffer = "";
|
|
1714
|
-
isStreaming = false;
|
|
1715
|
-
streamContent = "";
|
|
1716
|
-
scrollOffset = 0;
|
|
1717
|
-
expandDetails = false;
|
|
1718
|
-
rows = 24;
|
|
1719
|
-
cols = 80;
|
|
1720
|
-
started = false;
|
|
1721
|
-
renderTimer = null;
|
|
1722
|
-
displayLinesCache = null;
|
|
1723
|
-
debugLinesCache = null;
|
|
1724
|
-
options;
|
|
1725
|
-
// Debug panel state
|
|
1726
|
-
debugEntries = [];
|
|
1727
|
-
debugPanelVisible = false;
|
|
1728
|
-
debugLogStream = null;
|
|
1729
|
-
// Escape sequence accumulator for raw stdin parsing
|
|
1730
|
-
escBuf = "";
|
|
1731
|
-
escTimer = null;
|
|
1732
|
-
constructor(options) {
|
|
1733
|
-
this.options = options;
|
|
1734
|
-
this.debugPanelVisible = !!options.debug;
|
|
1735
|
-
}
|
|
1736
|
-
/** Whether the debug panel is currently being rendered */
|
|
1737
|
-
get debugVisible() {
|
|
1738
|
-
return this.debugPanelVisible;
|
|
1739
|
-
}
|
|
1740
|
-
// ─── Lifecycle ──────────────────────────────────────────────────────
|
|
1741
|
-
start() {
|
|
1742
|
-
this.updateDimensions();
|
|
1743
|
-
process.stdout.write(ALT_SCREEN_ON + MOUSE_ON);
|
|
1744
|
-
process.stdout.write(`${CSI}1;${Math.max(1, this.rows - 2)}r`);
|
|
1745
|
-
process.stdin.setEncoding("utf8");
|
|
1746
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
1747
|
-
process.stdin.on("data", this.onData);
|
|
1748
|
-
process.stdin.resume();
|
|
1749
|
-
process.stdout.on("resize", this.onResize);
|
|
1750
|
-
process.on("exit", this.restoreTerminal);
|
|
1751
|
-
if (this.options.debug && this.options.env !== "prod") {
|
|
1752
|
-
this.debugLogStream = createWriteStream("gr-cli-realtime-debug.log", { flags: "a" });
|
|
1753
|
-
this.debugLogStream.write(`
|
|
1754
|
-
--- session ${(/* @__PURE__ */ new Date()).toISOString()} ---
|
|
1755
|
-
`);
|
|
1756
|
-
}
|
|
1757
|
-
this.started = true;
|
|
1758
|
-
this.render();
|
|
1759
|
-
}
|
|
1760
|
-
stop() {
|
|
1761
|
-
if (!this.started) return;
|
|
1762
|
-
this.started = false;
|
|
1763
|
-
if (this.renderTimer) {
|
|
1764
|
-
clearTimeout(this.renderTimer);
|
|
1765
|
-
this.renderTimer = null;
|
|
1766
|
-
}
|
|
1767
|
-
this.clearEsc();
|
|
1768
|
-
if (this.debugLogStream) {
|
|
1769
|
-
this.debugLogStream.end();
|
|
1770
|
-
this.debugLogStream = null;
|
|
1771
|
-
}
|
|
1772
|
-
this.restoreTerminal();
|
|
1773
|
-
process.stdout.removeListener("resize", this.onResize);
|
|
1774
|
-
process.stdin.removeListener("data", this.onData);
|
|
1775
|
-
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
1776
|
-
process.stdin.pause();
|
|
1777
|
-
process.removeListener("exit", this.restoreTerminal);
|
|
1778
|
-
}
|
|
1779
|
-
// ─── Chat state ────────────────────────────────────────────────────
|
|
1780
|
-
loadHistory(items) {
|
|
1781
|
-
this.items = items.filter((item) => isToolCall(item) ? true : item.content?.trim());
|
|
1782
|
-
this.dirty();
|
|
1783
|
-
}
|
|
1784
|
-
streamStart() {
|
|
1785
|
-
this.isStreaming = true;
|
|
1786
|
-
this.streamContent = "";
|
|
1787
|
-
this.scrollOffset = 0;
|
|
1788
|
-
this.dirty();
|
|
1789
|
-
}
|
|
1790
|
-
streamDelta(delta) {
|
|
1791
|
-
this.streamContent += delta;
|
|
1792
|
-
this.dirty();
|
|
1793
|
-
}
|
|
1794
|
-
streamEnd() {
|
|
1795
|
-
if (this.streamContent.trim()) {
|
|
1796
|
-
this.items.push({ role: "assistant", content: this.streamContent });
|
|
1797
|
-
}
|
|
1798
|
-
this.isStreaming = false;
|
|
1799
|
-
this.streamContent = "";
|
|
1800
|
-
this.dirty();
|
|
1801
|
-
}
|
|
1802
|
-
toolCallStart(id, name) {
|
|
1803
|
-
this.items.push({ kind: "tool_call", id, name, args: "", done: false });
|
|
1804
|
-
this.dirty();
|
|
1805
|
-
}
|
|
1806
|
-
toolCallArgs(id, delta) {
|
|
1807
|
-
const tc = this.findToolCall(id);
|
|
1808
|
-
if (tc) {
|
|
1809
|
-
tc.args += delta;
|
|
1810
|
-
this.dirty();
|
|
1811
|
-
}
|
|
1812
|
-
}
|
|
1813
|
-
toolCallEnd(id) {
|
|
1814
|
-
const tc = this.findToolCall(id);
|
|
1815
|
-
if (tc) {
|
|
1816
|
-
tc.done = true;
|
|
1817
|
-
this.dirty();
|
|
1818
|
-
}
|
|
1819
|
-
}
|
|
1820
|
-
/** Add a transcription of the user's spoken words (voice mode) */
|
|
1821
|
-
addTranscription(text) {
|
|
1822
|
-
this.items.push({ role: "user", content: text });
|
|
1823
|
-
this.scrollOffset = 0;
|
|
1824
|
-
this.dirty();
|
|
1825
|
-
}
|
|
1826
|
-
/**
|
|
1827
|
-
* Truncate the most recent assistant message (voice interruption).
|
|
1828
|
-
* Replaces its content with the spoken portion and stores the original.
|
|
1829
|
-
*/
|
|
1830
|
-
truncateAssistant(content, originalContent, reason, truncatedAtMs) {
|
|
1831
|
-
if (this.isStreaming) {
|
|
1832
|
-
this.isStreaming = false;
|
|
1833
|
-
this.streamContent = "";
|
|
1834
|
-
}
|
|
1835
|
-
for (let i = this.items.length - 1; i >= 0; i--) {
|
|
1836
|
-
const item = this.items[i];
|
|
1837
|
-
if ("role" in item && item.role === "assistant") {
|
|
1838
|
-
item.content = content;
|
|
1839
|
-
item.truncationReason = reason;
|
|
1840
|
-
item.truncatedAtMs = truncatedAtMs;
|
|
1841
|
-
item.originalContent = originalContent;
|
|
1842
|
-
this.dirty();
|
|
1843
|
-
return;
|
|
1844
|
-
}
|
|
1845
|
-
}
|
|
1846
|
-
this.items.push({
|
|
1847
|
-
role: "assistant",
|
|
1848
|
-
content,
|
|
1849
|
-
truncationReason: reason,
|
|
1850
|
-
truncatedAtMs,
|
|
1851
|
-
originalContent
|
|
1852
|
-
});
|
|
1853
|
-
this.dirty();
|
|
1854
|
-
}
|
|
1855
|
-
toolCallResult(toolCallId, toolName, content) {
|
|
1856
|
-
this.items.push({ kind: "tool_result", toolCallId, toolName, content });
|
|
1857
|
-
this.dirty();
|
|
1858
|
-
}
|
|
1859
|
-
/** Find the tool name for a given tool call ID */
|
|
1860
|
-
findToolCallName(toolCallId) {
|
|
1861
|
-
for (let i = this.items.length - 1; i >= 0; i--) {
|
|
1862
|
-
const item = this.items[i];
|
|
1863
|
-
if (isToolCall(item) && item.id === toolCallId) return item.name;
|
|
1864
|
-
}
|
|
1865
|
-
return "tool";
|
|
1866
|
-
}
|
|
1867
|
-
showError(msg) {
|
|
1868
|
-
this.items.push({ role: "system", content: `Error: ${msg}` });
|
|
1869
|
-
this.dirty();
|
|
1870
|
-
}
|
|
1871
|
-
// ─── Debug panel ──────────────────────────────────────────────────
|
|
1872
|
-
/** Log a WebSocket message to the debug panel and (optionally) log file */
|
|
1873
|
-
logDebug(direction, raw) {
|
|
1874
|
-
const redacted = redactAudio(raw);
|
|
1875
|
-
const now = /* @__PURE__ */ new Date();
|
|
1876
|
-
const ts = `${pad2(now.getHours())}:${pad2(now.getMinutes())}:${pad2(now.getSeconds())}`;
|
|
1877
|
-
if (this.debugLogStream) {
|
|
1878
|
-
const logLine = JSON.stringify({ ts: now.toISOString(), dir: direction, ...redacted });
|
|
1879
|
-
this.debugLogStream.write(logLine + "\n");
|
|
1880
|
-
}
|
|
1881
|
-
const summary = buildDebugSummary(direction, redacted);
|
|
1882
|
-
let detail;
|
|
1883
|
-
try {
|
|
1884
|
-
detail = JSON.stringify(redacted, null, 2);
|
|
1885
|
-
} catch {
|
|
1886
|
-
detail = String(redacted);
|
|
1887
|
-
}
|
|
1888
|
-
this.debugEntries.push({ direction, summary, detail, ts });
|
|
1889
|
-
if (this.debugEntries.length > 500) {
|
|
1890
|
-
this.debugEntries = this.debugEntries.slice(-300);
|
|
1891
|
-
}
|
|
1892
|
-
this.debugLinesCache = null;
|
|
1893
|
-
this.dirty();
|
|
1894
|
-
}
|
|
1895
|
-
// ─── Private helpers ──────────────────────────────────────────────
|
|
1896
|
-
findToolCall(id) {
|
|
1897
|
-
for (let i = this.items.length - 1; i >= 0; i--) {
|
|
1898
|
-
const item = this.items[i];
|
|
1899
|
-
if (isToolCall(item) && item.id === id) return item;
|
|
1900
|
-
}
|
|
1901
|
-
return void 0;
|
|
1902
|
-
}
|
|
1903
|
-
dirty() {
|
|
1904
|
-
this.displayLinesCache = null;
|
|
1905
|
-
this.debugLinesCache = null;
|
|
1906
|
-
this.scheduleRender();
|
|
1907
|
-
}
|
|
1908
|
-
restoreTerminal = () => {
|
|
1909
|
-
process.stdout.write(`${MOUSE_OFF}${CSI}r${ALT_SCREEN_OFF}${SHOW_CURSOR}`);
|
|
1910
|
-
};
|
|
1911
|
-
updateDimensions() {
|
|
1912
|
-
this.rows = process.stdout.rows || 24;
|
|
1913
|
-
this.cols = process.stdout.columns || 80;
|
|
1914
|
-
}
|
|
1915
|
-
/** Width of the chat (left) panel */
|
|
1916
|
-
chatWidth() {
|
|
1917
|
-
if (!this.debugVisible) return this.cols;
|
|
1918
|
-
return Math.max(30, Math.floor(this.cols * 0.6) - 1);
|
|
1919
|
-
}
|
|
1920
|
-
/** Width of the debug (right) panel */
|
|
1921
|
-
debugWidth() {
|
|
1922
|
-
if (!this.debugVisible) return 0;
|
|
1923
|
-
return this.cols - this.chatWidth() - 1;
|
|
1924
|
-
}
|
|
1925
|
-
scrollHeight() {
|
|
1926
|
-
return Math.max(1, this.rows - 2);
|
|
1927
|
-
}
|
|
1928
|
-
pageSize() {
|
|
1929
|
-
return Math.max(1, this.rows - 4);
|
|
1930
|
-
}
|
|
1931
|
-
// ─── Scroll ───────────────────────────────────────────────────────
|
|
1932
|
-
scrollUp(lines) {
|
|
1933
|
-
const total = this.getDisplayLines().length;
|
|
1934
|
-
const max = Math.max(0, total - this.scrollHeight());
|
|
1935
|
-
this.scrollOffset = Math.min(max, this.scrollOffset + lines);
|
|
1936
|
-
this.render();
|
|
1937
|
-
}
|
|
1938
|
-
scrollDown(lines) {
|
|
1939
|
-
this.scrollOffset = Math.max(0, this.scrollOffset - lines);
|
|
1940
|
-
this.render();
|
|
1941
|
-
}
|
|
1942
|
-
scrollToTop() {
|
|
1943
|
-
const total = this.getDisplayLines().length;
|
|
1944
|
-
this.scrollOffset = Math.max(0, total - this.scrollHeight());
|
|
1945
|
-
this.render();
|
|
1946
|
-
}
|
|
1947
|
-
scrollToBottom() {
|
|
1948
|
-
this.scrollOffset = 0;
|
|
1949
|
-
this.render();
|
|
1950
|
-
}
|
|
1951
|
-
// ─── Raw stdin parsing ────────────────────────────────────────────
|
|
1952
|
-
//
|
|
1953
|
-
// We parse stdin directly instead of using readline.emitKeypressEvents
|
|
1954
|
-
// because readline doesn't understand SGR mouse sequences and leaks
|
|
1955
|
-
// their contents into keypress events as regular characters.
|
|
1956
|
-
onData = (data) => {
|
|
1957
|
-
for (let i = 0; i < data.length; i++) {
|
|
1958
|
-
const ch = data[i];
|
|
1959
|
-
if (this.escBuf) {
|
|
1960
|
-
this.escBuf += ch;
|
|
1961
|
-
if (this.escBuf.length === 2) {
|
|
1962
|
-
if (ch !== "[") {
|
|
1963
|
-
this.clearEsc();
|
|
1964
|
-
}
|
|
1965
|
-
continue;
|
|
1966
|
-
}
|
|
1967
|
-
if (/[A-Za-z~]/.test(ch)) {
|
|
1968
|
-
this.handleCSI(this.escBuf.slice(2, -1), ch);
|
|
1969
|
-
this.clearEsc();
|
|
1970
|
-
} else if (this.escBuf.length > 32) {
|
|
1971
|
-
this.clearEsc();
|
|
1972
|
-
}
|
|
1973
|
-
continue;
|
|
1974
|
-
}
|
|
1975
|
-
if (ch === "\x1B") {
|
|
1976
|
-
this.escBuf = "\x1B";
|
|
1977
|
-
this.escTimer = setTimeout(() => {
|
|
1978
|
-
this.escBuf = "";
|
|
1979
|
-
this.escTimer = null;
|
|
1980
|
-
}, 100);
|
|
1981
|
-
continue;
|
|
1982
|
-
}
|
|
1983
|
-
this.handleByte(ch);
|
|
1984
|
-
}
|
|
1985
|
-
};
|
|
1986
|
-
clearEsc() {
|
|
1987
|
-
this.escBuf = "";
|
|
1988
|
-
if (this.escTimer) {
|
|
1989
|
-
clearTimeout(this.escTimer);
|
|
1990
|
-
this.escTimer = null;
|
|
1991
|
-
}
|
|
1992
|
-
}
|
|
1993
|
-
/** Handle a parsed CSI sequence: params is everything between \x1B[ and the final char */
|
|
1994
|
-
handleCSI(params, code) {
|
|
1995
|
-
if (params.startsWith("<") && (code === "M" || code === "m")) {
|
|
1996
|
-
if (code === "m") return;
|
|
1997
|
-
const parts = params.slice(1).split(";");
|
|
1998
|
-
if (parts.length >= 1) {
|
|
1999
|
-
const button = parseInt(parts[0], 10);
|
|
2000
|
-
if (button === 64) this.scrollUp(3);
|
|
2001
|
-
else if (button === 65) this.scrollDown(3);
|
|
2002
|
-
}
|
|
2003
|
-
return;
|
|
2004
|
-
}
|
|
2005
|
-
if (!params) {
|
|
2006
|
-
if (code === "A") {
|
|
2007
|
-
this.scrollUp(1);
|
|
2008
|
-
return;
|
|
2009
|
-
}
|
|
2010
|
-
if (code === "B") {
|
|
2011
|
-
this.scrollDown(1);
|
|
2012
|
-
return;
|
|
2013
|
-
}
|
|
2014
|
-
if (code === "H") {
|
|
2015
|
-
this.scrollToTop();
|
|
2016
|
-
return;
|
|
2017
|
-
}
|
|
2018
|
-
if (code === "F") {
|
|
2019
|
-
this.scrollToBottom();
|
|
2020
|
-
return;
|
|
2021
|
-
}
|
|
2022
|
-
}
|
|
2023
|
-
if (code === "~") {
|
|
2024
|
-
if (params === "5") {
|
|
2025
|
-
this.scrollUp(this.pageSize());
|
|
2026
|
-
return;
|
|
2027
|
-
}
|
|
2028
|
-
if (params === "6") {
|
|
2029
|
-
this.scrollDown(this.pageSize());
|
|
2030
|
-
return;
|
|
2031
|
-
}
|
|
2032
|
-
if (params === "1") {
|
|
2033
|
-
this.scrollToTop();
|
|
2034
|
-
return;
|
|
2035
|
-
}
|
|
2036
|
-
if (params === "4") {
|
|
2037
|
-
this.scrollToBottom();
|
|
2038
|
-
return;
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
/** Handle a single non-escape byte */
|
|
2043
|
-
handleByte(ch) {
|
|
2044
|
-
const code = ch.charCodeAt(0);
|
|
2045
|
-
if (code === 3) {
|
|
2046
|
-
this.stop();
|
|
2047
|
-
this.options.onExit();
|
|
2048
|
-
return;
|
|
2049
|
-
}
|
|
2050
|
-
if (code === 2) {
|
|
2051
|
-
this.scrollUp(this.pageSize());
|
|
2052
|
-
return;
|
|
2053
|
-
}
|
|
2054
|
-
if (code === 6) {
|
|
2055
|
-
this.scrollDown(this.pageSize());
|
|
2056
|
-
return;
|
|
2057
|
-
}
|
|
2058
|
-
if (code === 4) {
|
|
2059
|
-
this.debugPanelVisible = !this.debugPanelVisible;
|
|
2060
|
-
this.displayLinesCache = null;
|
|
2061
|
-
this.debugLinesCache = null;
|
|
2062
|
-
this.render();
|
|
2063
|
-
return;
|
|
2064
|
-
}
|
|
2065
|
-
if (code === 15) {
|
|
2066
|
-
this.expandDetails = !this.expandDetails;
|
|
2067
|
-
this.displayLinesCache = null;
|
|
2068
|
-
this.debugLinesCache = null;
|
|
2069
|
-
this.render();
|
|
2070
|
-
return;
|
|
2071
|
-
}
|
|
2072
|
-
if (code === 13) {
|
|
2073
|
-
this.handleEnter();
|
|
2074
|
-
return;
|
|
2075
|
-
}
|
|
2076
|
-
if (code === 127 || code === 8) {
|
|
2077
|
-
if (this.inputBuffer.length > 0) {
|
|
2078
|
-
this.inputBuffer = this.inputBuffer.slice(0, -1);
|
|
2079
|
-
this.renderInputLine();
|
|
2080
|
-
}
|
|
2081
|
-
return;
|
|
2082
|
-
}
|
|
2083
|
-
if (code < 32) return;
|
|
2084
|
-
this.inputBuffer += ch;
|
|
2085
|
-
this.renderInputLine();
|
|
2086
|
-
}
|
|
2087
|
-
handleEnter() {
|
|
2088
|
-
const text = this.inputBuffer.trim();
|
|
2089
|
-
this.inputBuffer = "";
|
|
2090
|
-
if (!text) {
|
|
2091
|
-
this.renderInputLine();
|
|
2092
|
-
return;
|
|
2093
|
-
}
|
|
2094
|
-
if (text.toLowerCase() === "exit" || text.toLowerCase() === "quit") {
|
|
2095
|
-
this.stop();
|
|
2096
|
-
this.options.onExit();
|
|
2097
|
-
return;
|
|
2098
|
-
}
|
|
2099
|
-
this.items.push({ role: "user", content: text });
|
|
2100
|
-
this.scrollOffset = 0;
|
|
2101
|
-
this.displayLinesCache = null;
|
|
2102
|
-
this.render();
|
|
2103
|
-
this.options.onSend(text);
|
|
2104
|
-
}
|
|
2105
|
-
// ─── Events ───────────────────────────────────────────────────────
|
|
2106
|
-
onResize = () => {
|
|
2107
|
-
this.updateDimensions();
|
|
2108
|
-
process.stdout.write(`${CSI}1;${Math.max(1, this.rows - 2)}r`);
|
|
2109
|
-
this.displayLinesCache = null;
|
|
2110
|
-
this.debugLinesCache = null;
|
|
2111
|
-
this.render();
|
|
2112
|
-
};
|
|
2113
|
-
// ─── Rendering ────────────────────────────────────────────────────
|
|
2114
|
-
scheduleRender() {
|
|
2115
|
-
if (!this.started || this.renderTimer) return;
|
|
2116
|
-
this.renderTimer = setTimeout(() => {
|
|
2117
|
-
this.renderTimer = null;
|
|
2118
|
-
this.render();
|
|
2119
|
-
}, 30);
|
|
2120
|
-
}
|
|
2121
|
-
getDisplayLines() {
|
|
2122
|
-
if (!this.displayLinesCache) {
|
|
2123
|
-
this.displayLinesCache = this.buildDisplayLines();
|
|
2124
|
-
}
|
|
2125
|
-
return this.displayLinesCache;
|
|
2126
|
-
}
|
|
2127
|
-
getDebugLines() {
|
|
2128
|
-
if (!this.debugLinesCache) {
|
|
2129
|
-
this.debugLinesCache = this.buildDebugLines();
|
|
2130
|
-
}
|
|
2131
|
-
return this.debugLinesCache;
|
|
2132
|
-
}
|
|
2133
|
-
render() {
|
|
2134
|
-
if (!this.started) return;
|
|
2135
|
-
this.updateDimensions();
|
|
2136
|
-
const sh = this.scrollHeight();
|
|
2137
|
-
const displayLines = this.getDisplayLines();
|
|
2138
|
-
const total = displayLines.length;
|
|
2139
|
-
const maxOff = Math.max(0, total - sh);
|
|
2140
|
-
if (this.scrollOffset > maxOff) this.scrollOffset = maxOff;
|
|
2141
|
-
let visible;
|
|
2142
|
-
if (total <= sh) {
|
|
2143
|
-
visible = [...new Array(sh - total).fill(""), ...displayLines];
|
|
2144
|
-
} else {
|
|
2145
|
-
const end = total - this.scrollOffset;
|
|
2146
|
-
const start = end - sh;
|
|
2147
|
-
visible = displayLines.slice(start, end);
|
|
2148
|
-
}
|
|
2149
|
-
const buf = [HIDE_CURSOR];
|
|
2150
|
-
if (this.debugVisible) {
|
|
2151
|
-
this.renderSplitScreen(buf, visible, sh);
|
|
2152
|
-
} else {
|
|
2153
|
-
for (let i = 0; i < sh; i++) {
|
|
2154
|
-
buf.push(moveTo(i + 1, 1), visible[i] || "", CLEAR_EOL);
|
|
2155
|
-
}
|
|
2156
|
-
}
|
|
2157
|
-
buf.push(moveTo(this.rows - 1, 1), this.buildSeparator(), CLEAR_EOL);
|
|
2158
|
-
buf.push(moveTo(this.rows, 1), `${BLUE}You:${RESET} ${this.inputBuffer}${CLEAR_EOL}`);
|
|
2159
|
-
buf.push(SHOW_CURSOR);
|
|
2160
|
-
process.stdout.write(buf.join(""));
|
|
2161
|
-
}
|
|
2162
|
-
renderSplitScreen(buf, chatVisible, sh) {
|
|
2163
|
-
const cw = this.chatWidth();
|
|
2164
|
-
const dw = this.debugWidth();
|
|
2165
|
-
const divCol = cw + 1;
|
|
2166
|
-
const debugLines = this.getDebugLines();
|
|
2167
|
-
const debugTotal = debugLines.length;
|
|
2168
|
-
let debugVisible;
|
|
2169
|
-
if (debugTotal <= sh) {
|
|
2170
|
-
debugVisible = [...new Array(sh - debugTotal).fill(""), ...debugLines];
|
|
2171
|
-
} else {
|
|
2172
|
-
debugVisible = debugLines.slice(debugTotal - sh);
|
|
2173
|
-
}
|
|
2174
|
-
for (let i = 0; i < sh; i++) {
|
|
2175
|
-
const chatLine = truncateAnsi(chatVisible[i] || "", cw);
|
|
2176
|
-
const debugLine = truncateAnsi(debugVisible[i] || "", dw);
|
|
2177
|
-
buf.push(
|
|
2178
|
-
moveTo(i + 1, 1),
|
|
2179
|
-
chatLine,
|
|
2180
|
-
CLEAR_EOL,
|
|
2181
|
-
// clear to end handles rest, but we need to position divider
|
|
2182
|
-
moveTo(i + 1, divCol),
|
|
2183
|
-
`${DIM}\u2502${RESET}`,
|
|
2184
|
-
debugLine
|
|
2185
|
-
);
|
|
2186
|
-
buf.push(CLEAR_EOL);
|
|
2187
|
-
}
|
|
2188
|
-
}
|
|
2189
|
-
renderInputLine() {
|
|
2190
|
-
process.stdout.write(
|
|
2191
|
-
`${moveTo(this.rows, 1)}${BLUE}You:${RESET} ${this.inputBuffer}${CLEAR_EOL}`
|
|
2192
|
-
);
|
|
2193
|
-
}
|
|
2194
|
-
// ─── Separator ────────────────────────────────────────────────────
|
|
2195
|
-
buildSeparator() {
|
|
2196
|
-
const hints = [];
|
|
2197
|
-
if (this.scrollOffset > 0) {
|
|
2198
|
-
hints.push(`\u2191${this.scrollOffset}`);
|
|
2199
|
-
}
|
|
2200
|
-
const hasDetails = this.items.some((i) => isToolCall(i) || isToolResult(i)) || this.options.debug;
|
|
2201
|
-
if (hasDetails) {
|
|
2202
|
-
hints.push(`^O ${this.expandDetails ? "collapse" : "details"}`);
|
|
2203
|
-
}
|
|
2204
|
-
if (this.options.debug) {
|
|
2205
|
-
hints.push(`^D ${this.debugPanelVisible ? "hide" : "show"} debug`);
|
|
2206
|
-
}
|
|
2207
|
-
const lineWidth = Math.min(this.cols, 60);
|
|
2208
|
-
if (!hints.length) {
|
|
2209
|
-
return `${GRAY}${"\u2500".repeat(lineWidth)}${RESET}`;
|
|
2210
|
-
}
|
|
2211
|
-
const hintStr = ` ${hints.join(" ")} `;
|
|
2212
|
-
const dashes = Math.max(0, lineWidth - hintStr.length);
|
|
2213
|
-
const left = Math.max(2, Math.floor(dashes / 2));
|
|
2214
|
-
const right = Math.max(2, dashes - left);
|
|
2215
|
-
return `${GRAY}${"\u2500".repeat(left)}${hintStr}${"\u2500".repeat(right)}${RESET}`;
|
|
2216
|
-
}
|
|
2217
|
-
// ─── Chat display line building ───────────────────────────────────
|
|
2218
|
-
buildDisplayLines() {
|
|
2219
|
-
const lines = [];
|
|
2220
|
-
const w = this.chatWidth();
|
|
2221
|
-
for (const item of this.items) {
|
|
2222
|
-
if (isToolCall(item)) {
|
|
2223
|
-
this.appendToolCallLines(lines, item, w);
|
|
2224
|
-
} else if (isToolResult(item)) {
|
|
2225
|
-
this.appendToolResultLines(lines, item, w);
|
|
2226
|
-
} else {
|
|
2227
|
-
this.appendMessageLines(lines, item, w);
|
|
2228
|
-
}
|
|
2229
|
-
lines.push("");
|
|
2230
|
-
}
|
|
2231
|
-
if (this.isStreaming) {
|
|
2232
|
-
this.appendStreamLines(lines, w);
|
|
2233
|
-
}
|
|
2234
|
-
return lines;
|
|
2235
|
-
}
|
|
2236
|
-
appendToolCallLines(lines, tc, width) {
|
|
2237
|
-
const status = tc.done ? "Called" : "Calling";
|
|
2238
|
-
const summary = ` ${GRAY}${status} ${tc.name}${tc.done ? "" : "..."}${RESET}`;
|
|
2239
|
-
if (!this.expandDetails) {
|
|
2240
|
-
lines.push(summary);
|
|
2241
|
-
return;
|
|
2242
|
-
}
|
|
2243
|
-
lines.push(summary);
|
|
2244
|
-
if (tc.args) {
|
|
2245
|
-
let formatted = tc.args;
|
|
2246
|
-
try {
|
|
2247
|
-
formatted = JSON.stringify(JSON.parse(tc.args), null, 2);
|
|
2248
|
-
} catch {
|
|
2249
|
-
}
|
|
2250
|
-
const indent = " ";
|
|
2251
|
-
for (const argLine of formatted.split("\n")) {
|
|
2252
|
-
for (const wl of this.wrapText(argLine, width - indent.length)) {
|
|
2253
|
-
lines.push(`${indent}${GRAY}${wl}${RESET}`);
|
|
2254
|
-
}
|
|
2255
|
-
}
|
|
2256
|
-
}
|
|
2257
|
-
}
|
|
2258
|
-
appendToolResultLines(lines, tr, width) {
|
|
2259
|
-
if (!this.expandDetails) {
|
|
2260
|
-
const maxLen = width - 10;
|
|
2261
|
-
const preview = tr.content.replace(/\n/g, " ").slice(0, maxLen);
|
|
2262
|
-
const truncated = preview.length < tr.content.length ? "..." : "";
|
|
2263
|
-
lines.push(` ${GRAY}\u2192 ${tr.toolName}: ${preview}${truncated}${RESET}`);
|
|
2264
|
-
return;
|
|
2265
|
-
}
|
|
2266
|
-
lines.push(` ${GRAY}\u2192 ${tr.toolName} result:${RESET}`);
|
|
2267
|
-
let formatted = tr.content;
|
|
2268
|
-
try {
|
|
2269
|
-
formatted = JSON.stringify(JSON.parse(tr.content), null, 2);
|
|
2270
|
-
} catch {
|
|
2271
|
-
}
|
|
2272
|
-
const indent = " ";
|
|
2273
|
-
for (const resultLine of formatted.split("\n")) {
|
|
2274
|
-
for (const wl of this.wrapText(resultLine, width - indent.length)) {
|
|
2275
|
-
lines.push(`${indent}${GRAY}${wl}${RESET}`);
|
|
2276
|
-
}
|
|
2277
|
-
}
|
|
2278
|
-
}
|
|
2279
|
-
appendMessageLines(lines, msg, width) {
|
|
2280
|
-
if (msg.role === "system") {
|
|
2281
|
-
for (const para of msg.content.split("\n")) {
|
|
2282
|
-
for (const wl of this.wrapText(para, width - 2)) {
|
|
2283
|
-
lines.push(` ${GRAY}${wl}${RESET}`);
|
|
2284
|
-
}
|
|
2285
|
-
}
|
|
2286
|
-
return;
|
|
2287
|
-
}
|
|
2288
|
-
const isUser = msg.role === "user";
|
|
2289
|
-
const label = isUser ? `${BLUE}You:${RESET} ` : `${GREEN}Assistant:${RESET} `;
|
|
2290
|
-
const labelWidth = isUser ? 5 : 12;
|
|
2291
|
-
const indent = " ".repeat(labelWidth);
|
|
2292
|
-
let first = true;
|
|
2293
|
-
const displayContent = msg.truncationReason ? msg.content.replace(/—interrupted$/, "") : msg.content;
|
|
2294
|
-
for (const para of displayContent.split("\n")) {
|
|
2295
|
-
for (const wl of this.wrapText(para, width - labelWidth)) {
|
|
2296
|
-
lines.push(first ? label + wl : indent + wl);
|
|
2297
|
-
first = false;
|
|
2298
|
-
}
|
|
2299
|
-
}
|
|
2300
|
-
if (msg.truncationReason) {
|
|
2301
|
-
const reasonLabel = getTruncationLabel(msg.truncationReason, msg.truncatedAtMs);
|
|
2302
|
-
lines.push(`${indent}${YELLOW}[${reasonLabel}]${RESET}`);
|
|
2303
|
-
const extra = getExtraContent(msg.content, msg.originalContent);
|
|
2304
|
-
if (extra) {
|
|
2305
|
-
for (const para of extra.split("\n")) {
|
|
2306
|
-
for (const wl of this.wrapText(para, width - labelWidth)) {
|
|
2307
|
-
lines.push(`${indent}${DIM_ITALIC_GRAY}${wl}${RESET}`);
|
|
2308
|
-
}
|
|
2309
|
-
}
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
}
|
|
2313
|
-
appendStreamLines(lines, width) {
|
|
2314
|
-
const label = `${GREEN}Assistant:${RESET} `;
|
|
2315
|
-
const labelWidth = 12;
|
|
2316
|
-
const indent = " ".repeat(labelWidth);
|
|
2317
|
-
if (!this.streamContent) {
|
|
2318
|
-
lines.push(label);
|
|
2319
|
-
return;
|
|
2320
|
-
}
|
|
2321
|
-
let first = true;
|
|
2322
|
-
for (const para of this.streamContent.split("\n")) {
|
|
2323
|
-
for (const wl of this.wrapText(para, width - labelWidth)) {
|
|
2324
|
-
lines.push(first ? label + wl : indent + wl);
|
|
2325
|
-
first = false;
|
|
2326
|
-
}
|
|
2327
|
-
}
|
|
2328
|
-
}
|
|
2329
|
-
// ─── Debug display line building ──────────────────────────────────
|
|
2330
|
-
buildDebugLines() {
|
|
2331
|
-
const lines = [];
|
|
2332
|
-
const dw = this.debugWidth();
|
|
2333
|
-
if (dw <= 0) return lines;
|
|
2334
|
-
for (const entry of this.debugEntries) {
|
|
2335
|
-
const color = entry.direction === "in" ? ORANGE : LIGHT_BLUE;
|
|
2336
|
-
const arrow = entry.direction === "in" ? "\u25BC" : "\u25B2";
|
|
2337
|
-
const prefix = `${DIM}${entry.ts}${RESET} ${color}${arrow}${RESET} `;
|
|
2338
|
-
const prefixWidth = entry.ts.length + 3;
|
|
2339
|
-
if (!this.expandDetails) {
|
|
2340
|
-
const maxSummary = dw - prefixWidth;
|
|
2341
|
-
const summaryText = entry.summary.slice(0, maxSummary);
|
|
2342
|
-
lines.push(`${prefix}${color}${summaryText}${RESET}`);
|
|
2343
|
-
} else {
|
|
2344
|
-
lines.push(`${prefix}${color}${entry.summary}${RESET}`);
|
|
2345
|
-
const indent = " ".repeat(prefixWidth);
|
|
2346
|
-
for (const detailLine of entry.detail.split("\n")) {
|
|
2347
|
-
for (const wl of this.wrapText(detailLine, dw - prefixWidth)) {
|
|
2348
|
-
lines.push(`${indent}${color}${DIM}${wl}${RESET}`);
|
|
2349
|
-
}
|
|
2350
|
-
}
|
|
2351
|
-
lines.push("");
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
return lines;
|
|
2355
|
-
}
|
|
2356
|
-
// ─── Text utilities ───────────────────────────────────────────────
|
|
2357
|
-
wrapText(text, maxWidth) {
|
|
2358
|
-
if (maxWidth <= 0) maxWidth = 1;
|
|
2359
|
-
if (!text) return [""];
|
|
2360
|
-
if (text.length <= maxWidth) return [text];
|
|
2361
|
-
const words = text.split(" ");
|
|
2362
|
-
const result = [];
|
|
2363
|
-
let current = "";
|
|
2364
|
-
for (const word of words) {
|
|
2365
|
-
if (!current) {
|
|
2366
|
-
current = word;
|
|
2367
|
-
} else if (current.length + 1 + word.length <= maxWidth) {
|
|
2368
|
-
current += " " + word;
|
|
2369
|
-
} else {
|
|
2370
|
-
result.push(current);
|
|
2371
|
-
current = word;
|
|
2372
|
-
}
|
|
2373
|
-
}
|
|
2374
|
-
if (current) result.push(current);
|
|
2375
|
-
return result.length ? result : [""];
|
|
2376
|
-
}
|
|
2377
|
-
};
|
|
2378
|
-
function pad2(n) {
|
|
2379
|
-
return n < 10 ? `0${n}` : String(n);
|
|
2380
|
-
}
|
|
2381
|
-
function truncateAnsi(s, maxCols) {
|
|
2382
|
-
if (maxCols <= 0) return "";
|
|
2383
|
-
let visible = 0;
|
|
2384
|
-
let i = 0;
|
|
2385
|
-
while (i < s.length && visible < maxCols) {
|
|
2386
|
-
if (s[i] === "\x1B") {
|
|
2387
|
-
const end = s.indexOf("m", i);
|
|
2388
|
-
if (end !== -1) {
|
|
2389
|
-
i = end + 1;
|
|
2390
|
-
} else {
|
|
2391
|
-
i++;
|
|
2392
|
-
}
|
|
2393
|
-
} else {
|
|
2394
|
-
visible++;
|
|
2395
|
-
i++;
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
return s.slice(0, i) + RESET;
|
|
2399
|
-
}
|
|
2400
|
-
function redactAudio(msg) {
|
|
2401
|
-
if (!msg || typeof msg !== "object") return msg;
|
|
2402
|
-
const clone = { ...msg };
|
|
2403
|
-
if (clone.type === "CUSTOM" && clone.value && typeof clone.value === "object") {
|
|
2404
|
-
const name = clone.name || "";
|
|
2405
|
-
if (name === "voice.audio" && clone.value.data) {
|
|
2406
|
-
clone.value = { ...clone.value, data: "[audio]" };
|
|
2407
|
-
}
|
|
2408
|
-
}
|
|
2409
|
-
return clone;
|
|
2410
|
-
}
|
|
2411
|
-
function buildDebugSummary(_direction, msg) {
|
|
2412
|
-
if (!msg || typeof msg !== "object") return String(msg);
|
|
2413
|
-
if (msg.type === "CUSTOM") {
|
|
2414
|
-
const name = msg.name || "?";
|
|
2415
|
-
if (name === "voice.audio") return name;
|
|
2416
|
-
if (msg.value && typeof msg.value === "object") {
|
|
2417
|
-
const keys = Object.keys(msg.value);
|
|
2418
|
-
if (keys.length <= 3) {
|
|
2419
|
-
return `${name} {${keys.join(", ")}}`;
|
|
2420
|
-
}
|
|
2421
|
-
return `${name} {${keys.length} keys}`;
|
|
2422
|
-
}
|
|
2423
|
-
return name;
|
|
2424
|
-
}
|
|
2425
|
-
const type = msg.type || "?";
|
|
2426
|
-
if (msg.delta) {
|
|
2427
|
-
const preview = msg.delta.length > 30 ? msg.delta.slice(0, 30) + "..." : msg.delta;
|
|
2428
|
-
return `${type} "${preview}"`;
|
|
2429
|
-
}
|
|
2430
|
-
if (msg.toolCallName) {
|
|
2431
|
-
return `${type} ${msg.toolCallName}`;
|
|
2432
|
-
}
|
|
2433
|
-
return type;
|
|
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";
|
|
2440
|
-
var SAMPLE_RATE = 24e3;
|
|
2441
|
-
var MIC_CHUNK_BYTES = 4800;
|
|
2442
|
-
function checkSoxAvailable() {
|
|
2443
|
-
try {
|
|
2444
|
-
execSync2("which sox", { stdio: "pipe" });
|
|
2445
|
-
return true;
|
|
2446
|
-
} catch {
|
|
2447
|
-
return false;
|
|
2448
|
-
}
|
|
2449
|
-
}
|
|
2450
|
-
async function listInputDevices() {
|
|
2451
|
-
return listDevices("rec");
|
|
2452
|
-
}
|
|
2453
|
-
async function listOutputDevices() {
|
|
2454
|
-
return listDevices("play");
|
|
2455
|
-
}
|
|
2456
|
-
function listDevices(command) {
|
|
2457
|
-
return new Promise((resolve2) => {
|
|
2458
|
-
try {
|
|
2459
|
-
const proc = spawnSync(command, ["--list-devices"], {
|
|
2460
|
-
encoding: "utf-8",
|
|
2461
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
2462
|
-
timeout: 5e3
|
|
2463
|
-
});
|
|
2464
|
-
const output2 = (proc.stdout ?? "") + (proc.stderr ?? "");
|
|
2465
|
-
const devices = [];
|
|
2466
|
-
const lines = output2.split("\n");
|
|
2467
|
-
for (const line of lines) {
|
|
2468
|
-
const match = line.match(/(?:Input|Output)\s+Device\s+(\d+)\s*:\s*(.+)/i);
|
|
2469
|
-
if (match && match[1] && match[2]) {
|
|
2470
|
-
devices.push({
|
|
2471
|
-
id: match[1],
|
|
2472
|
-
name: match[2].trim(),
|
|
2473
|
-
isDefault: devices.length === 0
|
|
2474
|
-
// First device is typically the default
|
|
2475
|
-
});
|
|
2476
|
-
}
|
|
2477
|
-
}
|
|
2478
|
-
if (devices.length === 0) {
|
|
2479
|
-
devices.push({ id: "default", name: "System Default", isDefault: true });
|
|
2480
|
-
}
|
|
2481
|
-
resolve2(devices);
|
|
2482
|
-
} catch {
|
|
2483
|
-
resolve2([{ id: "default", name: "System Default", isDefault: true }]);
|
|
2484
|
-
}
|
|
2485
|
-
});
|
|
2486
|
-
}
|
|
2487
|
-
async function selectDevice(devices, label) {
|
|
2488
|
-
if (devices.length <= 1) {
|
|
2489
|
-
return devices[0];
|
|
2490
|
-
}
|
|
2491
|
-
const rl = readline4.createInterface({
|
|
2492
|
-
input: process.stdin,
|
|
2493
|
-
output: process.stderr,
|
|
2494
|
-
terminal: true
|
|
2495
|
-
});
|
|
2496
|
-
process.stderr.write(`
|
|
2497
|
-
${label}:
|
|
2498
|
-
`);
|
|
2499
|
-
for (let i = 0; i < devices.length; i++) {
|
|
2500
|
-
const dev = devices[i];
|
|
2501
|
-
const marker = dev.isDefault ? " (default)" : "";
|
|
2502
|
-
process.stderr.write(` ${i + 1}. ${dev.name}${marker}
|
|
2503
|
-
`);
|
|
2504
|
-
}
|
|
2505
|
-
return new Promise((resolve2) => {
|
|
2506
|
-
const defaultIdx = devices.findIndex((d) => d.isDefault);
|
|
2507
|
-
const defaultNum = defaultIdx >= 0 ? defaultIdx + 1 : 1;
|
|
2508
|
-
rl.question(` Select [${defaultNum}]: `, (answer) => {
|
|
2509
|
-
rl.close();
|
|
2510
|
-
const num = answer.trim() ? parseInt(answer.trim(), 10) : defaultNum;
|
|
2511
|
-
const idx = Math.max(0, Math.min(devices.length - 1, num - 1));
|
|
2512
|
-
resolve2(devices[idx]);
|
|
2513
|
-
});
|
|
2514
|
-
});
|
|
2515
|
-
}
|
|
2516
|
-
var MicCapture = class {
|
|
2517
|
-
process = null;
|
|
2518
|
-
onData;
|
|
2519
|
-
buffer = Buffer.alloc(0);
|
|
2520
|
-
constructor(onData) {
|
|
2521
|
-
this.onData = onData;
|
|
2522
|
-
}
|
|
2523
|
-
/** Start recording from the microphone */
|
|
2524
|
-
start(deviceId) {
|
|
2525
|
-
const args = [
|
|
2526
|
-
"-q",
|
|
2527
|
-
// quiet
|
|
2528
|
-
"-t",
|
|
2529
|
-
"raw",
|
|
2530
|
-
// raw PCM output
|
|
2531
|
-
"-r",
|
|
2532
|
-
String(SAMPLE_RATE),
|
|
2533
|
-
"-e",
|
|
2534
|
-
"signed-integer",
|
|
2535
|
-
"-b",
|
|
2536
|
-
"16",
|
|
2537
|
-
// 16-bit
|
|
2538
|
-
"-c",
|
|
2539
|
-
"1",
|
|
2540
|
-
// mono
|
|
2541
|
-
"-L",
|
|
2542
|
-
// little-endian
|
|
2543
|
-
"-"
|
|
2544
|
-
// stdout
|
|
2545
|
-
];
|
|
2546
|
-
const env = { ...process.env };
|
|
2547
|
-
if (deviceId && deviceId !== "default") {
|
|
2548
|
-
env.AUDIODEV = deviceId;
|
|
2549
|
-
}
|
|
2550
|
-
this.process = spawn("rec", args, {
|
|
2551
|
-
env,
|
|
2552
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
2553
|
-
});
|
|
2554
|
-
this.process.stdout?.on("data", (data) => {
|
|
2555
|
-
this.buffer = Buffer.concat([this.buffer, data]);
|
|
2556
|
-
while (this.buffer.length >= MIC_CHUNK_BYTES) {
|
|
2557
|
-
const chunk = this.buffer.subarray(0, MIC_CHUNK_BYTES);
|
|
2558
|
-
this.buffer = this.buffer.subarray(MIC_CHUNK_BYTES);
|
|
2559
|
-
this.onData(Buffer.from(chunk));
|
|
2560
|
-
}
|
|
2561
|
-
});
|
|
2562
|
-
this.process.stderr?.on("data", (data) => {
|
|
2563
|
-
const msg = data.toString().trim();
|
|
2564
|
-
if (msg && !msg.includes("WARN")) {
|
|
2565
|
-
process.stderr.write(`[mic] ${msg}
|
|
2566
|
-
`);
|
|
2567
|
-
}
|
|
2568
|
-
});
|
|
2569
|
-
this.process.on("error", (err) => {
|
|
2570
|
-
process.stderr.write(`[mic] Process error: ${err.message}
|
|
2571
|
-
`);
|
|
2572
|
-
});
|
|
2573
|
-
}
|
|
2574
|
-
/** Stop recording */
|
|
2575
|
-
stop() {
|
|
2576
|
-
if (this.process) {
|
|
2577
|
-
this.process.kill("SIGTERM");
|
|
2578
|
-
this.process = null;
|
|
2579
|
-
}
|
|
2580
|
-
this.buffer = Buffer.alloc(0);
|
|
2581
|
-
}
|
|
2582
|
-
};
|
|
2583
|
-
var AudioPlayer = class extends EventEmitter2 {
|
|
2584
|
-
process = null;
|
|
2585
|
-
deviceId;
|
|
2586
|
-
// When true, we own the SoX process's stdin and are actively streaming to it.
|
|
2587
|
-
// When false with a process, the stdin has been closed and SoX is draining.
|
|
2588
|
-
streaming = false;
|
|
2589
|
-
// Buffer chunks when another response is already playing
|
|
2590
|
-
pendingChunks = [];
|
|
2591
|
-
pendingBytes = 0;
|
|
2592
|
-
// Queue of complete response buffers waiting to play
|
|
2593
|
-
queue = [];
|
|
2594
|
-
// Playback position tracking for barge-in
|
|
2595
|
-
currentMessageId;
|
|
2596
|
-
playbackStartTime = 0;
|
|
2597
|
-
// Date.now() when SoX started
|
|
2598
|
-
bytesWritten = 0;
|
|
2599
|
-
// total bytes piped to SoX for duration estimation
|
|
2600
|
-
constructor(deviceId) {
|
|
2601
|
-
super();
|
|
2602
|
-
this.deviceId = deviceId;
|
|
2603
|
-
}
|
|
2604
|
-
/** Check if audio is currently playing or queued */
|
|
2605
|
-
get isPlaying() {
|
|
2606
|
-
return this.process !== null || this.queue.length > 0;
|
|
2607
|
-
}
|
|
2608
|
-
/** Get the message ID of the currently playing audio */
|
|
2609
|
-
get playingMessageId() {
|
|
2610
|
-
return this.currentMessageId;
|
|
2611
|
-
}
|
|
2612
|
-
/** Duration of audio written so far in ms */
|
|
2613
|
-
get currentDurationMs() {
|
|
2614
|
-
return this.bytesWritten / (SAMPLE_RATE * 2) * 1e3;
|
|
2615
|
-
}
|
|
2616
|
-
/**
|
|
2617
|
-
* Estimate current playback position in milliseconds.
|
|
2618
|
-
* Uses wall-clock time since SoX started playing.
|
|
2619
|
-
*/
|
|
2620
|
-
getPlaybackTimeMs() {
|
|
2621
|
-
if (!this.process || this.playbackStartTime === 0) return null;
|
|
2622
|
-
const elapsed = Date.now() - this.playbackStartTime;
|
|
2623
|
-
return Math.min(elapsed, this.currentDurationMs);
|
|
2624
|
-
}
|
|
2625
|
-
/**
|
|
2626
|
-
* Stream PCM16 audio data for playback.
|
|
2627
|
-
* On the first chunk, spawns SoX and begins playback immediately.
|
|
2628
|
-
*/
|
|
2629
|
-
write(pcm16) {
|
|
2630
|
-
if (this.process && !this.streaming) {
|
|
2631
|
-
this.pendingChunks.push(pcm16);
|
|
2632
|
-
this.pendingBytes += pcm16.length;
|
|
2633
|
-
return;
|
|
2634
|
-
}
|
|
2635
|
-
if (!this.process) {
|
|
2636
|
-
this.spawnSoxProcess();
|
|
2637
|
-
}
|
|
2638
|
-
this.process.stdin.write(pcm16);
|
|
2639
|
-
this.bytesWritten += pcm16.length;
|
|
2640
|
-
}
|
|
2641
|
-
/**
|
|
2642
|
-
* End the current audio response.
|
|
2643
|
-
* If streaming, closes stdin to let SoX finish playing.
|
|
2644
|
-
* If buffering behind another response, adds to the queue.
|
|
2645
|
-
*/
|
|
2646
|
-
endResponse(messageId) {
|
|
2647
|
-
if (this.streaming && this.process) {
|
|
2648
|
-
this.currentMessageId = messageId;
|
|
2649
|
-
this.streaming = false;
|
|
2650
|
-
this.process.stdin.end();
|
|
2651
|
-
} else if (this.pendingChunks.length > 0) {
|
|
2652
|
-
const audio = Buffer.concat(this.pendingChunks);
|
|
2653
|
-
this.pendingChunks = [];
|
|
2654
|
-
this.pendingBytes = 0;
|
|
2655
|
-
this.queue.push({ audio, messageId });
|
|
2656
|
-
}
|
|
2657
|
-
}
|
|
2658
|
-
/** Stop current playback, discard all queued audio */
|
|
2659
|
-
stop() {
|
|
2660
|
-
this.pendingChunks = [];
|
|
2661
|
-
this.pendingBytes = 0;
|
|
2662
|
-
this.queue = [];
|
|
2663
|
-
this.streaming = false;
|
|
2664
|
-
const wasPlaying = this.process !== null;
|
|
2665
|
-
if (this.process) {
|
|
2666
|
-
try {
|
|
2667
|
-
this.process.stdin?.destroy();
|
|
2668
|
-
this.process.kill("SIGTERM");
|
|
2669
|
-
} catch {
|
|
2670
|
-
}
|
|
2671
|
-
this.process = null;
|
|
2672
|
-
}
|
|
2673
|
-
this.currentMessageId = void 0;
|
|
2674
|
-
this.playbackStartTime = 0;
|
|
2675
|
-
this.bytesWritten = 0;
|
|
2676
|
-
if (wasPlaying) {
|
|
2677
|
-
this.emit("done");
|
|
2678
|
-
}
|
|
2679
|
-
}
|
|
2680
|
-
/** Spawn a SoX `play` process reading raw PCM from stdin */
|
|
2681
|
-
spawnSoxProcess() {
|
|
2682
|
-
this.bytesWritten = 0;
|
|
2683
|
-
this.currentMessageId = void 0;
|
|
2684
|
-
const args = [
|
|
2685
|
-
"-q",
|
|
2686
|
-
// quiet
|
|
2687
|
-
"-t",
|
|
2688
|
-
"raw",
|
|
2689
|
-
"-r",
|
|
2690
|
-
String(SAMPLE_RATE),
|
|
2691
|
-
"-e",
|
|
2692
|
-
"signed-integer",
|
|
2693
|
-
"-b",
|
|
2694
|
-
"16",
|
|
2695
|
-
"-c",
|
|
2696
|
-
"1",
|
|
2697
|
-
"-L",
|
|
2698
|
-
// little-endian
|
|
2699
|
-
"-"
|
|
2700
|
-
// read from stdin
|
|
2701
|
-
];
|
|
2702
|
-
const env = { ...process.env };
|
|
2703
|
-
if (this.deviceId && this.deviceId !== "default") {
|
|
2704
|
-
env.AUDIODEV = this.deviceId;
|
|
2705
|
-
}
|
|
2706
|
-
const proc = spawn("play", args, {
|
|
2707
|
-
env,
|
|
2708
|
-
stdio: ["pipe", "pipe", "pipe"]
|
|
2709
|
-
});
|
|
2710
|
-
proc.on("error", (err) => {
|
|
2711
|
-
process.stderr.write(`[player] Process error: ${err.message}
|
|
2712
|
-
`);
|
|
2713
|
-
});
|
|
2714
|
-
proc.on("exit", (code, _signal) => {
|
|
2715
|
-
if (code !== null && code !== 0 && code !== 143) {
|
|
2716
|
-
process.stderr.write(`[player] SoX exited with code ${code}
|
|
2717
|
-
`);
|
|
2718
|
-
}
|
|
2719
|
-
if (this.process === proc) {
|
|
2720
|
-
this.process = null;
|
|
2721
|
-
this.streaming = false;
|
|
2722
|
-
if (this.queue.length > 0) {
|
|
2723
|
-
const next = this.queue.shift();
|
|
2724
|
-
this.playQueued(next.audio, next.messageId);
|
|
2725
|
-
} else {
|
|
2726
|
-
this.emit("done");
|
|
2727
|
-
}
|
|
2728
|
-
}
|
|
2729
|
-
});
|
|
2730
|
-
proc.stderr?.on("data", (data) => {
|
|
2731
|
-
const msg = data.toString().trim();
|
|
2732
|
-
if (msg) {
|
|
2733
|
-
process.stderr.write(`[player:sox] ${msg}
|
|
2734
|
-
`);
|
|
2735
|
-
}
|
|
2736
|
-
});
|
|
2737
|
-
proc.stdout?.resume();
|
|
2738
|
-
this.process = proc;
|
|
2739
|
-
this.streaming = true;
|
|
2740
|
-
this.playbackStartTime = Date.now();
|
|
2741
|
-
this.emit("playing");
|
|
2742
|
-
}
|
|
2743
|
-
/** Play a complete pre-buffered response (from the queue) */
|
|
2744
|
-
playQueued(audio, messageId) {
|
|
2745
|
-
this.spawnSoxProcess();
|
|
2746
|
-
this.currentMessageId = messageId;
|
|
2747
|
-
this.process.stdin.write(audio);
|
|
2748
|
-
this.bytesWritten = audio.length;
|
|
2749
|
-
this.process.stdin.end();
|
|
2750
|
-
this.streaming = false;
|
|
2751
|
-
}
|
|
2752
|
-
};
|
|
2753
|
-
|
|
2754
|
-
// src/lib/chat-session.ts
|
|
2755
|
-
async function runChatSession(session, args) {
|
|
2756
|
-
const debug = !!args.debug;
|
|
2757
|
-
const isVoice = !!args.voice;
|
|
2758
|
-
const isRealtime = !!args.realtime || isVoice;
|
|
2759
|
-
const message = typeof args.message === "string" ? args.message : void 0;
|
|
2760
|
-
const autoApprove = !!args["auto-approve"];
|
|
2761
|
-
if (args["list-devices"]) {
|
|
2762
|
-
await listAudioDevices();
|
|
2763
|
-
return;
|
|
2764
|
-
}
|
|
2765
|
-
if (!message && !process.stdin.isTTY) {
|
|
2766
|
-
throw new CLIError(
|
|
2767
|
-
"Interactive mode requires a TTY. Use --message for non-interactive mode."
|
|
2768
|
-
);
|
|
2769
|
-
}
|
|
2770
|
-
const allowWrite = !!args["allow-write"] || !!session.allowWrite;
|
|
2771
|
-
if (message) {
|
|
2772
|
-
await singleTurnChatSession(session, message, debug, autoApprove, allowWrite);
|
|
2773
|
-
} else if (isVoice) {
|
|
2774
|
-
await voiceChatSession(session, vadMode(args), args, debug);
|
|
2775
|
-
} else if (isRealtime) {
|
|
2776
|
-
await realtimeChatSession(session, vadMode(args), debug);
|
|
2777
|
-
} else {
|
|
2778
|
-
await interactiveChatSession(session, debug, autoApprove, allowWrite);
|
|
2779
|
-
}
|
|
2780
|
-
}
|
|
2781
|
-
function vadMode(args) {
|
|
2782
|
-
return typeof args.vad === "string" ? args.vad : "server";
|
|
2783
|
-
}
|
|
2784
|
-
async function singleTurnChatSession(session, message, debug, autoApprove = false, allowWrite = false) {
|
|
2785
|
-
const aguiClient = new AGUIClient({
|
|
2786
|
-
baseUrl: session.ctx.envConfig.apiUrl,
|
|
2787
|
-
workspaceUuid: session.workspaceUuid,
|
|
2788
|
-
chatId: session.chatId,
|
|
2789
|
-
endpointUrl: session.aguiEndpointUrl,
|
|
2790
|
-
accessToken: session.ctx.accessToken,
|
|
2791
|
-
debug,
|
|
2792
|
-
model: session.model,
|
|
2793
|
-
appContext: session.appContext
|
|
2794
|
-
});
|
|
2795
|
-
const onEvent = (event) => {
|
|
2796
|
-
const type = event.type?.toLowerCase() ?? "";
|
|
2797
|
-
if (type === "tool_call_start") {
|
|
2798
|
-
const name = event.toolCallName ?? event.name ?? "";
|
|
2799
|
-
process.stderr.write(` [${name}]
|
|
2800
|
-
`);
|
|
2801
|
-
}
|
|
2802
|
-
};
|
|
2803
|
-
const { streamText, hasToolCalls } = await runConciergeAguiLoop(
|
|
2804
|
-
aguiClient,
|
|
2805
|
-
[{ role: "user", content: message }],
|
|
2806
|
-
onEvent,
|
|
2807
|
-
{
|
|
2808
|
-
autoApprove,
|
|
2809
|
-
allowWrite: allowWrite || !!session.allowWrite,
|
|
2810
|
-
debug,
|
|
2811
|
-
trustCwd: session.trustCwd,
|
|
2812
|
-
trustDir: session.trustDir,
|
|
2813
|
-
nonInteractive: true
|
|
2814
|
-
}
|
|
2815
|
-
);
|
|
2816
|
-
if (streamText) {
|
|
2817
|
-
process.stdout.write(`\x1B[32mAssistant:\x1B[0m ${streamText}
|
|
2818
|
-
`);
|
|
2819
|
-
return;
|
|
2820
|
-
}
|
|
2821
|
-
if (hasToolCalls) {
|
|
2822
|
-
process.stderr.write(" (waiting for completion...)\n");
|
|
2823
|
-
const maxWaitMs = 18e4;
|
|
2824
|
-
const pollIntervalMs = 2e3;
|
|
2825
|
-
const startTime = Date.now();
|
|
2826
|
-
let finalText = "";
|
|
2827
|
-
while (Date.now() - startTime < maxWaitMs) {
|
|
2828
|
-
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
2829
|
-
try {
|
|
2830
|
-
const msgs = await session.loadMessages();
|
|
2831
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
2832
|
-
const m = msgs[i];
|
|
2833
|
-
if (m.role === "assistant" && m.content && !m.tool_calls?.length) {
|
|
2834
|
-
finalText = m.content;
|
|
2835
|
-
break;
|
|
2836
|
-
}
|
|
2837
|
-
}
|
|
2838
|
-
if (finalText) break;
|
|
2839
|
-
} catch {
|
|
2840
|
-
}
|
|
2841
|
-
}
|
|
2842
|
-
process.stdout.write(`\x1B[32mAssistant:\x1B[0m ${finalText || "(no text response)"}
|
|
2843
|
-
`);
|
|
2844
|
-
} else {
|
|
2845
|
-
process.stdout.write(`\x1B[32mAssistant:\x1B[0m (no response)
|
|
2846
|
-
`);
|
|
2847
|
-
}
|
|
2848
|
-
}
|
|
2849
|
-
async function interactiveChatSession(session, debug, autoApprove, allowWrite) {
|
|
2850
|
-
const aguiClient = new AGUIClient({
|
|
2851
|
-
baseUrl: session.ctx.envConfig.apiUrl,
|
|
2852
|
-
workspaceUuid: session.workspaceUuid,
|
|
2853
|
-
chatId: session.chatId,
|
|
2854
|
-
endpointUrl: session.aguiEndpointUrl,
|
|
2855
|
-
accessToken: session.ctx.accessToken,
|
|
2856
|
-
debug,
|
|
2857
|
-
model: session.model,
|
|
2858
|
-
appContext: session.appContext
|
|
2859
|
-
});
|
|
2860
|
-
clearScreen();
|
|
2861
|
-
console.log("=".repeat(60));
|
|
2862
|
-
console.log(session.label);
|
|
2863
|
-
console.log("=".repeat(60));
|
|
2864
|
-
console.log(`Chat: ${session.chatTitle}`);
|
|
2865
|
-
console.log("Type your message and press Enter. Type 'exit' or Ctrl+C to quit.");
|
|
2866
|
-
console.log("=".repeat(60));
|
|
2867
|
-
console.log("");
|
|
2868
|
-
const existingMessages = await session.loadMessages();
|
|
2869
|
-
if (existingMessages.length > 0) {
|
|
2870
|
-
console.log("\x1B[90m--- Previous messages ---\x1B[0m\n");
|
|
2871
|
-
for (const msg of existingMessages.slice(-10)) {
|
|
2872
|
-
const text = typeof msg.content === "string" ? msg.content : "";
|
|
2873
|
-
if (text && text.trim()) {
|
|
2874
|
-
printMessage(text, msg.role);
|
|
2875
|
-
}
|
|
2876
|
-
}
|
|
2877
|
-
console.log("\x1B[90m--- End of history ---\x1B[0m\n");
|
|
2878
|
-
}
|
|
2879
|
-
const conversationMessages = [];
|
|
2880
|
-
for (const msg of existingMessages.slice(-10)) {
|
|
2881
|
-
const text = typeof msg.content === "string" ? msg.content : "";
|
|
2882
|
-
if (text && text.trim() && (msg.role === "user" || msg.role === "assistant")) {
|
|
2883
|
-
conversationMessages.push({ role: msg.role, content: text });
|
|
2884
|
-
}
|
|
2885
|
-
}
|
|
2886
|
-
const rl = readline5.createInterface({ input: process.stdin, output: process.stdout });
|
|
2887
|
-
return new Promise((resolve2) => {
|
|
2888
|
-
const prompt = () => {
|
|
2889
|
-
rl.question("\x1B[34mYou:\x1B[0m ", async (input2) => {
|
|
2890
|
-
const trimmed = input2.trim();
|
|
2891
|
-
if (!trimmed) {
|
|
2892
|
-
prompt();
|
|
2893
|
-
return;
|
|
2894
|
-
}
|
|
2895
|
-
if (trimmed.toLowerCase() === "exit" || trimmed.toLowerCase() === "quit") {
|
|
2896
|
-
console.log("\nGoodbye!");
|
|
2897
|
-
rl.close();
|
|
2898
|
-
return;
|
|
2899
|
-
}
|
|
2900
|
-
conversationMessages.push({ role: "user", content: trimmed });
|
|
2901
|
-
process.stdout.write("\n\x1B[32mAssistant:\x1B[0m ");
|
|
2902
|
-
let assistantContent = "";
|
|
2903
|
-
try {
|
|
2904
|
-
const { streamText } = await runConciergeAguiLoop(
|
|
2905
|
-
aguiClient,
|
|
2906
|
-
conversationMessages,
|
|
2907
|
-
(event) => {
|
|
2908
|
-
const delta = getTextDelta(event);
|
|
2909
|
-
if (delta) {
|
|
2910
|
-
process.stdout.write(delta);
|
|
2911
|
-
assistantContent += delta;
|
|
2912
|
-
}
|
|
2913
|
-
const errorMsg = getErrorMessage(event);
|
|
2914
|
-
if (errorMsg) console.log(`
|
|
2915
|
-
\x1B[31mError: ${errorMsg}\x1B[0m`);
|
|
2916
|
-
},
|
|
2917
|
-
{
|
|
2918
|
-
autoApprove,
|
|
2919
|
-
allowWrite: allowWrite || !!session.allowWrite,
|
|
2920
|
-
debug,
|
|
2921
|
-
trustCwd: session.trustCwd,
|
|
2922
|
-
trustDir: session.trustDir
|
|
2923
|
-
}
|
|
2924
|
-
);
|
|
2925
|
-
if (streamText && !assistantContent) {
|
|
2926
|
-
assistantContent = streamText;
|
|
2927
|
-
process.stdout.write(streamText);
|
|
2928
|
-
}
|
|
2929
|
-
if (assistantContent) {
|
|
2930
|
-
conversationMessages.push({ role: "assistant", content: assistantContent });
|
|
2931
|
-
}
|
|
2932
|
-
console.log("\n");
|
|
2933
|
-
} catch (error) {
|
|
2934
|
-
console.log(
|
|
2935
|
-
`
|
|
2936
|
-
\x1B[31mError: ${error instanceof Error ? error.message : error}\x1B[0m
|
|
2937
|
-
`
|
|
2938
|
-
);
|
|
2939
|
-
}
|
|
2940
|
-
prompt();
|
|
2941
|
-
});
|
|
2942
|
-
};
|
|
2943
|
-
rl.on("close", () => {
|
|
2944
|
-
console.log("\nGoodbye!");
|
|
2945
|
-
resolve2();
|
|
2946
|
-
});
|
|
2947
|
-
prompt();
|
|
2948
|
-
});
|
|
2949
|
-
}
|
|
2950
|
-
function buildHistory(messages) {
|
|
2951
|
-
const history = [];
|
|
2952
|
-
for (const msg of messages.slice(-30)) {
|
|
2953
|
-
const text = typeof msg.content === "string" ? msg.content : "";
|
|
2954
|
-
if (msg.role === "user" || msg.role === "assistant") {
|
|
2955
|
-
if (text.trim()) {
|
|
2956
|
-
history.push({ role: msg.role, content: text });
|
|
2957
|
-
}
|
|
2958
|
-
if (msg.role === "assistant" && msg.tool_calls) {
|
|
2959
|
-
for (const tc of msg.tool_calls) {
|
|
2960
|
-
history.push({
|
|
2961
|
-
kind: "tool_call",
|
|
2962
|
-
id: tc.id,
|
|
2963
|
-
name: tc.function.name,
|
|
2964
|
-
args: tc.function.arguments || "",
|
|
2965
|
-
done: true
|
|
2966
|
-
});
|
|
2967
|
-
}
|
|
2968
|
-
}
|
|
2969
|
-
} else if (msg.role === "tool") {
|
|
2970
|
-
const toolName = msg.name || "tool";
|
|
2971
|
-
history.push({
|
|
2972
|
-
kind: "tool_result",
|
|
2973
|
-
toolCallId: msg.tool_call_id || "",
|
|
2974
|
-
toolName,
|
|
2975
|
-
content: text
|
|
2976
|
-
});
|
|
2977
|
-
}
|
|
2978
|
-
}
|
|
2979
|
-
return history;
|
|
2980
|
-
}
|
|
2981
|
-
function wireRealtimeEvents(client, tui) {
|
|
2982
|
-
client.on("text_start", () => tui.streamStart());
|
|
2983
|
-
client.on("text_delta", (data) => tui.streamDelta(data.delta));
|
|
2984
|
-
client.on("text_end", () => tui.streamEnd());
|
|
2985
|
-
client.on("tool_call_start", (data) => {
|
|
2986
|
-
tui.toolCallStart(data.toolCallId, data.toolCallName);
|
|
2987
|
-
});
|
|
2988
|
-
client.on("tool_call_args", (data) => {
|
|
2989
|
-
tui.toolCallArgs(data.toolCallId, data.delta);
|
|
2990
|
-
});
|
|
2991
|
-
client.on("tool_call_end", (data) => {
|
|
2992
|
-
tui.toolCallEnd(data.toolCallId);
|
|
2993
|
-
});
|
|
2994
|
-
client.on("tool_call_result", (data) => {
|
|
2995
|
-
const toolName = tui.findToolCallName(data.toolCallId);
|
|
2996
|
-
const content = typeof data.result === "string" ? data.result : JSON.stringify(data.result);
|
|
2997
|
-
tui.toolCallResult(data.toolCallId, toolName, content);
|
|
2998
|
-
});
|
|
2999
|
-
client.on("run_error", (data) => {
|
|
3000
|
-
tui.showError(data.message);
|
|
3001
|
-
});
|
|
3002
|
-
client.on("error", (err) => {
|
|
3003
|
-
tui.showError(`Connection: ${err.message}`);
|
|
3004
|
-
});
|
|
3005
|
-
client.on("ws_recv", (msg) => tui.logDebug("in", msg));
|
|
3006
|
-
client.on("ws_send", (msg) => tui.logDebug("out", msg));
|
|
3007
|
-
}
|
|
3008
|
-
async function realtimeChatSession(session, vad, debug) {
|
|
3009
|
-
const client = new RealtimeClient({
|
|
3010
|
-
url: session.wsUrl,
|
|
3011
|
-
token: session.ctx.accessToken,
|
|
3012
|
-
chatId: session.chatId,
|
|
3013
|
-
vadMode: vad,
|
|
3014
|
-
debug
|
|
3015
|
-
});
|
|
3016
|
-
process.stderr.write(`Connecting to ${session.label.toLowerCase()}...
|
|
3017
|
-
`);
|
|
3018
|
-
try {
|
|
3019
|
-
await client.connect();
|
|
3020
|
-
} catch (err) {
|
|
3021
|
-
throw new CLIError(`Failed to connect: ${err instanceof Error ? err.message : err}`);
|
|
3022
|
-
}
|
|
3023
|
-
const existingMessages = await session.loadMessages();
|
|
3024
|
-
const history = buildHistory(existingMessages);
|
|
3025
|
-
const tui = new ChatTUI({
|
|
3026
|
-
onSend: (text) => {
|
|
3027
|
-
client.sendTextMessage(text);
|
|
3028
|
-
},
|
|
3029
|
-
onExit: () => {
|
|
3030
|
-
client.close();
|
|
3031
|
-
},
|
|
3032
|
-
debug,
|
|
3033
|
-
env: session.ctx.env
|
|
3034
|
-
});
|
|
3035
|
-
wireRealtimeEvents(client, tui);
|
|
3036
|
-
client.on("closed", () => {
|
|
3037
|
-
tui.stop();
|
|
3038
|
-
console.log("Disconnected.");
|
|
3039
|
-
});
|
|
3040
|
-
tui.loadHistory(history);
|
|
3041
|
-
tui.start();
|
|
3042
|
-
await new Promise((resolve2) => {
|
|
3043
|
-
client.once("closed", resolve2);
|
|
3044
|
-
});
|
|
3045
|
-
}
|
|
3046
|
-
async function voiceChatSession(session, vad, args, debug) {
|
|
3047
|
-
if (!checkSoxAvailable()) {
|
|
3048
|
-
throw new CLIError(
|
|
3049
|
-
"Voice mode requires SoX. Install it:\n macOS: brew install sox\n Linux: apt install sox\n Windows: choco install sox"
|
|
3050
|
-
);
|
|
3051
|
-
}
|
|
3052
|
-
const inputDeviceName = typeof args["input-device"] === "string" ? args["input-device"] : void 0;
|
|
3053
|
-
const outputDeviceName = typeof args["output-device"] === "string" ? args["output-device"] : void 0;
|
|
3054
|
-
let inputDeviceId;
|
|
3055
|
-
let outputDeviceId;
|
|
3056
|
-
if (!inputDeviceName || !outputDeviceName) {
|
|
3057
|
-
process.stderr.write("\n Audio Devices\n");
|
|
3058
|
-
}
|
|
3059
|
-
if (inputDeviceName) {
|
|
3060
|
-
inputDeviceId = inputDeviceName;
|
|
3061
|
-
} else {
|
|
3062
|
-
const inputDevices = await listInputDevices();
|
|
3063
|
-
const selected = await selectDevice(inputDevices, "Input (microphone)");
|
|
3064
|
-
inputDeviceId = selected.id;
|
|
3065
|
-
}
|
|
3066
|
-
if (outputDeviceName) {
|
|
3067
|
-
outputDeviceId = outputDeviceName;
|
|
3068
|
-
} else {
|
|
3069
|
-
const outputDevices = await listOutputDevices();
|
|
3070
|
-
const selected = await selectDevice(outputDevices, "Output (speaker)");
|
|
3071
|
-
outputDeviceId = selected.id;
|
|
3072
|
-
}
|
|
3073
|
-
const client = new RealtimeClient({
|
|
3074
|
-
url: session.wsUrl,
|
|
3075
|
-
token: session.ctx.accessToken,
|
|
3076
|
-
chatId: session.chatId,
|
|
3077
|
-
vadMode: vad,
|
|
3078
|
-
debug
|
|
3079
|
-
});
|
|
3080
|
-
process.stderr.write(`
|
|
3081
|
-
Connecting to ${session.label.toLowerCase()}...
|
|
3082
|
-
`);
|
|
3083
|
-
try {
|
|
3084
|
-
await client.connect();
|
|
3085
|
-
} catch (err) {
|
|
3086
|
-
throw new CLIError(`Failed to connect: ${err instanceof Error ? err.message : err}`);
|
|
3087
|
-
}
|
|
3088
|
-
const existingMessages = await session.loadMessages();
|
|
3089
|
-
const history = buildHistory(existingMessages);
|
|
3090
|
-
const mic = new MicCapture((pcm16) => {
|
|
3091
|
-
if (!client.isConnected()) return;
|
|
3092
|
-
client.sendAudio(pcm16);
|
|
3093
|
-
});
|
|
3094
|
-
const player = new AudioPlayer(outputDeviceId);
|
|
3095
|
-
function cleanup() {
|
|
3096
|
-
mic.stop();
|
|
3097
|
-
player.stop();
|
|
3098
|
-
client.close();
|
|
3099
|
-
}
|
|
3100
|
-
const tui = new ChatTUI({
|
|
3101
|
-
onSend: (text) => {
|
|
3102
|
-
client.sendTextMessage(text);
|
|
3103
|
-
},
|
|
3104
|
-
onExit: () => {
|
|
3105
|
-
cleanup();
|
|
3106
|
-
},
|
|
3107
|
-
debug,
|
|
3108
|
-
env: session.ctx.env
|
|
3109
|
-
});
|
|
3110
|
-
wireRealtimeEvents(client, tui);
|
|
3111
|
-
client.on("audio", (data) => {
|
|
3112
|
-
player.write(data.pcm16);
|
|
3113
|
-
});
|
|
3114
|
-
client.on("audio_end", (data) => {
|
|
3115
|
-
player.endResponse(data?.messageId);
|
|
3116
|
-
});
|
|
3117
|
-
client.on("interrupt", () => {
|
|
3118
|
-
const playbackMs = player.getPlaybackTimeMs();
|
|
3119
|
-
const messageId = player.playingMessageId;
|
|
3120
|
-
if (playbackMs !== null && playbackMs > 0 && player.isPlaying) {
|
|
3121
|
-
client.sendInterrupt(playbackMs, messageId);
|
|
3122
|
-
player.stop();
|
|
3123
|
-
if (debug) {
|
|
3124
|
-
tui.logDebug("in", {
|
|
3125
|
-
type: "CUSTOM",
|
|
3126
|
-
name: "voice.interrupt",
|
|
3127
|
-
value: { action: "echoed", audioEndMs: playbackMs, messageId }
|
|
3128
|
-
});
|
|
3129
|
-
}
|
|
3130
|
-
} else if (debug) {
|
|
3131
|
-
tui.logDebug("in", {
|
|
3132
|
-
type: "CUSTOM",
|
|
3133
|
-
name: "voice.interrupt",
|
|
3134
|
-
value: { action: "ignored", wasPlaying: player.isPlaying }
|
|
3135
|
-
});
|
|
3136
|
-
}
|
|
3137
|
-
});
|
|
3138
|
-
client.on("truncate", (data) => {
|
|
3139
|
-
player.stop();
|
|
3140
|
-
tui.truncateAssistant(
|
|
3141
|
-
data.content ?? "",
|
|
3142
|
-
data.originalContent,
|
|
3143
|
-
data.truncationReason ?? "barge_in",
|
|
3144
|
-
data.truncatedAtMs
|
|
3145
|
-
);
|
|
3146
|
-
});
|
|
3147
|
-
client.on("response_message", (data) => {
|
|
3148
|
-
if (data.role === "user" && data.content) {
|
|
3149
|
-
tui.addTranscription(data.content);
|
|
3150
|
-
}
|
|
3151
|
-
});
|
|
3152
|
-
client.on("closed", () => {
|
|
3153
|
-
tui.stop();
|
|
3154
|
-
cleanup();
|
|
3155
|
-
console.log("Disconnected.");
|
|
3156
|
-
});
|
|
3157
|
-
tui.loadHistory(history);
|
|
3158
|
-
tui.start();
|
|
3159
|
-
mic.start(inputDeviceId);
|
|
3160
|
-
await new Promise((resolve2) => {
|
|
3161
|
-
client.once("closed", resolve2);
|
|
3162
|
-
});
|
|
3163
|
-
}
|
|
3164
|
-
async function listAudioDevices() {
|
|
3165
|
-
if (!checkSoxAvailable()) {
|
|
3166
|
-
throw new CLIError(
|
|
3167
|
-
"Device listing requires SoX. Install it:\n macOS: brew install sox\n Linux: apt install sox\n Windows: choco install sox"
|
|
3168
|
-
);
|
|
3169
|
-
}
|
|
3170
|
-
const inputs = await listInputDevices();
|
|
3171
|
-
const outputs = await listOutputDevices();
|
|
3172
|
-
console.log("Input devices (microphone):");
|
|
3173
|
-
for (const dev of inputs) {
|
|
3174
|
-
const marker = dev.isDefault ? " (default)" : "";
|
|
3175
|
-
console.log(` ${dev.id}: ${dev.name}${marker}`);
|
|
3176
|
-
}
|
|
3177
|
-
console.log("\nOutput devices (speaker):");
|
|
3178
|
-
for (const dev of outputs) {
|
|
3179
|
-
const marker = dev.isDefault ? " (default)" : "";
|
|
3180
|
-
console.log(` ${dev.id}: ${dev.name}${marker}`);
|
|
3181
|
-
}
|
|
3182
|
-
}
|
|
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
|
-
|
|
3321
|
-
// src/commands/concierge.ts
|
|
3322
|
-
init_cli_utils();
|
|
3323
|
-
init_concierge_api();
|
|
3324
|
-
init_context();
|
|
3325
|
-
init_errors();
|
|
3326
|
-
async function conciergeOutboundCommand(hctx) {
|
|
3327
|
-
const ctx = requireAuth(hctx.ctx);
|
|
3328
|
-
const { args } = hctx;
|
|
3329
|
-
if (args.call && args.sms) {
|
|
3330
|
-
throw new CLIError("--call and --sms are mutually exclusive");
|
|
3331
|
-
}
|
|
3332
|
-
if (args.call) {
|
|
3333
|
-
const result = await originateConciergeCall(ctx);
|
|
3334
|
-
process.stderr.write(
|
|
3335
|
-
`Concierge call started. callSid=${result.callSid} startedAt=${result.startedAt}
|
|
3336
|
-
`
|
|
3337
|
-
);
|
|
3338
|
-
return;
|
|
3339
|
-
}
|
|
3340
|
-
if (args.sms) {
|
|
3341
|
-
const result = await originateConciergeSms(ctx);
|
|
3342
|
-
process.stderr.write(
|
|
3343
|
-
`Concierge SMS started. messageSid=${result.messageSid} conversationId=${result.conversationId}
|
|
3344
|
-
`
|
|
3345
|
-
);
|
|
3346
|
-
return;
|
|
3347
|
-
}
|
|
3348
|
-
throw new CLIError(
|
|
3349
|
-
"Use `gr concierge chat --call`, `gr concierge chat --sms`, or `gr concierge chat` for interactive mode."
|
|
3350
|
-
);
|
|
3351
|
-
}
|
|
3352
|
-
async function conciergeCommand(hctx) {
|
|
3353
|
-
const ctx = requireAuth(hctx.ctx);
|
|
3354
|
-
const { args } = hctx;
|
|
3355
|
-
if (args.call || args.sms) {
|
|
3356
|
-
if (args.call && args.sms) {
|
|
3357
|
-
throw new CLIError("--call and --sms are mutually exclusive");
|
|
3358
|
-
}
|
|
3359
|
-
if (args.voice || args.realtime) {
|
|
3360
|
-
throw new CLIError("--call/--sms cannot be combined with --voice or --realtime");
|
|
3361
|
-
}
|
|
3362
|
-
await conciergeOutboundCommand(hctx);
|
|
3363
|
-
return;
|
|
3364
|
-
}
|
|
3365
|
-
const chatIdArg = typeof args["chat-id"] === "string" ? args["chat-id"] : void 0;
|
|
3366
|
-
const message = typeof args.message === "string" ? args.message : void 0;
|
|
3367
|
-
const chat = await getOrCreateConciergeChat(ctx, chatIdArg, !!message);
|
|
3368
|
-
const session = buildConciergeSession(ctx, chat, args);
|
|
3369
|
-
await runChatSession(session, args);
|
|
3370
|
-
}
|
|
3371
|
-
function buildConciergeSession(ctx, chat, args) {
|
|
3372
|
-
const wsBase = ctx.envConfig.apiUrl.replace(/^https:/, "wss:").replace(/^http:/, "ws:");
|
|
3373
|
-
return {
|
|
3374
|
-
ctx,
|
|
3375
|
-
workspaceUuid: "",
|
|
3376
|
-
chatId: chat.id,
|
|
3377
|
-
chatUuid: chat.uuid,
|
|
3378
|
-
chatTitle: chat.title || chat.uuid,
|
|
3379
|
-
aguiEndpointUrl: `${ctx.envConfig.apiUrl}/api/v2/concierge/chats/${chat.id}/chat/agui`,
|
|
3380
|
-
wsUrl: `${wsBase}/api/v2/concierge/voice/stream-ws`,
|
|
3381
|
-
loadMessages: () => getConciergeChatMessages(ctx, chat.id).catch(() => []),
|
|
3382
|
-
label: "Concierge",
|
|
3383
|
-
model: void 0,
|
|
3384
|
-
// concierge doesn't send model field
|
|
3385
|
-
appContext: buildCliConciergeAppContext(),
|
|
3386
|
-
allowWrite: true,
|
|
3387
|
-
trustCwd: !!args["trust-cwd"],
|
|
3388
|
-
trustDir: typeof args["trust-dir"] === "string" ? args["trust-dir"] : void 0
|
|
3389
|
-
};
|
|
3390
|
-
}
|
|
3391
|
-
async function getOrCreateConciergeChat(ctx, chatIdArg, skipSelection) {
|
|
3392
|
-
if (chatIdArg) {
|
|
3393
|
-
const id = Number(chatIdArg);
|
|
3394
|
-
if (isNaN(id)) throw new CLIError("--chat-id must be a number");
|
|
3395
|
-
return getConciergeChat(ctx, id);
|
|
3396
|
-
}
|
|
3397
|
-
if (skipSelection) {
|
|
3398
|
-
const chat = await createConciergeChat(ctx);
|
|
3399
|
-
process.stderr.write(`Created concierge chat #${chat.id}
|
|
3400
|
-
`);
|
|
3401
|
-
return chat;
|
|
3402
|
-
}
|
|
3403
|
-
const chats = await listConciergeChats(ctx);
|
|
3404
|
-
const { item: selectedChat, isNew } = await selectFromList(chats, {
|
|
3405
|
-
title: "Select a concierge chat or create a new one",
|
|
3406
|
-
labelFn: (c) => c.title || `Chat ${c.uuid.slice(0, 8)}...`,
|
|
3407
|
-
detailFn: (c) => formatTimestamp(c.updatedAt),
|
|
3408
|
-
emptyMessage: "No existing concierge chats.",
|
|
3409
|
-
allowNew: true,
|
|
3410
|
-
newLabel: "+ Start new chat"
|
|
3411
|
-
});
|
|
3412
|
-
if (!selectedChat && !isNew) {
|
|
3413
|
-
throw new CLIError("No chat selected.");
|
|
3414
|
-
}
|
|
3415
|
-
if (isNew) {
|
|
3416
|
-
const chat = await createConciergeChat(ctx);
|
|
3417
|
-
process.stderr.write(`Created concierge chat #${chat.id}
|
|
3418
|
-
`);
|
|
3419
|
-
return chat;
|
|
3420
|
-
}
|
|
3421
|
-
return selectedChat;
|
|
3422
|
-
}
|
|
3423
|
-
|
|
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
|