@lamplitisles/codex-for-love 0.5.0 → 0.6.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.
@@ -1 +1 @@
1
- {"version":"1789828770771"}
1
+ {"version":"1790151901957"}
@@ -1,27 +1,27 @@
1
1
  <!doctype html>
2
2
  <html lang="en" data-theme="sticker-messenger">
3
- <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /><script>(function(){try{var appearance=localStorage.getItem('her.companion.appearance');var language=localStorage.getItem('her.companion.language');var systemDark=matchMedia('(prefers-color-scheme: dark)').matches;var dark=appearance==='dark'||(appearance!=='light'&&systemDark);document.documentElement.dataset.theme=dark?'night-voyage':'sticker-messenger';document.documentElement.lang=language==='zh'?'zh-Hans':'en'}catch(_){var systemDark=matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.dataset.theme=systemDark?'night-voyage':'sticker-messenger'}})();</script><link href="/_app/immutable/entry/start.DWuEW6S3.js" rel="modulepreload">
4
- <link href="/_app/immutable/chunks/DFTMMhXH.js" rel="modulepreload">
5
- <link href="/_app/immutable/chunks/C2DTc7yw.js" rel="modulepreload">
6
- <link href="/_app/immutable/chunks/CyqZh0gB.js" rel="modulepreload">
7
- <link href="/_app/immutable/entry/app.Ci6fUWwj.js" rel="modulepreload">
3
+ <head><meta charset="utf-8" /><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /><script>(function(){try{var appearance=localStorage.getItem('her.companion.appearance');var language=localStorage.getItem('her.companion.language');var systemDark=matchMedia('(prefers-color-scheme: dark)').matches;var dark=appearance==='dark'||(appearance!=='light'&&systemDark);document.documentElement.dataset.theme=dark?'night-voyage':'sticker-messenger';document.documentElement.lang=language==='zh'?'zh-Hans':'en'}catch(_){var systemDark=matchMedia('(prefers-color-scheme: dark)').matches;document.documentElement.dataset.theme=systemDark?'night-voyage':'sticker-messenger'}})();</script><link href="/_app/immutable/entry/start.ZF-Jvc9b.js" rel="modulepreload">
4
+ <link href="/_app/immutable/chunks/hmUBTeUN.js" rel="modulepreload">
5
+ <link href="/_app/immutable/chunks/CLwTq0ua.js" rel="modulepreload">
6
+ <link href="/_app/immutable/chunks/B4it5vzu.js" rel="modulepreload">
7
+ <link href="/_app/immutable/entry/app.DWpdogiL.js" rel="modulepreload">
8
8
  <link href="/_app/immutable/chunks/uBIymjUX.js" rel="modulepreload">
9
9
  <link href="/_app/immutable/chunks/xihTtKlq.js" rel="modulepreload">
10
- <link href="/_app/immutable/nodes/0.C8SV4li-.js" rel="modulepreload">
10
+ <link href="/_app/immutable/nodes/0.DsknM3BK.js" rel="modulepreload">
11
11
 
12
- <link href="/_app/immutable/assets/0.CkWBasWr.css" rel="stylesheet"></head>
12
+ <link href="/_app/immutable/assets/0.CfdLiH0M.css" rel="stylesheet"></head>
13
13
  <body data-sveltekit-preload-data="hover"><div style="display: contents">
14
14
  <script>
15
15
  {
16
- __sveltekit_17ylz81 = {
16
+ __sveltekit_st3em4 = {
17
17
  base: ""
18
18
  };
19
19
 
20
20
  const element = document.currentScript.parentElement;
21
21
 
22
22
  Promise.all([
23
- import("/_app/immutable/entry/start.DWuEW6S3.js"),
24
- import("/_app/immutable/entry/app.Ci6fUWwj.js")
23
+ import("/_app/immutable/entry/start.ZF-Jvc9b.js"),
24
+ import("/_app/immutable/entry/app.DWpdogiL.js")
25
25
  ]).then(([kit, app]) => {
26
26
  kit.start(app, element);
27
27
  });
@@ -63240,6 +63240,21 @@ var Store = class {
63240
63240
  };
63241
63241
  }));
63242
63242
  }
63243
+ async messageMeta(id) {
63244
+ return this.transaction(() => {
63245
+ const row = this.db.prepare(`
63246
+ SELECT m.sequence,m.id,m.created,r.revision
63247
+ FROM message_meta m JOIN message_revisions r ON r.message_id=m.id
63248
+ WHERE m.id=?
63249
+ `).get(id);
63250
+ return row ? {
63251
+ id: row.id,
63252
+ created: Number(row.created),
63253
+ sequence: Number(row.sequence),
63254
+ revision: Number(row.revision)
63255
+ } : void 0;
63256
+ });
63257
+ }
63243
63258
  async pendingMessages() {
63244
63259
  return this.transaction(() => this.db.prepare(`
63245
63260
  SELECT m.sequence,m.id,m.created,r.revision,p.input
@@ -64442,6 +64457,7 @@ function buildRollout(extraction, media, threadId, destination, persona, relatio
64442
64457
  lastByTurn.set(record.turn, index);
64443
64458
  }
64444
64459
  });
64460
+ const startedAtByTurn = new Map([...firstByTurn].map(([turn, index]) => [turn, Math.floor(extraction.records[index].time / 1e3)]));
64445
64461
  for (const [index, record] of extraction.records.entries()) {
64446
64462
  const timestamp = new Date(record.time).toISOString();
64447
64463
  if (record.type === "compact") {
@@ -64480,6 +64496,7 @@ function buildRollout(extraction, media, threadId, destination, persona, relatio
64480
64496
  type: "task_complete",
64481
64497
  turn_id: turnId,
64482
64498
  last_agent_message: null,
64499
+ started_at: Math.floor(record.time / 1e3),
64483
64500
  completed_at: Math.floor(record.time / 1e3)
64484
64501
  }, timestamp);
64485
64502
  boundaries.push({
@@ -64492,10 +64509,11 @@ function buildRollout(extraction, media, threadId, destination, persona, relatio
64492
64509
  continue;
64493
64510
  }
64494
64511
  const turnId = `turn-${record.turn}`;
64512
+ const startedAt = startedAtByTurn.get(record.turn);
64495
64513
  if (firstByTurn.get(record.turn) === index) add("event_msg", {
64496
64514
  type: "task_started",
64497
64515
  turn_id: turnId,
64498
- started_at: Math.floor(record.time / 1e3),
64516
+ started_at: startedAt,
64499
64517
  model_context_window: null,
64500
64518
  collaboration_mode_kind: "default"
64501
64519
  }, timestamp);
@@ -64551,7 +64569,7 @@ function buildRollout(extraction, media, threadId, destination, persona, relatio
64551
64569
  type: "task_complete",
64552
64570
  turn_id: turnId,
64553
64571
  last_agent_message: record.type === "assistant" ? record.text : null,
64554
- started_at: null,
64572
+ started_at: startedAt,
64555
64573
  completed_at: Math.floor(record.time / 1e3),
64556
64574
  duration_ms: null
64557
64575
  }, timestamp);
@@ -65556,16 +65574,17 @@ async function writeConversationImages(path, images) {
65556
65574
  await rename(temporary, target);
65557
65575
  }
65558
65576
  const HISTORY_PAGE_MESSAGES = 50;
65577
+ const HISTORICAL_FALLBACK_START = new Date(2026, 8, 1).getTime();
65559
65578
  /** Match DSH's 50-message history window against CFL's rendered projection. */
65560
65579
  function visibleHistoryPage(messages, results, before) {
65561
65580
  const candidates = messages.filter((message) => before === void 0 || message.sequence < before);
65562
- const resultByOwner = new Map(results.map((result) => [result.sourceIds.at(-1), result]));
65581
+ const resultBySource = new Map(results.map((result) => [result.sourceIds.at(-1), result]));
65563
65582
  const selected = [];
65564
65583
  let visible = 0;
65565
65584
  let index = candidates.length - 1;
65566
65585
  for (; index >= 0; index -= 1) {
65567
65586
  const message = candidates[index];
65568
- const result = resultByOwner.get(message.id);
65587
+ const result = resultBySource.get(message.id);
65569
65588
  const units = 1 + (result ? result.answers.length + result.generatedIds.length + result.voiceIds.length + (result.error || [
65570
65589
  "failed",
65571
65590
  "interrupted",
@@ -65608,11 +65627,17 @@ function turnStatus(turn) {
65608
65627
  function isActiveTurn(turn) {
65609
65628
  return turnStatus(turn) === "inProgress" || turnStatus(turn) === "in_progress" || turnStatus(turn) === "active";
65610
65629
  }
65611
- function turnTime(value, fallback) {
65630
+ function recordedTurnTime(value) {
65612
65631
  const number = numberOrNull(value);
65613
- if (number === null) return fallback;
65632
+ if (number === null) return void 0;
65614
65633
  return number > 1e10 ? Math.round(number) : Math.round(number * 1e3);
65615
65634
  }
65635
+ function turnTime(value, fallback) {
65636
+ return recordedTurnTime(value) ?? fallback;
65637
+ }
65638
+ function historicalFallbackTime(sequence) {
65639
+ return HISTORICAL_FALLBACK_START + Math.max(0, sequence - 1) * 1e3;
65640
+ }
65616
65641
  function completedTurnTime(value) {
65617
65642
  const raw = numberOrNull(value);
65618
65643
  if (raw === null) return void 0;
@@ -65623,7 +65648,7 @@ function inputTimeContext(source, now) {
65623
65648
  const time = `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`;
65624
65649
  return {
65625
65650
  kind: "application",
65626
- value: `${source === "owner" ? "Current owner input" : "Qualifying Keet input"} received around local ${time}. Trusted delivery metadata; not user-authored text or an instruction.`
65651
+ value: `${source === "human" ? "Current human input" : "Qualifying Keet input"} received around local ${time}. Trusted delivery metadata; not user-authored text or an instruction.`
65627
65652
  };
65628
65653
  }
65629
65654
  function textFromUserItem(item) {
@@ -65871,7 +65896,12 @@ async function createPartner(config, credentials, dependencies = {}) {
65871
65896
  const store = new Store(paths.database);
65872
65897
  const now = dependencies.now ?? Date.now;
65873
65898
  const listeners = /* @__PURE__ */ new Set();
65874
- const messageIds = new Set((await store.allMessages()).map((message) => message.id));
65899
+ const storedMessages = await store.allMessages();
65900
+ const messageIds = new Set(storedMessages.map((message) => message.id));
65901
+ const messagesById = new Map(storedMessages.map((message) => [message.id, {
65902
+ created: message.created,
65903
+ sequence: message.sequence
65904
+ }]));
65875
65905
  const turns = /* @__PURE__ */ new Map();
65876
65906
  /** One turn owns an ordered list of application input identities. */
65877
65907
  const turnInputs = /* @__PURE__ */ new Map();
@@ -66068,11 +66098,11 @@ async function createPartner(config, credentials, dependencies = {}) {
66068
66098
  return status === "failed" ? "Native image generation failed" : "Native image artifact unavailable";
66069
66099
  }
66070
66100
  async function markAttachmentError(sourceIds, error) {
66071
- const owner = sourceIds.at(-1);
66072
- if (!owner) return;
66073
- const previous = await store.outcome(owner);
66101
+ const sourceId = sourceIds.at(-1);
66102
+ if (!sourceId) return;
66103
+ const previous = await store.outcome(sourceId);
66074
66104
  if (previous?.status === "replaced") return;
66075
- if (previous?.status !== "attachment-error" || previous.error !== error) await store.markOutcome(owner, "attachment-error", error);
66105
+ if (previous?.status !== "attachment-error" || previous.error !== error) await store.markOutcome(sourceId, "attachment-error", error);
66076
66106
  }
66077
66107
  async function projectGeneratedImage(operationId, sourceIds, item, created) {
66078
66108
  if (item.type !== "imageGeneration") return void 0;
@@ -66089,7 +66119,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66089
66119
  const existing = await store.generatedImageForItem(operationId, itemId);
66090
66120
  if (existing) try {
66091
66121
  await access(existing.path);
66092
- await catalogueImage("partner", existing, created);
66122
+ await catalogueImage("agent", existing, created);
66093
66123
  return existing.id;
66094
66124
  } catch {
66095
66125
  storageError = true;
@@ -66112,7 +66142,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66112
66142
  return;
66113
66143
  }
66114
66144
  await store.saveGeneratedImage(generated, itemId);
66115
- await catalogueImage("partner", generated, created);
66145
+ await catalogueImage("agent", generated, created);
66116
66146
  return generated.id;
66117
66147
  } catch (error) {
66118
66148
  storageError = true;
@@ -66199,11 +66229,21 @@ async function createPartner(config, credentials, dependencies = {}) {
66199
66229
  text_offset: 0,
66200
66230
  text_length: source.input.length
66201
66231
  };
66202
- if (!messageIds.has(sourceId)) {
66203
- await store.ensureMessage(sourceId, turnTime(turn.startedAt, now()), source.input, source.images);
66232
+ let message = messagesById.get(sourceId);
66233
+ if (!message) {
66234
+ const created = turnTime(turn.startedAt, now());
66235
+ const stored = await store.ensureMessage(sourceId, created, source.input, source.images);
66204
66236
  messageIds.add(sourceId);
66237
+ message = {
66238
+ created: stored.created,
66239
+ sequence: stored.sequence
66240
+ };
66241
+ messagesById.set(sourceId, message);
66242
+ }
66243
+ if (!sourceId.startsWith("keet:")) for (const image of source.images) {
66244
+ const origin = isUnder(join(paths.managedRoot, "historical-media"), image.path) ? "historical" : "human";
66245
+ await catalogueImage(origin, image, origin === "historical" ? recordedTurnTime(turn.startedAt) ?? historicalFallbackTime(message.sequence) : message.created);
66205
66246
  }
66206
- if (!sourceId.startsWith("keet:")) for (const image of source.images) await catalogueImage(isUnder(join(paths.managedRoot, "historical-media"), image.path) ? "historical" : "owner", image, turnTime(turn.startedAt, now()));
66207
66247
  await store.markObserved(sourceId, segment);
66208
66248
  }
66209
66249
  acknowledgeInputIds(officialIds);
@@ -66423,7 +66463,7 @@ async function createPartner(config, credentials, dependencies = {}) {
66423
66463
  input: intents.map((intent) => intent.input).join("\n"),
66424
66464
  images: intents.flatMap((intent) => intent.images),
66425
66465
  transportId: clientIdForInputs(ids),
66426
- source: intents.every((intent) => intent.source === "owner") ? "owner" : "none"
66466
+ source: intents.every((intent) => intent.source === "human") ? "human" : "none"
66427
66467
  };
66428
66468
  recoveryPromise = (async () => {
66429
66469
  try {
@@ -67108,6 +67148,11 @@ async function createPartner(config, credentials, dependencies = {}) {
67108
67148
  const materialized = await materializeImages(paths.workspaceRoot, images);
67109
67149
  await store.admit(id, input, materialized);
67110
67150
  messageIds.add(id);
67151
+ const message = await store.messageMeta(id);
67152
+ if (message) messagesById.set(id, {
67153
+ created: message.created,
67154
+ sequence: message.sequence
67155
+ });
67111
67156
  if (operationTurn(id) || pendingStarts.has(id) || pendingSteers.some((intent) => intent.ids.includes(id)) || await store.outcome(id)) return;
67112
67157
  if (unresolvedInputIds.has(id) && !replaces.includes(id)) throw new Error("Message delivery is unresolved; inspect the conversation before retrying");
67113
67158
  for (const replacedId of replaces) {
@@ -67125,7 +67170,7 @@ async function createPartner(config, credentials, dependencies = {}) {
67125
67170
  input,
67126
67171
  images: materialized,
67127
67172
  transportId: id,
67128
- source: "owner"
67173
+ source: "human"
67129
67174
  };
67130
67175
  if (compacting && !activeTurnId) {
67131
67176
  rejectedSteers.push(intent);
@@ -67938,6 +67983,27 @@ function createWebServer(partner, assets, options = {}) {
67938
67983
  }).strict().refine((value) => value.before === void 0 || value.after === void 0).parse(params);
67939
67984
  return json(response, await partner.snapshot(options));
67940
67985
  }
67986
+ if (path === "/api/conversation-search" && request.method === "GET") {
67987
+ const params = Object.fromEntries(new URL(request.url, "http://localhost").searchParams);
67988
+ const parsed = object({ q: string().trim().min(1).max(500) }).strict().safeParse(params);
67989
+ if (!parsed.success) return json(response, { error: "Invalid search query" }, 400);
67990
+ try {
67991
+ if (!options.conversationSearch) throw new Error("Search unavailable");
67992
+ return json(response, await options.conversationSearch.search(parsed.data.q));
67993
+ } catch {
67994
+ return json(response, { error: "Search unavailable" }, 503);
67995
+ }
67996
+ }
67997
+ if (path.startsWith("/api/conversation-search/") && request.method === "GET") {
67998
+ const id = path.slice(25);
67999
+ if (!/^[a-f0-9]{64}$/u.test(id)) return json(response, { error: "Invalid record id" }, 400);
68000
+ try {
68001
+ if (!options.conversationSearch) throw new Error("Search unavailable");
68002
+ return json(response, await options.conversationSearch.read(id));
68003
+ } catch {
68004
+ return json(response, { error: "Record unavailable" }, 404);
68005
+ }
68006
+ }
67941
68007
  if (path === "/api/diary" && request.method === "GET") try {
67942
68008
  return json(response, { entries: await partner.diary() });
67943
68009
  } catch {
@@ -68088,6 +68154,110 @@ function createWebServer(partner, assets, options = {}) {
68088
68154
  };
68089
68155
  }
68090
68156
  //#endregion
68157
+ //#region apps/partner/runtime/conversation-search.ts
68158
+ const executeFile = promisify(execFile);
68159
+ const idSchema = string().regex(/^[a-f0-9]{64}$/u);
68160
+ const querySchema = string().trim().min(1).max(500);
68161
+ const resultSchema = object({
68162
+ query: string(),
68163
+ estimatedTotalHits: number$1().int().nonnegative(),
68164
+ hits: array(object({
68165
+ id: idSchema,
68166
+ kind: _enum(["message", "compaction"]),
68167
+ sessionId: string(),
68168
+ sessionName: string().optional(),
68169
+ cwd: string(),
68170
+ role: _enum(["user", "assistant"]).optional(),
68171
+ phase: _enum(["commentary", "final_answer"]).optional(),
68172
+ createdAt: string().optional(),
68173
+ snippet: string()
68174
+ })).max(20)
68175
+ });
68176
+ const recordSchema = object({
68177
+ id: idSchema,
68178
+ kind: _enum(["message", "compaction"]),
68179
+ sessionId: string(),
68180
+ sessionName: string().optional(),
68181
+ cwd: string(),
68182
+ role: _enum(["user", "assistant"]).optional(),
68183
+ phase: _enum(["commentary", "final_answer"]).optional(),
68184
+ createdAt: string().optional(),
68185
+ content: string()
68186
+ });
68187
+ const contextSchema = object({
68188
+ messageId: idSchema,
68189
+ targetSourceRecordIndex: number$1().int(),
68190
+ truncated: boolean(),
68191
+ items: array(object({
68192
+ sourceRecordIndex: number$1().int(),
68193
+ kind: _enum([
68194
+ "message",
68195
+ "compaction",
68196
+ "tool"
68197
+ ]),
68198
+ role: _enum(["user", "assistant"]).optional(),
68199
+ phase: string().optional(),
68200
+ content: string(),
68201
+ truncated: boolean().optional()
68202
+ }))
68203
+ });
68204
+ function createConversationSearch(options) {
68205
+ const workspace = resolve(options.workspace);
68206
+ const env = {
68207
+ ...process.env,
68208
+ ...options.env,
68209
+ ...options.codexHome ? { CODEX_HOME: options.codexHome } : {}
68210
+ };
68211
+ const binary = env.FLICKLOG_BIN ?? "flicklog";
68212
+ let pending = Promise.resolve();
68213
+ function run(args) {
68214
+ const task = pending.then(async () => {
68215
+ if (!env.FLICKLOG_MEILI_KEY && env.CREDENTIALS_DIRECTORY) try {
68216
+ env.FLICKLOG_MEILI_KEY = (await readFile(join(env.CREDENTIALS_DIRECTORY, "flicklog-meili-key"), "utf8")).trim();
68217
+ } catch (error) {
68218
+ if (error.code !== "ENOENT") throw error;
68219
+ }
68220
+ const { stdout } = await executeFile(binary, args, {
68221
+ cwd: workspace,
68222
+ env,
68223
+ timeout: 3e5,
68224
+ maxBuffer: 8388608,
68225
+ windowsHide: true
68226
+ });
68227
+ return JSON.parse(stdout);
68228
+ });
68229
+ pending = task.then(() => void 0, () => void 0);
68230
+ return task;
68231
+ }
68232
+ return {
68233
+ async search(query, limit = 20) {
68234
+ const q = querySchema.parse(query);
68235
+ const count = number$1().int().min(1).max(20).parse(limit);
68236
+ const value = object({ results: resultSchema }).parse(await run([
68237
+ "search",
68238
+ q,
68239
+ "--limit",
68240
+ String(count)
68241
+ ]));
68242
+ return {
68243
+ ...value.results,
68244
+ hits: value.results.hits.filter((hit) => resolve(hit.cwd) === workspace)
68245
+ };
68246
+ },
68247
+ async read(id) {
68248
+ const selected = idSchema.parse(id);
68249
+ const record = recordSchema.parse(await run(["get", selected]));
68250
+ if (resolve(record.cwd) !== workspace) throw new Error("Conversation record is outside this workspace");
68251
+ const context = contextSchema.parse(await run(["context", selected]));
68252
+ if (context.messageId !== selected) throw new Error("Conversation context does not match the record");
68253
+ return {
68254
+ record,
68255
+ context
68256
+ };
68257
+ }
68258
+ };
68259
+ }
68260
+ //#endregion
68091
68261
  //#region apps/partner/runtime/migrate-relationship-journal.ts
68092
68262
  /** One-time, explicit conversion of a legacy CFL relationship table. */
68093
68263
  async function migrateRelationshipJournal(workspace) {
@@ -68189,7 +68359,10 @@ if (command === "credential") {
68189
68359
  const partner = await createPartner(config, credentials);
68190
68360
  let app;
68191
68361
  try {
68192
- app = createWebServer(partner, assets);
68362
+ app = createWebServer(partner, assets, { conversationSearch: createConversationSearch({
68363
+ workspace: config.workspace,
68364
+ codexHome: config.codex.home
68365
+ }) });
68193
68366
  } catch (error) {
68194
68367
  await partner.close();
68195
68368
  throw error;
@@ -23011,12 +23011,120 @@ async function withAvailability(images) {
23011
23011
  }));
23012
23012
  }
23013
23013
  //#endregion
23014
+ //#region apps/partner/runtime/conversation-search.ts
23015
+ const executeFile = promisify(execFile);
23016
+ const idSchema = string().regex(/^[a-f0-9]{64}$/u);
23017
+ const querySchema = string().trim().min(1).max(500);
23018
+ const resultSchema = object({
23019
+ query: string(),
23020
+ estimatedTotalHits: number().int().nonnegative(),
23021
+ hits: array(object({
23022
+ id: idSchema,
23023
+ kind: _enum(["message", "compaction"]),
23024
+ sessionId: string(),
23025
+ sessionName: string().optional(),
23026
+ cwd: string(),
23027
+ role: _enum(["user", "assistant"]).optional(),
23028
+ phase: _enum(["commentary", "final_answer"]).optional(),
23029
+ createdAt: string().optional(),
23030
+ snippet: string()
23031
+ })).max(20)
23032
+ });
23033
+ const recordSchema = object({
23034
+ id: idSchema,
23035
+ kind: _enum(["message", "compaction"]),
23036
+ sessionId: string(),
23037
+ sessionName: string().optional(),
23038
+ cwd: string(),
23039
+ role: _enum(["user", "assistant"]).optional(),
23040
+ phase: _enum(["commentary", "final_answer"]).optional(),
23041
+ createdAt: string().optional(),
23042
+ content: string()
23043
+ });
23044
+ const contextSchema = object({
23045
+ messageId: idSchema,
23046
+ targetSourceRecordIndex: number().int(),
23047
+ truncated: boolean(),
23048
+ items: array(object({
23049
+ sourceRecordIndex: number().int(),
23050
+ kind: _enum([
23051
+ "message",
23052
+ "compaction",
23053
+ "tool"
23054
+ ]),
23055
+ role: _enum(["user", "assistant"]).optional(),
23056
+ phase: string().optional(),
23057
+ content: string(),
23058
+ truncated: boolean().optional()
23059
+ }))
23060
+ });
23061
+ function createConversationSearch(options) {
23062
+ const workspace = resolve(options.workspace);
23063
+ const env = {
23064
+ ...process.env,
23065
+ ...options.env,
23066
+ ...options.codexHome ? { CODEX_HOME: options.codexHome } : {}
23067
+ };
23068
+ const binary = env.FLICKLOG_BIN ?? "flicklog";
23069
+ let pending = Promise.resolve();
23070
+ function run(args) {
23071
+ const task = pending.then(async () => {
23072
+ if (!env.FLICKLOG_MEILI_KEY && env.CREDENTIALS_DIRECTORY) try {
23073
+ env.FLICKLOG_MEILI_KEY = (await readFile(join(env.CREDENTIALS_DIRECTORY, "flicklog-meili-key"), "utf8")).trim();
23074
+ } catch (error) {
23075
+ if (error.code !== "ENOENT") throw error;
23076
+ }
23077
+ const { stdout } = await executeFile(binary, args, {
23078
+ cwd: workspace,
23079
+ env,
23080
+ timeout: 3e5,
23081
+ maxBuffer: 8388608,
23082
+ windowsHide: true
23083
+ });
23084
+ return JSON.parse(stdout);
23085
+ });
23086
+ pending = task.then(() => void 0, () => void 0);
23087
+ return task;
23088
+ }
23089
+ return {
23090
+ async search(query, limit = 20) {
23091
+ const q = querySchema.parse(query);
23092
+ const count = number().int().min(1).max(20).parse(limit);
23093
+ const value = object({ results: resultSchema }).parse(await run([
23094
+ "search",
23095
+ q,
23096
+ "--limit",
23097
+ String(count)
23098
+ ]));
23099
+ return {
23100
+ ...value.results,
23101
+ hits: value.results.hits.filter((hit) => resolve(hit.cwd) === workspace)
23102
+ };
23103
+ },
23104
+ async read(id) {
23105
+ const selected = idSchema.parse(id);
23106
+ const record = recordSchema.parse(await run(["get", selected]));
23107
+ if (resolve(record.cwd) !== workspace) throw new Error("Conversation record is outside this workspace");
23108
+ const context = contextSchema.parse(await run(["context", selected]));
23109
+ if (context.messageId !== selected) throw new Error("Conversation context does not match the record");
23110
+ return {
23111
+ record,
23112
+ context
23113
+ };
23114
+ }
23115
+ };
23116
+ }
23117
+ //#endregion
23014
23118
  //#region apps/partner/runtime/companion-mcp.ts
23015
23119
  const workspace = process.argv[2];
23016
23120
  if (!workspace) throw new Error("Companion MCP requires a workspace path");
23017
23121
  const configPath = process.argv[3];
23018
23122
  const journal = partnerPaths(workspace).relationshipJournal;
23019
23123
  const conversationImages = partnerPaths(workspace).conversationImages;
23124
+ const conversationSearch = createConversationSearch({
23125
+ workspace,
23126
+ codexHome: configPath ? (await loadConfig(configPath)).codex.home : void 0
23127
+ });
23020
23128
  const server = new McpServer({
23021
23129
  name: "companion",
23022
23130
  version: "0.1.0"
@@ -23060,7 +23168,7 @@ server.registerTool("read_relationship_history", {
23060
23168
  text: JSON.stringify((await readRelationshipJournal(journal)).reverse().slice(0, canonicalizeHistoryRead(input)))
23061
23169
  }] }));
23062
23170
  server.registerTool("list_photos", {
23063
- description: "List our shared photo library: owner-sent, Partner-generated, and restored historical conversation images, newest first in bounded pages. Returned local paths may be inspected deliberately with native tools.",
23171
+ description: "List our shared photo library: human-sent, Agent-generated, and restored historical conversation images, newest first in bounded pages. Returned local paths may be inspected deliberately with native tools.",
23064
23172
  inputSchema: {
23065
23173
  limit: number().int().min(1).max(50).optional(),
23066
23174
  cursor: string().regex(/^[A-Za-z0-9_-]{54}$/u).optional()
@@ -23083,6 +23191,47 @@ server.registerTool("list_photos", {
23083
23191
  })
23084
23192
  }] };
23085
23193
  });
23194
+ server.registerTool("search_conversation", {
23195
+ description: "Find text in our conversation records. Returns bounded highlighted excerpts; read a selected record for its full text and nearby context.",
23196
+ inputSchema: {
23197
+ query: string().trim().min(1).max(500),
23198
+ limit: number().int().min(1).max(20).optional()
23199
+ }
23200
+ }, async ({ query, limit }) => {
23201
+ try {
23202
+ return { content: [{
23203
+ type: "text",
23204
+ text: JSON.stringify(await conversationSearch.search(query, limit ?? 5))
23205
+ }] };
23206
+ } catch {
23207
+ return {
23208
+ content: [{
23209
+ type: "text",
23210
+ text: "Conversation search is unavailable"
23211
+ }],
23212
+ isError: true
23213
+ };
23214
+ }
23215
+ });
23216
+ server.registerTool("read_conversation_record", {
23217
+ description: "Read one selected conversation search result in full with nearby context.",
23218
+ inputSchema: { id: string().regex(/^[a-f0-9]{64}$/u) }
23219
+ }, async ({ id }) => {
23220
+ try {
23221
+ return { content: [{
23222
+ type: "text",
23223
+ text: JSON.stringify(await conversationSearch.read(id))
23224
+ }] };
23225
+ } catch {
23226
+ return {
23227
+ content: [{
23228
+ type: "text",
23229
+ text: "Conversation record is unavailable"
23230
+ }],
23231
+ isError: true
23232
+ };
23233
+ }
23234
+ });
23086
23235
  server.registerTool("roll_dice", {
23087
23236
  description: "Roll dice with an optional modifier and label.",
23088
23237
  inputSchema: {