@bulolo/hermes-link 0.3.0 → 0.3.2

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.
@@ -780,7 +780,7 @@ async function dismissUpdate(paths) {
780
780
 
781
781
  // src/security/credentials.ts
782
782
  import { randomBytes, randomUUID as randomUUID2, timingSafeEqual, createHash } from "crypto";
783
- var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
783
+ var ACCESS_TOKEN_TTL_MS = 2 * 60 * 60 * 1e3;
784
784
  var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
785
785
  var DEVICE_SEEN_WRITE_INTERVAL_MS = 60 * 60 * 1e3;
786
786
  function credentialsPath(paths) {
@@ -841,8 +841,8 @@ function formatDeviceListItem(device) {
841
841
  };
842
842
  }
843
843
  async function rotateDeviceSession(store, device, now, paths) {
844
- const accessToken = randomToken("hpat_");
845
- const refreshToken = randomToken("hprt_");
844
+ const accessToken = randomToken("hlat_");
845
+ const refreshToken = randomToken("hlrt_");
846
846
  device.access_token_hash = sha256(accessToken);
847
847
  device.access_expires_at = new Date(now.getTime() + ACCESS_TOKEN_TTL_MS).toISOString();
848
848
  device.refresh_token_hash = sha256(refreshToken);
@@ -1106,7 +1106,7 @@ async function authenticateRequest(ctx, paths = resolveRuntimePaths()) {
1106
1106
  if (device) {
1107
1107
  return { kind: "device", device };
1108
1108
  }
1109
- if (token.startsWith("hpat_")) {
1109
+ if (token.startsWith("hlat_")) {
1110
1110
  throw new LinkHttpError(401, "device_access_token_invalid", "Device access token is invalid or expired");
1111
1111
  }
1112
1112
  const localToken = await consumeAppConnectToken(token, paths);
@@ -2046,6 +2046,43 @@ function createConversationsRouter(options) {
2046
2046
  });
2047
2047
  ctx.body = { ok: true, conversations: result.conversations, page: result.page };
2048
2048
  });
2049
+ router.get("/api/v1/conversations/archived", async (ctx) => {
2050
+ await authenticateRequest(ctx, paths);
2051
+ ctx.set("cache-control", "no-store");
2052
+ const result = await conversations.listArchivedConversationPage({
2053
+ limit: readLimit(ctx.query.limit),
2054
+ cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after)
2055
+ });
2056
+ ctx.body = { ok: true, conversations: result.conversations, page: result.page };
2057
+ });
2058
+ router.get("/api/v1/conversations/archived/search", async (ctx) => {
2059
+ await authenticateRequest(ctx, paths);
2060
+ ctx.set("cache-control", "no-store");
2061
+ const result = await conversations.searchArchivedConversationPage({
2062
+ limit: readLimit(ctx.query.limit),
2063
+ cursor: readQueryString(ctx.query.cursor) ?? readQueryString(ctx.query.after),
2064
+ query: readQueryString(ctx.query.query) ?? readQueryString(ctx.query.q) ?? readQueryString(ctx.query.keyword) ?? ""
2065
+ });
2066
+ ctx.body = { ok: true, conversations: result.conversations, page: result.page };
2067
+ });
2068
+ router.post("/api/v1/conversations/archive-plans", async (ctx) => {
2069
+ await authenticateRequest(ctx, paths);
2070
+ const body = await readJsonBody3(ctx.req);
2071
+ const excludeIds = readStringArray2(body, "exclude_conversation_ids", "excludeConversationIds") ?? [];
2072
+ const plan = await conversations.prepareArchiveAllConversationPlan({ excludeConversationIds: excludeIds });
2073
+ ctx.status = 201;
2074
+ ctx.body = { ok: true, plan };
2075
+ });
2076
+ router.get("/api/v1/conversations/archive-plans/:planId", async (ctx) => {
2077
+ await authenticateRequest(ctx, paths);
2078
+ ctx.set("cache-control", "no-store");
2079
+ ctx.body = { ok: true, plan: await conversations.readArchiveAllConversationPlan(ctx.params.planId) };
2080
+ });
2081
+ router.post("/api/v1/conversations/archive-plans/:planId/execute", async (ctx) => {
2082
+ await authenticateRequest(ctx, paths);
2083
+ const plan = await conversations.executeArchiveAllConversationPlan(ctx.params.planId);
2084
+ ctx.body = { ok: true, plan };
2085
+ });
2049
2086
  router.post("/api/v1/conversations/clear-plans", async (ctx) => {
2050
2087
  await authenticateRequest(ctx, paths);
2051
2088
  const plan = await conversations.prepareClearAllConversationPlan();
@@ -2173,6 +2210,14 @@ function createConversationsRouter(options) {
2173
2210
  if (!title) throw new LinkHttpError(400, "title_required", "title is required");
2174
2211
  ctx.body = { ok: true, ...await conversations.renameConversation(ctx.params.conversationId, title) };
2175
2212
  });
2213
+ router.post("/api/v1/conversations/:conversationId/archive", async (ctx) => {
2214
+ await authenticateRequest(ctx, paths);
2215
+ ctx.body = { ok: true, ...await conversations.archiveConversation(ctx.params.conversationId) };
2216
+ });
2217
+ router.post("/api/v1/conversations/:conversationId/unarchive", async (ctx) => {
2218
+ await authenticateRequest(ctx, paths);
2219
+ ctx.body = { ok: true, ...await conversations.unarchiveConversation(ctx.params.conversationId) };
2220
+ });
2176
2221
  router.post("/api/v1/conversations/:conversationId/ack", async (ctx) => {
2177
2222
  await authenticateRequest(ctx, paths);
2178
2223
  ctx.body = { ok: true };
@@ -7602,6 +7647,13 @@ async function readManifest(paths, conversationId) {
7602
7647
  return readJson(manifestPath(paths, conversationId), null);
7603
7648
  }
7604
7649
  async function readActiveManifest(paths, conversationId) {
7650
+ const manifest = await readManifest(paths, conversationId);
7651
+ if (!manifest || manifest.status === "deleted_soft" || manifest.status === "archived") {
7652
+ throw new LinkHttpError(404, "conversation_not_found", "Conversation was not found");
7653
+ }
7654
+ return manifest;
7655
+ }
7656
+ async function readExistingManifest(paths, conversationId) {
7605
7657
  const manifest = await readManifest(paths, conversationId);
7606
7658
  if (!manifest || manifest.status === "deleted_soft") {
7607
7659
  throw new LinkHttpError(404, "conversation_not_found", "Conversation was not found");
@@ -7884,6 +7936,8 @@ function normalizeLimit(value, defaultValue, max) {
7884
7936
  if (!Number.isFinite(n) || n < 1) return defaultValue;
7885
7937
  return Math.min(n, max);
7886
7938
  }
7939
+ var clearPlans = /* @__PURE__ */ new Map();
7940
+ var archivePlans = /* @__PURE__ */ new Map();
7887
7941
  var ConversationService = class extends EventEmitter4 {
7888
7942
  paths;
7889
7943
  logger;
@@ -7952,13 +8006,82 @@ var ConversationService = class extends EventEmitter4 {
7952
8006
  if (after === void 0 || after === null) return events;
7953
8007
  return events.filter((e) => e.seq > after);
7954
8008
  }
8009
+ summarizeManifest(manifest, snapshot) {
8010
+ const stats = manifest.stats;
8011
+ const profileName = manifest.profile ?? manifest.profile_name_snapshot ?? "default";
8012
+ let lastMessage = null;
8013
+ if (snapshot && snapshot.messages.length > 0) {
8014
+ const last = snapshot.messages[snapshot.messages.length - 1];
8015
+ if (last) {
8016
+ const text = last.parts.find((p) => p.type === "text")?.text ?? "";
8017
+ lastMessage = { id: last.id, role: last.role, content_preview: text.slice(0, 200) };
8018
+ }
8019
+ }
8020
+ return {
8021
+ id: manifest.id,
8022
+ title: manifest.title,
8023
+ created_at: manifest.created_at,
8024
+ updated_at: manifest.updated_at,
8025
+ last_event_seq: manifest.last_event_seq,
8026
+ usage: {
8027
+ input_tokens: stats?.input_tokens ?? 0,
8028
+ output_tokens: stats?.output_tokens ?? 0,
8029
+ total_tokens: stats?.total_tokens ?? 0,
8030
+ ...stats?.updated_at ? { updated_at: stats.updated_at } : {}
8031
+ },
8032
+ profile: {
8033
+ uid: manifest.profile_uid ?? void 0,
8034
+ name: profileName,
8035
+ display_name: profileName,
8036
+ avatar_url: null
8037
+ },
8038
+ last_message: lastMessage
8039
+ };
8040
+ }
8041
+ buildRuntimeMetadata(manifest) {
8042
+ const stats = manifest.stats;
8043
+ const profileName = manifest.profile ?? manifest.profile_name_snapshot ?? "default";
8044
+ const usagePercent = stats?.context_window && stats.total_tokens ? Math.min(100, Math.round(stats.total_tokens / stats.context_window * 100)) : void 0;
8045
+ return {
8046
+ profile: {
8047
+ uid: manifest.profile_uid ?? void 0,
8048
+ name: profileName,
8049
+ display_name: profileName,
8050
+ avatar_url: null
8051
+ },
8052
+ model: {
8053
+ id: stats?.model ?? "unknown",
8054
+ ...stats?.provider ? { provider: stats.provider } : {},
8055
+ ...stats?.context_window ? { context_window: stats.context_window } : {}
8056
+ },
8057
+ context: {
8058
+ input_tokens: stats?.input_tokens ?? 0,
8059
+ output_tokens: stats?.output_tokens ?? 0,
8060
+ total_tokens: stats?.total_tokens ?? 0,
8061
+ ...stats?.context_window ? { context_window: stats.context_window } : {},
8062
+ ...usagePercent !== void 0 ? { usage_percent: usagePercent } : {},
8063
+ source: stats?.model ? "estimated" : "unknown",
8064
+ ...stats?.updated_at ? { updated_at: stats.updated_at } : {}
8065
+ }
8066
+ };
8067
+ }
8068
+ async listConversations() {
8069
+ const ids = await listConversationIds(this.paths);
8070
+ const manifests = [];
8071
+ for (const id of ids) {
8072
+ const m = await readManifest(this.paths, id);
8073
+ if (m && m.status !== "deleted_soft") manifests.push(m);
8074
+ }
8075
+ manifests.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
8076
+ return manifests.map((m) => this.summarizeManifest(m));
8077
+ }
7955
8078
  async listConversationPage(options = {}) {
7956
8079
  const limit = normalizeLimit(options.limit, 20, 100);
7957
8080
  const ids = await listConversationIds(this.paths);
7958
8081
  const manifests = [];
7959
8082
  for (const id of ids) {
7960
8083
  const m = await readManifest(this.paths, id);
7961
- if (m && m.status !== "deleted_soft") manifests.push(m);
8084
+ if (m && m.status === "active") manifests.push(m);
7962
8085
  }
7963
8086
  manifests.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
7964
8087
  let startIndex = 0;
@@ -7985,7 +8108,7 @@ var ConversationService = class extends EventEmitter4 {
7985
8108
  const results = [];
7986
8109
  for (const id of ids) {
7987
8110
  const m = await readManifest(this.paths, id);
7988
- if (m && m.status !== "deleted_soft" && m.title.toLowerCase().includes(q)) {
8111
+ if (m && m.status === "active" && m.title.toLowerCase().includes(q)) {
7989
8112
  results.push(m);
7990
8113
  }
7991
8114
  }
@@ -7996,6 +8119,48 @@ var ConversationService = class extends EventEmitter4 {
7996
8119
  page: { limit, has_more: results.length > limit, next_cursor: null }
7997
8120
  };
7998
8121
  }
8122
+ async listArchivedConversationPage(options = {}) {
8123
+ const limit = normalizeLimit(options.limit, 20, 100);
8124
+ const ids = await listConversationIds(this.paths);
8125
+ const manifests = [];
8126
+ for (const id of ids) {
8127
+ const m = await readManifest(this.paths, id);
8128
+ if (m && m.status === "archived") manifests.push(m);
8129
+ }
8130
+ manifests.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
8131
+ let startIndex = 0;
8132
+ if (options.cursor) {
8133
+ const idx = manifests.findIndex((m) => m.id === options.cursor);
8134
+ if (idx >= 0) startIndex = idx + 1;
8135
+ }
8136
+ const page = manifests.slice(startIndex, startIndex + limit);
8137
+ const hasMore = startIndex + limit < manifests.length;
8138
+ return {
8139
+ conversations: page.map((m) => this.summarizeManifest(m)),
8140
+ page: {
8141
+ limit,
8142
+ has_more: hasMore,
8143
+ next_cursor: hasMore && page.length > 0 ? page[page.length - 1]?.id ?? null : null
8144
+ }
8145
+ };
8146
+ }
8147
+ async searchArchivedConversationPage(options = {}) {
8148
+ if (!options.query?.trim()) return this.listArchivedConversationPage(options);
8149
+ const q = options.query.trim().toLowerCase();
8150
+ const limit = normalizeLimit(options.limit, 20, 100);
8151
+ const ids = await listConversationIds(this.paths);
8152
+ const results = [];
8153
+ for (const id of ids) {
8154
+ const m = await readManifest(this.paths, id);
8155
+ if (m && m.status === "archived" && m.title.toLowerCase().includes(q)) results.push(m);
8156
+ }
8157
+ results.sort((a, b) => b.updated_at.localeCompare(a.updated_at));
8158
+ const page = results.slice(0, limit);
8159
+ return {
8160
+ conversations: page.map((m) => this.summarizeManifest(m)),
8161
+ page: { limit, has_more: results.length > limit, next_cursor: null }
8162
+ };
8163
+ }
7999
8164
  async createConversation(options = {}) {
8000
8165
  const now = (/* @__PURE__ */ new Date()).toISOString();
8001
8166
  const id = createConversationId();
@@ -8017,7 +8182,7 @@ var ConversationService = class extends EventEmitter4 {
8017
8182
  return this.summarizeManifest(manifest);
8018
8183
  }
8019
8184
  async getMessages(conversationId, options = {}) {
8020
- const manifest = await readActiveManifest(this.paths, conversationId);
8185
+ const manifest = await readExistingManifest(this.paths, conversationId);
8021
8186
  const snapshot = await readSnapshot(this.paths, conversationId);
8022
8187
  const limit = normalizeLimit(options.limit, 50, 200);
8023
8188
  const total = snapshot.messages.length;
@@ -8028,6 +8193,7 @@ var ConversationService = class extends EventEmitter4 {
8028
8193
  return {
8029
8194
  messages,
8030
8195
  last_event_seq: manifest.last_event_seq,
8196
+ runtime: this.buildRuntimeMetadata(manifest),
8031
8197
  page: {
8032
8198
  limit,
8033
8199
  has_more_before: startIndex > 0,
@@ -8043,7 +8209,16 @@ var ConversationService = class extends EventEmitter4 {
8043
8209
  async sendMessageLocked(input) {
8044
8210
  const content = input.content.trim();
8045
8211
  if (!content) throw new LinkHttpError(400, "message_content_required", "message content is required");
8046
- const manifest = await readActiveManifest(this.paths, input.conversationId);
8212
+ const raw = await readManifest(this.paths, input.conversationId);
8213
+ if (!raw || raw.status === "deleted_soft") {
8214
+ throw new LinkHttpError(404, "conversation_not_found", "Conversation was not found");
8215
+ }
8216
+ if (raw.status === "archived") {
8217
+ raw.status = "active";
8218
+ raw.updated_at = (/* @__PURE__ */ new Date()).toISOString();
8219
+ await writeManifest(this.paths, raw);
8220
+ }
8221
+ const manifest = raw;
8047
8222
  const snapshot = await readSnapshot(this.paths, input.conversationId);
8048
8223
  const now = (/* @__PURE__ */ new Date()).toISOString();
8049
8224
  const runId = createRunId();
@@ -8109,9 +8284,14 @@ var ConversationService = class extends EventEmitter4 {
8109
8284
  assistant_message: { id: assistantMessageId, status: assistantMessage.status },
8110
8285
  run: { id: runId, status: run.status },
8111
8286
  last_event_seq: latestEvent.seq,
8112
- conversation: this.summarizeManifest(manifest)
8287
+ conversation: this.summarizeManifest(manifest, snapshot)
8113
8288
  };
8114
8289
  }
8290
+ async cancelRunById(runId) {
8291
+ const active = this.activeRunControllers.get(runId);
8292
+ if (!active) throw new LinkHttpError(404, "run_not_found", "Run was not found");
8293
+ return this.cancelRun(active.conversationId, runId);
8294
+ }
8115
8295
  async cancelRun(conversationId, runId) {
8116
8296
  const active = this.activeRunControllers.get(runId);
8117
8297
  if (active) active.controller.abort();
@@ -8330,13 +8510,50 @@ var ConversationService = class extends EventEmitter4 {
8330
8510
  async deleteConversation(conversationId) {
8331
8511
  assertValidConversationId(conversationId);
8332
8512
  return withConversationLock(conversationId, async () => {
8333
- const manifest = await readActiveManifest(this.paths, conversationId);
8513
+ const manifest = await readExistingManifest(this.paths, conversationId);
8334
8514
  const now = (/* @__PURE__ */ new Date()).toISOString();
8515
+ const hermesSessionIds = manifest.hermes_session_ids ?? (manifest.hermes_session_id ? [manifest.hermes_session_id] : []);
8335
8516
  manifest.status = "deleted_soft";
8336
8517
  manifest.deleted_at = now;
8337
8518
  manifest.updated_at = now;
8338
8519
  await writeManifest(this.paths, manifest);
8339
- return { conversation_id: conversationId, deleted_at: now };
8520
+ return {
8521
+ conversation_id: conversationId,
8522
+ hermes_deleted: false,
8523
+ ...hermesSessionIds.length > 0 ? { hermes_session_ids: hermesSessionIds } : {},
8524
+ deleted_at: now
8525
+ };
8526
+ });
8527
+ }
8528
+ async archiveConversation(conversationId) {
8529
+ assertValidConversationId(conversationId);
8530
+ return withConversationLock(conversationId, async () => {
8531
+ const manifest = await readActiveManifest(this.paths, conversationId);
8532
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8533
+ manifest.status = "archived";
8534
+ manifest.updated_at = now;
8535
+ await writeManifest(this.paths, manifest);
8536
+ const conv = this.summarizeManifest(manifest);
8537
+ await this.appendAndEmit(conversationId, { type: "conversation.archived", payload: { conversation: conv } }, manifest);
8538
+ await writeManifest(this.paths, manifest);
8539
+ return { conversation_id: conversationId, archived_at: now };
8540
+ });
8541
+ }
8542
+ async unarchiveConversation(conversationId) {
8543
+ assertValidConversationId(conversationId);
8544
+ return withConversationLock(conversationId, async () => {
8545
+ const manifest = await readExistingManifest(this.paths, conversationId);
8546
+ if (manifest.status !== "archived") {
8547
+ throw new LinkHttpError(400, "conversation_not_archived", "Conversation is not archived");
8548
+ }
8549
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8550
+ manifest.status = "active";
8551
+ manifest.updated_at = now;
8552
+ await writeManifest(this.paths, manifest);
8553
+ const conv = this.summarizeManifest(manifest);
8554
+ await this.appendAndEmit(conversationId, { type: "conversation.unarchived", payload: { conversation: conv } }, manifest);
8555
+ await writeManifest(this.paths, manifest);
8556
+ return { conversation_id: conversationId, unarchived_at: now };
8340
8557
  });
8341
8558
  }
8342
8559
  async deleteConversations(conversationIds) {
@@ -8363,17 +8580,27 @@ var ConversationService = class extends EventEmitter4 {
8363
8580
  manifest.title = title;
8364
8581
  manifest.updated_at = (/* @__PURE__ */ new Date()).toISOString();
8365
8582
  await writeManifest(this.paths, manifest);
8366
- await this.appendAndEmit(conversationId, { type: "conversation.updated", payload: { conversation: this.summarizeManifest(manifest) } }, manifest);
8583
+ const conv = this.summarizeManifest(manifest);
8584
+ const event = await this.appendAndEmit(conversationId, { type: "conversation.updated", payload: { conversation: conv } }, manifest);
8367
8585
  await writeManifest(this.paths, manifest);
8368
- return { conversation_id: conversationId, title };
8586
+ return { conversation_id: conversationId, title, conversation: conv, hermes_synced: false, last_event_seq: event.seq };
8369
8587
  });
8370
8588
  }
8371
8589
  async setConversationModel(conversationId, modelId) {
8372
8590
  return withConversationLock(conversationId, async () => {
8373
8591
  const manifest = await readActiveManifest(this.paths, conversationId);
8592
+ if (manifest.stats) manifest.stats.model = modelId;
8374
8593
  manifest.updated_at = (/* @__PURE__ */ new Date()).toISOString();
8375
8594
  await writeManifest(this.paths, manifest);
8376
- return { conversation_id: conversationId, model_id: modelId };
8595
+ const conv = this.summarizeManifest(manifest);
8596
+ const event = await this.appendAndEmit(conversationId, { type: "conversation.updated", payload: { conversation: conv } }, manifest);
8597
+ await writeManifest(this.paths, manifest);
8598
+ return {
8599
+ conversation_id: conversationId,
8600
+ model_override: modelId,
8601
+ runtime: this.buildRuntimeMetadata(manifest),
8602
+ last_event_seq: event.seq
8603
+ };
8377
8604
  });
8378
8605
  }
8379
8606
  async setConversationProfile(conversationId, profileName) {
@@ -8383,7 +8610,21 @@ var ConversationService = class extends EventEmitter4 {
8383
8610
  manifest.profile_name_snapshot = profileName;
8384
8611
  manifest.updated_at = (/* @__PURE__ */ new Date()).toISOString();
8385
8612
  await writeManifest(this.paths, manifest);
8386
- return { conversation_id: conversationId, profile: profileName };
8613
+ const conv = this.summarizeManifest(manifest);
8614
+ const event = await this.appendAndEmit(conversationId, { type: "conversation.updated", payload: { conversation: conv } }, manifest);
8615
+ await writeManifest(this.paths, manifest);
8616
+ return {
8617
+ conversation_id: conversationId,
8618
+ profile: {
8619
+ uid: manifest.profile_uid ?? "",
8620
+ name: profileName,
8621
+ display_name: profileName,
8622
+ avatar_url: null
8623
+ },
8624
+ runtime: this.buildRuntimeMetadata(manifest),
8625
+ conversation: conv,
8626
+ last_event_seq: event.seq
8627
+ };
8387
8628
  });
8388
8629
  }
8389
8630
  async ackConversation(conversationId, lastEventSeq) {
@@ -8413,55 +8654,194 @@ var ConversationService = class extends EventEmitter4 {
8413
8654
  }
8414
8655
  return { deleted_count: count };
8415
8656
  }
8416
- async prepareClearAllConversationPlan() {
8657
+ async shouldPublishNotificationEvent(event) {
8658
+ const publishable = ["conversation.updated", "message.created", "run.started", "run.completed", "run.failed", "run.cancelled"];
8659
+ return publishable.includes(event.type);
8660
+ }
8661
+ async prepareClearAllConversationPlan(targetStatus = "active") {
8417
8662
  const planId = `plan_${crypto4.randomUUID().replaceAll("-", "")}`;
8663
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8418
8664
  const ids = await listConversationIds(this.paths);
8419
- const activeIds = [];
8665
+ const targetIds = [];
8420
8666
  for (const id of ids) {
8421
8667
  const m = await readManifest(this.paths, id);
8422
- if (m && m.status === "active") activeIds.push(id);
8668
+ if (m && m.status === targetStatus) targetIds.push(id);
8423
8669
  }
8424
- return {
8670
+ const plan = {
8425
8671
  id: planId,
8426
- status: "pending",
8427
- conversation_count: activeIds.length,
8428
- conversation_ids: activeIds,
8429
- created_at: (/* @__PURE__ */ new Date()).toISOString()
8672
+ status: "prepared",
8673
+ target_status: targetStatus,
8674
+ created_at: now,
8675
+ updated_at: now,
8676
+ total_count: targetIds.length,
8677
+ deleted_count: 0,
8678
+ failed_count: 0,
8679
+ conversation_ids: targetIds,
8680
+ conversations: []
8430
8681
  };
8682
+ clearPlans.set(planId, plan);
8683
+ return plan;
8431
8684
  }
8432
8685
  async readClearAllConversationPlan(planId) {
8433
- return { id: planId, status: "pending", conversation_count: 0, conversation_ids: [], created_at: (/* @__PURE__ */ new Date()).toISOString() };
8686
+ const plan = clearPlans.get(planId);
8687
+ if (!plan) throw new LinkHttpError(404, "plan_not_found", "Clear plan was not found");
8688
+ return plan;
8689
+ }
8690
+ async executeClearAllConversationPlan(planId) {
8691
+ return this.startClearAllConversationPlan(planId);
8434
8692
  }
8435
8693
  async startClearAllConversationPlan(planId) {
8694
+ const plan = clearPlans.get(planId) ?? { id: planId, conversation_ids: [], total_count: 0 };
8695
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8696
+ clearPlans.set(planId, { ...plan, status: "executing", updated_at: now });
8697
+ const conversationIds = Array.isArray(plan.conversation_ids) ? plan.conversation_ids : [];
8698
+ let deletedCount = 0, failedCount = 0;
8699
+ const conversations = [];
8700
+ for (const id of conversationIds) {
8701
+ try {
8702
+ const result = await this.deleteConversation(id);
8703
+ conversations.push({ ...result, status: "deleted" });
8704
+ deletedCount++;
8705
+ } catch (err) {
8706
+ failedCount++;
8707
+ conversations.push({
8708
+ conversation_id: id,
8709
+ status: "failed",
8710
+ error: { code: err instanceof LinkHttpError ? err.code : "internal_error", message: err.message }
8711
+ });
8712
+ }
8713
+ }
8714
+ const completed = {
8715
+ ...plan,
8716
+ status: "completed",
8717
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
8718
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
8719
+ deleted_count: deletedCount,
8720
+ failed_count: failedCount,
8721
+ conversations
8722
+ };
8723
+ clearPlans.set(planId, completed);
8724
+ return completed;
8725
+ }
8726
+ async prepareArchiveAllConversationPlan(input = {}) {
8727
+ const planId = `plan_${crypto4.randomUUID().replaceAll("-", "")}`;
8728
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8729
+ const excluded = new Set(input.excludeConversationIds ?? []);
8436
8730
  const ids = await listConversationIds(this.paths);
8437
- let count = 0;
8731
+ const targetIds = [];
8438
8732
  for (const id of ids) {
8733
+ if (excluded.has(id)) continue;
8439
8734
  const m = await readManifest(this.paths, id);
8440
- if (m && m.status === "active") {
8441
- await this.deleteConversation(id).catch(() => void 0);
8442
- count++;
8735
+ if (m && m.status === "active") targetIds.push(id);
8736
+ }
8737
+ const plan = {
8738
+ id: planId,
8739
+ status: "prepared",
8740
+ created_at: now,
8741
+ updated_at: now,
8742
+ total_count: targetIds.length,
8743
+ archived_count: 0,
8744
+ failed_count: 0,
8745
+ conversation_ids: targetIds,
8746
+ conversations: []
8747
+ };
8748
+ archivePlans.set(planId, plan);
8749
+ return plan;
8750
+ }
8751
+ async readArchiveAllConversationPlan(planId) {
8752
+ const plan = archivePlans.get(planId);
8753
+ if (!plan) throw new LinkHttpError(404, "plan_not_found", "Archive plan was not found");
8754
+ return plan;
8755
+ }
8756
+ async executeArchiveAllConversationPlan(planId) {
8757
+ return this.startArchiveAllConversationPlan(planId);
8758
+ }
8759
+ async startArchiveAllConversationPlan(planId) {
8760
+ const plan = archivePlans.get(planId) ?? { id: planId, conversation_ids: [], total_count: 0 };
8761
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8762
+ archivePlans.set(planId, { ...plan, status: "executing", updated_at: now });
8763
+ const conversationIds = Array.isArray(plan.conversation_ids) ? plan.conversation_ids : [];
8764
+ let archivedCount = 0, failedCount = 0;
8765
+ const conversations = [];
8766
+ for (const id of conversationIds) {
8767
+ try {
8768
+ const result = await this.archiveConversation(id);
8769
+ conversations.push({ ...result, status: "archived" });
8770
+ archivedCount++;
8771
+ } catch (err) {
8772
+ failedCount++;
8773
+ conversations.push({
8774
+ conversation_id: id,
8775
+ status: "failed",
8776
+ error: { code: err instanceof LinkHttpError ? err.code : "internal_error", message: err.message }
8777
+ });
8443
8778
  }
8444
8779
  }
8445
- return { id: planId, status: "completed", deleted_count: count, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
8780
+ const completed = {
8781
+ ...plan,
8782
+ status: "completed",
8783
+ updated_at: (/* @__PURE__ */ new Date()).toISOString(),
8784
+ completed_at: (/* @__PURE__ */ new Date()).toISOString(),
8785
+ archived_count: archivedCount,
8786
+ failed_count: failedCount,
8787
+ conversations
8788
+ };
8789
+ archivePlans.set(planId, completed);
8790
+ return completed;
8446
8791
  }
8447
8792
  async resolveApproval(input) {
8448
- return { conversation_id: input.conversationId, approval_id: input.approvalId, decision: input.decision };
8793
+ const now = (/* @__PURE__ */ new Date()).toISOString();
8794
+ let lastEventSeq = 0;
8795
+ try {
8796
+ const m = await readManifest(this.paths, input.conversationId);
8797
+ if (m) lastEventSeq = m.last_event_seq;
8798
+ } catch {
8799
+ }
8800
+ return {
8801
+ conversation_id: input.conversationId,
8802
+ message_id: "",
8803
+ approval: {
8804
+ id: input.approvalId,
8805
+ status: input.decision === "deny" ? "denied" : "approved",
8806
+ kind: "terminal_command",
8807
+ command: "",
8808
+ choices: ["once", "session", "always", "deny"],
8809
+ created_at: now,
8810
+ resolved_at: now,
8811
+ decision: input.decision,
8812
+ resume_available: false
8813
+ },
8814
+ command_allowlist_updated: false,
8815
+ requires_gateway_reload: false,
8816
+ resume_available: false,
8817
+ last_event_seq: lastEventSeq
8818
+ };
8449
8819
  }
8450
- async getStatistics(options) {
8820
+ async getStatistics(options = {}) {
8451
8821
  const ids = await listConversationIds(this.paths);
8452
- let total = 0, active = 0, messages = 0, runs = 0;
8822
+ let total = 0, active = 0, archived = 0, deleted = 0;
8823
+ let inputTokens = 0, outputTokens = 0, totalTokens = 0;
8824
+ let messages = 0, runs = 0;
8453
8825
  for (const id of ids) {
8454
8826
  const m = await readManifest(this.paths, id);
8455
8827
  if (!m) continue;
8456
8828
  if (options.profileName && m.profile !== options.profileName) continue;
8829
+ if (options.profileUid && m.profile_uid !== options.profileUid) continue;
8457
8830
  total++;
8458
8831
  if (m.status === "active") active++;
8459
- const snap = await readSnapshot(this.paths, id);
8460
- messages += snap.messages.length;
8461
- runs += snap.runs.filter((r) => r.kind === "agent").length;
8832
+ else if (m.status === "archived") archived++;
8833
+ else if (m.status === "deleted_soft") deleted++;
8834
+ if (m.stats) {
8835
+ inputTokens += m.stats.input_tokens;
8836
+ outputTokens += m.stats.output_tokens;
8837
+ totalTokens += m.stats.total_tokens;
8838
+ messages += m.stats.message_count;
8839
+ runs += m.stats.run_count;
8840
+ }
8462
8841
  }
8463
8842
  return {
8464
- conversations: { total, active },
8843
+ conversations: { total, active, archived, deleted },
8844
+ tokens: { input_tokens: inputTokens, output_tokens: outputTokens, total_tokens: totalTokens },
8465
8845
  messages: { total: messages },
8466
8846
  runs: { total: runs },
8467
8847
  models: { total: 0 },
@@ -8470,20 +8850,6 @@ var ConversationService = class extends EventEmitter4 {
8470
8850
  profiles: { total: 0 }
8471
8851
  };
8472
8852
  }
8473
- summarizeManifest(manifest) {
8474
- return {
8475
- id: manifest.id,
8476
- kind: manifest.kind,
8477
- title: manifest.title,
8478
- status: manifest.status,
8479
- profile: manifest.profile,
8480
- profile_uid: manifest.profile_uid,
8481
- last_event_seq: manifest.last_event_seq,
8482
- created_at: manifest.created_at,
8483
- updated_at: manifest.updated_at,
8484
- stats: manifest.stats ?? null
8485
- };
8486
- }
8487
8853
  };
8488
8854
 
8489
8855
  // src/http/app.ts
@@ -8734,7 +9100,7 @@ async function buildPairingPage(options) {
8734
9100
  showStatus('success', '\u914D\u5BF9\u6210\u529F\uFF01');
8735
9101
  const results = document.getElementById('results');
8736
9102
  results.innerHTML = \`
8737
- <div class="result-row"><span class="tag">access_token \xB7 15min</span><div class="mono" onclick="copyText(this)">\${data.access_token.token}</div></div>
9103
+ <div class="result-row"><span class="tag">access_token \xB7 2h</span><div class="mono" onclick="copyText(this)">\${data.access_token.token}</div></div>
8738
9104
  <div class="result-row" style="margin-top:0.5rem"><span class="tag">refresh_token \xB7 90days</span><div class="mono" onclick="copyText(this)">\${data.refresh_token.token}</div></div>
8739
9105
  \`;
8740
9106
  } else {
package/dist/cli/index.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  saveConfig,
22
22
  startLinkService,
23
23
  writeJsonFile
24
- } from "../chunk-AA2LZ6QZ.js";
24
+ } from "../chunk-YNBTALOX.js";
25
25
  import "../chunk-NP3Y2NVF.js";
26
26
 
27
27
  // src/cli/index.ts
package/dist/http/app.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  startLinkService
3
- } from "../chunk-AA2LZ6QZ.js";
3
+ } from "../chunk-YNBTALOX.js";
4
4
  import "../chunk-NP3Y2NVF.js";
5
5
  export {
6
6
  startLinkService