chat-logbook 0.7.1 → 0.9.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.
package/api/dist/index.js CHANGED
@@ -4758,12 +4758,12 @@ function mapRelationalRow(tablesConfig, tableConfig, row, buildQueryResultSelect
4758
4758
  var schema_exports = {};
4759
4759
  __export(schema_exports, {
4760
4760
  archiveMeta: () => archiveMeta,
4761
+ chatScanState: () => chatScanState,
4762
+ chats: () => chats,
4761
4763
  ingestionEvents: () => ingestionEvents,
4762
4764
  messages: () => messages,
4763
4765
  rawMessages: () => rawMessages,
4764
- schemaVersion: () => schemaVersion,
4765
- sessionScanState: () => sessionScanState,
4766
- sessions: () => sessions
4766
+ schemaVersion: () => schemaVersion
4767
4767
  });
4768
4768
 
4769
4769
  // ../node_modules/.pnpm/drizzle-orm@0.45.2_@types+better-sqlite3@7.6.13_better-sqlite3@12.9.0/node_modules/drizzle-orm/sqlite-core/foreign-keys.js
@@ -8179,26 +8179,25 @@ var schemaVersion = sqliteTable("schema_version", {
8179
8179
  version: integer("version").primaryKey(),
8180
8180
  appliedAt: integer("applied_at", { mode: "timestamp_ms" }).notNull()
8181
8181
  });
8182
- var sessions = sqliteTable(
8183
- "sessions",
8182
+ var chats = sqliteTable(
8183
+ "chats",
8184
8184
  {
8185
8185
  id: text("id").primaryKey(),
8186
- shortCode: text("short_code").notNull().unique(),
8186
+ chatId: text("chat_id").notNull().unique(),
8187
8187
  agent: text("agent").notNull(),
8188
- sourceSessionId: text("source_session_id").notNull(),
8188
+ sourceId: text("source_id").notNull(),
8189
8189
  firstSeenAt: integer("first_seen_at", { mode: "timestamp_ms" }).notNull(),
8190
- project: text("project")
8190
+ project: text("project"),
8191
+ projectPath: text("project_path")
8191
8192
  },
8192
- (t) => [
8193
- uniqueIndex("sessions_agent_source_idx").on(t.agent, t.sourceSessionId)
8194
- ]
8193
+ (t) => [uniqueIndex("chats_agent_source_idx").on(t.agent, t.sourceId)]
8195
8194
  );
8196
8195
  var rawMessages = sqliteTable(
8197
8196
  "raw_messages",
8198
8197
  {
8199
8198
  id: integer("id").primaryKey({ autoIncrement: true }),
8200
8199
  agent: text("agent").notNull(),
8201
- sessionId: text("session_id").notNull(),
8200
+ sourceId: text("source_id").notNull(),
8202
8201
  sourcePath: text("source_path").notNull(),
8203
8202
  sourceLocator: text("source_locator").notNull(),
8204
8203
  rawPayload: text("raw_payload").notNull(),
@@ -8206,11 +8205,7 @@ var rawMessages = sqliteTable(
8206
8205
  ingestedAt: integer("ingested_at", { mode: "timestamp_ms" }).notNull()
8207
8206
  },
8208
8207
  (t) => [
8209
- uniqueIndex("raw_messages_idem_idx").on(
8210
- t.agent,
8211
- t.sessionId,
8212
- t.payloadHash
8213
- )
8208
+ uniqueIndex("raw_messages_idem_idx").on(t.agent, t.sourceId, t.payloadHash)
8214
8209
  ]
8215
8210
  );
8216
8211
  var messages = sqliteTable(
@@ -8218,7 +8213,7 @@ var messages = sqliteTable(
8218
8213
  {
8219
8214
  id: integer("id").primaryKey({ autoIncrement: true }),
8220
8215
  agent: text("agent").notNull(),
8221
- sessionId: text("session_id").notNull(),
8216
+ sourceId: text("source_id").notNull(),
8222
8217
  messageId: text("message_id").notNull(),
8223
8218
  role: text("role").notNull(),
8224
8219
  ts: integer("ts", { mode: "timestamp_ms" }).notNull(),
@@ -8227,15 +8222,15 @@ var messages = sqliteTable(
8227
8222
  rawId: integer("raw_id").notNull().references(() => rawMessages.id)
8228
8223
  },
8229
8224
  (t) => [
8230
- uniqueIndex("messages_canonical_idx").on(t.agent, t.sessionId, t.messageId)
8225
+ uniqueIndex("messages_canonical_idx").on(t.agent, t.sourceId, t.messageId)
8231
8226
  ]
8232
8227
  );
8233
- var sessionScanState = sqliteTable(
8228
+ var chatScanState = sqliteTable(
8234
8229
  "session_scan_state",
8235
8230
  {
8236
8231
  id: integer("id").primaryKey({ autoIncrement: true }),
8237
8232
  agent: text("agent").notNull(),
8238
- sessionId: text("session_id").notNull(),
8233
+ sourceId: text("source_id").notNull(),
8239
8234
  sourcePath: text("source_path").notNull(),
8240
8235
  lastMtimeMs: integer("last_mtime_ms").notNull(),
8241
8236
  lastSizeBytes: integer("last_size_bytes").notNull(),
@@ -8243,12 +8238,12 @@ var sessionScanState = sqliteTable(
8243
8238
  mode: "timestamp_ms"
8244
8239
  }).notNull()
8245
8240
  },
8246
- (t) => [uniqueIndex("session_scan_state_idx").on(t.agent, t.sessionId)]
8241
+ (t) => [uniqueIndex("session_scan_state_idx").on(t.agent, t.sourceId)]
8247
8242
  );
8248
8243
  var ingestionEvents = sqliteTable("ingestion_events", {
8249
8244
  id: integer("id").primaryKey({ autoIncrement: true }),
8250
8245
  agent: text("agent").notNull(),
8251
- sessionId: text("session_id").notNull(),
8246
+ sourceId: text("source_id").notNull(),
8252
8247
  sourcePath: text("source_path").notNull(),
8253
8248
  eventType: text("event_type").notNull(),
8254
8249
  detail: text("detail", { mode: "json" }).notNull(),
@@ -8256,7 +8251,7 @@ var ingestionEvents = sqliteTable("ingestion_events", {
8256
8251
  });
8257
8252
 
8258
8253
  // src/visibility.ts
8259
- function loadSessionVisibility(metadata2, opts) {
8254
+ function loadChatVisibility(metadata2, opts) {
8260
8255
  const trashed = new Set(metadata2.listDeletedIds());
8261
8256
  const showTrashed = opts.includeTrashed === true;
8262
8257
  return {
@@ -8279,59 +8274,73 @@ function toApiBlock(block) {
8279
8274
  var CLAUDE_CODE_AGENT = "claude-code";
8280
8275
  function createApp({ archive: archive2, metadata: metadata2, webDistDir: webDistDir2 }) {
8281
8276
  const app2 = new Hono2();
8282
- function findArchiveSessionBySourceId(sourceSessionId) {
8283
- return archive2.db.select().from(sessions).where(eq(sessions.sourceSessionId, sourceSessionId)).get();
8277
+ function findArchiveChatBySourceId(sourceId) {
8278
+ return archive2.db.select().from(chats).where(eq(chats.sourceId, sourceId)).get();
8284
8279
  }
8285
- app2.get("/api/sessions", (c) => {
8286
- const visibility = loadSessionVisibility(metadata2, {
8280
+ app2.get("/api/chats", (c) => {
8281
+ const visibility = loadChatVisibility(metadata2, {
8287
8282
  includeTrashed: c.req.query("includeTrashed") === "true"
8288
8283
  });
8289
- const rows = archive2.db.select().from(sessions).all();
8290
- const sessions2 = [];
8284
+ const rows = archive2.db.select().from(chats).all();
8285
+ const chats2 = [];
8291
8286
  for (const row of rows) {
8292
8287
  if (!visibility.isVisible(row.id)) continue;
8293
8288
  const isDeleted = visibility.isTrashed(row.id);
8294
- const lastMessage = archive2.db.select({ ts: messages.ts }).from(messages).where(
8289
+ const latestRaw = archive2.db.select({ sourcePath: rawMessages.sourcePath }).from(rawMessages).where(
8290
+ and(
8291
+ eq(rawMessages.agent, CLAUDE_CODE_AGENT),
8292
+ eq(rawMessages.sourceId, row.sourceId)
8293
+ )
8294
+ ).orderBy(desc(rawMessages.ingestedAt)).limit(1).get();
8295
+ const tsRange = archive2.db.select({
8296
+ minTs: sql`min(${messages.ts})`,
8297
+ maxTs: sql`max(${messages.ts})`
8298
+ }).from(messages).where(
8295
8299
  and(
8296
8300
  eq(messages.agent, CLAUDE_CODE_AGENT),
8297
- eq(messages.sessionId, row.sourceSessionId)
8301
+ eq(messages.sourceId, row.sourceId)
8298
8302
  )
8299
- ).orderBy(desc(messages.ts)).limit(1).get();
8300
- const session = {
8301
- id: row.sourceSessionId,
8303
+ ).get();
8304
+ const firstSeenAtMs = row.firstSeenAt.getTime();
8305
+ const chat = {
8306
+ id: row.sourceId,
8307
+ chatId: row.chatId,
8308
+ agent: row.agent,
8302
8309
  title: deriveTitle(archive2, metadata2, row),
8303
8310
  project: row.project ?? "",
8304
- createdAt: row.firstSeenAt.getTime(),
8305
- updatedAt: lastMessage ? lastMessage.ts.getTime() : row.firstSeenAt.getTime()
8311
+ projectPath: row.projectPath ?? null,
8312
+ sourceFilePath: latestRaw?.sourcePath ?? null,
8313
+ createdAt: tsRange?.minTs ?? firstSeenAtMs,
8314
+ updatedAt: tsRange?.maxTs ?? firstSeenAtMs
8306
8315
  };
8307
- if (isDeleted) session.isDeleted = true;
8308
- sessions2.push(session);
8316
+ if (isDeleted) chat.isDeleted = true;
8317
+ chats2.push(chat);
8309
8318
  }
8310
- return c.json({ sessions: sessions2 });
8319
+ return c.json({ chats: chats2 });
8311
8320
  });
8312
- app2.delete("/api/sessions/:id", (c) => {
8313
- const sessionId = c.req.param("id");
8314
- const row = findArchiveSessionBySourceId(sessionId);
8321
+ app2.delete("/api/chats/:id", (c) => {
8322
+ const id = c.req.param("id");
8323
+ const row = findArchiveChatBySourceId(id);
8315
8324
  if (!row) {
8316
- return c.json({ error: "Session not found" }, 404);
8325
+ return c.json({ error: "Chat not found" }, 404);
8317
8326
  }
8318
8327
  metadata2.softDelete(row.id);
8319
8328
  return c.body(null, 204);
8320
8329
  });
8321
- app2.post("/api/sessions/:id/restore", (c) => {
8322
- const sessionId = c.req.param("id");
8323
- const row = findArchiveSessionBySourceId(sessionId);
8330
+ app2.post("/api/chats/:id/restore", (c) => {
8331
+ const id = c.req.param("id");
8332
+ const row = findArchiveChatBySourceId(id);
8324
8333
  if (!row) {
8325
- return c.json({ error: "Session not found" }, 404);
8334
+ return c.json({ error: "Chat not found" }, 404);
8326
8335
  }
8327
8336
  metadata2.restore(row.id);
8328
8337
  return c.body(null, 204);
8329
8338
  });
8330
- app2.patch("/api/sessions/:id/title", async (c) => {
8331
- const sessionId = c.req.param("id");
8332
- const row = findArchiveSessionBySourceId(sessionId);
8339
+ app2.patch("/api/chats/:id/title", async (c) => {
8340
+ const id = c.req.param("id");
8341
+ const row = findArchiveChatBySourceId(id);
8333
8342
  if (!row) {
8334
- return c.json({ error: "Session not found" }, 404);
8343
+ return c.json({ error: "Chat not found" }, 404);
8335
8344
  }
8336
8345
  let body;
8337
8346
  try {
@@ -8350,22 +8359,22 @@ function createApp({ archive: archive2, metadata: metadata2, webDistDir: webDist
8350
8359
  metadata2.setCustomTitle(row.id, trimmed.length > 0 ? trimmed : null);
8351
8360
  return c.body(null, 204);
8352
8361
  });
8353
- app2.get("/api/sessions/:id", (c) => {
8354
- const sessionId = c.req.param("id");
8355
- const row = findArchiveSessionBySourceId(sessionId);
8362
+ app2.get("/api/chats/:id", (c) => {
8363
+ const id = c.req.param("id");
8364
+ const row = findArchiveChatBySourceId(id);
8356
8365
  if (!row) {
8357
- return c.json({ error: "Session not found" }, 404);
8366
+ return c.json({ error: "Chat not found" }, 404);
8358
8367
  }
8359
- const visibility = loadSessionVisibility(metadata2, {
8368
+ const visibility = loadChatVisibility(metadata2, {
8360
8369
  includeTrashed: c.req.query("includeTrashed") === "true"
8361
8370
  });
8362
8371
  if (!visibility.isVisible(row.id)) {
8363
- return c.json({ error: "Session not found" }, 404);
8372
+ return c.json({ error: "Chat not found" }, 404);
8364
8373
  }
8365
8374
  const rows = archive2.db.select().from(messages).where(
8366
8375
  and(
8367
8376
  eq(messages.agent, CLAUDE_CODE_AGENT),
8368
- eq(messages.sessionId, sessionId)
8377
+ eq(messages.sourceId, id)
8369
8378
  )
8370
8379
  ).orderBy(asc(messages.ts)).all();
8371
8380
  const messages2 = rows.map((m) => ({
@@ -8381,7 +8390,7 @@ function createApp({ archive: archive2, metadata: metadata2, webDistDir: webDist
8381
8390
  const firstUser = archive3.db.select({ text: messages.text }).from(messages).where(
8382
8391
  and(
8383
8392
  eq(messages.agent, CLAUDE_CODE_AGENT),
8384
- eq(messages.sessionId, row.sourceSessionId),
8393
+ eq(messages.sourceId, row.sourceId),
8385
8394
  eq(messages.role, "user")
8386
8395
  )
8387
8396
  ).orderBy(asc(messages.ts)).limit(1).get();
@@ -8601,18 +8610,18 @@ function migrate(db, config) {
8601
8610
  db.dialect.migrate(migrations, db.session, config);
8602
8611
  }
8603
8612
 
8604
- // src/archive/short-code.ts
8613
+ // src/archive/chat-id.ts
8605
8614
  import crypto4 from "crypto";
8606
8615
  var CROCKFORD_ALPHABET = "0123456789abcdefghjkmnpqrstvwxyz";
8607
- var SHORT_CODE_LENGTH = 6;
8616
+ var CHAT_ID_LENGTH = 6;
8608
8617
  var MAX_RETRIES = 5;
8609
- function generateShortCode({
8618
+ function generateChatId({
8610
8619
  isTaken,
8611
8620
  randomIndex = () => crypto4.randomInt(CROCKFORD_ALPHABET.length)
8612
8621
  }) {
8613
8622
  for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
8614
8623
  let code = "";
8615
- for (let i = 0; i < SHORT_CODE_LENGTH; i++) {
8624
+ for (let i = 0; i < CHAT_ID_LENGTH; i++) {
8616
8625
  code += CROCKFORD_ALPHABET[randomIndex()];
8617
8626
  }
8618
8627
  if (!isTaken(code)) {
@@ -8620,7 +8629,7 @@ function generateShortCode({
8620
8629
  }
8621
8630
  }
8622
8631
  throw new Error(
8623
- `Failed to generate unique short_code after ${MAX_RETRIES + 1} attempts`
8632
+ `Failed to generate unique chat_id after ${MAX_RETRIES + 1} attempts`
8624
8633
  );
8625
8634
  }
8626
8635
 
@@ -8668,9 +8677,9 @@ function createArchiveRepository({
8668
8677
  getAppliedMigrations() {
8669
8678
  return db.select().from(schemaVersion).all().map((r) => ({ version: r.version, appliedAt: r.appliedAt }));
8670
8679
  },
8671
- generateShortCode() {
8672
- return generateShortCode({
8673
- isTaken: (candidate) => db.select({ id: sessions.id }).from(sessions).where(eq(sessions.shortCode, candidate)).get() !== void 0
8680
+ generateChatId() {
8681
+ return generateChatId({
8682
+ isTaken: (candidate) => db.select({ id: chats.id }).from(chats).where(eq(chats.chatId, candidate)).get() !== void 0
8674
8683
  });
8675
8684
  },
8676
8685
  close() {
@@ -8686,7 +8695,7 @@ import { fileURLToPath as fileURLToPath2 } from "url";
8686
8695
  import Database2 from "better-sqlite3";
8687
8696
 
8688
8697
  // src/metadata/schema.ts
8689
- var sessionsMeta = sqliteTable("sessions_meta", {
8698
+ var chatsMeta = sqliteTable("chats_meta", {
8690
8699
  id: text("id").primaryKey(),
8691
8700
  isDeleted: integer("is_deleted", { mode: "boolean" }).notNull().default(false),
8692
8701
  customTitle: text("custom_title"),
@@ -8711,71 +8720,71 @@ var CLAUDE_CODE_AGENT2 = "claude-code";
8711
8720
  function createMetadataRepository({
8712
8721
  dataDir: dataDir2,
8713
8722
  lookupInternalId,
8714
- ensureSession: ensureSession2
8723
+ ensureChat: ensureChat2
8715
8724
  }) {
8716
8725
  fs3.mkdirSync(dataDir2, { recursive: true });
8717
8726
  const sqlite = new Database2(path2.join(dataDir2, "data.db"));
8718
8727
  const db = drizzle(sqlite);
8719
8728
  migrate(db, { migrationsFolder: resolveMigrationsFolder2() });
8720
8729
  if (lookupInternalId) {
8721
- rekeyLegacyRows(sqlite, db, lookupInternalId, ensureSession2);
8730
+ rekeyLegacyRows(sqlite, db, lookupInternalId, ensureChat2);
8722
8731
  }
8723
8732
  return {
8724
8733
  softDelete(internalId) {
8725
8734
  const now = /* @__PURE__ */ new Date();
8726
- db.insert(sessionsMeta).values({
8735
+ db.insert(chatsMeta).values({
8727
8736
  id: internalId,
8728
8737
  isDeleted: true,
8729
8738
  createdAt: now,
8730
8739
  updatedAt: now
8731
8740
  }).onConflictDoUpdate({
8732
- target: sessionsMeta.id,
8741
+ target: chatsMeta.id,
8733
8742
  set: { isDeleted: true, updatedAt: now }
8734
8743
  }).run();
8735
8744
  },
8736
8745
  restore(internalId) {
8737
8746
  const now = /* @__PURE__ */ new Date();
8738
- db.update(sessionsMeta).set({ isDeleted: false, updatedAt: now }).where(eq(sessionsMeta.id, internalId)).run();
8747
+ db.update(chatsMeta).set({ isDeleted: false, updatedAt: now }).where(eq(chatsMeta.id, internalId)).run();
8739
8748
  },
8740
8749
  isDeleted(internalId) {
8741
- const row = db.select({ isDeleted: sessionsMeta.isDeleted }).from(sessionsMeta).where(eq(sessionsMeta.id, internalId)).get();
8750
+ const row = db.select({ isDeleted: chatsMeta.isDeleted }).from(chatsMeta).where(eq(chatsMeta.id, internalId)).get();
8742
8751
  return row?.isDeleted ?? false;
8743
8752
  },
8744
8753
  listDeletedIds() {
8745
- const rows = db.select({ id: sessionsMeta.id }).from(sessionsMeta).where(eq(sessionsMeta.isDeleted, true)).all();
8754
+ const rows = db.select({ id: chatsMeta.id }).from(chatsMeta).where(eq(chatsMeta.isDeleted, true)).all();
8746
8755
  return rows.map((r) => r.id);
8747
8756
  },
8748
8757
  getCustomTitle(internalId) {
8749
- const row = db.select({ customTitle: sessionsMeta.customTitle }).from(sessionsMeta).where(eq(sessionsMeta.id, internalId)).get();
8758
+ const row = db.select({ customTitle: chatsMeta.customTitle }).from(chatsMeta).where(eq(chatsMeta.id, internalId)).get();
8750
8759
  return row?.customTitle ?? null;
8751
8760
  },
8752
8761
  setCustomTitle(internalId, title) {
8753
8762
  const now = /* @__PURE__ */ new Date();
8754
- db.insert(sessionsMeta).values({
8763
+ db.insert(chatsMeta).values({
8755
8764
  id: internalId,
8756
8765
  customTitle: title,
8757
8766
  createdAt: now,
8758
8767
  updatedAt: now
8759
8768
  }).onConflictDoUpdate({
8760
- target: sessionsMeta.id,
8769
+ target: chatsMeta.id,
8761
8770
  set: { customTitle: title, updatedAt: now }
8762
8771
  }).run();
8763
8772
  }
8764
8773
  };
8765
8774
  }
8766
8775
  var REKEY_USER_VERSION = 4;
8767
- function rekeyLegacyRows(sqlite, db, lookupInternalId, ensureSession2) {
8776
+ function rekeyLegacyRows(sqlite, db, lookupInternalId, ensureChat2) {
8768
8777
  const current = sqlite.pragma("user_version", { simple: true });
8769
8778
  if (current >= REKEY_USER_VERSION) return;
8770
- const rows = db.select({ id: sessionsMeta.id }).from(sessionsMeta).all();
8779
+ const rows = db.select({ id: chatsMeta.id }).from(chatsMeta).all();
8771
8780
  for (const row of rows) {
8772
8781
  let target = lookupInternalId(CLAUDE_CODE_AGENT2, row.id);
8773
8782
  if (target === null) {
8774
- if (!ensureSession2) continue;
8775
- target = ensureSession2(CLAUDE_CODE_AGENT2, row.id);
8783
+ if (!ensureChat2) continue;
8784
+ target = ensureChat2(CLAUDE_CODE_AGENT2, row.id);
8776
8785
  }
8777
8786
  if (target === row.id) continue;
8778
- db.update(sessionsMeta).set({ id: target, updatedAt: /* @__PURE__ */ new Date() }).where(eq(sessionsMeta.id, row.id)).run();
8787
+ db.update(chatsMeta).set({ id: target, updatedAt: /* @__PURE__ */ new Date() }).where(eq(chatsMeta.id, row.id)).run();
8779
8788
  }
8780
8789
  sqlite.pragma(`user_version = ${REKEY_USER_VERSION}`);
8781
8790
  }
@@ -8795,13 +8804,20 @@ async function runIngestion(opts) {
8795
8804
  for await (const ref of plugin.discover(opts.env)) {
8796
8805
  result.scanned += 1;
8797
8806
  const stat4 = safeStat(ref.sourcePath);
8798
- const prior = opts.archive.db.select().from(sessionScanState).where(
8807
+ const prior = opts.archive.db.select().from(chatScanState).where(
8799
8808
  and(
8800
- eq(sessionScanState.agent, plugin.id),
8801
- eq(sessionScanState.sessionId, ref.sessionId)
8809
+ eq(chatScanState.agent, plugin.id),
8810
+ eq(chatScanState.sourceId, ref.sourceId)
8802
8811
  )
8803
8812
  ).get();
8804
- ensureSession(opts.archive, plugin.id, ref.sessionId, now(), ref.project);
8813
+ ensureChat(
8814
+ opts.archive,
8815
+ plugin.id,
8816
+ ref.sourceId,
8817
+ now(),
8818
+ ref.project,
8819
+ ref.projectPath
8820
+ );
8805
8821
  if (stat4 && prior && prior.lastMtimeMs === stat4.mtimeMs && prior.lastSizeBytes === stat4.size) {
8806
8822
  result.skippedByMtime += 1;
8807
8823
  continue;
@@ -8812,7 +8828,7 @@ async function runIngestion(opts) {
8812
8828
  const existing = opts.archive.db.select({ id: rawMessages.id }).from(rawMessages).where(
8813
8829
  and(
8814
8830
  eq(rawMessages.agent, plugin.id),
8815
- eq(rawMessages.sessionId, ref.sessionId),
8831
+ eq(rawMessages.sourceId, ref.sourceId),
8816
8832
  eq(rawMessages.payloadHash, payloadHash)
8817
8833
  )
8818
8834
  ).get();
@@ -8822,7 +8838,7 @@ async function runIngestion(opts) {
8822
8838
  } else {
8823
8839
  const inserted = opts.archive.db.insert(rawMessages).values({
8824
8840
  agent: plugin.id,
8825
- sessionId: ref.sessionId,
8841
+ sourceId: ref.sourceId,
8826
8842
  sourcePath: raw2.sourcePath,
8827
8843
  sourceLocator: raw2.sourceLocator,
8828
8844
  rawPayload: payloadJson,
@@ -8837,7 +8853,7 @@ async function runIngestion(opts) {
8837
8853
  const upserted = upsertCanonical(
8838
8854
  opts.archive,
8839
8855
  plugin.id,
8840
- ref.sessionId,
8856
+ ref.sourceId,
8841
8857
  canonical,
8842
8858
  rawId
8843
8859
  );
@@ -8847,7 +8863,7 @@ async function runIngestion(opts) {
8847
8863
  recordScanState(
8848
8864
  opts.archive,
8849
8865
  plugin.id,
8850
- ref.sessionId,
8866
+ ref.sourceId,
8851
8867
  ref.sourcePath,
8852
8868
  stat4,
8853
8869
  now()
@@ -8864,24 +8880,21 @@ function safeStat(p) {
8864
8880
  return null;
8865
8881
  }
8866
8882
  }
8867
- function recordScanState(archive2, agent, sessionId, sourcePath, stat4, scannedAt) {
8868
- const existing = archive2.db.select({ id: sessionScanState.id }).from(sessionScanState).where(
8869
- and(
8870
- eq(sessionScanState.agent, agent),
8871
- eq(sessionScanState.sessionId, sessionId)
8872
- )
8883
+ function recordScanState(archive2, agent, sourceId, sourcePath, stat4, scannedAt) {
8884
+ const existing = archive2.db.select({ id: chatScanState.id }).from(chatScanState).where(
8885
+ and(eq(chatScanState.agent, agent), eq(chatScanState.sourceId, sourceId))
8873
8886
  ).get();
8874
8887
  if (existing) {
8875
- archive2.db.update(sessionScanState).set({
8888
+ archive2.db.update(chatScanState).set({
8876
8889
  sourcePath,
8877
8890
  lastMtimeMs: stat4.mtimeMs,
8878
8891
  lastSizeBytes: stat4.size,
8879
8892
  lastScannedAt: scannedAt
8880
- }).where(eq(sessionScanState.id, existing.id)).run();
8893
+ }).where(eq(chatScanState.id, existing.id)).run();
8881
8894
  } else {
8882
- archive2.db.insert(sessionScanState).values({
8895
+ archive2.db.insert(chatScanState).values({
8883
8896
  agent,
8884
- sessionId,
8897
+ sourceId,
8885
8898
  sourcePath,
8886
8899
  lastMtimeMs: stat4.mtimeMs,
8887
8900
  lastSizeBytes: stat4.size,
@@ -8889,36 +8902,37 @@ function recordScanState(archive2, agent, sessionId, sourcePath, stat4, scannedA
8889
8902
  }).run();
8890
8903
  }
8891
8904
  }
8892
- function ensureSession(archive2, agent, sourceSessionId, firstSeenAt, project) {
8893
- const existing = archive2.db.select().from(sessions).where(
8894
- and(
8895
- eq(sessions.agent, agent),
8896
- eq(sessions.sourceSessionId, sourceSessionId)
8897
- )
8898
- ).get();
8905
+ function ensureChat(archive2, agent, sourceId, firstSeenAt, project, projectPath) {
8906
+ const existing = archive2.db.select().from(chats).where(and(eq(chats.agent, agent), eq(chats.sourceId, sourceId))).get();
8899
8907
  if (existing) {
8900
- if (project && existing.project !== project) {
8901
- archive2.db.update(sessions).set({ project }).where(eq(sessions.id, existing.id)).run();
8908
+ const updates = {};
8909
+ if (project && existing.project !== project) updates.project = project;
8910
+ if (projectPath && existing.projectPath !== projectPath) {
8911
+ updates.projectPath = projectPath;
8912
+ }
8913
+ if (Object.keys(updates).length > 0) {
8914
+ archive2.db.update(chats).set(updates).where(eq(chats.id, existing.id)).run();
8902
8915
  }
8903
8916
  return existing.id;
8904
8917
  }
8905
8918
  const id = crypto6.randomUUID();
8906
- const shortCode = archive2.generateShortCode();
8907
- archive2.db.insert(sessions).values({
8919
+ const chatId = archive2.generateChatId();
8920
+ archive2.db.insert(chats).values({
8908
8921
  id,
8909
- shortCode,
8922
+ chatId,
8910
8923
  agent,
8911
- sourceSessionId,
8924
+ sourceId,
8912
8925
  firstSeenAt,
8913
- project: project ?? null
8926
+ project: project ?? null,
8927
+ projectPath: projectPath ?? null
8914
8928
  }).run();
8915
8929
  return id;
8916
8930
  }
8917
- function upsertCanonical(archive2, agent, sessionId, msg, rawId) {
8931
+ function upsertCanonical(archive2, agent, sourceId, msg, rawId) {
8918
8932
  const existing = archive2.db.select().from(messages).where(
8919
8933
  and(
8920
8934
  eq(messages.agent, agent),
8921
- eq(messages.sessionId, sessionId),
8935
+ eq(messages.sourceId, sourceId),
8922
8936
  eq(messages.messageId, msg.messageId)
8923
8937
  )
8924
8938
  ).get();
@@ -8926,7 +8940,7 @@ function upsertCanonical(archive2, agent, sessionId, msg, rawId) {
8926
8940
  if (!existing) {
8927
8941
  archive2.db.insert(messages).values({
8928
8942
  agent,
8929
- sessionId,
8943
+ sourceId,
8930
8944
  messageId: msg.messageId,
8931
8945
  role: msg.role,
8932
8946
  ts,
@@ -10741,7 +10755,7 @@ function startWatcher(opts) {
10741
10755
  try {
10742
10756
  opts.archive.db.insert(ingestionEvents).values({
10743
10757
  agent: binding.plugin.id,
10744
- sessionId: binding.ref.sessionId,
10758
+ sourceId: binding.ref.sourceId,
10745
10759
  sourcePath: removedPath,
10746
10760
  eventType: "unlink_observed",
10747
10761
  detail: { path: removedPath },
@@ -10794,13 +10808,14 @@ var ClaudeCodePlugin = class {
10794
10808
  for (const file of files) {
10795
10809
  if (!file.isFile() || !file.name.endsWith(".jsonl")) continue;
10796
10810
  const sourcePath = path3.join(projectPath, file.name);
10797
- const sessionId = file.name.replace(/\.jsonl$/, "");
10811
+ const sourceId = file.name.replace(/\.jsonl$/, "");
10798
10812
  const cwd = await readCwdFromJsonl(sourcePath);
10799
10813
  yield {
10800
- sessionId,
10814
+ sourceId,
10801
10815
  sourcePath,
10802
10816
  watchPaths: [sourcePath],
10803
- project: cwd ? path3.basename(cwd) : void 0
10817
+ project: cwd ? path3.basename(cwd) : void 0,
10818
+ projectPath: cwd ?? void 0
10804
10819
  };
10805
10820
  }
10806
10821
  }
@@ -10814,7 +10829,7 @@ var ClaudeCodePlugin = class {
10814
10829
  lineNo += 1;
10815
10830
  if (!line) continue;
10816
10831
  yield {
10817
- sessionId: ref.sessionId,
10832
+ sourceId: ref.sourceId,
10818
10833
  sourcePath: ref.sourcePath,
10819
10834
  sourceLocator: `L${lineNo}`,
10820
10835
  payload: JSON.parse(line)
@@ -10917,6 +10932,7 @@ function parseCliArgs(argv, env = {}) {
10917
10932
  if (argv.includes("--help") || argv.includes("-h")) {
10918
10933
  return { kind: "help" };
10919
10934
  }
10935
+ const open2 = !env.CHAT_LOGBOOK_NO_OPEN;
10920
10936
  const portIdx = argv.findIndex((a) => a === "--port" || a === "-p");
10921
10937
  if (portIdx !== -1) {
10922
10938
  const raw2 = argv[portIdx + 1];
@@ -10939,12 +10955,12 @@ function parseCliArgs(argv, env = {}) {
10939
10955
  message: `Invalid port "${raw2}": must be between 1 and 65535`
10940
10956
  };
10941
10957
  }
10942
- return { kind: "run", port: port2 };
10958
+ return { kind: "run", port: port2, open: open2 };
10943
10959
  }
10944
10960
  if (env.PORT) {
10945
- return { kind: "run", port: Number(env.PORT) };
10961
+ return { kind: "run", port: Number(env.PORT), open: open2 };
10946
10962
  }
10947
- return { kind: "run", port: DEFAULT_PORT };
10963
+ return { kind: "run", port: DEFAULT_PORT, open: open2 };
10948
10964
  }
10949
10965
 
10950
10966
  // src/cli/help.ts
@@ -11011,7 +11027,7 @@ function openBrowser(url) {
11011
11027
  var server = serve({ fetch: app.fetch, port }, (info) => {
11012
11028
  const url = `http://localhost:${info.port}`;
11013
11029
  console.log(`chat-logbook is running at \x1B[36m${url}\x1B[0m`);
11014
- openBrowser(url);
11030
+ if (action.open) openBrowser(url);
11015
11031
  });
11016
11032
  server.on("error", (err) => {
11017
11033
  if (err.code === "EADDRINUSE") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chat-logbook",
3
- "version": "0.7.1",
3
+ "version": "0.9.0",
4
4
  "description": "A local-first conversation manager for Claude Code",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",