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 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
- function handleLogin(authKey) {
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
- ensureConfigDir();
143
- let contents = "";
144
- if (fs.existsSync(CONFIG_PATH)) {
145
- contents = fs.readFileSync(CONFIG_PATH, "utf8");
146
- }
147
- const updatedContents = upsertConfigValue(contents, "API_KEY", authKey);
148
- fs.writeFileSync(CONFIG_PATH, updatedContents, "utf8");
149
- console.log("✅ Auth key saved.\n");
150
- console.log(`Next:
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
- process.exit(0);
178
+ process.exit(0);
179
+ } else {
180
+ console.error(" Invalid Key");
181
+ }
154
182
  } catch (err) {
155
- console.error("❌ Failed to save auth key:", err);
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
@@ -1 +1,2 @@
1
+ export declare const isValidAuthKey: (authKey: string) => Promise<any>;
1
2
  export declare const toolFunctionCall: (tool_call_id: any, resultArgs: any, args: any, function_name: any) => Promise<any>;
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-node";
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 || (typeof process !== "undefined" ? process.cwd() : undefined);
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 output = await rg.run().asString();
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) || 'N/A'}`);
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 { loadProjectMetadata, fetchProjectsFromCluster, logd } from "./helpers/cli-helpers.js";
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 = '*#COLON|*';
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), 'g');
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
- //scrollable content component
86
- const ScrollableContent = ({ lines, maxHeight, isFocused, onScrollChange, scrollOffset, }) => {
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 rendered = undoColon(marked.parseInline(line.text));
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
- if (obj?.type !== "progress")
260
- return undefined;
261
- const messages = (obj.data && obj.data?.messages) ?? [];
262
- const latestAI = [...messages]
263
- .reverse()
264
- .find((m) => m.id?.includes("AIMessage"));
265
- const content = latestAI?.kwargs?.content?.trim();
266
- if (!content)
267
- return undefined;
268
- if (content === lastAIMessage) {
269
- return undefined;
270
- }
271
- lastAIMessage = content;
272
- if (content === undefined)
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 = (new Date()).getTime() + "";
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 extratMessageContent(messages) {
326
- return messages.reduce((prev, message) => {
327
- return prev + message.content;
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 && message.tool_call_chunks[0] && message.tool_call_chunks[0].args)
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(() => rawLogData.map((l) => l.text).join("\n"), [rawLogData]);
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
- const availableWidth = Math.floor(terminalColsRef.current / 2) - 6;
423
- const expandedText = text.replace(/\t/g, " ".repeat(8)).replace(/\x1B\[1G/g, '').replace(/\x1B\[0K/g, '');
424
- const width = stringWidth(expandedText);
425
- if (width > availableWidth && availableWidth > 3) {
426
- const truncated = sliceAnsi(expandedText, 0, Math.max(0, availableWidth - 3));
427
- return truncated + "...";
428
- }
429
- // fs.appendFileSync("./temp-logs", text)
430
- // fs.appendFileSync("./temp-logs", "\n")
431
- // fs.appendFileSync("./temp-logs", expandedText)
432
- return expandedText;
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
- const ptyProcess = ptySpawn(shell, ["-c", command], {
503
+ ptyRef.current = ptySpawn(shell, ["-c", command], {
467
504
  cwd: process.cwd(),
468
505
  env: process.env,
469
- cols: cols,
470
- rows: 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: getProcessedLine(line), // process once here
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: getProcessedLine(partialLine.current),
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: getProcessedLine(partialLine.current),
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
- setChatResponseMessages((prev) => [
569
- ...prev,
570
- userMessage
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
- socket?.close();
609
- setIsConnected(false);
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
- setMode((prev) => {
617
- if (prev === "NORMAL")
618
- return "COPY";
619
- if (prev === "COPY")
620
- return "LOGS";
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 === "chat", width: "100%", children: _jsx(ScrollableContent, { lines: logLines, maxHeight: LOGS_HEIGHT - 2, isFocused: activePane === "logs", scrollOffset: logScroll, onScrollChange: setLogScroll }) }) }), _jsx(ShortcutsFooter, { shortcuts: shortcuts })] }));
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
@@ -1,4 +1,4 @@
1
- import { RipGrep } from "ripgrep-node";
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 output = await rg.run().asString();
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
  }
@@ -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("Im watching your app run locally. You can ask me about errors, logs, performance,or anything else related to this run.");
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
- setVisibleChats(old => [...old, ...messages]);
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
- setChatResponseMessages(old => [...old, ...messages]);
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, ...messages]);
237
- return [...prev, ...messages];
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
- // console.warn("WebSocket message handling warning:", error);
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) {
@@ -1,2 +1,3 @@
1
1
  export function checkVersionCompatibility(forceCheck?: boolean): Promise<boolean>;
2
2
  export function checkVersionAndExit(): Promise<void>;
3
+ export function checkForUpdates(packageName?: string, forceCheck?: boolean): Promise<void>;
@@ -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
- return messages.reduce((prev, message) => {
64
- return prev + message.content;
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",
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
- };