grok-bot-cli 0.3.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +54 -22
  3. package/dist/.agents/plugins/marketplace.json +1 -0
  4. package/dist/.claude-plugin/marketplace.json +1 -0
  5. package/dist/.claude-plugin/plugin.json +1 -0
  6. package/dist/.codex-plugin/mcp.json +1 -0
  7. package/dist/.codex-plugin/plugin.json +1 -0
  8. package/dist/.cursor-plugin/marketplace.json +1 -0
  9. package/dist/.cursor-plugin/mcp.json +1 -0
  10. package/dist/.cursor-plugin/plugin.json +1 -0
  11. package/dist/.mcp.json +1 -0
  12. package/dist/INSTALL.md +269 -0
  13. package/dist/LICENSE +21 -0
  14. package/dist/README.md +173 -0
  15. package/dist/agent-bundle.compile-evidence.json +1 -0
  16. package/dist/agent-bundle.manifest.json +1 -0
  17. package/dist/agent-bundle.package-compile-evidence.json +1 -0
  18. package/dist/bin/gbot-flight.mjs +29162 -0
  19. package/dist/bin/gbot-install.js +135699 -0
  20. package/dist/bin/gbot.mjs +87242 -0
  21. package/dist/install.mjs +1322 -0
  22. package/dist/mcp/mcp-grok-bot-b8c2461e-flight.mjs +26413 -0
  23. package/dist/mcp/mcp-grok-bot-b8c2461e.mjs +107760 -0
  24. package/dist/mcp.json +1 -0
  25. package/dist/package.json +60 -0
  26. package/dist/plugin.json +1 -0
  27. package/dist/skills/talk-to-grok-bot/SKILL.md +37 -0
  28. package/package.json +30 -10
  29. package/src/app-session.js +0 -249
  30. package/src/cli.js +0 -548
  31. package/src/codex-bridge.js +0 -480
  32. package/src/commands.js +0 -52
  33. package/src/gateway.js +0 -361
  34. package/src/headers.js +0 -53
  35. package/src/history.js +0 -83
  36. package/src/store.js +0 -358
  37. package/src/transcript.js +0 -50
  38. package/src/url-policy.js +0 -161
package/src/gateway.js DELETED
@@ -1,361 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { ensureSandboxHeaders, headersFromEnsureSandbox, headersFromEnv, mergeGatewayHeaders, normalizeHeaderMap, requestHeaders } from "./headers.js";
3
- import { hasGrokBotGatewaySession, loadGrokBotGatewaySession } from "./app-session.js";
4
- import { AVATAR_COLORS, AVATAR_SHAPES, MAX_GROUP_MEMBERS } from "./store.js";
5
- import { assertAllowedCredentialUrl, redactSecrets } from "./url-policy.js";
6
-
7
- export class GatewayError extends Error {
8
- constructor(message, { status, method } = {}) {
9
- super(message);
10
- this.name = "GatewayError";
11
- this.status = status;
12
- this.method = method;
13
- }
14
- }
15
-
16
- // ponytail: fixed 30 s deadline and buffered byte cap; upgrade path is per-method budgets plus streaming reads.
17
- export const GATEWAY_TIMEOUT_MS = 30000;
18
- export const GATEWAY_MAX_RESPONSE_BYTES = 2 * 1024 * 1024;
19
-
20
- function backendBase() {
21
- return (
22
- process.env.SAND_BACKEND_URL ||
23
- process.env.CURSOR_API_BASE_URL ||
24
- "https://api2.cursor.sh"
25
- ).replace(/\/$/, "");
26
- }
27
-
28
- function accessTokenFromEnv() {
29
- return (
30
- process.env.CURSOR_ACCESS_TOKEN ||
31
- process.env.GROK_BOT_ACCESS_TOKEN ||
32
- process.env.SAND_ACCESS_TOKEN ||
33
- ""
34
- ).trim();
35
- }
36
-
37
- function gatewayTokenFromEnv() {
38
- return (
39
- process.env.GROK_BOT_GATEWAY_TOKEN ||
40
- process.env.SAND_HOST_GATEWAY_TOKEN ||
41
- process.env.SAND_GATEWAY_TOKEN ||
42
- ""
43
- ).trim();
44
- }
45
-
46
- function gatewayOverride() {
47
- const token = gatewayTokenFromEnv();
48
- const explicitUrl = (process.env.GROK_BOT_GATEWAY_URL || process.env.SAND_HOST_GATEWAY_URL || "").trim();
49
- const localUrl = token
50
- ? "http://127.0.0.1:" + (process.env.SAND_HOST_PORT || "1340")
51
- : "";
52
- const url = explicitUrl || localUrl;
53
- if (url && token) {
54
- return {
55
- gatewayUrl: assertAllowedCredentialUrl(url.replace(/\/$/, ""), { kind: "gateway" }),
56
- gatewayToken: token,
57
- gatewayHeaders: headersFromEnv(),
58
- };
59
- }
60
- return null;
61
- }
62
-
63
- function sessionFromApp() {
64
- let loaded;
65
- try {
66
- loaded = loadGrokBotGatewaySession();
67
- } catch (error) {
68
- // Descriptor present but unusable (e.g. Windows Local State missing). Fall
69
- // through to CURSOR_ACCESS_TOKEN → EnsureSandBox when that token is set.
70
- if (accessTokenFromEnv()) return null;
71
- throw error instanceof Error ? new GatewayError(error.message) : error;
72
- }
73
- if (!loaded) return null;
74
- return {
75
- gatewayUrl: assertAllowedCredentialUrl(loaded.gatewayUrl, { kind: "gateway" }),
76
- gatewayToken: loaded.gatewayToken,
77
- gatewayHeaders: mergeGatewayHeaders(normalizeHeaderMap(loaded.headers), headersFromEnv()),
78
- };
79
- }
80
-
81
- export function hasGatewayAuth() {
82
- return Boolean(gatewayOverride() || accessTokenFromEnv() || hasGrokBotGatewaySession());
83
- }
84
-
85
- async function readJson(res) {
86
- const text = await res.text();
87
- if (text.length > GATEWAY_MAX_RESPONSE_BYTES) {
88
- throw new GatewayError("Gateway response too large (" + text.length + " bytes, limit " + GATEWAY_MAX_RESPONSE_BYTES + ")");
89
- }
90
- if (!text) return {};
91
- try {
92
- return JSON.parse(text);
93
- } catch {
94
- return { raw: text };
95
- }
96
- }
97
-
98
- function pick(obj, ...keys) {
99
- if (!obj || typeof obj !== "object") return undefined;
100
- for (const key of keys) {
101
- if (obj[key] != null && obj[key] !== "") return obj[key];
102
- }
103
- return undefined;
104
- }
105
-
106
- export async function ensureSandbox(accessToken) {
107
- const url = assertAllowedCredentialUrl(backendBase(), { kind: "backend" }) + "/aiserver.v1.GrokBotService/EnsureSandBox";
108
- const res = await fetch(url, {
109
- method: "POST",
110
- redirect: "error",
111
- signal: AbortSignal.timeout(GATEWAY_TIMEOUT_MS),
112
- headers: ensureSandboxHeaders(accessToken),
113
- body: "{}",
114
- });
115
- const body = await readJson(res);
116
- if (!res.ok) {
117
- const detail = body.message || body.error || body.raw || res.statusText;
118
- throw new GatewayError("EnsureSandBox failed: " + res.status + " " + redactSecrets(detail), { status: res.status, method: "EnsureSandBox" });
119
- }
120
- const gatewayUrl = pick(body, "gatewayUrl", "gateway_url");
121
- const gatewayToken = pick(body, "gatewayToken", "gateway_token");
122
- if (!gatewayUrl || !gatewayToken) {
123
- throw new GatewayError("EnsureSandBox returned no gatewayUrl/gatewayToken. Auth may be a dashboard API key (those do not work).");
124
- }
125
- return { gatewayUrl: assertAllowedCredentialUrl(String(gatewayUrl).replace(/\/$/, ""), { kind: "gateway" }), gatewayToken: String(gatewayToken), gatewayHeaders: mergeGatewayHeaders(headersFromEnsureSandbox(body), headersFromEnv()) };
126
- }
127
-
128
- export async function connectGateway() {
129
- const override = gatewayOverride();
130
- if (override) return override;
131
- const fromApp = sessionFromApp();
132
- if (fromApp) return fromApp;
133
- const token = accessTokenFromEnv();
134
- if (!token) {
135
- throw new GatewayError("Set CURSOR_ACCESS_TOKEN, or GROK_BOT_GATEWAY_URL + GROK_BOT_GATEWAY_TOKEN. Do not use a Cursor dashboard API key.");
136
- }
137
- return ensureSandbox(token);
138
- }
139
-
140
- export async function gatewayCall(session, method, body = {}) {
141
- const base = assertAllowedCredentialUrl(session.gatewayUrl, { kind: "gateway" });
142
- const url = base + "/api/" + method;
143
- const res = await fetch(url, {
144
- method: "POST",
145
- redirect: "error",
146
- signal: AbortSignal.timeout(GATEWAY_TIMEOUT_MS),
147
- headers: requestHeaders(session),
148
- body: JSON.stringify(body),
149
- });
150
- const data = await readJson(res);
151
- if (!res.ok) {
152
- const detail = data.message || data.error || data.raw || res.statusText;
153
- throw new GatewayError(method + " failed: " + res.status + " " + redactSecrets(String(detail).slice(0, 300)), { status: res.status, method });
154
- }
155
- return data;
156
- }
157
-
158
- function asRecord(agent) {
159
- if (!agent) return null;
160
- const id = agent.id || agent.agentId;
161
- const memberIds = agent.memberIds || agent.memberAgentIds || [];
162
- const notify = agent.notifyOnUpdatesEnabled ?? agent.notifyOnAgentUpdates;
163
- const hidden = agent.isHiddenFromSidebar ?? agent.hiddenFromSidebar;
164
- return {
165
- id,
166
- name: agent.name || "",
167
- title: agent.title || "",
168
- description: agent.description || "",
169
- avatarShape: agent.avatarShape || "",
170
- avatarColor: agent.avatarColor || "",
171
- ...(notify !== undefined ? { notifyOnAgentUpdates: Boolean(notify) } : {}),
172
- ...(hidden !== undefined ? { hiddenFromSidebar: Boolean(hidden) } : {}),
173
- isGroup: agent.isGroup === true || (agent.isGroup == null && Array.isArray(memberIds) && memberIds.length > 0),
174
- memberIds: Array.isArray(memberIds) ? memberIds : [],
175
- };
176
- }
177
-
178
- function assertAvatar(kind, value, allowed) {
179
- const trimmed = (value ?? "").trim();
180
- if (!trimmed) return "";
181
- if (!allowed.includes(trimmed)) {
182
- throw new GatewayError("Unknown avatar " + kind + " \"" + value + "\". Use: " + allowed.join(" "));
183
- }
184
- return trimmed;
185
- }
186
-
187
- function unwrapList(data) {
188
- if (Array.isArray(data)) return data;
189
- if (Array.isArray(data.agents)) return data.agents;
190
- if (data.agent) return [data.agent];
191
- return [];
192
- }
193
-
194
- function unwrapOne(data) {
195
- return asRecord(data.agent || data);
196
- }
197
-
198
- export async function listAgents(session) {
199
- const data = await gatewayCall(session, "listAgents", {});
200
- return unwrapList(data).map(asRecord).filter((r) => r && r.id);
201
- }
202
-
203
- function resolveFromList(records, ref) {
204
- const needle = String(ref).trim().toLowerCase();
205
- const byId = records.find((r) => r.id.toLowerCase() === needle);
206
- if (byId) return byId;
207
- const matches = records.filter((r) => r.name.toLowerCase() === needle || r.title.toLowerCase() === needle);
208
- if (matches.length === 1) return matches[0];
209
- if (matches.length === 0) throw new GatewayError("No bot or group named \"" + ref + "\"");
210
- throw new GatewayError("Ambiguous name \"" + ref + "\"");
211
- }
212
-
213
- export async function resolveRef(session, ref) {
214
- return resolveFromList(await listAgents(session), ref);
215
- }
216
-
217
- export async function createAgent(session, input) {
218
- const data = await gatewayCall(session, "createAgent", {
219
- name: input.name,
220
- description: input.description || "",
221
- title: input.title || "",
222
- avatarShape: assertAvatar("shape", input.avatarShape, AVATAR_SHAPES),
223
- avatarColor: assertAvatar("color", input.avatarColor, AVATAR_COLORS),
224
- origin: "user",
225
- });
226
- return unwrapOne(data);
227
- }
228
-
229
- export async function updateAgent(session, ref, patch = {}) {
230
- const rec = await resolveRef(session, ref);
231
- if (patch.name !== undefined && !String(patch.name).trim()) {
232
- throw new GatewayError("Name cannot be blank.");
233
- }
234
- const profile = {
235
- name: patch.name !== undefined ? String(patch.name).trim() : rec.name,
236
- description: patch.description !== undefined ? String(patch.description) : rec.description,
237
- };
238
- if (patch.title !== undefined) profile.title = String(patch.title);
239
- if (patch.avatarShape !== undefined) profile.avatarShape = assertAvatar("shape", patch.avatarShape, AVATAR_SHAPES);
240
- if (patch.avatarColor !== undefined) profile.avatarColor = assertAvatar("color", patch.avatarColor, AVATAR_COLORS);
241
- await gatewayCall(session, "updateAgent", { id: rec.id, profile });
242
- if (patch.notifyOnAgentUpdates !== undefined) {
243
- await gatewayCall(session, "setAgentNotifyOnUpdates", { id: rec.id, isEnabled: Boolean(patch.notifyOnAgentUpdates) });
244
- }
245
- if (patch.hiddenFromSidebar !== undefined) {
246
- await gatewayCall(session, "setAgentHiddenFromSidebar", { id: rec.id, isHidden: Boolean(patch.hiddenFromSidebar) });
247
- }
248
- const fresh = (await listAgents(session)).find((r) => r.id === rec.id);
249
- return fresh || rec;
250
- }
251
-
252
- export async function deleteAgent(session, ref) {
253
- const rec = await resolveRef(session, ref);
254
- await gatewayCall(session, "deleteAgent", { id: rec.id });
255
- return rec;
256
- }
257
-
258
- function normalizeMemberIds(records, memberRefs) {
259
- const memberIds = new Set();
260
- for (const ref of memberRefs) {
261
- const rec = resolveFromList(records, ref);
262
- if (rec.isGroup) {
263
- throw new GatewayError(`Cannot add group "${rec.name}" as a member. Nested groups are not allowed.`);
264
- }
265
- memberIds.add(rec.id);
266
- }
267
- if (memberIds.size === 0) {
268
- throw new GatewayError("A group needs at least one existing member agent.");
269
- }
270
- if (memberIds.size > MAX_GROUP_MEMBERS) {
271
- throw new GatewayError(`A group can have at most ${MAX_GROUP_MEMBERS} members.`);
272
- }
273
- return [...memberIds];
274
- }
275
-
276
- export async function createGroup(session, input) {
277
- const records = await listAgents(session);
278
- const memberAgentIds = normalizeMemberIds(records, input.memberIds || []);
279
- const data = await gatewayCall(session, "createGroup", {
280
- name: input.name,
281
- description: input.description || "",
282
- memberAgentIds,
283
- });
284
- const rec = unwrapOne(data);
285
- const extras = {};
286
- if (input.title) extras.title = input.title;
287
- if (input.avatarShape) extras.avatarShape = input.avatarShape;
288
- if (input.avatarColor) extras.avatarColor = input.avatarColor;
289
- if (Object.keys(extras).length && rec?.id) return updateAgent(session, rec.id, extras);
290
- return rec;
291
- }
292
-
293
- export async function setGroupMembers(session, groupRef, memberRefs) {
294
- const records = await listAgents(session);
295
- const group = resolveFromList(records, groupRef);
296
- if (!group.isGroup) throw new GatewayError(`"${group.name}" is a bot, not a group.`);
297
- const memberAgentIds = normalizeMemberIds(records, memberRefs);
298
- const data = await gatewayCall(session, "setGroupMembers", {
299
- id: group.id,
300
- memberAgentIds,
301
- });
302
- return unwrapOne(data) || { ...group, memberIds: memberAgentIds, isGroup: true };
303
- }
304
-
305
- export async function addGroupMember(session, groupRef, memberRef) {
306
- const records = await listAgents(session);
307
- const group = resolveFromList(records, groupRef);
308
- if (!group.isGroup) throw new GatewayError(`"${group.name}" is a bot, not a group.`);
309
- const member = resolveFromList(records, memberRef);
310
- const next = [...new Set([...group.memberIds, member.id])];
311
- return setGroupMembers(session, group.id, next);
312
- }
313
-
314
- export async function removeGroupMember(session, groupRef, memberRef) {
315
- const records = await listAgents(session);
316
- const group = resolveFromList(records, groupRef);
317
- if (!group.isGroup) throw new GatewayError(`"${group.name}" is a bot, not a group.`);
318
- const member = resolveFromList(records, memberRef);
319
- const next = group.memberIds.filter((id) => id !== member.id);
320
- return setGroupMembers(session, group.id, next);
321
- }
322
-
323
- export async function sendPrompt(session, ref, prompt, extra = {}) {
324
- const rec = await resolveRef(session, ref);
325
- const body = {
326
- agentId: rec.id,
327
- prompt,
328
- clientNonce: extra.clientNonce || randomUUID(),
329
- };
330
- if (extra.replyToId) body.replyToId = extra.replyToId;
331
- let data;
332
- try {
333
- data = await gatewayCall(session, "sendPrompt", body);
334
- } catch (err) {
335
- // Delivery states: the server answered no (rejected) vs the request may have landed (unknown).
336
- // Never retry an unknown delivery blindly; read the thread first.
337
- if (err && err.delivery == null) {
338
- err.delivery = err instanceof GatewayError && err.status != null && err.status < 500 ? "rejected" : "unknown";
339
- }
340
- if (err && err.targetId == null) err.targetId = rec.id;
341
- if (err && err.delivery === "unknown" && err instanceof Error && !/delivery unknown/.test(err.message)) {
342
- err.message += " (delivery unknown; check the thread before resending)";
343
- }
344
- throw err;
345
- }
346
- const messageId = data && typeof data.messageId === "string" ? data.messageId : null;
347
- return { target: rec, result: data, delivery: "accepted", ...(messageId ? { messageId } : {}) };
348
- }
349
-
350
- export async function getTranscriptTail(session, ref, limit = 40) {
351
- const rec = await resolveRef(session, ref);
352
- const bounded = Math.min(Math.max(Math.trunc(limit) || 40, 1), 200);
353
- const data = await gatewayCall(session, "getAgentTranscriptTail", { id: rec.id, limit: bounded });
354
- return { target: rec, transcript: data };
355
- }
356
-
357
- export async function getThread(session, ref, rootId) {
358
- const rec = await resolveRef(session, ref);
359
- const data = await gatewayCall(session, "getAgentThread", { id: rec.id, rootId });
360
- return { target: rec, thread: data };
361
- }
package/src/headers.js DELETED
@@ -1,53 +0,0 @@
1
- export function normalizeHeaderMap(obj) {
2
- if (!obj || typeof obj !== "object" || Array.isArray(obj)) return {};
3
- const out = {};
4
- for (const [key, value] of Object.entries(obj)) {
5
- if (value == null || value === "") continue;
6
- out[String(key).toLowerCase()] = String(value);
7
- }
8
- return out;
9
- }
10
-
11
- export function parseGatewayHeaders(raw) {
12
- if (raw == null || String(raw).trim() === "") return {};
13
- const parsed = JSON.parse(String(raw));
14
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
15
- throw new Error("GROK_BOT_GATEWAY_HEADERS must be a JSON object");
16
- }
17
- return normalizeHeaderMap(parsed);
18
- }
19
-
20
- export function headersFromEnsureSandbox(body) {
21
- if (!body || typeof body !== "object") return {};
22
- const mapped = normalizeHeaderMap(body.gatewayHeaders || body.gateway_headers);
23
- if (Object.keys(mapped).length) return mapped;
24
- const token = body.anyrunNetworkToken || body.anyrun_network_token || body.networkToken;
25
- return token ? { "x-anyrun-network-token": String(token) } : {};
26
- }
27
-
28
- export function headersFromEnv() {
29
- return parseGatewayHeaders(process.env.GROK_BOT_GATEWAY_HEADERS);
30
- }
31
-
32
- export function mergeGatewayHeaders(sessionHeaders, envHeaders) {
33
- return { ...(sessionHeaders || {}), ...(envHeaders || {}) };
34
- }
35
-
36
- export function requestHeaders(session) {
37
- return {
38
- "content-type": "application/json",
39
- authorization: "Bearer " + session.gatewayToken,
40
- ...(session.gatewayHeaders || {}),
41
- };
42
- }
43
-
44
- export function ensureSandboxHeaders(accessToken) {
45
- return {
46
- "content-type": "application/json",
47
- "connect-protocol-version": "1",
48
- authorization: "Bearer " + accessToken,
49
- "x-cursor-client-type": "sand",
50
- "x-cursor-client-version": process.env.SAND_CLIENT_VERSION || "0.20.0",
51
- "x-sand-box-namespace": process.env.SAND_BOX_NAMESPACE || "prod",
52
- };
53
- }
package/src/history.js DELETED
@@ -1,83 +0,0 @@
1
- import { appendFileSync, closeSync, constants, createReadStream, fstatSync, mkdirSync, openSync, readSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { dirname, join, resolve } from "node:path";
4
- import { createInterface } from "node:readline";
5
- import { entryText, transcriptEntries } from "./transcript.js";
6
-
7
- export function historyPath(dir) {
8
- return join(resolve(dir || process.env.GROK_BOT_HISTORY_DIR || join(homedir(), ".grok-bot-cli")), "history.jsonl");
9
- }
10
-
11
- // Keep only conversation fields, never gateway responses, session credentials or bot instructions.
12
- export function saveHistory(out, { dir, disabled, event, prompt, rootId } = {}) {
13
- // Opt-in only: plaintext local history stays off unless GROK_BOT_HISTORY=on (or true/1).
14
- if (disabled || !/^(on|true|1)$/i.test(process.env.GROK_BOT_HISTORY || "")) return;
15
- try {
16
- const recordedAt = new Date().toISOString();
17
- const target = { id: out.target.id, name: out.target.name, kind: out.target.isGroup ? "group" : "bot" };
18
- const payload = out.transcript || out.thread || out;
19
- const entries = event === "send" ? [{ role: "user", text: prompt }] : transcriptEntries(payload);
20
- const rows = entries.map((entry) => ({
21
- version: 1,
22
- recordedAt,
23
- event,
24
- target,
25
- ...(rootId ? { rootId } : {}),
26
- role: String(entry.role || entry.kind || entry.sender || entry.type || "msg"),
27
- ...(entry.id || entry.messageId ? { messageId: String(entry.id || entry.messageId) } : {}),
28
- ...(entry.timestamp || entry.createdAt ? { timestamp: String(entry.timestamp || entry.createdAt) } : {}),
29
- text: entryText(entry),
30
- }));
31
- if (!rows.length) return;
32
- const path = historyPath(dir);
33
- mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
34
- const fd = openSync(path, constants.O_CREAT | constants.O_APPEND | constants.O_RDWR | constants.O_NOFOLLOW, 0o600);
35
- try {
36
- // Separate a previous interrupted append from the next complete record.
37
- const size = fstatSync(fd).size;
38
- const last = Buffer.alloc(1);
39
- if (size) readSync(fd, last, 0, 1, size - 1);
40
- const prefix = size && last[0] !== 10 ? "\n" : "";
41
- appendFileSync(fd, prefix + rows.map((row) => JSON.stringify(row)).join("\n") + "\n");
42
- } finally {
43
- closeSync(fd);
44
- }
45
- } catch {
46
- // A successful remote send must not look failed and invite an accidental resend.
47
- process.stderr.write("Warning: could not save local history. Check the history directory and permissions.\n");
48
- }
49
- }
50
-
51
- export async function readHistory(path, { ref, search, limit = 40 } = {}) {
52
- const rows = [];
53
- let malformed = 0;
54
- const input = createReadStream(path, { encoding: "utf8" });
55
- const lines = createInterface({ input, crlfDelay: Infinity });
56
- try {
57
- for await (const line of lines) {
58
- if (!line.trim()) continue;
59
- let row;
60
- try {
61
- row = JSON.parse(line);
62
- if (row?.version !== 1 || typeof row.text !== "string" || typeof row.role !== "string" ||
63
- typeof row.recordedAt !== "string" || typeof row.target?.id !== "string" || typeof row.target?.name !== "string") {
64
- throw new Error("Invalid history record");
65
- }
66
- } catch {
67
- malformed++;
68
- continue;
69
- }
70
- if (ref && row.target.id !== ref && row.target.name.toLowerCase() !== ref.toLowerCase()) continue;
71
- if (search !== undefined && !row.text.toLowerCase().includes(search.toLowerCase())) continue;
72
- rows.push(row);
73
- if (rows.length > limit) rows.shift();
74
- }
75
- } catch (err) {
76
- if (err.code !== "ENOENT") throw err;
77
- } finally {
78
- lines.close();
79
- input.destroy();
80
- }
81
- if (malformed) process.stderr.write("Warning: skipped " + malformed + " malformed local history record(s).\n");
82
- return rows;
83
- }