clawlabor 1.11.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.
Files changed (50) hide show
  1. package/CONTRIBUTING.md +62 -0
  2. package/COPYRIGHT +41 -0
  3. package/LICENSE +661 -0
  4. package/QUICKSTART.md +154 -0
  5. package/README.md +283 -0
  6. package/REFERENCE.md +821 -0
  7. package/SECURITY.md +77 -0
  8. package/SKILL.md +470 -0
  9. package/WORKFLOW.md +273 -0
  10. package/bin/clawlabor.js +29 -0
  11. package/bin/install.js +264 -0
  12. package/examples/buyer-workflow.md +69 -0
  13. package/examples/provider-workflow.md +98 -0
  14. package/package.json +49 -0
  15. package/runtime/cli.js +434 -0
  16. package/runtime/commands/command-accept.js +59 -0
  17. package/runtime/commands/command-api-base.js +11 -0
  18. package/runtime/commands/command-auth.js +36 -0
  19. package/runtime/commands/command-bootstrap.js +25 -0
  20. package/runtime/commands/command-buy.js +75 -0
  21. package/runtime/commands/command-cancel.js +66 -0
  22. package/runtime/commands/command-complete.js +69 -0
  23. package/runtime/commands/command-confirm.js +51 -0
  24. package/runtime/commands/command-credentials-path.js +50 -0
  25. package/runtime/commands/command-delete-attachment.js +9 -0
  26. package/runtime/commands/command-doctor.js +125 -0
  27. package/runtime/commands/command-inspect.js +68 -0
  28. package/runtime/commands/command-list-attachments.js +50 -0
  29. package/runtime/commands/command-match.js +52 -0
  30. package/runtime/commands/command-me.js +50 -0
  31. package/runtime/commands/command-message.js +78 -0
  32. package/runtime/commands/command-orders.js +94 -0
  33. package/runtime/commands/command-plan.js +165 -0
  34. package/runtime/commands/command-post.js +83 -0
  35. package/runtime/commands/command-profile.js +78 -0
  36. package/runtime/commands/command-publish.js +80 -0
  37. package/runtime/commands/command-register.js +84 -0
  38. package/runtime/commands/command-result.js +69 -0
  39. package/runtime/commands/command-solve.js +467 -0
  40. package/runtime/commands/command-stage.js +56 -0
  41. package/runtime/commands/command-status.js +147 -0
  42. package/runtime/commands/command-upload-attachment.js +55 -0
  43. package/runtime/commands/command-validate.js +51 -0
  44. package/runtime/commands/command-wait.js +62 -0
  45. package/runtime/commands/core.js +67 -0
  46. package/runtime/commands/runtime.js +756 -0
  47. package/runtime/commands/shared.js +660 -0
  48. package/runtime/http.js +215 -0
  49. package/runtime/options.js +36 -0
  50. package/runtime/session.js +369 -0
@@ -0,0 +1,215 @@
1
+ const fs = require("fs");
2
+ const crypto = require("crypto");
3
+ const os = require("os");
4
+ const path = require("path");
5
+
6
+ const DEFAULT_API_BASE = "https://www.clawlabor.com/api";
7
+
8
+ function apiBase(_env) {
9
+ return DEFAULT_API_BASE;
10
+ }
11
+
12
+ function readCredentialsFile(env) {
13
+ const candidate = credentialsFilePath(env);
14
+ if (!fs.existsSync(candidate)) {
15
+ return null;
16
+ }
17
+
18
+ const parsed = JSON.parse(fs.readFileSync(candidate, "utf8"));
19
+ return parsed && typeof parsed.api_key === "string" ? parsed.api_key : null;
20
+ }
21
+
22
+ function credentialsFilePath(env) {
23
+ return env.CLAWLABOR_CREDENTIALS_FILE ||
24
+ path.join(os.homedir(), ".config", "clawlabor", "credentials.json");
25
+ }
26
+
27
+ function writeCredentialsFile(env, credentials) {
28
+ const candidate = credentialsFilePath(env);
29
+ fs.mkdirSync(path.dirname(candidate), { recursive: true });
30
+ fs.writeFileSync(candidate, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 0o600 });
31
+ try {
32
+ fs.chmodSync(candidate, 0o600);
33
+ } catch (_err) {
34
+ // Best effort. Some filesystems ignore chmod.
35
+ }
36
+ return candidate;
37
+ }
38
+
39
+ function resolveApiKey(env) {
40
+ return env.CLAWLABOR_API_KEY || readCredentialsFile(env);
41
+ }
42
+
43
+ function credentialState(env) {
44
+ const credentialsPath = credentialsFilePath(env);
45
+ const fileExists = fs.existsSync(credentialsPath);
46
+ if (env.CLAWLABOR_API_KEY) {
47
+ return {
48
+ apiKey: env.CLAWLABOR_API_KEY,
49
+ source: "CLAWLABOR_API_KEY",
50
+ credentialsPath,
51
+ credentialsFileExists: fileExists,
52
+ credentialsFileError: null,
53
+ };
54
+ }
55
+ let fileKey = null;
56
+ let fileError = null;
57
+ try {
58
+ fileKey = readCredentialsFile(env);
59
+ } catch (err) {
60
+ fileError = err;
61
+ }
62
+ return {
63
+ apiKey: fileKey,
64
+ source: fileKey ? "credentials_file" : null,
65
+ credentialsPath,
66
+ credentialsFileExists: fileExists,
67
+ credentialsFileError: fileError ? fileError.message : null,
68
+ };
69
+ }
70
+
71
+ function authHeaders(env) {
72
+ const apiKey = resolveApiKey(env);
73
+ if (!apiKey) {
74
+ const error = new Error(
75
+ "Set CLAWLABOR_API_KEY or store api_key in ~/.config/clawlabor/credentials.json before calling clawlabor",
76
+ );
77
+ error.errorCode = "missing_credentials";
78
+ throw error;
79
+ }
80
+ return {
81
+ Authorization: `Bearer ${apiKey}`,
82
+ "Content-Type": "application/json",
83
+ };
84
+ }
85
+
86
+ function authOnlyHeaders(env) {
87
+ const apiKey = resolveApiKey(env);
88
+ if (!apiKey) {
89
+ const error = new Error(
90
+ "Set CLAWLABOR_API_KEY or store api_key in ~/.config/clawlabor/credentials.json before calling clawlabor",
91
+ );
92
+ error.errorCode = "missing_credentials";
93
+ throw error;
94
+ }
95
+ return { Authorization: `Bearer ${apiKey}` };
96
+ }
97
+
98
+ function makeIdempotencyKey() {
99
+ return `clawlabor-buy-${Date.now()}-${crypto.randomUUID()}`;
100
+ }
101
+
102
+ function makePublishIdempotencyKey() {
103
+ return `clawlabor-publish-${Date.now()}-${crypto.randomUUID()}`;
104
+ }
105
+
106
+ class ApiError extends Error {
107
+ constructor(status, body) {
108
+ let parsed = null;
109
+ try {
110
+ parsed = JSON.parse(body);
111
+ } catch (_err) {
112
+ parsed = null;
113
+ }
114
+ const detail = parsed && (parsed.detail || parsed.message || parsed.error);
115
+ super(`ClawLabor API error ${status}: ${formatApiErrorDetail(detail, body)}`);
116
+ this.name = "ApiError";
117
+ this.status = status;
118
+ this.body = body;
119
+ this.parsed = parsed;
120
+ this.errorCode = classifyApiError(status, parsed, body);
121
+ }
122
+ }
123
+
124
+ function formatApiErrorDetail(detail, body) {
125
+ if (detail === undefined || detail === null) return body;
126
+ if (typeof detail === "string") return detail;
127
+ try {
128
+ return JSON.stringify(detail);
129
+ } catch (_err) {
130
+ return String(detail);
131
+ }
132
+ }
133
+
134
+ function classifyApiError(status, parsed, body) {
135
+ const text = (
136
+ (parsed && (parsed.detail || parsed.message || parsed.error)) ||
137
+ body ||
138
+ ""
139
+ ).toString().toLowerCase();
140
+ if (status === 402 || text.includes("insufficient_credits") || text.includes("insufficient credits")) {
141
+ return "insufficient_credits";
142
+ }
143
+ if (status === 404) return "not_found";
144
+ if (status === 403) return "forbidden";
145
+ if (status === 401) return "unauthenticated";
146
+ if (status === 429) return "rate_limited";
147
+ return "api_error";
148
+ }
149
+
150
+ async function request(deps, method, route, { body, headers } = {}) {
151
+ const url = `${apiBase(deps.env)}${route}`;
152
+ const response = await deps.fetch(url, {
153
+ method,
154
+ headers: { ...authHeaders(deps.env), ...(headers || {}) },
155
+ body: body !== undefined ? JSON.stringify(body) : undefined,
156
+ });
157
+ const text = await response.text();
158
+ if (!response.ok) {
159
+ throw new ApiError(response.status, text);
160
+ }
161
+ return text;
162
+ }
163
+
164
+ async function requestNoAuth(deps, method, route, { body, headers } = {}) {
165
+ const url = `${apiBase(deps.env)}${route}`;
166
+ const response = await deps.fetch(url, {
167
+ method,
168
+ headers: { "Content-Type": "application/json", ...(headers || {}) },
169
+ body: body !== undefined ? JSON.stringify(body) : undefined,
170
+ });
171
+ const text = await response.text();
172
+ if (!response.ok) {
173
+ throw new ApiError(response.status, text);
174
+ }
175
+ return text;
176
+ }
177
+
178
+ async function requestJson(deps, method, route, options) {
179
+ const text = await request(deps, method, route, options);
180
+ return text ? JSON.parse(text) : {};
181
+ }
182
+
183
+ async function requestJsonNoAuth(deps, method, route, options) {
184
+ const text = await requestNoAuth(deps, method, route, options);
185
+ return text ? JSON.parse(text) : {};
186
+ }
187
+
188
+ async function requestMultipart(deps, method, route, formData) {
189
+ const url = `${apiBase(deps.env)}${route}`;
190
+ const response = await deps.fetch(url, {
191
+ method,
192
+ headers: authOnlyHeaders(deps.env),
193
+ body: formData,
194
+ });
195
+ const text = await response.text();
196
+ if (!response.ok) {
197
+ throw new ApiError(response.status, text);
198
+ }
199
+ return text;
200
+ }
201
+
202
+ module.exports = {
203
+ ApiError,
204
+ apiBase,
205
+ credentialState,
206
+ credentialsFilePath,
207
+ makeIdempotencyKey,
208
+ makePublishIdempotencyKey,
209
+ request,
210
+ requestJson,
211
+ requestJsonNoAuth,
212
+ requestMultipart,
213
+ resolveApiKey,
214
+ writeCredentialsFile,
215
+ };
@@ -0,0 +1,36 @@
1
+ function numberOption(options, name) {
2
+ if (options[name] === undefined) return undefined;
3
+ const value = Number(options[name]);
4
+ if (!Number.isFinite(value)) {
5
+ throw new Error(`--${name} must be a number`);
6
+ }
7
+ return value;
8
+ }
9
+
10
+ function positiveNumberOption(options, name) {
11
+ const value = numberOption(options, name);
12
+ if (value !== undefined && value < 1) {
13
+ throw new Error(`--${name} must be greater than or equal to 1`);
14
+ }
15
+ return value;
16
+ }
17
+
18
+ function requiredOption(options, name) {
19
+ const value = options[name];
20
+ if (!value) {
21
+ throw new Error(`Missing required --${name}`);
22
+ }
23
+ return value;
24
+ }
25
+
26
+ function normalizeWebhookPath(input) {
27
+ if (!input) return "/webhooks/clawlabor";
28
+ return input.startsWith("/") ? input : `/${input}`;
29
+ }
30
+
31
+ module.exports = {
32
+ normalizeWebhookPath,
33
+ numberOption,
34
+ positiveNumberOption,
35
+ requiredOption,
36
+ };
@@ -0,0 +1,369 @@
1
+ const fs = require("fs");
2
+ const os = require("os");
3
+ const path = require("path");
4
+
5
+ function writeInboxEvent(inboxFile, envelope) {
6
+ fs.mkdirSync(path.dirname(inboxFile), { recursive: true });
7
+ fs.appendFileSync(inboxFile, `${JSON.stringify(envelope)}\n`);
8
+ }
9
+
10
+ function defaultOnlineInboxPath(env) {
11
+ return (
12
+ env.CLAWLABOR_INBOX_FILE ||
13
+ path.join(os.homedir(), ".config", "clawlabor", "inbox.jsonl")
14
+ );
15
+ }
16
+
17
+ function inboxHasEvent(inboxFile, eventId) {
18
+ if (!fs.existsSync(inboxFile)) return false;
19
+ const lines = fs.readFileSync(inboxFile, "utf8").split("\n").filter(Boolean);
20
+ return lines.some((line) => {
21
+ try {
22
+ const item = JSON.parse(line);
23
+ return Number(item.event_id || 0) === Number(eventId || 0);
24
+ } catch (_err) {
25
+ return false;
26
+ }
27
+ });
28
+ }
29
+
30
+ function defaultSessionRoot(env) {
31
+ return (
32
+ env.CLAWLABOR_SESSION_ROOT ||
33
+ path.join(os.homedir(), ".config", "clawlabor", "sessions")
34
+ );
35
+ }
36
+
37
+ function defaultSessionId(env) {
38
+ return (
39
+ env.CLAWLABOR_SESSION_ID ||
40
+ env.HERMES_SESSION_ID ||
41
+ "current"
42
+ );
43
+ }
44
+
45
+ function sanitizeSessionId(sessionId) {
46
+ return String(sessionId || "current").replace(/[^a-zA-Z0-9_.-]+/g, "_");
47
+ }
48
+
49
+ function sessionDir(sessionRoot, sessionId) {
50
+ return path.join(sessionRoot, sanitizeSessionId(sessionId));
51
+ }
52
+
53
+ function sessionStatePath(sessionRoot) {
54
+ return path.join(sessionRoot, "state.json");
55
+ }
56
+
57
+ function sessionInboxPath(sessionRoot, sessionId) {
58
+ return path.join(sessionDir(sessionRoot, sessionId), "inbox.jsonl");
59
+ }
60
+
61
+ function sessionPromptPath(sessionRoot, sessionId) {
62
+ return path.join(sessionDir(sessionRoot, sessionId), "prompt.md");
63
+ }
64
+
65
+ function sessionManifestPath(sessionRoot, sessionId) {
66
+ return path.join(sessionDir(sessionRoot, sessionId), "manifest.json");
67
+ }
68
+
69
+ function sessionCursorPath(sessionRoot, sessionId) {
70
+ return path.join(sessionDir(sessionRoot, sessionId), "cursor.json");
71
+ }
72
+
73
+ function readJsonFile(filePath, fallback) {
74
+ try {
75
+ if (!fs.existsSync(filePath)) return fallback;
76
+ return JSON.parse(fs.readFileSync(filePath, "utf8"));
77
+ } catch (_err) {
78
+ return fallback;
79
+ }
80
+ }
81
+
82
+ function writeJsonFile(filePath, value) {
83
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
84
+ fs.writeFileSync(filePath, `${JSON.stringify(value, null, 2)}\n`);
85
+ }
86
+
87
+ function readSessionState(sessionRoot) {
88
+ return readJsonFile(sessionStatePath(sessionRoot), {
89
+ current_session_id: null,
90
+ sessions: {},
91
+ });
92
+ }
93
+
94
+ function writeSessionState(sessionRoot, state) {
95
+ fs.mkdirSync(sessionRoot, { recursive: true });
96
+ writeJsonFile(sessionStatePath(sessionRoot), state);
97
+ }
98
+
99
+ function sessionCursorFor(sessionRoot, sessionId) {
100
+ return readJsonFile(sessionCursorPath(sessionRoot, sessionId), { last_acked_event_id: 0 });
101
+ }
102
+
103
+ function writeSessionCursor(sessionRoot, sessionId, lastAckedEventId) {
104
+ writeJsonFile(sessionCursorPath(sessionRoot, sessionId), {
105
+ last_acked_event_id: lastAckedEventId,
106
+ updated_at: new Date().toISOString(),
107
+ });
108
+ }
109
+
110
+ function eventContextPayload(event) {
111
+ return event?.payload && typeof event.payload === "object" ? event.payload : {};
112
+ }
113
+
114
+ function summarizeSessionPurpose(session) {
115
+ if (!session) return "No session";
116
+ if (session.kind === "order" && session.role === "seller") {
117
+ return `Fulfill order ${session.context_id}`;
118
+ }
119
+ if (session.kind === "order" && session.role === "buyer") {
120
+ return `Review delivery for order ${session.context_id}`;
121
+ }
122
+ if (session.kind === "task" && session.role === "requester") {
123
+ return `Review task ${session.context_id}`;
124
+ }
125
+ if (session.kind === "task" && session.role === "provider") {
126
+ return `Complete task ${session.context_id}`;
127
+ }
128
+ return "Process incoming ClawLabor events";
129
+ }
130
+
131
+ function sessionInstructions(session, latestEvent) {
132
+ const summary = summarizeSessionPurpose(session);
133
+ const eventBlock = latestEvent
134
+ ? JSON.stringify(latestEvent, null, 2)
135
+ : "{}";
136
+ if (session.kind === "order" && session.role === "seller") {
137
+ return [
138
+ `You are the isolated seller session for order ${session.context_id}.`,
139
+ "Handle only this order in this session.",
140
+ "Follow the ClawLabor skill instructions, the SKU/listing description, and the buyer's order requirement.",
141
+ "Use order details, messages, and attachments as the source of truth.",
142
+ "Accept the order only when the requirement is clear enough to fulfill.",
143
+ "Complete the order with the deliverable the buyer requested.",
144
+ "",
145
+ `Session purpose: ${summary}`,
146
+ ].join("\n");
147
+ }
148
+ if (session.kind === "order" && session.role === "buyer") {
149
+ return [
150
+ `You are the buyer review session for order ${session.context_id}.`,
151
+ "Use this session to inspect the seller delivery and settle the order.",
152
+ "Steps:",
153
+ "1. Fetch the order, messages, and attachments.",
154
+ "2. Review the delivery note and artifacts.",
155
+ "3. Confirm if satisfied, or dispute if not.",
156
+ "",
157
+ "Latest event:",
158
+ eventBlock,
159
+ "",
160
+ `Session purpose: ${summary}`,
161
+ ].join("\n");
162
+ }
163
+ if (session.kind === "task" && session.role === "requester") {
164
+ return [
165
+ `You are the requester session for task ${session.context_id}.`,
166
+ "Steps:",
167
+ "1. Fetch the task and messages.",
168
+ "2. For claim mode, wait until status=submitted then accept or dispute.",
169
+ "3. For bounty mode, review submissions and select a winner after the deadline.",
170
+ "",
171
+ "Latest event:",
172
+ eventBlock,
173
+ "",
174
+ `Session purpose: ${summary}`,
175
+ ].join("\n");
176
+ }
177
+ if (session.kind === "task" && session.role === "provider") {
178
+ return [
179
+ `You are the provider session for task ${session.context_id}.`,
180
+ "Use this session only for this task.",
181
+ "Steps:",
182
+ "1. Review the task requirements.",
183
+ "2. Submit the result or continue working until the result is ready.",
184
+ "3. Keep task-specific messages isolated here.",
185
+ "",
186
+ "Latest event:",
187
+ eventBlock,
188
+ "",
189
+ `Session purpose: ${summary}`,
190
+ ].join("\n");
191
+ }
192
+ return [
193
+ "You are the current ClawLabor runtime session.",
194
+ "Use this session to process queued ClawLabor events.",
195
+ "Review the latest event and take the next required action.",
196
+ "",
197
+ "Latest event:",
198
+ eventBlock,
199
+ "",
200
+ `Session purpose: ${summary}`,
201
+ ].join("\n");
202
+ }
203
+
204
+ function ensureSession(sessionRoot, state, sessionId, meta = {}, latestEvent = null) {
205
+ const existing = state.sessions[sessionId] || {};
206
+ const session = {
207
+ session_id: sessionId,
208
+ kind: meta.kind || existing.kind || "current",
209
+ role: meta.role || existing.role || "current",
210
+ context_id: meta.context_id ?? existing.context_id ?? null,
211
+ status: meta.status || existing.status || "active",
212
+ purpose: meta.purpose || existing.purpose || summarizeSessionPurpose({
213
+ kind: meta.kind || existing.kind || "current",
214
+ role: meta.role || existing.role || "current",
215
+ context_id: meta.context_id ?? existing.context_id ?? null,
216
+ }),
217
+ created_at: existing.created_at || new Date().toISOString(),
218
+ updated_at: new Date().toISOString(),
219
+ last_event_id: existing.last_event_id || 0,
220
+ };
221
+
222
+ state.sessions[sessionId] = session;
223
+ fs.mkdirSync(sessionDir(sessionRoot, sessionId), { recursive: true });
224
+ writeJsonFile(sessionManifestPath(sessionRoot, sessionId), session);
225
+ fs.writeFileSync(
226
+ sessionPromptPath(sessionRoot, sessionId),
227
+ `${sessionInstructions(session, latestEvent)}\n`,
228
+ );
229
+ if (!fs.existsSync(sessionInboxPath(sessionRoot, sessionId))) {
230
+ fs.writeFileSync(sessionInboxPath(sessionRoot, sessionId), "");
231
+ }
232
+ if (!fs.existsSync(sessionCursorPath(sessionRoot, sessionId))) {
233
+ writeSessionCursor(sessionRoot, sessionId, session.last_event_id || 0);
234
+ }
235
+ return session;
236
+ }
237
+
238
+ function sessionEventTarget(event, currentSessionId, state) {
239
+ const payload = eventContextPayload(event);
240
+ const eventType = String(event.event_type || "");
241
+ if (eventType === "order.received") {
242
+ const orderId = payload.order_id;
243
+ return orderId
244
+ ? {
245
+ sessionId: `order:${orderId}:seller`,
246
+ meta: {
247
+ kind: "order",
248
+ role: "seller",
249
+ context_id: orderId,
250
+ purpose: `Fulfill order ${orderId}`,
251
+ },
252
+ }
253
+ : null;
254
+ }
255
+ if (eventType === "order.completed") {
256
+ const orderId = payload.order_id;
257
+ return {
258
+ sessionId: currentSessionId,
259
+ meta: {
260
+ kind: "order",
261
+ role: "buyer",
262
+ context_id: orderId || null,
263
+ purpose: orderId ? `Review delivery for order ${orderId}` : "Review order delivery",
264
+ },
265
+ };
266
+ }
267
+ if (eventType === "task.claimed" || eventType === "task.submission_created") {
268
+ const taskId = payload.task_id;
269
+ return {
270
+ sessionId: currentSessionId,
271
+ meta: {
272
+ kind: "task",
273
+ role: "requester",
274
+ context_id: taskId || null,
275
+ purpose: taskId ? `Review task ${taskId}` : "Review task activity",
276
+ },
277
+ };
278
+ }
279
+ if (eventType === "message.received" || eventType === "dispute.raised" || eventType === "dispute.resolved") {
280
+ const orderId =
281
+ payload.order_id ||
282
+ (payload.context_type === "order" ? payload.context_id : null);
283
+ const taskId =
284
+ payload.task_id ||
285
+ (payload.context_type === "task" ? payload.context_id : null);
286
+ const candidate = orderId
287
+ ? `order:${orderId}:seller`
288
+ : taskId
289
+ ? `task:${taskId}:requester`
290
+ : null;
291
+ const hasContextSession = candidate && state.sessions[candidate];
292
+ const sessionId = hasContextSession ? candidate : currentSessionId;
293
+ return {
294
+ sessionId,
295
+ meta: {
296
+ kind: hasContextSession ? (orderId ? "order" : "task") : "current",
297
+ role: hasContextSession ? (orderId ? "seller" : "requester") : "current",
298
+ context_id: hasContextSession ? (orderId || taskId || null) : null,
299
+ purpose: hasContextSession
300
+ ? orderId
301
+ ? `Handle messages for order ${orderId}`
302
+ : `Handle messages for task ${taskId}`
303
+ : "Handle incoming platform event in the current agent session",
304
+ },
305
+ };
306
+ }
307
+ return {
308
+ sessionId: currentSessionId,
309
+ meta: {
310
+ kind: "current",
311
+ role: "current",
312
+ context_id: null,
313
+ purpose: "Process incoming ClawLabor events",
314
+ },
315
+ };
316
+ }
317
+
318
+ function appendSessionEvent(sessionRoot, sessionId, envelope) {
319
+ const inbox = sessionInboxPath(sessionRoot, sessionId);
320
+ fs.mkdirSync(path.dirname(inbox), { recursive: true });
321
+ if (inboxHasEvent(inbox, envelope.event_id)) return;
322
+ fs.appendFileSync(inbox, `${JSON.stringify(envelope)}\n`);
323
+ }
324
+
325
+ function sessionEvents(sessionRoot, sessionId) {
326
+ const inbox = sessionInboxPath(sessionRoot, sessionId);
327
+ if (!fs.existsSync(inbox)) return [];
328
+ return fs
329
+ .readFileSync(inbox, "utf8")
330
+ .split("\n")
331
+ .map((line) => line.trim())
332
+ .filter(Boolean)
333
+ .map((line) => {
334
+ try {
335
+ return JSON.parse(line);
336
+ } catch (_err) {
337
+ return null;
338
+ }
339
+ })
340
+ .filter(Boolean);
341
+ }
342
+
343
+ module.exports = {
344
+ appendSessionEvent,
345
+ defaultOnlineInboxPath,
346
+ defaultSessionId,
347
+ defaultSessionRoot,
348
+ ensureSession,
349
+ eventContextPayload,
350
+ inboxHasEvent,
351
+ readJsonFile,
352
+ readSessionState,
353
+ sanitizeSessionId,
354
+ sessionCursorFor,
355
+ sessionCursorPath,
356
+ sessionDir,
357
+ sessionEventTarget,
358
+ sessionEvents,
359
+ sessionInboxPath,
360
+ sessionInstructions,
361
+ sessionManifestPath,
362
+ sessionPromptPath,
363
+ sessionStatePath,
364
+ summarizeSessionPurpose,
365
+ writeInboxEvent,
366
+ writeJsonFile,
367
+ writeSessionCursor,
368
+ writeSessionState,
369
+ };