@taptapai/taptapai-openclaw 0.0.1

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.
@@ -0,0 +1,366 @@
1
+ import * as fs from "fs";
2
+ import * as os from "os";
3
+ import * as path from "path";
4
+ import type { LoggerLike } from "./types";
5
+ import type { RpcRequest } from "./types";
6
+ import { sanitizePathComponent, ensurePathWithin } from "./sanitize";
7
+ import { sendBackendResponse, type BackendWsState } from "./backendWs";
8
+ import { callGatewayHttp } from "./gatewayHttp";
9
+ import { dispatchViaGateway, type GatewayWsState } from "./gatewayWs";
10
+ import {
11
+ listTapTapAiSessions,
12
+ purgeSession,
13
+ readSessionStore,
14
+ readTranscript,
15
+ resolveSessionFromStore,
16
+ isTapTapAiSessionKey,
17
+ } from "./sessions";
18
+ import { readOpenClawConfig } from "./openclawConfig";
19
+ import { getTranscriptPath } from "./paths";
20
+
21
+ export type RequestHandlerDeps = {
22
+ backendState: BackendWsState;
23
+ gatewayState: GatewayWsState;
24
+ runtime: any;
25
+ pluginConfig: any;
26
+ logger: LoggerLike;
27
+ };
28
+
29
+ /** Allowed RPC methods. Requests with unknown methods are rejected. */
30
+ const ALLOWED_METHODS = new Set([
31
+ "agent.send",
32
+ "agent.cancel",
33
+ "conversations.list",
34
+ "conversations.get",
35
+ "conversations.delete",
36
+ "agents.list",
37
+ "agents.get",
38
+ "identity.link",
39
+ "identity.unlink",
40
+ "identity.list",
41
+ "sessions.list",
42
+ "taptapai.sessions.list",
43
+ "taptapai.sessions.delete",
44
+ "health",
45
+ "memory.search",
46
+ "memory.get",
47
+ ]);
48
+
49
+ export async function handleBackendRequest(deps: RequestHandlerDeps, req: RpcRequest): Promise<void> {
50
+ const { backendState, gatewayState, runtime, pluginConfig, logger } = deps;
51
+ const { id, method, params } = req;
52
+
53
+ // Validate request structure
54
+ if (!id || typeof id !== "string") {
55
+ logger.warn?.("[taptapai] Rejected request: missing or invalid id");
56
+ return;
57
+ }
58
+ if (!method || typeof method !== "string") {
59
+ sendBackendResponse(backendState, id, "error", undefined, { message: "Invalid method", code: 400 });
60
+ return;
61
+ }
62
+ if (!ALLOWED_METHODS.has(method)) {
63
+ sendBackendResponse(backendState, id, "error", undefined, { message: `Unknown method: ${method}`, code: 404 });
64
+ return;
65
+ }
66
+
67
+ logger.info?.(`[taptapai] handleRequest: method=${method} id=${id}`);
68
+
69
+ const send = (type: string, data?: any, extra?: Record<string, any>) =>
70
+ sendBackendResponse(backendState, id, type, data, extra);
71
+
72
+ try {
73
+ switch (method) {
74
+ // ================================================================
75
+ // Agent
76
+ // ================================================================
77
+ case "agent.send": {
78
+ const { session, text } = params as any;
79
+ const t0 = Date.now();
80
+ logger.info?.(`[taptapai] agent.send: session=${session} text="${String(text).substring(0, 80)}"`);
81
+ send("ack");
82
+
83
+ try {
84
+ if (gatewayState.connected) {
85
+ logger.info?.(`[taptapai] Dispatching via gateway WS (chat.send) [ack sent in ${Date.now() - t0}ms]`);
86
+ const responseText = await dispatchViaGateway({ state: gatewayState, logger, text: String(text), session: String(session) });
87
+ const t1 = Date.now();
88
+ logger.info?.(`[taptapai] ⏱️ agent.send DONE: total=${t1 - t0}ms | Gateway WS response: "${responseText.substring(0, 80)}..."`);
89
+ send("stream", { text: responseText }, { event: "done" });
90
+ } else {
91
+ logger.info?.("[taptapai] Gateway WS not connected — falling back to HTTP");
92
+ const responseText = await callGatewayHttp({
93
+ text: String(text),
94
+ session: String(session),
95
+ runtime,
96
+ pluginConfig,
97
+ readOpenClawConfig: () => readOpenClawConfig(logger),
98
+ logger,
99
+ });
100
+ const t1 = Date.now();
101
+ logger.info?.(`[taptapai] ⏱️ agent.send HTTP fallback DONE: total=${t1 - t0}ms`);
102
+ send("stream", { text: responseText }, { event: "done" });
103
+ }
104
+ } catch (e: any) {
105
+ logger.error?.(`[taptapai] agent.send error after ${Date.now() - t0}ms: ${e?.message || e}`);
106
+ send("stream", { text: `Error: ${e?.message || e}` }, { event: "done" });
107
+ }
108
+
109
+ break;
110
+ }
111
+
112
+ case "agent.cancel": {
113
+ send("result", { ok: true });
114
+ break;
115
+ }
116
+
117
+ // ================================================================
118
+ // Conversations
119
+ // ================================================================
120
+ case "conversations.list": {
121
+ const agentId = sanitizePathComponent(String((params as any).agent || "main"), "agent");
122
+ const limit = Number((params as any).limit || 50);
123
+ const offset = Number((params as any).offset || 0);
124
+
125
+ const store = readSessionStore(agentId);
126
+ const entries = Object.entries(store)
127
+ .map(([key, val]: [string, any]) => ({
128
+ id: val.sessionId || key,
129
+ key,
130
+ channel: val.channel || key.split(":")[2] || "unknown",
131
+ updatedAt: val.updatedAt || val.lastMessageAt,
132
+ title: val.title || key,
133
+ }))
134
+ .sort((a, b) => new Date(b.updatedAt || 0).getTime() - new Date(a.updatedAt || 0).getTime())
135
+ .slice(offset, offset + limit);
136
+
137
+ send("result", entries);
138
+ break;
139
+ }
140
+
141
+ case "conversations.get": {
142
+ const sessionId = sanitizePathComponent(String((params as any).id || ""), "sessionId");
143
+ const agentId = sanitizePathComponent(String((params as any).agent || "main"), "agent");
144
+
145
+ const store = readSessionStore(agentId);
146
+ let resolvedSessionId = sessionId;
147
+ if (store[sessionId]) resolvedSessionId = store[sessionId].sessionId || sessionId;
148
+
149
+ const transcript = readTranscript(agentId, resolvedSessionId);
150
+ const metadata = store[sessionId] || {};
151
+
152
+ send("result", { id: resolvedSessionId, metadata, messages: transcript });
153
+ break;
154
+ }
155
+
156
+ case "conversations.delete": {
157
+ const sessionId = sanitizePathComponent(String((params as any).id || ""), "sessionId");
158
+ const agentId = sanitizePathComponent(String((params as any).agent || "main"), "agent");
159
+ try {
160
+ const transcriptPath = getTranscriptPath(agentId, sessionId);
161
+ if (fs.existsSync(transcriptPath)) fs.unlinkSync(transcriptPath);
162
+ send("result", { ok: true });
163
+ } catch (e: any) {
164
+ send("error", undefined, { message: `Failed to delete: ${e?.message || e}` , code: 500 });
165
+ }
166
+ break;
167
+ }
168
+
169
+ // ================================================================
170
+ // Agents
171
+ // ================================================================
172
+ case "agents.list": {
173
+ const config = readOpenClawConfig(logger);
174
+ const agents = config?.agents?.list || [];
175
+ const defaults = config?.agents?.defaults || {};
176
+
177
+ const result = agents.map((a: any) => ({
178
+ id: a.id,
179
+ name: a.identity?.name || a.id,
180
+ model: a.model?.primary || defaults.model?.primary || "unknown",
181
+ workspace: a.workspace || defaults.workspace,
182
+ isDefault: !!a.default,
183
+ }));
184
+
185
+ send("result", result);
186
+ break;
187
+ }
188
+
189
+ case "agents.get": {
190
+ const agentId = sanitizePathComponent(String((params as any).id || ""), "agentId");
191
+ const config = readOpenClawConfig(logger);
192
+ const agents = config?.agents?.list || [];
193
+ const agent = agents.find((a: any) => a.id === agentId);
194
+
195
+ if (agent) send("result", agent);
196
+ else send("error", undefined, { message: `Agent '${agentId}' not found`, code: 404 });
197
+ break;
198
+ }
199
+
200
+ // ================================================================
201
+ // Identity links (deprecated/no-op)
202
+ // ================================================================
203
+ case "identity.link":
204
+ case "identity.unlink": {
205
+ send("result", { ok: true, message: "identity links deprecated" });
206
+ break;
207
+ }
208
+
209
+ case "identity.list": {
210
+ send("result", { links: {} });
211
+ break;
212
+ }
213
+
214
+ // ================================================================
215
+ // Sessions
216
+ // ================================================================
217
+ case "sessions.list": {
218
+ const agentId = sanitizePathComponent(String((params as any).agent || "main"), "agent");
219
+ const store = readSessionStore(agentId);
220
+ const entries = Object.entries(store).map(([key, val]: [string, any]) => ({
221
+ key,
222
+ sessionId: val.sessionId || key,
223
+ channel: val.channel || "unknown",
224
+ updatedAt: val.updatedAt,
225
+ }));
226
+ send("result", entries);
227
+ break;
228
+ }
229
+
230
+ // ================================================================
231
+ // TapTapAI session maintenance
232
+ // ================================================================
233
+ case "taptapai.sessions.list": {
234
+ const agentId = sanitizePathComponent(String((params as any).agent || "main"), "agent");
235
+ const limit = Number((params as any).limit || 100);
236
+ const offset = Number((params as any).offset || 0);
237
+
238
+ const all = listTapTapAiSessions(agentId);
239
+ send("result", { agent: agentId, total: all.length, offset, limit, sessions: all.slice(offset, offset + limit) });
240
+ break;
241
+ }
242
+
243
+ case "taptapai.sessions.delete": {
244
+ const agentId = sanitizePathComponent(String((params as any).agent || "main"), "agent");
245
+ const idParam = String((params as any)?.id || "").trim();
246
+ const all = Boolean((params as any)?.all);
247
+ const confirm = Boolean((params as any)?.confirm);
248
+ const dryRun = Boolean((params as any)?.dryRun);
249
+
250
+ if (all && !confirm && !dryRun) {
251
+ send("error", undefined, { message: "Refusing to delete all sessions without confirm=true (or dryRun=true)", code: 400 });
252
+ break;
253
+ }
254
+
255
+ const targets = all
256
+ ? listTapTapAiSessions(agentId).map((s) => ({ key: s.key, sessionId: s.sessionId }))
257
+ : (() => {
258
+ if (!idParam) return [];
259
+ const resolved = resolveSessionFromStore(agentId, idParam);
260
+ if (!resolved) return [];
261
+ if (!isTapTapAiSessionKey(resolved.key)) return [];
262
+ return [resolved];
263
+ })();
264
+
265
+ if (!targets.length) {
266
+ send("result", {
267
+ ok: true,
268
+ agent: agentId,
269
+ dryRun,
270
+ deleted: [],
271
+ deletedCount: 0,
272
+ message: all ? "No TapTapAI sessions found" : "Session not found (or not a TapTapAI session)",
273
+ });
274
+ break;
275
+ }
276
+
277
+ const deleted = targets.map((t) => purgeSession(agentId, t.key, t.sessionId, { dryRun }));
278
+ send("result", { ok: true, agent: agentId, dryRun, deletedCount: deleted.length, deleted });
279
+ break;
280
+ }
281
+
282
+ // ================================================================
283
+ // System
284
+ // ================================================================
285
+ case "health": {
286
+ const config = readOpenClawConfig(logger);
287
+ const agents = config?.agents?.list || [];
288
+ const channels = Object.keys(config?.channels || {}).filter((k) => config.channels[k]?.dmPolicy && config.channels[k].dmPolicy !== "disabled");
289
+
290
+ send("result", {
291
+ status: "healthy",
292
+ version: runtime?.version ?? "unknown",
293
+ agents: agents.length,
294
+ channels: channels.length,
295
+ channelNames: channels,
296
+ wsConnected: true,
297
+ gatewayWsConnected: gatewayState.connected,
298
+ });
299
+ break;
300
+ }
301
+
302
+ case "memory.search": {
303
+ const query = String((params as any).query || "");
304
+ const limit = Number((params as any).limit || 5);
305
+
306
+ const workspacePath = runtime?.config?.loadConfig?.()?.agents?.defaults?.workspace || path.join(os.homedir(), ".openclaw", "workspace");
307
+ const memoryDir = path.join(workspacePath, "memory");
308
+
309
+ const results: any[] = [];
310
+ try {
311
+ if (fs.existsSync(memoryDir)) {
312
+ const files = fs.readdirSync(memoryDir).filter((f: string) => f.endsWith(".md"));
313
+ const queryLower = query.toLowerCase();
314
+ for (const file of files) {
315
+ const safePath = ensurePathWithin(memoryDir, file, "memory file");
316
+ const content = fs.readFileSync(safePath, "utf-8");
317
+ if (content.toLowerCase().includes(queryLower)) {
318
+ results.push({ source: `memory/${file}`, text: content.substring(0, 500), score: 1.0 });
319
+ if (results.length >= limit) break;
320
+ }
321
+ }
322
+ }
323
+ } catch (e: any) {
324
+ logger.error?.(`[taptapai] Memory search error: ${e?.message || e}`);
325
+ }
326
+
327
+ send("result", results);
328
+ break;
329
+ }
330
+
331
+ case "memory.get": {
332
+ const memPath = (params as any).path as string | undefined;
333
+
334
+ const workspacePath = runtime?.config?.loadConfig?.()?.agents?.defaults?.workspace || path.join(os.homedir(), ".openclaw", "workspace");
335
+ let filePath: string;
336
+ try {
337
+ filePath = memPath
338
+ ? ensurePathWithin(workspacePath, memPath, "memory path")
339
+ : path.join(workspacePath, "MEMORY.md");
340
+ } catch (e: any) {
341
+ send("error", undefined, { message: "Invalid path", code: 400 });
342
+ break;
343
+ }
344
+
345
+ try {
346
+ if (fs.existsSync(filePath)) {
347
+ const content = fs.readFileSync(filePath, "utf-8");
348
+ send("result", { path: filePath, content });
349
+ } else {
350
+ send("error", undefined, { message: "File not found", code: 404 });
351
+ }
352
+ } catch (e: any) {
353
+ send("error", undefined, { message: "Read error", code: 500 });
354
+ }
355
+
356
+ break;
357
+ }
358
+
359
+ default:
360
+ send("error", undefined, { message: `Unknown method: ${method}`, code: 404 });
361
+ }
362
+ } catch (e: any) {
363
+ logger.error?.(`[taptapai] Handler error for ${method}: ${e?.stack || e?.message || e}`);
364
+ send("error", undefined, { message: "Internal handler error", code: 500 });
365
+ }
366
+ }
@@ -0,0 +1,57 @@
1
+ import * as path from "path";
2
+
3
+ /**
4
+ * Validates that a path component (agentId, sessionId, filename) is safe
5
+ * to use in filesystem operations. Rejects traversal attempts, slashes,
6
+ * null bytes, and other dangerous characters.
7
+ *
8
+ * @throws Error if the component is invalid
9
+ */
10
+ export function sanitizePathComponent(value: string, label: string = "value"): string {
11
+ const trimmed = String(value ?? "").trim();
12
+
13
+ if (!trimmed) {
14
+ throw new Error(`${label} must not be empty`);
15
+ }
16
+
17
+ // Reject null bytes
18
+ if (trimmed.includes("\0")) {
19
+ throw new Error(`${label} contains null bytes`);
20
+ }
21
+
22
+ // Reject path separators
23
+ if (trimmed.includes("/") || trimmed.includes("\\")) {
24
+ throw new Error(`${label} must not contain path separators`);
25
+ }
26
+
27
+ // Reject traversal patterns
28
+ if (trimmed === "." || trimmed === ".." || trimmed.includes("..")) {
29
+ throw new Error(`${label} must not contain path traversal sequences`);
30
+ }
31
+
32
+ // Reject control characters (U+0000 to U+001F, U+007F)
33
+ if (/[\x00-\x1f\x7f]/.test(trimmed)) {
34
+ throw new Error(`${label} must not contain control characters`);
35
+ }
36
+
37
+ return trimmed;
38
+ }
39
+
40
+ /**
41
+ * Ensures that a resolved file path stays within a given base directory.
42
+ * Prevents path traversal via symlinks or encoded components.
43
+ *
44
+ * @returns The resolved absolute path
45
+ * @throws Error if the path escapes the base directory
46
+ */
47
+ export function ensurePathWithin(basePath: string, untrustedPath: string, label: string = "path"): string {
48
+ const resolvedBase = path.resolve(basePath);
49
+ const resolvedFull = path.resolve(basePath, untrustedPath);
50
+
51
+ // Ensure the resolved path starts with the base path + separator (or is exactly the base)
52
+ if (resolvedFull !== resolvedBase && !resolvedFull.startsWith(resolvedBase + path.sep)) {
53
+ throw new Error(`${label} escapes the allowed directory`);
54
+ }
55
+
56
+ return resolvedFull;
57
+ }
package/src/secrets.ts ADDED
@@ -0,0 +1,93 @@
1
+ import * as crypto from "crypto";
2
+ import * as fs from "fs";
3
+ import process from "process";
4
+ import type { StoredSecret } from "./types";
5
+ import { getSecretCachePath, getUuidCachePath, getPluginDataDir } from "./paths";
6
+
7
+ /** Write data atomically: write to temp file, then rename. */
8
+ function atomicWriteFileSync(filePath: string, data: string, mode: number = 0o600): void {
9
+ const tmpPath = filePath + ".tmp." + process.pid;
10
+ fs.writeFileSync(tmpPath, data, { encoding: "utf8", mode });
11
+ try { fs.chmodSync(tmpPath, mode); } catch {}
12
+ fs.renameSync(tmpPath, filePath);
13
+ }
14
+
15
+ function sha256Hex(value: string): string {
16
+ return crypto.createHash("sha256").update(value, "utf8").digest("hex");
17
+ }
18
+
19
+ /**
20
+ * @deprecated Kept only for backward compatibility with existing tokens.
21
+ * New tokens should use computeScryptHash() instead.
22
+ */
23
+ export function computeDoubleSha256Hex(password: string): string {
24
+ return sha256Hex(sha256Hex(password));
25
+ }
26
+
27
+ /**
28
+ * Derives a password hash using scrypt with a random salt.
29
+ * Returns { hash, salt } where both are hex strings.
30
+ */
31
+ export function computeScryptHash(password: string, existingSalt?: string): { hash: string; salt: string } {
32
+ const salt = existingSalt || crypto.randomBytes(32).toString("hex");
33
+ const derived = crypto.scryptSync(password, salt, 64, { N: 16384, r: 8, p: 1 });
34
+ return { hash: derived.toString("hex"), salt };
35
+ }
36
+
37
+ export function sha256HexPrefix(value: string, length: number): string {
38
+ return sha256Hex(value).slice(0, length);
39
+ }
40
+
41
+ export function encodeSecretToken(versionByte: number, uuidHex: string, passwordHashHex: string, n: number): string {
42
+ const uuidBuf = Buffer.from(uuidHex, "hex");
43
+ const passBuf = Buffer.from(passwordHashHex, "hex");
44
+ const verBuf = Buffer.from([versionByte & 0xff]);
45
+ const nBuf = Buffer.allocUnsafe(4);
46
+ nBuf.writeUInt32BE(n >>> 0, 0);
47
+ const raw = Buffer.concat([verBuf, uuidBuf, passBuf, nBuf]);
48
+ return `tta1_${raw.toString("base64url")}`;
49
+ }
50
+
51
+ export function ensurePluginDataDir(): void {
52
+ fs.mkdirSync(getPluginDataDir(), { recursive: true });
53
+ }
54
+
55
+ export function loadOrCreateUuidHex(): string {
56
+ ensurePluginDataDir();
57
+ const p = getUuidCachePath();
58
+ try {
59
+ const parsed = JSON.parse(fs.readFileSync(p, "utf8"));
60
+ const uuidHex = String(parsed?.uuidHex || "").trim();
61
+ if (/^[0-9a-f]{64}$/i.test(uuidHex)) return uuidHex.toLowerCase();
62
+ } catch (e: any) {
63
+ // First run or corrupted file — will regenerate below
64
+ }
65
+
66
+ const uuidHex = crypto.randomBytes(32).toString("hex");
67
+ atomicWriteFileSync(p, JSON.stringify({ uuidHex, createdAt: new Date().toISOString() }, null, 2));
68
+ return uuidHex;
69
+ }
70
+
71
+ export function loadStoredSecret(): StoredSecret | null {
72
+ try {
73
+ const p = getSecretCachePath();
74
+ const parsed = JSON.parse(fs.readFileSync(p, "utf8"));
75
+ if (!parsed?.token || !parsed?.uuidHex) return null;
76
+ return {
77
+ version: Number(parsed.version ?? 0),
78
+ uuidHex: String(parsed.uuidHex),
79
+ n: Number(parsed.n ?? 1),
80
+ token: String(parsed.token),
81
+ salt: parsed.salt ? String(parsed.salt) : undefined,
82
+ generatedAt: String(parsed.generatedAt ?? new Date().toISOString()),
83
+ };
84
+ } catch {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ export function saveStoredSecret(secret: StoredSecret): void {
90
+ ensurePluginDataDir();
91
+ const p = getSecretCachePath();
92
+ atomicWriteFileSync(p, JSON.stringify(secret, null, 2));
93
+ }
@@ -0,0 +1,117 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import process from "process";
4
+ import { getSessionStorePath, getTranscriptPath } from "./paths";
5
+
6
+ export function readSessionStore(agentId: string = "main"): Record<string, any> {
7
+ try {
8
+ const raw = fs.readFileSync(getSessionStorePath(agentId), "utf-8");
9
+ return JSON.parse(raw);
10
+ } catch {
11
+ return {};
12
+ }
13
+ }
14
+
15
+ export function writeSessionStore(agentId: string, store: Record<string, any>): void {
16
+ const p = getSessionStorePath(agentId);
17
+ try {
18
+ fs.mkdirSync(path.dirname(p), { recursive: true });
19
+ } catch {}
20
+ // Atomic write: write to temp then rename to prevent partial writes
21
+ const tmpPath = p + ".tmp." + process.pid;
22
+ fs.writeFileSync(tmpPath, JSON.stringify(store, null, 2), "utf-8");
23
+ fs.renameSync(tmpPath, p);
24
+ }
25
+
26
+ export function isTapTapAiSessionKey(key: string): boolean {
27
+ return String(key || "").trim().startsWith("taptapai:");
28
+ }
29
+
30
+ export function resolveSessionFromStore(agentId: string, keyOrId: string): { key: string; sessionId: string } | null {
31
+ const id = String(keyOrId || "").trim();
32
+ if (!id) return null;
33
+
34
+ const store = readSessionStore(agentId);
35
+
36
+ if (store[id]) {
37
+ const sessionId = String(store[id]?.sessionId || id);
38
+ return { key: id, sessionId };
39
+ }
40
+
41
+ for (const [key, val] of Object.entries(store)) {
42
+ const sid = String((val as any)?.sessionId || "");
43
+ if (sid && sid === id) return { key, sessionId: sid };
44
+ }
45
+
46
+ return null;
47
+ }
48
+
49
+ export function listTapTapAiSessions(agentId: string): Array<{ key: string; sessionId: string; updatedAt: any; title: any }> {
50
+ const store = readSessionStore(agentId);
51
+ return Object.entries(store)
52
+ .filter(([key]) => isTapTapAiSessionKey(key))
53
+ .map(([key, val]: [string, any]) => ({
54
+ key,
55
+ sessionId: String(val?.sessionId || key),
56
+ updatedAt: val?.updatedAt || val?.lastMessageAt,
57
+ title: val?.title || key,
58
+ }))
59
+ .sort((a, b) => new Date(b.updatedAt || 0).getTime() - new Date(a.updatedAt || 0).getTime());
60
+ }
61
+
62
+ export function purgeSession(
63
+ agentId: string,
64
+ key: string,
65
+ sessionId: string,
66
+ opts?: { deleteTranscript?: boolean; deleteStoreEntry?: boolean; dryRun?: boolean },
67
+ ): {
68
+ key: string;
69
+ sessionId: string;
70
+ storeEntryDeleted: boolean;
71
+ transcriptDeleted: boolean;
72
+ } {
73
+ const dryRun = !!opts?.dryRun;
74
+ const deleteTranscript = opts?.deleteTranscript !== false;
75
+ const deleteStoreEntry = opts?.deleteStoreEntry !== false;
76
+
77
+ let storeEntryDeleted = false;
78
+ let transcriptDeleted = false;
79
+
80
+ if (deleteStoreEntry) {
81
+ const store = readSessionStore(agentId);
82
+ if (store[key] !== undefined) {
83
+ if (!dryRun) {
84
+ delete store[key];
85
+ writeSessionStore(agentId, store);
86
+ }
87
+ storeEntryDeleted = true;
88
+ }
89
+ }
90
+
91
+ if (deleteTranscript) {
92
+ const transcriptPath = getTranscriptPath(agentId, sessionId);
93
+ if (fs.existsSync(transcriptPath)) {
94
+ if (!dryRun) {
95
+ try { fs.unlinkSync(transcriptPath); } catch {}
96
+ }
97
+ transcriptDeleted = true;
98
+ }
99
+ }
100
+
101
+ return { key, sessionId, storeEntryDeleted, transcriptDeleted };
102
+ }
103
+
104
+ export function readTranscript(agentId: string, sessionId: string): any[] {
105
+ try {
106
+ const raw = fs.readFileSync(getTranscriptPath(agentId, sessionId), "utf-8");
107
+ return raw
108
+ .split("\n")
109
+ .filter((line: string) => line.trim())
110
+ .map((line: string) => {
111
+ try { return JSON.parse(line); } catch { return null; }
112
+ })
113
+ .filter(Boolean);
114
+ } catch {
115
+ return [];
116
+ }
117
+ }