oncall-cli 3.0.3 → 3.0.5
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/bin/oncall.js +74 -13
- package/dist/api.d.ts +1 -0
- package/dist/api.js +11 -0
- package/dist/helpers/ripgrep-tool.js +148 -5
- package/dist/index.js +120 -85
- package/dist/tools/ripgrep-wrapper.d.ts +14 -0
- package/dist/tools/ripgrep-wrapper.js +67 -0
- package/dist/tools/ripgrep.js +3 -2
- package/dist/useWebSocket.js +91 -10
- package/dist/utils/version-check.d.ts +1 -0
- package/dist/utils/version-check.js +63 -0
- package/dist/utils.d.ts +1 -0
- package/dist/utils.js +112 -2
- package/package.json +2 -2
- package/patches/ripgrep-node+1.0.0.patch +0 -13
package/bin/oncall.js
CHANGED
|
@@ -28,8 +28,9 @@ import {
|
|
|
28
28
|
import {
|
|
29
29
|
checkVersionAndExit,
|
|
30
30
|
checkVersionCompatibility,
|
|
31
|
+
checkForUpdates,
|
|
31
32
|
} from "../dist/utils/version-check.js";
|
|
32
|
-
|
|
33
|
+
//f7871b3a372bc5e23768ec80a8a5c3f86704788bb2b4eda0
|
|
33
34
|
const __filename = fileURLToPath(import.meta.url);
|
|
34
35
|
const __dirname = path.dirname(__filename);
|
|
35
36
|
|
|
@@ -128,7 +129,24 @@ Check directory permissions and try again.
|
|
|
128
129
|
}
|
|
129
130
|
}
|
|
130
131
|
|
|
131
|
-
|
|
132
|
+
//check for valid auth key
|
|
133
|
+
// const isValidAuthKey = async (authKey) => {
|
|
134
|
+
// try {
|
|
135
|
+
// const isValid = await axios.post(`${BASE_URL}/auth/login`, { authKey });
|
|
136
|
+
// return isValid.data;
|
|
137
|
+
// } catch (error) {
|
|
138
|
+
// if (error.response.message === "Invalid auth key") {
|
|
139
|
+
// console.error(`Authentication failed.
|
|
140
|
+
// The provided auth key is invalid or has expired.
|
|
141
|
+
// Get a new auth key from your OnCall account and try again.
|
|
142
|
+
// `);
|
|
143
|
+
// } else {
|
|
144
|
+
// console.error(" Failed to save auth key:", error);
|
|
145
|
+
// }
|
|
146
|
+
// }
|
|
147
|
+
// };
|
|
148
|
+
|
|
149
|
+
async function handleLogin(authKey) {
|
|
132
150
|
if (!authKey || typeof authKey !== "string" || !authKey.trim()) {
|
|
133
151
|
// console.error("\n❌ Usage: oncall login <your-auth-key>");
|
|
134
152
|
console.error(`Missing auth key.
|
|
@@ -139,20 +157,39 @@ Usage:
|
|
|
139
157
|
process.exit(1);
|
|
140
158
|
}
|
|
141
159
|
try {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
160
|
+
// const isValid = await isValidAuthKey(authKey);
|
|
161
|
+
const isValid = await axios.post(
|
|
162
|
+
`https://api.oncall.build/v2/api/auth/login`,
|
|
163
|
+
{ authKey }
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (isValid) {
|
|
167
|
+
ensureConfigDir();
|
|
168
|
+
let contents = "";
|
|
169
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
170
|
+
contents = fs.readFileSync(CONFIG_PATH, "utf8");
|
|
171
|
+
}
|
|
172
|
+
const updatedContents = upsertConfigValue(contents, "API_KEY", authKey);
|
|
173
|
+
fs.writeFileSync(CONFIG_PATH, updatedContents, "utf8");
|
|
174
|
+
console.log("✅ Auth key saved.\n");
|
|
175
|
+
console.log(`Next:
|
|
151
176
|
Initialize a project with:
|
|
152
177
|
oncall init -id "name this project" -m "describe this service"`);
|
|
153
|
-
|
|
178
|
+
process.exit(0);
|
|
179
|
+
} else {
|
|
180
|
+
console.error(" Invalid Key");
|
|
181
|
+
}
|
|
154
182
|
} catch (err) {
|
|
155
|
-
|
|
183
|
+
if (err.response.data.message === "Invalid auth key") {
|
|
184
|
+
console.error(`Authentication failed.
|
|
185
|
+
|
|
186
|
+
The provided auth key is invalid or has expired.
|
|
187
|
+
|
|
188
|
+
Get a new auth key from your OnCall account and try again.
|
|
189
|
+
`);
|
|
190
|
+
} else {
|
|
191
|
+
console.error("❌❌ Failed to save auth key:", err);
|
|
192
|
+
}
|
|
156
193
|
process.exit(1);
|
|
157
194
|
}
|
|
158
195
|
}
|
|
@@ -331,11 +368,13 @@ async function main() {
|
|
|
331
368
|
checkVersionCompatibility();
|
|
332
369
|
process.exit(0);
|
|
333
370
|
}
|
|
371
|
+
|
|
334
372
|
if (args[0] === "init" && (args[1] === "--help" || args[1] === "-h")) {
|
|
335
373
|
printInitHelp();
|
|
336
374
|
checkVersionCompatibility();
|
|
337
375
|
process.exit(0);
|
|
338
376
|
}
|
|
377
|
+
|
|
339
378
|
if (args[0] === "cluster" && (args[1] === "--help" || args[1] === "-h")) {
|
|
340
379
|
printClusterHelp();
|
|
341
380
|
checkVersionCompatibility();
|
|
@@ -349,22 +388,31 @@ async function main() {
|
|
|
349
388
|
|
|
350
389
|
await checkVersionAndExit();
|
|
351
390
|
|
|
391
|
+
// Check for updates asynchronously (non-blocking)
|
|
392
|
+
// This runs in the background and won't block the CLI execution
|
|
393
|
+
checkForUpdates(pkgJson.name, false).catch(() => {
|
|
394
|
+
// Silently fail - we don't want to interrupt the user's workflow
|
|
395
|
+
});
|
|
396
|
+
|
|
352
397
|
const command = args[0];
|
|
353
398
|
if (command === "init") {
|
|
354
399
|
projectRegistrationPromise.catch(() => null);
|
|
355
400
|
await handleInit(args);
|
|
356
401
|
return;
|
|
357
402
|
}
|
|
403
|
+
|
|
358
404
|
if (command === "login") {
|
|
359
405
|
projectRegistrationPromise.catch(() => null);
|
|
360
406
|
handleLogin(args[1]);
|
|
361
407
|
return;
|
|
362
408
|
}
|
|
409
|
+
|
|
363
410
|
if (command === "cluster") {
|
|
364
411
|
projectRegistrationPromise.catch(() => null);
|
|
365
412
|
await handleClusterCommand(args.slice(1));
|
|
366
413
|
return;
|
|
367
414
|
}
|
|
415
|
+
|
|
368
416
|
if (command === "config") {
|
|
369
417
|
projectRegistrationPromise.catch(() => null);
|
|
370
418
|
await handleConfig(args);
|
|
@@ -372,6 +420,7 @@ async function main() {
|
|
|
372
420
|
}
|
|
373
421
|
|
|
374
422
|
const clusterReady = await ensureClusterReady();
|
|
423
|
+
const yamlPath = path.join(process.cwd(), "oncall.yaml");
|
|
375
424
|
if (!clusterReady) {
|
|
376
425
|
console.error(
|
|
377
426
|
`\nOnCall server is not running.
|
|
@@ -383,6 +432,18 @@ Start it in another terminal:
|
|
|
383
432
|
process.exit(1);
|
|
384
433
|
}
|
|
385
434
|
|
|
435
|
+
if (!fs.existsSync(yamlPath)) {
|
|
436
|
+
console.error(
|
|
437
|
+
`\nThis process is not registered with an OnCall project.
|
|
438
|
+
|
|
439
|
+
Register it with:
|
|
440
|
+
oncall init --id <project-id> -m "<process description>"
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
\n`
|
|
444
|
+
);
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
386
447
|
try {
|
|
387
448
|
await import("../dist/index.js");
|
|
388
449
|
} catch (err) {
|
package/dist/api.d.ts
CHANGED
package/dist/api.js
CHANGED
|
@@ -3,6 +3,12 @@ import { config } from "./config.js";
|
|
|
3
3
|
import { checkVersionCompatibility } from "./utils/version-check.js";
|
|
4
4
|
const BASE_URL = config.api_base_url;
|
|
5
5
|
axios.interceptors.response.use((response) => response, async (error) => {
|
|
6
|
+
// Network error (no response received)
|
|
7
|
+
if (!error.response) {
|
|
8
|
+
const networkError = new Error("Network error: No internet connection");
|
|
9
|
+
networkError.code = "NETWORK_ERROR";
|
|
10
|
+
return Promise.reject(networkError);
|
|
11
|
+
}
|
|
6
12
|
if (error.response) {
|
|
7
13
|
const isCompatible = await checkVersionCompatibility(true);
|
|
8
14
|
if (!isCompatible) {
|
|
@@ -11,6 +17,11 @@ axios.interceptors.response.use((response) => response, async (error) => {
|
|
|
11
17
|
}
|
|
12
18
|
return Promise.reject(error);
|
|
13
19
|
});
|
|
20
|
+
//check for valid api key
|
|
21
|
+
export const isValidAuthKey = async (authKey) => {
|
|
22
|
+
const isValid = await axios.post(`${BASE_URL}/auth/login`, { authKey });
|
|
23
|
+
return isValid.data;
|
|
24
|
+
};
|
|
14
25
|
export const toolFunctionCall = async (tool_call_id, resultArgs, args, function_name) => {
|
|
15
26
|
const result = await axios.post(`${BASE_URL}/tool/toolFunctionCall`, {
|
|
16
27
|
tool_call_id,
|
|
@@ -1,5 +1,112 @@
|
|
|
1
|
-
import { RipGrep } from "ripgrep-
|
|
1
|
+
import { RipGrep } from "../tools/ripgrep-wrapper.js";
|
|
2
2
|
import { logd } from "./cli-helpers.js";
|
|
3
|
+
import path from "path";
|
|
4
|
+
function looksLikeFilename(query) {
|
|
5
|
+
const trimmed = query.trim();
|
|
6
|
+
if (!trimmed || trimmed.length === 0)
|
|
7
|
+
return false;
|
|
8
|
+
if (/\.\w{1,10}$/.test(trimmed))
|
|
9
|
+
return true;
|
|
10
|
+
if (trimmed.includes("/") || trimmed.includes("\\"))
|
|
11
|
+
return true;
|
|
12
|
+
if (trimmed.startsWith("."))
|
|
13
|
+
return true;
|
|
14
|
+
if (!trimmed.includes(" ") && trimmed.length >= 3 && trimmed.length <= 50) {
|
|
15
|
+
if (/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
async function searchFilesByName(query, searchDir, options) {
|
|
22
|
+
const { maxResults = 20, caseSensitive = false, excludePatterns = [], } = options;
|
|
23
|
+
const defaultExcludePatterns = [
|
|
24
|
+
"node_modules",
|
|
25
|
+
".git",
|
|
26
|
+
"dist",
|
|
27
|
+
"build",
|
|
28
|
+
".next",
|
|
29
|
+
".cache",
|
|
30
|
+
"coverage",
|
|
31
|
+
".nyc_output",
|
|
32
|
+
".vscode",
|
|
33
|
+
".idea",
|
|
34
|
+
"*.log",
|
|
35
|
+
"*.lock",
|
|
36
|
+
"package-lock.json",
|
|
37
|
+
"yarn.lock",
|
|
38
|
+
"pnpm-lock.yaml",
|
|
39
|
+
".env",
|
|
40
|
+
".env.*",
|
|
41
|
+
"*.min.js",
|
|
42
|
+
"*.min.css",
|
|
43
|
+
".DS_Store",
|
|
44
|
+
"Thumbs.db",
|
|
45
|
+
];
|
|
46
|
+
const allExcludePatterns = [...defaultExcludePatterns, ...excludePatterns];
|
|
47
|
+
try {
|
|
48
|
+
const escapedQuery = query.replace(/[\[\]{}()]/g, "\\$&");
|
|
49
|
+
const globPattern = `**/*${escapedQuery}*`;
|
|
50
|
+
logd(`[filenameSearch] Using ripgrep to search for files matching pattern: ${globPattern}`);
|
|
51
|
+
let rg = new RipGrep(".", searchDir);
|
|
52
|
+
rg.glob(globPattern);
|
|
53
|
+
for (const pattern of allExcludePatterns) {
|
|
54
|
+
const hasFileExtension = /\.(json|lock|yaml|js|css|log)$/.test(pattern) ||
|
|
55
|
+
pattern.startsWith("*.") ||
|
|
56
|
+
pattern === ".env" ||
|
|
57
|
+
pattern.startsWith(".env.") ||
|
|
58
|
+
pattern === ".DS_Store" ||
|
|
59
|
+
pattern === "Thumbs.db";
|
|
60
|
+
const isFilePattern = pattern.includes("*") || hasFileExtension;
|
|
61
|
+
const excludePattern = isFilePattern ? `!${pattern}` : `!${pattern}/**`;
|
|
62
|
+
rg.glob(excludePattern);
|
|
63
|
+
}
|
|
64
|
+
rg.withFilename().lineNumber();
|
|
65
|
+
rg.fixedStrings();
|
|
66
|
+
if (!caseSensitive) {
|
|
67
|
+
rg.ignoreCase();
|
|
68
|
+
}
|
|
69
|
+
const runResult = await rg.run();
|
|
70
|
+
const output = await runResult.asString();
|
|
71
|
+
if (!output || output.trim().length === 0) {
|
|
72
|
+
logd(`[filenameSearch] No files found matching pattern: ${globPattern}`);
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const lines = output.trim().split("\n");
|
|
76
|
+
const filePaths = new Set();
|
|
77
|
+
const results = [];
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
if (results.length >= maxResults)
|
|
80
|
+
break;
|
|
81
|
+
const match = line.match(/^(.+?):(\d+):(.+)$/);
|
|
82
|
+
if (match) {
|
|
83
|
+
const filePath = match[1].trim();
|
|
84
|
+
const relativePath = path.relative(searchDir, filePath);
|
|
85
|
+
if (!filePaths.has(relativePath)) {
|
|
86
|
+
filePaths.add(relativePath);
|
|
87
|
+
const lineNumber = parseInt(match[2], 10) || 1;
|
|
88
|
+
const preview = match[3]?.trim().slice(0, 200) ||
|
|
89
|
+
`File: ${path.basename(filePath)}`;
|
|
90
|
+
results.push({
|
|
91
|
+
filePath: relativePath,
|
|
92
|
+
line: lineNumber,
|
|
93
|
+
preview: preview,
|
|
94
|
+
score: 1.0,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
logd(`[filenameSearch] Found ${results.length} files matching pattern: ${globPattern}`);
|
|
100
|
+
return results;
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
logd(`[filenameSearch] Error searching for files: ${error.message}, code: ${error.code}`);
|
|
104
|
+
if (error.code === 1 || error.message?.includes("No matches")) {
|
|
105
|
+
return [];
|
|
106
|
+
}
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
3
110
|
export async function ripgrepSearch(query, options = {}) {
|
|
4
111
|
const defaultExcludePatterns = [
|
|
5
112
|
"node_modules",
|
|
@@ -28,7 +135,8 @@ export async function ripgrepSearch(query, options = {}) {
|
|
|
28
135
|
if (!query || query.trim().length === 0) {
|
|
29
136
|
return [];
|
|
30
137
|
}
|
|
31
|
-
const searchDir = workingDirectory ||
|
|
138
|
+
const searchDir = workingDirectory ||
|
|
139
|
+
(typeof process !== "undefined" ? process.cwd() : undefined);
|
|
32
140
|
if (!searchDir) {
|
|
33
141
|
logd("[ripgrep] ERROR: workingDirectory is required for client-side usage");
|
|
34
142
|
return [];
|
|
@@ -57,7 +165,8 @@ export async function ripgrepSearch(query, options = {}) {
|
|
|
57
165
|
const excludePattern = isFilePattern ? `!${pattern}` : `!${pattern}/**`;
|
|
58
166
|
rg.glob(excludePattern);
|
|
59
167
|
}
|
|
60
|
-
const
|
|
168
|
+
const runResult = await rg.run();
|
|
169
|
+
const output = await runResult.asString();
|
|
61
170
|
logd(`[ripgrep] Raw output: outputLength=${output?.length || 0}, hasOutput=${!!(output && output.trim().length > 0)}, outputPreview=${output?.substring(0, 200) || "N/A"}`);
|
|
62
171
|
if (!output || output.trim().length === 0) {
|
|
63
172
|
logd("[ripgrep] ⚠️ No output from ripgrep - returning empty results");
|
|
@@ -83,19 +192,53 @@ export async function ripgrepSearch(query, options = {}) {
|
|
|
83
192
|
logd(`[ripgrep] ⚠️ Line didn't match expected format: line=${line.substring(0, 100)}, lineLength=${line.length}`);
|
|
84
193
|
}
|
|
85
194
|
}
|
|
86
|
-
logd(`[ripgrep] ✅ Parsed results: totalResults=${results.length}, results=${JSON.stringify(results.map(r => ({
|
|
195
|
+
logd(`[ripgrep] ✅ Parsed results: totalResults=${results.length}, results=${JSON.stringify(results.map((r) => ({
|
|
87
196
|
filePath: r.filePath,
|
|
88
197
|
line: r.line,
|
|
89
198
|
previewLength: r.preview?.length || 0,
|
|
90
199
|
})))}`);
|
|
200
|
+
const hasExactFilenameMatch = results.some((r) => {
|
|
201
|
+
const fileName = path.basename(r.filePath);
|
|
202
|
+
return fileName.toLowerCase() === query.toLowerCase();
|
|
203
|
+
});
|
|
204
|
+
if (looksLikeFilename(query) && (!hasExactFilenameMatch || results.length === 0)) {
|
|
205
|
+
logd(`[ripgrep] 🔄 Content search ${results.length === 0 ? 'found no results' : `found ${results.length} results but no exact filename match`}, but query looks like filename. Trying filename search...`);
|
|
206
|
+
const filenameResults = await searchFilesByName(query, searchDir, options);
|
|
207
|
+
if (filenameResults.length > 0) {
|
|
208
|
+
logd(`[ripgrep] ✅ Filename search found ${filenameResults.length} matches`);
|
|
209
|
+
if (results.length > 0 && !hasExactFilenameMatch) {
|
|
210
|
+
const combined = [...filenameResults, ...results];
|
|
211
|
+
const unique = combined.filter((r, index, self) => index === self.findIndex((t) => t.filePath === r.filePath));
|
|
212
|
+
return unique.slice(0, maxResults);
|
|
213
|
+
}
|
|
214
|
+
return filenameResults;
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
logd(`[ripgrep] ⚠️ Filename search also found no matches`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
91
220
|
return results;
|
|
92
221
|
}
|
|
93
222
|
catch (error) {
|
|
94
|
-
logd(`[ripgrep] ❌ Exception caught: error=${error.message}, code=${error.code}, stack=${error.stack?.substring(0, 200) ||
|
|
223
|
+
logd(`[ripgrep] ❌ Exception caught: error=${error.message}, code=${error.code}, stack=${error.stack?.substring(0, 200) || "N/A"}`);
|
|
95
224
|
if (error.message?.includes("No matches") ||
|
|
96
225
|
error.message?.includes("not found") ||
|
|
97
226
|
error.code === 1) {
|
|
98
227
|
logd("[ripgrep] ℹ️ No matches found (this is normal if search term doesn't exist)");
|
|
228
|
+
// Fallback to filename search if it looks like a filename
|
|
229
|
+
if (looksLikeFilename(query)) {
|
|
230
|
+
logd(`[ripgrep] 🔄 Error occurred, but query looks like filename. Trying filename search as fallback...`);
|
|
231
|
+
try {
|
|
232
|
+
const filenameResults = await searchFilesByName(query, searchDir, options);
|
|
233
|
+
if (filenameResults.length > 0) {
|
|
234
|
+
logd(`[ripgrep] ✅ Filename search fallback found ${filenameResults.length} matches`);
|
|
235
|
+
return filenameResults;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
catch (fallbackError) {
|
|
239
|
+
logd(`[ripgrep] ❌ Filename search fallback also failed: ${fallbackError}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
99
242
|
return [];
|
|
100
243
|
}
|
|
101
244
|
logd(`[ripgrep] Error executing ripgrep: ${error.message}`);
|
package/dist/index.js
CHANGED
|
@@ -12,10 +12,12 @@ import { markedTerminal } from "marked-terminal";
|
|
|
12
12
|
import { useWebSocket } from "./useWebSocket.js";
|
|
13
13
|
import { config } from "./config.js";
|
|
14
14
|
import Spinner from "ink-spinner";
|
|
15
|
-
import { HumanMessage } from "langchain";
|
|
16
|
-
import {
|
|
15
|
+
import { HumanMessage, } from "langchain";
|
|
16
|
+
import { SystemMessage } from "@langchain/core/messages";
|
|
17
|
+
import { loadProjectMetadata, fetchProjectsFromCluster, logd, } from "./helpers/cli-helpers.js";
|
|
17
18
|
import logsManager from "./logsManager.js";
|
|
18
19
|
import { extractMessageContent } from "./utils.js";
|
|
20
|
+
const initialAssistantMessage = new SystemMessage("I'm watching your app run locally. You can ask me about errors, logs, performance,or anything else related to this run.");
|
|
19
21
|
dotenv.config();
|
|
20
22
|
if (typeof process !== "undefined" && process.on) {
|
|
21
23
|
process.on("SIGINT", () => {
|
|
@@ -27,15 +29,15 @@ marked.use(markedTerminal({
|
|
|
27
29
|
reflowText: false,
|
|
28
30
|
showSectionPrefix: false,
|
|
29
31
|
unescape: true,
|
|
30
|
-
emoji: true
|
|
32
|
+
emoji: true,
|
|
31
33
|
}));
|
|
32
|
-
const COLON_REPLACER =
|
|
34
|
+
const COLON_REPLACER = "*#COLON|*";
|
|
33
35
|
function escapeRegExp(str) {
|
|
34
|
-
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,
|
|
36
|
+
return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
|
|
35
37
|
}
|
|
36
|
-
const COLON_REPLACER_REGEXP = new RegExp(escapeRegExp(COLON_REPLACER),
|
|
38
|
+
const COLON_REPLACER_REGEXP = new RegExp(escapeRegExp(COLON_REPLACER), "g");
|
|
37
39
|
function undoColon(str) {
|
|
38
|
-
return str.replace(COLON_REPLACER_REGEXP,
|
|
40
|
+
return str.replace(COLON_REPLACER_REGEXP, ":");
|
|
39
41
|
}
|
|
40
42
|
// Override just the 'text' renderer to handle inline tokens:
|
|
41
43
|
marked.use({
|
|
@@ -82,8 +84,19 @@ function wrapText(text, maxWidth) {
|
|
|
82
84
|
}
|
|
83
85
|
return lines.length > 0 ? lines : [""];
|
|
84
86
|
}
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
+
//truncate the line depending on the width available
|
|
88
|
+
function getProcessedLine(text, maxWidth) {
|
|
89
|
+
if (maxWidth <= 0)
|
|
90
|
+
return text;
|
|
91
|
+
const expanded = text.replace(/\t/g, " ".repeat(8));
|
|
92
|
+
const width = stringWidth(expanded);
|
|
93
|
+
if (width > maxWidth && maxWidth > 3) {
|
|
94
|
+
return sliceAnsi(expanded, 0, Math.max(0, maxWidth - 3)) + "...";
|
|
95
|
+
}
|
|
96
|
+
return expanded;
|
|
97
|
+
}
|
|
98
|
+
//scrollable content for logs
|
|
99
|
+
const ScrollableContent = ({ lines, maxHeight, isFocused, onScrollChange, scrollOffset, availableWidth, }) => {
|
|
87
100
|
const totalLines = lines.length;
|
|
88
101
|
const visibleLines = Math.max(0, Math.min(maxHeight, totalLines));
|
|
89
102
|
const maxOffset = Math.max(0, totalLines - visibleLines);
|
|
@@ -107,7 +120,8 @@ const ScrollableContent = ({ lines, maxHeight, isFocused, onScrollChange, scroll
|
|
|
107
120
|
}
|
|
108
121
|
const scrollBarIndicator = totalLines > visibleLines ? `[${scrollPosition}%]` : "";
|
|
109
122
|
return (_jsxs(Box, { flexDirection: "column", height: maxHeight, overflow: "hidden", children: [displayedLines.map((line) => {
|
|
110
|
-
const
|
|
123
|
+
const truncated = getProcessedLine(line.text, availableWidth);
|
|
124
|
+
const rendered = marked.parseInline(truncated);
|
|
111
125
|
return _jsx(Text, { children: rendered }, line.key);
|
|
112
126
|
}), _jsx(Box, { position: "absolute", children: _jsx(Text, { color: "yellowBright", bold: true, children: scrollBarIndicator }) })] }));
|
|
113
127
|
};
|
|
@@ -195,7 +209,7 @@ const getShortcutsForMode = (mode) => {
|
|
|
195
209
|
{ shortcut: "[Enter]", description: "Send" },
|
|
196
210
|
{ shortcut: "[Ctrl+D]", description: ctrlDAction },
|
|
197
211
|
{ shortcut: "[Ctrl+C]", description: "Exit" },
|
|
198
|
-
{ shortcut: "[Ctrl+R]", description: "Reload AI chat" },
|
|
212
|
+
// { shortcut: "[Ctrl+R]", description: "Reload AI chat" },
|
|
199
213
|
];
|
|
200
214
|
};
|
|
201
215
|
export const App = () => {
|
|
@@ -206,6 +220,9 @@ export const App = () => {
|
|
|
206
220
|
const [planningDoc, setPlanningDoc] = useState("");
|
|
207
221
|
const partialLine = useRef("");
|
|
208
222
|
const logKeyCounter = useRef(0);
|
|
223
|
+
//auto truncate logs depending on width
|
|
224
|
+
const ptyRef = useRef(null);
|
|
225
|
+
const ptyAliveRef = useRef(false);
|
|
209
226
|
const [activePane, setActivePane] = useState("input");
|
|
210
227
|
const [logScroll, setLogScroll] = useState(0);
|
|
211
228
|
const [chatScroll, setChatScroll] = useState(0);
|
|
@@ -229,20 +246,27 @@ export const App = () => {
|
|
|
229
246
|
//websocket hook
|
|
230
247
|
const { connectWebSocket, sendQuery, chatResponseMessages, // Full history including ToolMessage (for backend queries)
|
|
231
248
|
visibleChats, // Filtered for UI display (excludes ToolMessage except reflections)
|
|
232
|
-
setChatResponseMessages, isConnected, isLoading, setIsLoading, setTrimmedChats, API_KEY, connectionError, setSocketId, setIsConnected, socket, setShowControlR, showControlR, setCompleteChatHistory, customMessage, setVisibleChats, graphState, setGraphState } = useWebSocket(config.websocket_url, logsManager);
|
|
249
|
+
setChatResponseMessages, isConnected, isLoading, setIsLoading, setTrimmedChats, API_KEY, connectionError, setSocketId, setIsConnected, socket, setShowControlR, showControlR, setCompleteChatHistory, customMessage, setVisibleChats, graphState, setGraphState, } = useWebSocket(config.websocket_url, logsManager);
|
|
233
250
|
const SCROLL_HEIGHT = terminalRows - 6;
|
|
234
251
|
const LOGS_HEIGHT = SCROLL_HEIGHT;
|
|
235
252
|
const INPUT_BOX_HEIGHT = 5; // Border (2) + marginTop (1) + content (1) + padding (~1)
|
|
236
253
|
const CHAT_HISTORY_HEIGHT = SCROLL_HEIGHT - INPUT_BOX_HEIGHT;
|
|
237
254
|
useEffect(() => {
|
|
238
255
|
const handleResize = () => {
|
|
239
|
-
if (stdout?.rows)
|
|
256
|
+
if (stdout?.rows)
|
|
240
257
|
setTerminalRows(stdout.rows);
|
|
241
|
-
|
|
242
|
-
if (stdout?.columns) {
|
|
258
|
+
if (stdout?.columns)
|
|
243
259
|
setTerminalCols(stdout.columns);
|
|
260
|
+
const cols = Math.max(10, Math.floor((stdout?.columns || 80) / 2) - 6);
|
|
261
|
+
const rows = Math.max(1, logsHeightRef.current - 2);
|
|
262
|
+
if (ptyAliveRef.current && ptyRef.current) {
|
|
263
|
+
try {
|
|
264
|
+
ptyRef.current.resize(cols, rows);
|
|
265
|
+
}
|
|
266
|
+
catch {
|
|
267
|
+
//ignore
|
|
268
|
+
}
|
|
244
269
|
}
|
|
245
|
-
// DO NOT re-run or re-process logs here
|
|
246
270
|
};
|
|
247
271
|
process.stdout.on("resize", handleResize);
|
|
248
272
|
return () => {
|
|
@@ -254,25 +278,22 @@ export const App = () => {
|
|
|
254
278
|
connectWebSocket();
|
|
255
279
|
}, []);
|
|
256
280
|
//get the AIMessage content inside the progress event
|
|
257
|
-
let lastAIMessage = "";
|
|
258
|
-
function extractAIMessages(obj) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
return undefined;
|
|
274
|
-
return content;
|
|
275
|
-
}
|
|
281
|
+
// let lastAIMessage = "";
|
|
282
|
+
// function extractAIMessages(obj: ProgressObject): string | undefined {
|
|
283
|
+
// if (obj?.type !== "progress") return undefined;
|
|
284
|
+
// const messages = (obj.data && obj.data?.messages) ?? [];
|
|
285
|
+
// const latestAI = [...messages]
|
|
286
|
+
// .reverse()
|
|
287
|
+
// .find((m) => m.id?.includes("AIMessage"));
|
|
288
|
+
// const content = latestAI?.kwargs?.content?.trim();
|
|
289
|
+
// if (!content) return undefined;
|
|
290
|
+
// if (content === lastAIMessage) {
|
|
291
|
+
// return undefined;
|
|
292
|
+
// }
|
|
293
|
+
// lastAIMessage = content;
|
|
294
|
+
// if (content === undefined) return undefined;
|
|
295
|
+
// return content;
|
|
296
|
+
// }
|
|
276
297
|
// Auto-switch to chat pane when server finishes responding
|
|
277
298
|
useEffect(() => {
|
|
278
299
|
// Use visibleChats for UI-related checks
|
|
@@ -309,7 +330,7 @@ export const App = () => {
|
|
|
309
330
|
if (!prev)
|
|
310
331
|
prev = { lastId: "", messages: [] };
|
|
311
332
|
if (!message.id)
|
|
312
|
-
message.id =
|
|
333
|
+
message.id = new Date().getTime() + "";
|
|
313
334
|
if (message.id !== prev.lastId) {
|
|
314
335
|
prev.messages.push([message]);
|
|
315
336
|
prev.lastId = message.id;
|
|
@@ -322,16 +343,18 @@ export const App = () => {
|
|
|
322
343
|
return prev;
|
|
323
344
|
}, { lastId: "", messages: [] });
|
|
324
345
|
}
|
|
325
|
-
function
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
}
|
|
346
|
+
// function extractMessageContent(messages: BaseMessage[]) {
|
|
347
|
+
// return messages.reduce((prev, message) => {
|
|
348
|
+
// return prev + message.content
|
|
349
|
+
// }, "")
|
|
350
|
+
// }
|
|
330
351
|
function extractThinkMessage(messages) {
|
|
331
352
|
if (messages.length < 5)
|
|
332
353
|
return "";
|
|
333
354
|
return messages.slice(4, -3).reduce((prev, message) => {
|
|
334
|
-
if (message.tool_call_chunks &&
|
|
355
|
+
if (message.tool_call_chunks &&
|
|
356
|
+
message.tool_call_chunks[0] &&
|
|
357
|
+
message.tool_call_chunks[0].args)
|
|
335
358
|
return prev + message.tool_call_chunks[0].args;
|
|
336
359
|
return prev;
|
|
337
360
|
}, "");
|
|
@@ -349,6 +372,16 @@ export const App = () => {
|
|
|
349
372
|
const prefixWidth = stringWidth(prefix);
|
|
350
373
|
// let content = `${index}: ` + extractMessageContent(msgs)
|
|
351
374
|
let content = extractMessageContent(msgs);
|
|
375
|
+
// Debug: Log if content looks like JSON
|
|
376
|
+
if (content.trim().startsWith("{") && content.trim().endsWith("}")) {
|
|
377
|
+
logd(`[DEBUG] Found JSON-like content in message group ${index}: ${content.substring(0, 100)}`);
|
|
378
|
+
logd(`[DEBUG] Message details: ${JSON.stringify(msgs.map((m) => ({
|
|
379
|
+
type: m.constructor.name,
|
|
380
|
+
content: typeof m.content === "string"
|
|
381
|
+
? m.content.substring(0, 100)
|
|
382
|
+
: typeof m.content,
|
|
383
|
+
})), null, 2)}`);
|
|
384
|
+
}
|
|
352
385
|
const fMessage = msgs[0];
|
|
353
386
|
if (fMessage.tool_calls && fMessage.tool_calls.length > 0) {
|
|
354
387
|
logd("---------------- tool calls received --------------");
|
|
@@ -390,7 +423,7 @@ export const App = () => {
|
|
|
390
423
|
// return groupMessageChunks(messagesToRender).messages.flatMap((msgs, index) => {
|
|
391
424
|
// // Get message type from BaseMessage (LangChain messages use getType() or constructor name)
|
|
392
425
|
// const msgAny = msgs as any;
|
|
393
|
-
// const msgType =
|
|
426
|
+
// const msgType =
|
|
394
427
|
// (typeof msgAny[0].getType === "function" && msgAny[0].getType()) ||
|
|
395
428
|
// msgAny[0]._type ||
|
|
396
429
|
// msgs.constructor.name ||
|
|
@@ -416,21 +449,25 @@ export const App = () => {
|
|
|
416
449
|
// }
|
|
417
450
|
// });
|
|
418
451
|
}, [visibleChats, chatResponseMessages]);
|
|
419
|
-
const currentLogDataString = useMemo(
|
|
452
|
+
// const currentLogDataString = useMemo(
|
|
453
|
+
// () => rawLogData.map((l) => l.text).join("\n"),
|
|
454
|
+
// [rawLogData]
|
|
455
|
+
// );
|
|
420
456
|
// Process truncation once when inserting logs (so resize won't re-process)
|
|
421
|
-
const getProcessedLine = (text) => {
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
457
|
+
// const getProcessedLine = (text: string) => {
|
|
458
|
+
// const availableWidth = Math.floor(terminalColsRef.current / 2) - 6;
|
|
459
|
+
// const expandedText = text.replace(/\t/g, " ".repeat(8));
|
|
460
|
+
// const width = stringWidth(expandedText);
|
|
461
|
+
// if (width > availableWidth && availableWidth > 3) {
|
|
462
|
+
// const truncated = sliceAnsi(
|
|
463
|
+
// expandedText,
|
|
464
|
+
// 0,
|
|
465
|
+
// Math.max(0, availableWidth - 3)
|
|
466
|
+
// );
|
|
467
|
+
// return truncated + "...";
|
|
468
|
+
// }
|
|
469
|
+
// return text;
|
|
470
|
+
// };
|
|
434
471
|
// Keep logLines purely tied to stored processed lines
|
|
435
472
|
const logLines = useMemo(() => rawLogData, [rawLogData]);
|
|
436
473
|
// Prefer a known-good shell over a potentially broken $SHELL on some machines
|
|
@@ -463,12 +500,14 @@ export const App = () => {
|
|
|
463
500
|
const shell = process.env.SHELL || "/bin/zsh";
|
|
464
501
|
const cols = Math.max(10, Math.floor(terminalColsRef.current / 2) - 6);
|
|
465
502
|
const rows = Math.max(1, logsHeightRef.current - 2);
|
|
466
|
-
|
|
503
|
+
ptyRef.current = ptySpawn(shell, ["-c", command], {
|
|
467
504
|
cwd: process.cwd(),
|
|
468
505
|
env: process.env,
|
|
469
|
-
cols
|
|
470
|
-
rows
|
|
506
|
+
cols,
|
|
507
|
+
rows,
|
|
471
508
|
});
|
|
509
|
+
ptyAliveRef.current = true;
|
|
510
|
+
const ptyProcess = ptyRef.current;
|
|
472
511
|
ptyProcess.onData((chunk) => {
|
|
473
512
|
setUnTamperedLogs((oldLines) => oldLines + chunk);
|
|
474
513
|
logsManager.addChunk(chunk);
|
|
@@ -478,17 +517,19 @@ export const App = () => {
|
|
|
478
517
|
if (lines.length > 0) {
|
|
479
518
|
const newLines = lines.map((line) => ({
|
|
480
519
|
key: `log-${logKeyCounter.current++}`,
|
|
481
|
-
text:
|
|
520
|
+
text: line,
|
|
482
521
|
}));
|
|
483
522
|
// Append in single update
|
|
484
523
|
setRawLogData((prevLines) => [...prevLines, ...newLines]);
|
|
485
524
|
}
|
|
486
525
|
});
|
|
487
526
|
ptyProcess.onExit(({ exitCode }) => {
|
|
527
|
+
ptyAliveRef.current = false;
|
|
528
|
+
ptyRef.current = null;
|
|
488
529
|
if (partialLine.current.length > 0) {
|
|
489
530
|
const remainingLine = {
|
|
490
531
|
key: `log-${logKeyCounter.current++}`,
|
|
491
|
-
text:
|
|
532
|
+
text: partialLine.current,
|
|
492
533
|
};
|
|
493
534
|
setRawLogData((prevLines) => [...prevLines, remainingLine]);
|
|
494
535
|
partialLine.current = "";
|
|
@@ -520,7 +561,7 @@ export const App = () => {
|
|
|
520
561
|
if (partialLine.current.length > 0) {
|
|
521
562
|
const remainingLine = {
|
|
522
563
|
key: `log-${logKeyCounter.current++}`,
|
|
523
|
-
text:
|
|
564
|
+
text: partialLine.current,
|
|
524
565
|
};
|
|
525
566
|
setRawLogData((prev) => [...prev, remainingLine]);
|
|
526
567
|
partialLine.current = "";
|
|
@@ -565,15 +606,9 @@ export const App = () => {
|
|
|
565
606
|
const userMessage = new HumanMessage(chatInput);
|
|
566
607
|
const projects = await fetchProjectsFromCluster(loadProjectMetadata());
|
|
567
608
|
sendQuery([...chatResponseMessages, userMessage], generateArchitecture(projects.projects), logs, planningDoc);
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
]);
|
|
572
|
-
setTrimmedChats((prev) => [
|
|
573
|
-
...prev,
|
|
574
|
-
userMessage
|
|
575
|
-
]);
|
|
576
|
-
setVisibleChats(prev => [...prev, userMessage]);
|
|
609
|
+
setVisibleChats((prev) => [...prev, userMessage]);
|
|
610
|
+
setChatResponseMessages((prev) => [...prev, userMessage]);
|
|
611
|
+
setTrimmedChats((prev) => [...prev, userMessage]);
|
|
577
612
|
setChatInput("");
|
|
578
613
|
}
|
|
579
614
|
useInput((inputStr, key) => {
|
|
@@ -603,23 +638,23 @@ export const App = () => {
|
|
|
603
638
|
setChatResponseMessages(() => []);
|
|
604
639
|
setTrimmedChats(() => []);
|
|
605
640
|
setCompleteChatHistory(() => []);
|
|
641
|
+
setVisibleChats([initialAssistantMessage]);
|
|
606
642
|
setGraphState(null);
|
|
607
|
-
setSocketId
|
|
608
|
-
|
|
609
|
-
|
|
643
|
+
// setSocketId("");
|
|
644
|
+
setChatScroll(0);
|
|
645
|
+
// socket?.close();
|
|
646
|
+
// setIsConnected(false);
|
|
610
647
|
setIsLoading(false);
|
|
611
|
-
connectWebSocket();
|
|
648
|
+
// connectWebSocket();
|
|
612
649
|
setActivePane("input");
|
|
613
650
|
return;
|
|
614
651
|
}
|
|
615
652
|
if (key.ctrl && inputStr === "d") {
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
return "NORMAL";
|
|
622
|
-
});
|
|
653
|
+
const modes = ["NORMAL", "COPY", "LOGS"];
|
|
654
|
+
setMode((prev) => modes[(modes.indexOf(prev) + 1) % modes.length]);
|
|
655
|
+
setActivePane("logs");
|
|
656
|
+
setLogScroll((s) => s); // keep the scroll position
|
|
657
|
+
return;
|
|
623
658
|
}
|
|
624
659
|
if (key.tab) {
|
|
625
660
|
if (activePane === "input")
|
|
@@ -661,10 +696,10 @@ export const App = () => {
|
|
|
661
696
|
return (_jsxs(Box, { flexDirection: "column", height: terminalRows, width: "100%", padding: 0, overflow: "hidden", children: [_jsx(Box, { flexDirection: "row", width: "100%", height: LOGS_HEIGHT, padding: 0, overflow: "hidden", children: _jsxs(BorderBoxNoBorder, { title: "AI Chat", isFocused: activePane === "chat", width: "100%", children: [!API_KEY ? (_jsxs(Text, { children: ["Auth key not found.", "\n", "Login using:", " ", _jsx(Text, { color: "blue", children: "oncall login <your-auth-key>" })] })) : isConnected ? (_jsx(ScrollableContentChat, { lines: chatLinesChat, maxHeight: CHAT_HISTORY_HEIGHT - 1, isFocused: activePane === "chat", scrollOffset: chatScroll, onScrollChange: setChatScroll, isLoading: isLoading, showControlR: showControlR, customMessage: customMessage })) : (_jsx(Text, { children: "AI Chat not connected" })), isConnected && (_jsxs(Box, { borderStyle: "round", borderColor: activePane === "input" ? "greenBright" : "white", paddingX: 1, width: "100%", overflow: "hidden", children: [_jsx(Text, { color: "white", bold: activePane === "input", children: "Input:" }), _jsx(TextInput, { placeholder: "What's bugging you today?", value: chatInput, onChange: handleInputChange, onSubmit: userMessageSubmitted, focus: activePane === "input" })] }))] }) }), _jsx(ShortcutsFooter, { shortcuts: shortcuts })] }));
|
|
662
697
|
}
|
|
663
698
|
else if (mode === "LOGS") {
|
|
664
|
-
return (_jsxs(Box, { flexDirection: "column", height: terminalRows, width: "100%", padding: 0, overflow: "hidden", children: [_jsx(Box, { flexDirection: "row", width: "100%", height: LOGS_HEIGHT, padding: 0, overflow: "hidden", children: _jsx(BorderBoxNoBorder, { title: "Command Logs", isFocused: activePane === "
|
|
699
|
+
return (_jsxs(Box, { flexDirection: "column", height: terminalRows, width: "100%", padding: 0, overflow: "hidden", children: [_jsx(Box, { flexDirection: "row", width: "100%", height: LOGS_HEIGHT, padding: 0, overflow: "hidden", children: _jsx(BorderBoxNoBorder, { title: "Command Logs", isFocused: activePane === "logs", width: "100%", children: _jsx(ScrollableContent, { lines: logLines, maxHeight: LOGS_HEIGHT - 2, isFocused: activePane === "logs", scrollOffset: logScroll, onScrollChange: setLogScroll, availableWidth: Math.floor(terminalCols - 6) }) }) }), _jsx(ShortcutsFooter, { shortcuts: shortcuts })] }));
|
|
665
700
|
}
|
|
666
701
|
else {
|
|
667
|
-
return (_jsxs(Box, { flexDirection: "column", height: terminalRows, width: "100%", padding: 0, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "row", width: "100%", height: LOGS_HEIGHT, padding: 0, overflow: "hidden", children: [_jsx(BorderBox, { title: "Command Logs", isFocused: activePane === "logs", width: "50%", children: _jsx(ScrollableContent, { lines: logLines, maxHeight: LOGS_HEIGHT - 2, isFocused: activePane === "logs", scrollOffset: logScroll, onScrollChange: setLogScroll }) }), _jsxs(BorderBox, { title: "AI Chat", isFocused: activePane === "chat", width: "50%", children: [!API_KEY ? (_jsxs(Text, { children: ["Auth key not found.", "\n", "Login using:", " ", _jsx(Text, { color: "blue", children: "oncall login <your-auth-key>" })] })) : isConnected ? (_jsx(ScrollableContentChat, { lines: chatLinesChat, maxHeight: CHAT_HISTORY_HEIGHT - 1, isFocused: activePane === "chat", scrollOffset: chatScroll, onScrollChange: setChatScroll, isLoading: isLoading, showControlR: showControlR, customMessage: customMessage })) : (_jsx(Text, { children: "AI Chat not connected" })), isConnected && (_jsxs(Box, { borderStyle: "round", borderColor: activePane === "input" ? "greenBright" : "white", paddingX: 1, width: "100%", overflow: "hidden", children: [_jsx(Text, { color: "white", bold: activePane === "input", children: "Input:" }), _jsx(TextInput, { placeholder: "What's bugging you today?", value: chatInput, onChange: handleInputChange, onSubmit: userMessageSubmitted, focus: activePane === "input" })] }))] })] }), _jsx(ShortcutsFooter, { shortcuts: shortcuts })] }));
|
|
702
|
+
return (_jsxs(Box, { flexDirection: "column", height: terminalRows, width: "100%", padding: 0, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "row", width: "100%", height: LOGS_HEIGHT, padding: 0, overflow: "hidden", children: [_jsx(BorderBox, { title: "Command Logs", isFocused: activePane === "logs", width: "50%", children: _jsx(ScrollableContent, { lines: logLines, maxHeight: LOGS_HEIGHT - 2, isFocused: activePane === "logs", scrollOffset: logScroll, onScrollChange: setLogScroll, availableWidth: Math.floor(terminalCols / 2) - 6 }) }), _jsxs(BorderBox, { title: "AI Chat", isFocused: activePane === "chat", width: "50%", children: [!API_KEY ? (_jsxs(Text, { children: ["Auth key not found.", "\n", "Login using:", " ", _jsx(Text, { color: "blue", children: "oncall login <your-auth-key>" })] })) : isConnected ? (_jsx(ScrollableContentChat, { lines: chatLinesChat, maxHeight: CHAT_HISTORY_HEIGHT - 1, isFocused: activePane === "chat", scrollOffset: chatScroll, onScrollChange: setChatScroll, isLoading: isLoading, showControlR: showControlR, customMessage: customMessage })) : (_jsx(Text, { children: "AI Chat not connected" })), isConnected && (_jsxs(Box, { borderStyle: "round", borderColor: activePane === "input" ? "greenBright" : "white", paddingX: 1, width: "100%", overflow: "hidden", children: [_jsx(Text, { color: "white", bold: activePane === "input", children: "Input:" }), _jsx(TextInput, { placeholder: "What's bugging you today?", value: chatInput, onChange: handleInputChange, onSubmit: userMessageSubmitted, focus: activePane === "input" })] }))] })] }), _jsx(ShortcutsFooter, { shortcuts: shortcuts })] }));
|
|
668
703
|
}
|
|
669
704
|
};
|
|
670
705
|
render(_jsx(App, {}));
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export declare class RipGrep {
|
|
2
|
+
private query;
|
|
3
|
+
private searchDir;
|
|
4
|
+
private args;
|
|
5
|
+
constructor(query: string, searchDir: string);
|
|
6
|
+
withFilename(): this;
|
|
7
|
+
lineNumber(): this;
|
|
8
|
+
ignoreCase(): this;
|
|
9
|
+
glob(pattern: string): this;
|
|
10
|
+
fixedStrings(): this;
|
|
11
|
+
run(): Promise<{
|
|
12
|
+
asString: () => Promise<string>;
|
|
13
|
+
}>;
|
|
14
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// Wrapper for @vscode/ripgrep to match ripgrep-node API
|
|
2
|
+
import { rgPath } from "@vscode/ripgrep";
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
export class RipGrep {
|
|
5
|
+
constructor(query, searchDir) {
|
|
6
|
+
this.args = [];
|
|
7
|
+
this.query = query;
|
|
8
|
+
this.searchDir = searchDir;
|
|
9
|
+
}
|
|
10
|
+
withFilename() {
|
|
11
|
+
this.args.push("--with-filename");
|
|
12
|
+
return this;
|
|
13
|
+
}
|
|
14
|
+
lineNumber() {
|
|
15
|
+
this.args.push("--line-number");
|
|
16
|
+
return this;
|
|
17
|
+
}
|
|
18
|
+
ignoreCase() {
|
|
19
|
+
this.args.push("--ignore-case");
|
|
20
|
+
return this;
|
|
21
|
+
}
|
|
22
|
+
glob(pattern) {
|
|
23
|
+
this.args.push("--glob", pattern);
|
|
24
|
+
return this;
|
|
25
|
+
}
|
|
26
|
+
fixedStrings() {
|
|
27
|
+
this.args.push("--fixed-strings");
|
|
28
|
+
return this;
|
|
29
|
+
}
|
|
30
|
+
async run() {
|
|
31
|
+
const allArgs = [
|
|
32
|
+
...this.args,
|
|
33
|
+
this.query,
|
|
34
|
+
this.searchDir,
|
|
35
|
+
];
|
|
36
|
+
return {
|
|
37
|
+
asString: async () => {
|
|
38
|
+
return new Promise((resolve, reject) => {
|
|
39
|
+
const rgProcess = spawn(rgPath, allArgs, {
|
|
40
|
+
cwd: this.searchDir,
|
|
41
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
42
|
+
});
|
|
43
|
+
let stdout = "";
|
|
44
|
+
let stderr = "";
|
|
45
|
+
rgProcess.stdout.on("data", (data) => {
|
|
46
|
+
stdout += data.toString();
|
|
47
|
+
});
|
|
48
|
+
rgProcess.stderr.on("data", (data) => {
|
|
49
|
+
stderr += data.toString();
|
|
50
|
+
});
|
|
51
|
+
rgProcess.on("close", (code) => {
|
|
52
|
+
if (code === 0 || code === 1) {
|
|
53
|
+
resolve(stdout);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
reject(new Error(`ripgrep exited with code ${code}: ${stderr || stdout}`));
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
rgProcess.on("error", (error) => {
|
|
60
|
+
reject(error);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=ripgrep-wrapper.js.map
|
package/dist/tools/ripgrep.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RipGrep } from "ripgrep-
|
|
1
|
+
import { RipGrep } from "./ripgrep-wrapper.js";
|
|
2
2
|
export async function ripgrepSearch(query, options = {}) {
|
|
3
3
|
const defaultExcludePatterns = [
|
|
4
4
|
"node_modules",
|
|
@@ -54,7 +54,8 @@ export async function ripgrepSearch(query, options = {}) {
|
|
|
54
54
|
const excludePattern = isFilePattern ? `!${pattern}` : `!${pattern}/**`;
|
|
55
55
|
rg.glob(excludePattern);
|
|
56
56
|
}
|
|
57
|
-
const
|
|
57
|
+
const runResult = await rg.run();
|
|
58
|
+
const output = await runResult.asString();
|
|
58
59
|
if (!output || output.trim().length === 0) {
|
|
59
60
|
return [];
|
|
60
61
|
}
|
package/dist/useWebSocket.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
2
|
import WebSocket from "ws";
|
|
3
|
-
import { getContextLines } from "./utils.js";
|
|
3
|
+
import { getContextLines, cleanMessagesContent } from "./utils.js";
|
|
4
|
+
import { config } from "./config.js";
|
|
4
5
|
import { toolFunctionCall } from "./api.js";
|
|
5
6
|
import fs from "fs";
|
|
6
7
|
import os from "os";
|
|
7
8
|
import path from "path";
|
|
8
|
-
import { mapChatMessagesToStoredMessages, mapStoredMessagesToChatMessages, SystemMessage, } from "@langchain/core/messages";
|
|
9
|
+
import { mapChatMessagesToStoredMessages, mapStoredMessagesToChatMessages, SystemMessage, AIMessage } from "@langchain/core/messages";
|
|
10
|
+
import axios from "axios";
|
|
9
11
|
import { ripgrepSearch } from "./helpers/ripgrep-tool.js";
|
|
10
12
|
import { loadProjectMetadata, logd } from "./helpers/cli-helpers.js";
|
|
11
13
|
// Load OnCall config from ~/.oncall/config
|
|
@@ -29,10 +31,12 @@ catch (err) {
|
|
|
29
31
|
export function useWebSocket(url, rawLogData) {
|
|
30
32
|
//refs
|
|
31
33
|
const socketRef = useRef(null);
|
|
34
|
+
const summarizeInProgressRef = useRef(false);
|
|
35
|
+
const graphStateRef = useRef(null);
|
|
32
36
|
const [socketId, setSocketId] = useState(null);
|
|
33
37
|
const [chatResponseMessages, setChatResponseMessages] = useState([]);
|
|
34
38
|
const [trimmedChats, setTrimmedChats] = useState([]);
|
|
35
|
-
const initialAssistantMessage = new SystemMessage("I
|
|
39
|
+
const initialAssistantMessage = new SystemMessage("I'm watching your app run locally. You can ask me about errors, logs, performance,or anything else related to this run.");
|
|
36
40
|
const [visibleChats, setVisibleChats] = useState([
|
|
37
41
|
initialAssistantMessage,
|
|
38
42
|
]);
|
|
@@ -43,6 +47,9 @@ export function useWebSocket(url, rawLogData) {
|
|
|
43
47
|
const [customMessage, setCustomMessage] = useState(null);
|
|
44
48
|
const [graphState, setGraphState] = useState(null);
|
|
45
49
|
const authKey = API_KEY;
|
|
50
|
+
useEffect(() => {
|
|
51
|
+
graphStateRef.current = graphState;
|
|
52
|
+
}, [graphState]);
|
|
46
53
|
const initialProjectMetadata = loadProjectMetadata();
|
|
47
54
|
const getProjectMetadata = () => loadProjectMetadata() || initialProjectMetadata;
|
|
48
55
|
const getServiceId = () => getProjectMetadata()?.window_id;
|
|
@@ -61,6 +68,70 @@ export function useWebSocket(url, rawLogData) {
|
|
|
61
68
|
process.exit();
|
|
62
69
|
}
|
|
63
70
|
}, [API_KEY]);
|
|
71
|
+
const callSummarizeAPI = useCallback(async (currentGraphState) => {
|
|
72
|
+
if (summarizeInProgressRef.current) {
|
|
73
|
+
logd("[summarize] Summarize API call already in progress.");
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
if (!currentGraphState || typeof currentGraphState !== "object") {
|
|
77
|
+
logd("[summarize] No graphState available");
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
summarizeInProgressRef.current = true;
|
|
81
|
+
logd("[summarize] Calling summarize API with graphState");
|
|
82
|
+
try {
|
|
83
|
+
const response = await axios.post(`${config.api_base_url}/graph/summarize`, {
|
|
84
|
+
authKey: authKey,
|
|
85
|
+
graphState: currentGraphState,
|
|
86
|
+
});
|
|
87
|
+
if (response.data && response.data.success && response.data.summary) {
|
|
88
|
+
const summaryText = response.data.summary;
|
|
89
|
+
const summaryMessage = new AIMessage(`Current Approach Summary:\n${summaryText}`);
|
|
90
|
+
setVisibleChats((old) => [...old, summaryMessage]);
|
|
91
|
+
setTrimmedChats((old) => [...old, summaryMessage]);
|
|
92
|
+
setChatResponseMessages((old) => [...old, summaryMessage]);
|
|
93
|
+
setGraphState((prevState) => {
|
|
94
|
+
if (!prevState)
|
|
95
|
+
return prevState;
|
|
96
|
+
const storedMessages = mapChatMessagesToStoredMessages([summaryMessage]);
|
|
97
|
+
const storedSummaryMessage = storedMessages && storedMessages.length > 0 ? storedMessages[0] : null;
|
|
98
|
+
if (!storedSummaryMessage) {
|
|
99
|
+
return prevState;
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
...prevState,
|
|
103
|
+
messages: [...(prevState.messages || []), storedSummaryMessage],
|
|
104
|
+
};
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
throw new Error("Invalid response from summarize API");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
logd(`[summarize] ❌ Error calling summarize API: ${error instanceof Error ? error.message : String(error)}`);
|
|
113
|
+
const errorMessage = new AIMessage("Something went wrong. Please try asking the question again." + error.message);
|
|
114
|
+
setVisibleChats((old) => [...old, errorMessage]);
|
|
115
|
+
setTrimmedChats((old) => [...old, errorMessage]);
|
|
116
|
+
setChatResponseMessages((old) => [...old, errorMessage]);
|
|
117
|
+
setGraphState((prevState) => {
|
|
118
|
+
if (!prevState)
|
|
119
|
+
return prevState;
|
|
120
|
+
const storedMessages = mapChatMessagesToStoredMessages([errorMessage]);
|
|
121
|
+
const storedErrorMessage = storedMessages && storedMessages.length > 0 ? storedMessages[0] : null;
|
|
122
|
+
if (!storedErrorMessage) {
|
|
123
|
+
return prevState;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
...prevState,
|
|
127
|
+
messages: [...(prevState.messages || []), storedErrorMessage],
|
|
128
|
+
};
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
finally {
|
|
132
|
+
summarizeInProgressRef.current = false;
|
|
133
|
+
}
|
|
134
|
+
}, [authKey]);
|
|
64
135
|
//web socket connection
|
|
65
136
|
const connectWebSocket = useCallback(() => {
|
|
66
137
|
// const d = fs.readFileSync('logs')
|
|
@@ -217,8 +288,9 @@ export function useWebSocket(url, rawLogData) {
|
|
|
217
288
|
messages = mapStoredMessagesToChatMessages(messages);
|
|
218
289
|
switch (data.data.type) {
|
|
219
290
|
case "messages":
|
|
220
|
-
if (data.data.sender !== "toolNode") {
|
|
221
|
-
|
|
291
|
+
if (data.data.sender !== "toolNode" && data.data.sender !== "routerNode") {
|
|
292
|
+
const cleanedMessages = cleanMessagesContent(messages);
|
|
293
|
+
setVisibleChats(old => [...old, ...cleanedMessages]);
|
|
222
294
|
}
|
|
223
295
|
// logd(`LOGGING RESPONSE SENDER: ${data.data.sender}`);
|
|
224
296
|
break;
|
|
@@ -229,12 +301,14 @@ export function useWebSocket(url, rawLogData) {
|
|
|
229
301
|
break;
|
|
230
302
|
case "updates":
|
|
231
303
|
if (data.data.sender !== "toolNode" && data.data.sender !== "routerNode") {
|
|
232
|
-
|
|
304
|
+
const cleanedMessages = cleanMessagesContent(messages);
|
|
305
|
+
setChatResponseMessages(old => [...old, ...cleanedMessages]);
|
|
233
306
|
}
|
|
234
307
|
if (data.data.sender === "answerNode") {
|
|
308
|
+
const cleanedMessages = cleanMessagesContent(messages);
|
|
235
309
|
setTrimmedChats(prev => {
|
|
236
|
-
setVisibleChats([...prev, ...
|
|
237
|
-
return [...prev, ...
|
|
310
|
+
setVisibleChats([...prev, ...cleanedMessages]);
|
|
311
|
+
return [...prev, ...cleanedMessages];
|
|
238
312
|
});
|
|
239
313
|
setIsLoading(false);
|
|
240
314
|
}
|
|
@@ -250,11 +324,18 @@ export function useWebSocket(url, rawLogData) {
|
|
|
250
324
|
data.type === "ask_user") {
|
|
251
325
|
setIsLoading(false);
|
|
252
326
|
setCustomMessage(null);
|
|
327
|
+
const currentGraphState = graphStateRef.current;
|
|
328
|
+
if (currentGraphState) {
|
|
329
|
+
callSummarizeAPI(currentGraphState);
|
|
330
|
+
}
|
|
253
331
|
}
|
|
254
332
|
}
|
|
255
333
|
catch (error) {
|
|
256
334
|
setIsLoading(false);
|
|
257
|
-
|
|
335
|
+
const currentGraphState = graphStateRef.current;
|
|
336
|
+
if (currentGraphState) {
|
|
337
|
+
callSummarizeAPI(currentGraphState);
|
|
338
|
+
}
|
|
258
339
|
return;
|
|
259
340
|
}
|
|
260
341
|
};
|
|
@@ -274,7 +355,7 @@ export function useWebSocket(url, rawLogData) {
|
|
|
274
355
|
// process.exit();
|
|
275
356
|
// };
|
|
276
357
|
// }
|
|
277
|
-
}, [url, authKey]);
|
|
358
|
+
}, [url, authKey, callSummarizeAPI]);
|
|
278
359
|
const sendQuery = useCallback((messages, architecture, logs, planningDoc) => {
|
|
279
360
|
const socket = socketRef.current;
|
|
280
361
|
if (socket && socket.readyState === WebSocket.OPEN) {
|
|
@@ -121,4 +121,67 @@ export async function checkVersionAndExit() {
|
|
|
121
121
|
process.exit(1);
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
+
async function fetchLatestVersionFromNpm(packageName) {
|
|
125
|
+
try {
|
|
126
|
+
const response = await axios.get(`https://registry.npmjs.org/${packageName}/latest`, {
|
|
127
|
+
timeout: 5000, // 5 second timeout
|
|
128
|
+
});
|
|
129
|
+
return response.data?.version || null;
|
|
130
|
+
}
|
|
131
|
+
catch (error) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function formatUpdateNotification(currentVersion, latestVersion, packageName) {
|
|
136
|
+
return `
|
|
137
|
+
📦 Update Available
|
|
138
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
139
|
+
|
|
140
|
+
Your version: ${currentVersion}
|
|
141
|
+
Latest version: ${latestVersion}
|
|
142
|
+
|
|
143
|
+
To update, run:
|
|
144
|
+
npm install -g ${packageName}@latest
|
|
145
|
+
|
|
146
|
+
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
147
|
+
`;
|
|
148
|
+
}
|
|
149
|
+
export async function checkForUpdates(packageName = "oncall-cli", forceCheck = false) {
|
|
150
|
+
if (!forceCheck) {
|
|
151
|
+
const cache = readVersionCache();
|
|
152
|
+
if (cache?.lastUpdateCheck) {
|
|
153
|
+
const now = Date.now();
|
|
154
|
+
const timeSinceLastCheck = now - cache.lastUpdateCheck;
|
|
155
|
+
if (timeSinceLastCheck < CACHE_DURATION_MS) {
|
|
156
|
+
if (cache.lastKnownVersion) {
|
|
157
|
+
const currentVersion = getCurrentVersion();
|
|
158
|
+
if (compareVersions(currentVersion, cache.lastKnownVersion) >= 0) {
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const currentVersion = getCurrentVersion();
|
|
170
|
+
const latestVersion = await fetchLatestVersionFromNpm(packageName);
|
|
171
|
+
if (!latestVersion) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const cache = readVersionCache() || {};
|
|
175
|
+
writeVersionCache({
|
|
176
|
+
...cache,
|
|
177
|
+
lastUpdateCheck: Date.now(),
|
|
178
|
+
lastKnownVersion: latestVersion,
|
|
179
|
+
});
|
|
180
|
+
if (compareVersions(currentVersion, latestVersion) < 0) {
|
|
181
|
+
console.warn(formatUpdateNotification(currentVersion, latestVersion, packageName));
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
}
|
|
186
|
+
}
|
|
124
187
|
//# sourceMappingURL=version-check.js.map
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BaseMessage } from "langchain";
|
|
2
2
|
export declare function getContextLines(fileName: string, lineNumber: number, before?: number, after?: number): string;
|
|
3
|
+
export declare function cleanMessagesContent(messages: BaseMessage[]): BaseMessage[];
|
|
3
4
|
export declare function extractMessageContent(messages: BaseMessage[]): string;
|
|
4
5
|
/**
|
|
5
6
|
* Get a unique key for a message for deduplication purposes
|
package/dist/utils.js
CHANGED
|
@@ -59,10 +59,120 @@ export function getContextLines(fileName, lineNumber, before = 30, after = 30) {
|
|
|
59
59
|
// });
|
|
60
60
|
// redisClient.connect();
|
|
61
61
|
// export default redisClient;
|
|
62
|
+
function isIncompleteJSONChunk(content) {
|
|
63
|
+
if (typeof content !== "string")
|
|
64
|
+
return false;
|
|
65
|
+
const trimmed = content.trim();
|
|
66
|
+
const hasJSONFields = trimmed.includes('"next"') || trimmed.includes('"message"');
|
|
67
|
+
if (!hasJSONFields) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
if (trimmed.startsWith("{") && !trimmed.endsWith("}")) {
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (!trimmed.startsWith("{") && hasJSONFields) {
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
if (trimmed.endsWith("}") && !trimmed.startsWith("{")) {
|
|
77
|
+
return true;
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function cleanMessageContent(content) {
|
|
82
|
+
if (typeof content !== "string")
|
|
83
|
+
return content;
|
|
84
|
+
const trimmed = content.trim();
|
|
85
|
+
if (isIncompleteJSONChunk(trimmed)) {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
if ((trimmed.includes('"next"') || trimmed.includes('"message"')) && !trimmed.startsWith("{")) {
|
|
89
|
+
if (trimmed.endsWith("}")) {
|
|
90
|
+
return "";
|
|
91
|
+
}
|
|
92
|
+
if (trimmed.includes('":"') && (trimmed.includes('"next"') || trimmed.includes('"message"'))) {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
97
|
+
try {
|
|
98
|
+
const parsed = JSON.parse(trimmed);
|
|
99
|
+
if (parsed && typeof parsed === "object" && typeof parsed.message === "string") {
|
|
100
|
+
return parsed.message;
|
|
101
|
+
}
|
|
102
|
+
else if (parsed && typeof parsed === "object") {
|
|
103
|
+
return "";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
const jsonMatch = trimmed.match(/\{[\s\S]*?"message"[\s\S]*?\}/);
|
|
110
|
+
if (jsonMatch) {
|
|
111
|
+
try {
|
|
112
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
113
|
+
if (parsed && typeof parsed === "object" && typeof parsed.message === "string") {
|
|
114
|
+
return parsed.message;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return content;
|
|
121
|
+
}
|
|
122
|
+
export function cleanMessagesContent(messages) {
|
|
123
|
+
return messages.map(msg => {
|
|
124
|
+
if (typeof msg.content === "string") {
|
|
125
|
+
const cleaned = cleanMessageContent(msg.content);
|
|
126
|
+
const cleanedMsg = { ...msg };
|
|
127
|
+
cleanedMsg.content = cleaned;
|
|
128
|
+
return cleanedMsg;
|
|
129
|
+
}
|
|
130
|
+
return msg;
|
|
131
|
+
});
|
|
132
|
+
}
|
|
62
133
|
export function extractMessageContent(messages) {
|
|
63
|
-
|
|
64
|
-
|
|
134
|
+
const fullContent = messages.reduce((prev, message) => {
|
|
135
|
+
let content = message.content;
|
|
136
|
+
if (typeof content === "string") {
|
|
137
|
+
return prev + content;
|
|
138
|
+
}
|
|
139
|
+
return prev;
|
|
65
140
|
}, "");
|
|
141
|
+
if (typeof fullContent === "string") {
|
|
142
|
+
const trimmed = fullContent.trim();
|
|
143
|
+
if (isIncompleteJSONChunk(trimmed)) {
|
|
144
|
+
return "";
|
|
145
|
+
}
|
|
146
|
+
if (trimmed.includes('"next"') || trimmed.includes('"message"')) {
|
|
147
|
+
const jsonMatch = trimmed.match(/\{[\s\S]*?"message"[\s\S]*?\}/);
|
|
148
|
+
if (jsonMatch) {
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
151
|
+
if (parsed && typeof parsed === "object" && typeof parsed.message === "string") {
|
|
152
|
+
return parsed.message;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (trimmed.startsWith("{") && trimmed.endsWith("}")) {
|
|
159
|
+
try {
|
|
160
|
+
const parsed = JSON.parse(trimmed);
|
|
161
|
+
if (parsed && typeof parsed === "object" && typeof parsed.message === "string") {
|
|
162
|
+
return parsed.message;
|
|
163
|
+
}
|
|
164
|
+
else if (parsed && typeof parsed === "object") {
|
|
165
|
+
return "";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// Not valid JSON
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return fullContent;
|
|
174
|
+
}
|
|
175
|
+
return fullContent;
|
|
66
176
|
}
|
|
67
177
|
/**
|
|
68
178
|
* Get a unique key for a message for deduplication purposes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oncall-cli",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"bin": {
|
|
6
6
|
"oncall": "bin/oncall.js"
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
"@langchain/core": "^1.1.0",
|
|
28
28
|
"@langchain/openai": "^1.2.0",
|
|
29
29
|
"@types/ws": "^8.18.1",
|
|
30
|
+
"@vscode/ripgrep": "^1.17.0",
|
|
30
31
|
"axios": "^1.12.2",
|
|
31
32
|
"dotenv": "^17.2.3",
|
|
32
33
|
"ignore": "^5.3.2",
|
|
@@ -40,7 +41,6 @@
|
|
|
40
41
|
"node-pty": "0.10.1",
|
|
41
42
|
"patch-package": "^8.0.1",
|
|
42
43
|
"react": "^19.2.0",
|
|
43
|
-
"ripgrep-node": "^1.0.0",
|
|
44
44
|
"ws": "^8.18.3",
|
|
45
45
|
"yaml": "^2.8.2"
|
|
46
46
|
},
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
diff --git a/node_modules/ripgrep-node/dist/index.js b/node_modules/ripgrep-node/dist/index.js
|
|
2
|
-
index 215d5e7..6c96596 100644
|
|
3
|
-
--- a/node_modules/ripgrep-node/dist/index.js
|
|
4
|
-
+++ b/node_modules/ripgrep-node/dist/index.js
|
|
5
|
-
@@ -71,7 +71,7 @@ class RipGrep {
|
|
6
|
-
this.output = child_process_1.execSync(rgCommand).toString();
|
|
7
|
-
}
|
|
8
|
-
catch (error) {
|
|
9
|
-
- console.log(error.stdout);
|
|
10
|
-
+ // console.log(error.stdout);
|
|
11
|
-
}
|
|
12
|
-
return this;
|
|
13
|
-
};
|