codeksei 0.1.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 (80) hide show
  1. package/LICENSE +661 -0
  2. package/README.en.md +215 -0
  3. package/README.md +259 -0
  4. package/bin/codeksei.js +10 -0
  5. package/bin/cyberboss.js +11 -0
  6. package/package.json +86 -0
  7. package/scripts/install-background-tasks.ps1 +135 -0
  8. package/scripts/open_shared_wechat_thread.sh +94 -0
  9. package/scripts/open_wechat_thread.sh +117 -0
  10. package/scripts/shared-common.js +791 -0
  11. package/scripts/shared-open.js +46 -0
  12. package/scripts/shared-start.js +41 -0
  13. package/scripts/shared-status.js +74 -0
  14. package/scripts/shared-supervisor.js +141 -0
  15. package/scripts/shared-task-runner.ps1 +87 -0
  16. package/scripts/shared-watchdog.js +290 -0
  17. package/scripts/show_shared_status.sh +53 -0
  18. package/scripts/start_shared_app_server.sh +65 -0
  19. package/scripts/start_shared_wechat.sh +108 -0
  20. package/scripts/timeline-screenshot.sh +15 -0
  21. package/scripts/uninstall-background-tasks.ps1 +23 -0
  22. package/src/adapters/channel/weixin/account-store.js +135 -0
  23. package/src/adapters/channel/weixin/api-v2.js +258 -0
  24. package/src/adapters/channel/weixin/api.js +180 -0
  25. package/src/adapters/channel/weixin/context-token-store.js +84 -0
  26. package/src/adapters/channel/weixin/index.js +605 -0
  27. package/src/adapters/channel/weixin/legacy.js +567 -0
  28. package/src/adapters/channel/weixin/login-common.js +63 -0
  29. package/src/adapters/channel/weixin/login-legacy.js +124 -0
  30. package/src/adapters/channel/weixin/login-v2.js +186 -0
  31. package/src/adapters/channel/weixin/media-mime.js +22 -0
  32. package/src/adapters/channel/weixin/media-receive.js +370 -0
  33. package/src/adapters/channel/weixin/media-send.js +331 -0
  34. package/src/adapters/channel/weixin/message-utils-v2.js +282 -0
  35. package/src/adapters/channel/weixin/message-utils.js +199 -0
  36. package/src/adapters/channel/weixin/protocol.js +77 -0
  37. package/src/adapters/channel/weixin/redact.js +41 -0
  38. package/src/adapters/channel/weixin/reminder-queue-store.js +101 -0
  39. package/src/adapters/channel/weixin/sync-buffer-store.js +35 -0
  40. package/src/adapters/runtime/codex/events.js +252 -0
  41. package/src/adapters/runtime/codex/index.js +502 -0
  42. package/src/adapters/runtime/codex/message-utils.js +141 -0
  43. package/src/adapters/runtime/codex/model-catalog.js +106 -0
  44. package/src/adapters/runtime/codex/protocol-leak-monitor.js +75 -0
  45. package/src/adapters/runtime/codex/rpc-client.js +443 -0
  46. package/src/adapters/runtime/codex/session-store.js +376 -0
  47. package/src/app/channel-send-file-cli.js +57 -0
  48. package/src/app/diary-write-cli.js +620 -0
  49. package/src/app/note-auto-cli.js +201 -0
  50. package/src/app/note-sync-cli.js +130 -0
  51. package/src/app/project-radar-cli.js +165 -0
  52. package/src/app/reminder-write-cli.js +210 -0
  53. package/src/app/review-cli.js +134 -0
  54. package/src/app/system-checkin-poller.js +100 -0
  55. package/src/app/system-send-cli.js +129 -0
  56. package/src/app/timeline-event-cli.js +273 -0
  57. package/src/app/timeline-screenshot-cli.js +109 -0
  58. package/src/core/app.js +1810 -0
  59. package/src/core/branding.js +167 -0
  60. package/src/core/command-registry.js +609 -0
  61. package/src/core/config.js +84 -0
  62. package/src/core/default-targets.js +163 -0
  63. package/src/core/durable-note-schema.js +325 -0
  64. package/src/core/instructions-template.js +31 -0
  65. package/src/core/note-sync.js +433 -0
  66. package/src/core/project-radar.js +402 -0
  67. package/src/core/review-semantic.js +524 -0
  68. package/src/core/review.js +1081 -0
  69. package/src/core/shared-bridge-heartbeat.js +140 -0
  70. package/src/core/stream-delivery.js +990 -0
  71. package/src/core/system-message-dispatcher.js +68 -0
  72. package/src/core/system-message-queue-store.js +128 -0
  73. package/src/core/thread-state-store.js +135 -0
  74. package/src/core/timeline-screenshot-queue-store.js +134 -0
  75. package/src/core/workspace-alias.js +163 -0
  76. package/src/core/workspace-bootstrap.js +338 -0
  77. package/src/index.js +270 -0
  78. package/src/integrations/timeline/index.js +191 -0
  79. package/templates/weixin-instructions.md +53 -0
  80. package/templates/weixin-operations.md +69 -0
@@ -0,0 +1,524 @@
1
+ const { CodexRpcClient } = require("../adapters/runtime/codex/rpc-client");
2
+ const {
3
+ extractAssistantText,
4
+ extractFailureText,
5
+ extractThreadId,
6
+ extractThreadIdFromParams,
7
+ extractTurnIdFromParams,
8
+ isAssistantItemCompleted,
9
+ } = require("../adapters/runtime/codex/message-utils");
10
+ const { resolveCodexWorkspaceRoot } = require("./workspace-alias");
11
+
12
+ const DEFAULT_TIMEOUT_MS = 120_000;
13
+
14
+ async function maybeGenerateSemanticReview(config = {}, input = {}) {
15
+ const kind = normalizeText(input?.profile?.kind);
16
+ const mode = normalizeSemanticMode(
17
+ input?.options?.deterministic ? "deterministic" : config.reviewSemanticMode
18
+ );
19
+ if (!kind || mode === "deterministic") {
20
+ return {
21
+ used: false,
22
+ source: "deterministic",
23
+ reason: mode === "deterministic" ? "disabled" : "missing-kind",
24
+ data: null,
25
+ };
26
+ }
27
+
28
+ const diaryEntries = Array.isArray(input.diaryEntries) ? input.diaryEntries : [];
29
+ if (!diaryEntries.length) {
30
+ return {
31
+ used: false,
32
+ source: "deterministic",
33
+ reason: "no-diary",
34
+ data: null,
35
+ };
36
+ }
37
+
38
+ try {
39
+ const raw = typeof config.reviewSemanticGenerator === "function"
40
+ ? await config.reviewSemanticGenerator(buildSemanticGeneratorInput(config, input))
41
+ : await runCodexSemanticReview(config, input);
42
+ const data = normalizeSemanticResult(kind, raw);
43
+ if (!data || !hasSemanticPayload(kind, data)) {
44
+ return {
45
+ used: false,
46
+ source: "deterministic",
47
+ reason: "empty-semantic",
48
+ data: null,
49
+ };
50
+ }
51
+ return {
52
+ used: true,
53
+ source: typeof config.reviewSemanticGenerator === "function" ? "injected" : "codex",
54
+ reason: "",
55
+ data,
56
+ };
57
+ } catch (error) {
58
+ return {
59
+ used: false,
60
+ source: "deterministic",
61
+ reason: formatErrorMessage(error),
62
+ data: null,
63
+ };
64
+ }
65
+ }
66
+
67
+ async function runCodexSemanticReview(config = {}, input = {}) {
68
+ const prompt = buildSemanticPrompt(input);
69
+ const timeoutMs = normalizeTimeout(config.reviewSemanticTimeoutMs) || DEFAULT_TIMEOUT_MS;
70
+ const model = normalizeText(input?.options?.model || config.reviewSemanticModel);
71
+ const workspaceRoot = resolveCodexWorkspaceRoot(input?.profile?.workspaceRoot || config.workspaceRoot || process.cwd());
72
+ const client = new CodexRpcClient({
73
+ endpoint: config.codexEndpoint,
74
+ codexCommand: config.codexCommand,
75
+ env: process.env,
76
+ extraWritableRoots: [config.stateDir],
77
+ });
78
+
79
+ try {
80
+ await client.connect();
81
+ await client.initialize();
82
+ const response = await client.startThread({ cwd: workspaceRoot });
83
+ const threadId = extractThreadId(response);
84
+ if (!threadId) {
85
+ throw new Error("semantic review did not return a thread id");
86
+ }
87
+ const completion = waitForSemanticTurnCompletion(client, threadId, timeoutMs);
88
+ await client.sendUserMessage({
89
+ threadId,
90
+ text: prompt,
91
+ model: model || null,
92
+ workspaceRoot,
93
+ });
94
+ const text = await completion;
95
+ return parseSemanticJson(text);
96
+ } finally {
97
+ await client.close().catch(() => {});
98
+ }
99
+ }
100
+
101
+ function buildSemanticGeneratorInput(config = {}, input = {}) {
102
+ return {
103
+ config,
104
+ profile: input.profile,
105
+ window: input.window,
106
+ diaryEntries: input.diaryEntries,
107
+ nightlyEntries: input.nightlyEntries,
108
+ deterministicDraft: input.deterministicDraft,
109
+ options: input.options || {},
110
+ sourcePack: buildSemanticSourcePack(input),
111
+ };
112
+ }
113
+
114
+ function buildSemanticPrompt(input = {}) {
115
+ const kind = normalizeText(input?.profile?.kind);
116
+ const schema = kind === "nightly"
117
+ ? NIGHTLY_SCHEMA_PROMPT
118
+ : PERIODIC_SCHEMA_PROMPT;
119
+ const sourcePack = buildSemanticSourcePack(input);
120
+
121
+ return [
122
+ "你在做 Codeksei 生活助理复盘的语义提炼。",
123
+ "你已经拿到了全部需要的源材料,不要调用工具,不要读文件,不要追问,不要补充不存在的事实。",
124
+ "目标不是覆盖所有事件,而是压出高信号复盘:真正的推进、值得记住的摩擦、仍开着的线头、下一次更容易接上的入口。",
125
+ "排除微小切换、机械流水账、睡前洗漱/短暂浏览/无后果的小动作,除非它明显改变了解释。",
126
+ "如果证据不够强,就宁可少写,不要凑数。",
127
+ "每条尽量短、具体、非评判。",
128
+ "只返回 JSON,不要 Markdown,不要代码块,不要解释。",
129
+ "",
130
+ "JSON schema:",
131
+ schema,
132
+ "",
133
+ "Source pack:",
134
+ JSON.stringify(sourcePack, null, 2),
135
+ ].join("\n");
136
+ }
137
+
138
+ const NIGHTLY_SCHEMA_PROMPT = [
139
+ "{",
140
+ ' "progress": ["..."],',
141
+ ' "friction": ["..."],',
142
+ ' "open_loops": ["..."],',
143
+ ' "carry_forward": ["..."],',
144
+ ' "closeout": ["..."],',
145
+ ' "signals": ["..."]',
146
+ "}",
147
+ ].join("\n");
148
+
149
+ const PERIODIC_SCHEMA_PROMPT = [
150
+ "{",
151
+ ' "progress": ["..."],',
152
+ ' "friction": ["..."],',
153
+ ' "open_loops": ["..."],',
154
+ ' "carry_forward": ["..."],',
155
+ ' "daily_summaries": [{"date": "YYYY-MM-DD", "lines": ["..."]}],',
156
+ ' "supplement_groups": [{"date": "YYYY-MM-DD", "title": "...", "body_lines": ["..."]}]',
157
+ "}",
158
+ ].join("\n");
159
+
160
+ function buildSemanticSourcePack(input = {}) {
161
+ const deterministicDraft = input.deterministicDraft || {};
162
+ const kind = normalizeText(input?.profile?.kind);
163
+ return {
164
+ kind,
165
+ periodLabel: deterministicDraft.periodLabel || "",
166
+ window: {
167
+ startDate: input?.window?.startDate || "",
168
+ endDate: input?.window?.endDate || "",
169
+ },
170
+ windowFacts: Array.isArray(deterministicDraft.windowFacts) ? deterministicDraft.windowFacts : [],
171
+ deterministicBaseline: buildDeterministicBaseline(kind, deterministicDraft.insights || {}),
172
+ diaryDays: compactDiaryDays(input.diaryEntries),
173
+ nightlyDays: compactNightlyDays(input.nightlyEntries),
174
+ };
175
+ }
176
+
177
+ function buildDeterministicBaseline(kind, insights = {}) {
178
+ if (kind === "nightly") {
179
+ return {
180
+ progress: normalizeStringList(insights.progress, 6, 160),
181
+ friction: normalizeStringList(insights.friction, 6, 220),
182
+ open_loops: normalizeStringList(insights.openLoops, 6, 160),
183
+ carry_forward: normalizeStringList(insights.carryForward, 6, 160),
184
+ closeout: normalizeStringList(insights.closeout, 6, 180),
185
+ signals: normalizeStringList(insights.signals, 6, 180),
186
+ };
187
+ }
188
+ return {
189
+ progress: normalizeStringList(insights.progress, 8, 180),
190
+ friction: normalizeStringList(insights.friction, 8, 220),
191
+ open_loops: normalizeStringList(insights.openLoops, 8, 160),
192
+ carry_forward: normalizeStringList(insights.carryForward, 6, 160),
193
+ daily_summaries: normalizeDayGroups(insights.dailySummaries, 14, 3, 160),
194
+ supplement_groups: normalizeSupplementGroups(insights.supplements, 8, 4, 180),
195
+ };
196
+ }
197
+
198
+ function compactDiaryDays(entries) {
199
+ return (Array.isArray(entries) ? entries : []).map((entry) => ({
200
+ date: normalizeText(entry?.date),
201
+ openTodos: normalizeStringList(entry?.todo?.open, 5, 140),
202
+ doneTodos: normalizeStringList(entry?.todo?.done, 5, 140),
203
+ summary: normalizeStringList(entry?.summary, 6, 180),
204
+ timeline: normalizeStringList(entry?.timeline, 4, 160),
205
+ fragment: normalizeStringList(entry?.fragment, 4, 180),
206
+ supplement: normalizeSupplementGroups(
207
+ (Array.isArray(entry?.supplement) ? entry.supplement : []).map((item) => ({
208
+ date: normalizeText(entry?.date),
209
+ title: normalizeText(item?.title),
210
+ body_lines: compactBodyLines(item?.body, 3, 180),
211
+ })),
212
+ 5,
213
+ 3,
214
+ 180
215
+ ),
216
+ }));
217
+ }
218
+
219
+ function compactNightlyDays(entries) {
220
+ return (Array.isArray(entries) ? entries : []).map((entry) => ({
221
+ date: normalizeText(entry?.date),
222
+ progress: normalizeStringList(entry?.progress, 4, 160),
223
+ friction: normalizeStringList(entry?.friction, 4, 180),
224
+ open_loops: normalizeStringList(entry?.openLoops, 4, 140),
225
+ carry_forward: normalizeStringList(entry?.carryForward, 4, 140),
226
+ closeout: normalizeStringList(entry?.closeout, 4, 160),
227
+ signals: normalizeStringList(entry?.signals, 4, 160),
228
+ }));
229
+ }
230
+
231
+ function waitForSemanticTurnCompletion(client, threadId, timeoutMs) {
232
+ return new Promise((resolve, reject) => {
233
+ let activeTurnId = "";
234
+ const textByItemId = new Map();
235
+ const itemOrder = [];
236
+
237
+ const cleanup = () => {
238
+ unsubscribe();
239
+ clearTimeout(timer);
240
+ };
241
+
242
+ const timer = setTimeout(() => {
243
+ cleanup();
244
+ reject(new Error(`semantic review timed out after ${timeoutMs}ms`));
245
+ }, timeoutMs);
246
+
247
+ const unsubscribe = client.onMessage((message) => {
248
+ const params = message?.params || {};
249
+ const messageThreadId = extractThreadIdFromParams(params);
250
+ if (messageThreadId && messageThreadId !== threadId) {
251
+ return;
252
+ }
253
+
254
+ if (message?.method === "turn/started" || message?.method === "turn/start") {
255
+ activeTurnId = extractTurnIdFromParams(params) || activeTurnId;
256
+ return;
257
+ }
258
+
259
+ // Review summarization is meant to be a pure thinking pass over the
260
+ // provided source pack. If Codex wants tools or escalation here, the
261
+ // safe behavior is to abort and fall back to deterministic extraction.
262
+ if (isApprovalRequest(message)) {
263
+ cleanup();
264
+ reject(new Error("semantic review requested approval"));
265
+ return;
266
+ }
267
+
268
+ if (message?.method === "item/agentMessage/delta" || isAssistantItemCompleted(message)) {
269
+ const itemId = normalizeText(params?.itemId || params?.item?.id) || `item-${itemOrder.length + 1}`;
270
+ const nextText = extractAssistantText(params);
271
+ if (!textByItemId.has(itemId)) {
272
+ itemOrder.push(itemId);
273
+ textByItemId.set(itemId, "");
274
+ }
275
+ if (nextText) {
276
+ if (message?.method === "item/agentMessage/delta") {
277
+ textByItemId.set(itemId, `${textByItemId.get(itemId) || ""}${nextText}`);
278
+ } else {
279
+ textByItemId.set(itemId, nextText);
280
+ }
281
+ }
282
+ return;
283
+ }
284
+
285
+ if (message?.method === "turn/failed") {
286
+ cleanup();
287
+ reject(new Error(extractFailureText(params)));
288
+ return;
289
+ }
290
+
291
+ if (message?.method === "turn/completed") {
292
+ const completedTurnId = extractTurnIdFromParams(params);
293
+ if (activeTurnId && completedTurnId && completedTurnId !== activeTurnId) {
294
+ return;
295
+ }
296
+ cleanup();
297
+ // Semantic review expects the terminal JSON payload. Codex can emit
298
+ // multiple assistant messages within one turn, so concatenating every
299
+ // message here risks mixing progress chatter into the final JSON blob.
300
+ const text = itemOrder
301
+ .slice()
302
+ .reverse()
303
+ .map((itemId) => textByItemId.get(itemId) || "")
304
+ .find((value) => String(value || "").trim()) || "";
305
+ if (!text) {
306
+ reject(new Error("semantic review returned empty text"));
307
+ return;
308
+ }
309
+ resolve(String(text).trim());
310
+ }
311
+ });
312
+ });
313
+ }
314
+
315
+ function isApprovalRequest(message) {
316
+ return typeof message?.method === "string" && message.method.endsWith("requestApproval");
317
+ }
318
+
319
+ function parseSemanticJson(text) {
320
+ const direct = tryParseJson(text);
321
+ if (direct) {
322
+ return direct;
323
+ }
324
+
325
+ const fenced = /```(?:json)?\s*([\s\S]*?)```/iu.exec(String(text || ""));
326
+ if (fenced?.[1]) {
327
+ const parsed = tryParseJson(fenced[1]);
328
+ if (parsed) {
329
+ return parsed;
330
+ }
331
+ }
332
+
333
+ const objectCandidate = extractFirstJsonObject(text);
334
+ const parsed = tryParseJson(objectCandidate);
335
+ if (parsed) {
336
+ return parsed;
337
+ }
338
+
339
+ throw new Error("semantic review did not return valid JSON");
340
+ }
341
+
342
+ function normalizeSemanticResult(kind, raw) {
343
+ if (!raw || typeof raw !== "object") {
344
+ return null;
345
+ }
346
+ if (kind === "nightly") {
347
+ return {
348
+ progress: normalizeStringList(raw.progress, 5, 160),
349
+ friction: normalizeStringList(raw.friction, 5, 220),
350
+ openLoops: normalizeStringList(raw.open_loops || raw.openLoops, 6, 160),
351
+ carryForward: normalizeStringList(raw.carry_forward || raw.carryForward, 4, 160),
352
+ closeout: normalizeStringList(raw.closeout, 5, 180),
353
+ signals: normalizeStringList(raw.signals, 5, 160),
354
+ };
355
+ }
356
+ return {
357
+ progress: normalizeStringList(raw.progress, 6, 180),
358
+ friction: normalizeStringList(raw.friction, 6, 220),
359
+ openLoops: normalizeStringList(raw.open_loops || raw.openLoops, 6, 160),
360
+ carryForward: normalizeStringList(raw.carry_forward || raw.carryForward, 4, 160),
361
+ dailySummaries: normalizeDayGroups(raw.daily_summaries || raw.dailySummaries, 31, 3, 160),
362
+ supplements: normalizeSupplementGroups(raw.supplement_groups || raw.supplements, 8, 4, 180),
363
+ };
364
+ }
365
+
366
+ function hasSemanticPayload(kind, data) {
367
+ if (!data || typeof data !== "object") {
368
+ return false;
369
+ }
370
+ if (kind === "nightly") {
371
+ return [
372
+ data.progress,
373
+ data.friction,
374
+ data.openLoops,
375
+ data.carryForward,
376
+ data.closeout,
377
+ data.signals,
378
+ ].some((items) => Array.isArray(items) && items.length);
379
+ }
380
+ return [
381
+ data.progress,
382
+ data.friction,
383
+ data.openLoops,
384
+ data.carryForward,
385
+ data.dailySummaries,
386
+ data.supplements,
387
+ ].some((items) => Array.isArray(items) && items.length);
388
+ }
389
+
390
+ function normalizeDayGroups(value, maxGroups, maxLines, maxLength) {
391
+ const groups = [];
392
+ const seen = new Set();
393
+ for (const item of Array.isArray(value) ? value : []) {
394
+ const date = normalizeText(item?.date);
395
+ const lines = normalizeStringList(item?.lines, maxLines, maxLength);
396
+ if (!date || !lines.length) {
397
+ continue;
398
+ }
399
+ const signature = `${date}:${lines.join("|").toLowerCase()}`;
400
+ if (seen.has(signature)) {
401
+ continue;
402
+ }
403
+ seen.add(signature);
404
+ groups.push({ date, lines });
405
+ if (groups.length >= maxGroups) {
406
+ break;
407
+ }
408
+ }
409
+ return groups;
410
+ }
411
+
412
+ function normalizeSupplementGroups(value, maxGroups, maxLines, maxLength) {
413
+ const groups = [];
414
+ const seen = new Set();
415
+ for (const item of Array.isArray(value) ? value : []) {
416
+ const date = normalizeText(item?.date);
417
+ const title = normalizeText(item?.title);
418
+ const bodyLines = normalizeStringList(item?.body_lines || item?.bodyLines || item?.body, maxLines, maxLength);
419
+ if (!date || !bodyLines.length) {
420
+ continue;
421
+ }
422
+ const signature = `${date}:${title}:${bodyLines.join("|").toLowerCase()}`;
423
+ if (seen.has(signature)) {
424
+ continue;
425
+ }
426
+ seen.add(signature);
427
+ groups.push({
428
+ date,
429
+ title,
430
+ body: bodyLines.join("\n"),
431
+ });
432
+ if (groups.length >= maxGroups) {
433
+ break;
434
+ }
435
+ }
436
+ return groups;
437
+ }
438
+
439
+ function normalizeStringList(value, maxItems, maxLength) {
440
+ const items = [];
441
+ const seen = new Set();
442
+ const rawItems = Array.isArray(value)
443
+ ? value
444
+ : typeof value === "string"
445
+ ? splitLines(value)
446
+ : [];
447
+ for (const raw of rawItems) {
448
+ const normalized = truncateSentence(normalizeText(String(raw || "").replace(/^\s*-\s*/u, "")), maxLength);
449
+ if (!normalized) {
450
+ continue;
451
+ }
452
+ const signature = normalized.toLowerCase();
453
+ if (seen.has(signature)) {
454
+ continue;
455
+ }
456
+ seen.add(signature);
457
+ items.push(normalized);
458
+ if (items.length >= maxItems) {
459
+ break;
460
+ }
461
+ }
462
+ return items;
463
+ }
464
+
465
+ function compactBodyLines(value, maxLines, maxLength) {
466
+ return normalizeStringList(splitLines(String(value || "")), maxLines, maxLength);
467
+ }
468
+
469
+ function splitLines(value) {
470
+ return String(value || "")
471
+ .replace(/\r\n/g, "\n")
472
+ .split("\n")
473
+ .map((line) => line.trim())
474
+ .filter(Boolean);
475
+ }
476
+
477
+ function truncateSentence(value, maxLength) {
478
+ const normalized = normalizeText(value);
479
+ if (!normalized || normalized.length <= maxLength) {
480
+ return normalized;
481
+ }
482
+ return `${normalized.slice(0, Math.max(0, maxLength - 1)).replace(/[,。;,;:\s]+$/u, "")}…`;
483
+ }
484
+
485
+ function tryParseJson(text) {
486
+ try {
487
+ const parsed = JSON.parse(String(text || "").trim());
488
+ return parsed && typeof parsed === "object" ? parsed : null;
489
+ } catch {
490
+ return null;
491
+ }
492
+ }
493
+
494
+ function extractFirstJsonObject(text) {
495
+ const input = String(text || "");
496
+ const start = input.indexOf("{");
497
+ const end = input.lastIndexOf("}");
498
+ if (start < 0 || end <= start) {
499
+ return "";
500
+ }
501
+ return input.slice(start, end + 1);
502
+ }
503
+
504
+ function normalizeSemanticMode(value) {
505
+ const normalized = normalizeText(value).toLowerCase();
506
+ return normalized === "deterministic" ? "deterministic" : "hybrid";
507
+ }
508
+
509
+ function normalizeTimeout(value) {
510
+ const numeric = Number(value);
511
+ return Number.isFinite(numeric) && numeric > 0 ? numeric : 0;
512
+ }
513
+
514
+ function normalizeText(value) {
515
+ return typeof value === "string" ? value.trim() : "";
516
+ }
517
+
518
+ function formatErrorMessage(error) {
519
+ return error instanceof Error ? error.message : String(error || "unknown error");
520
+ }
521
+
522
+ module.exports = {
523
+ maybeGenerateSemanticReview,
524
+ };