@hermespilot/link 0.7.7-beta.0 → 0.7.7

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.
@@ -4,7 +4,8 @@ import Router from "@koa/router";
4
4
 
5
5
  // src/conversations/conversation-service.ts
6
6
  import { EventEmitter } from "events";
7
- import { createHash as createHash6, randomUUID as randomUUID10 } from "crypto";
7
+ import { createHash as createHash6, randomUUID as randomUUID11 } from "crypto";
8
+ import path24 from "path";
8
9
 
9
10
  // src/database/link-database.ts
10
11
  import { mkdir } from "fs/promises";
@@ -35,6 +36,7 @@ var MIGRATIONS = [
35
36
  title TEXT NOT NULL,
36
37
  status TEXT NOT NULL,
37
38
  hermes_session_id TEXT NOT NULL,
39
+ workspace_id TEXT,
38
40
  profile TEXT,
39
41
  model TEXT,
40
42
  provider TEXT,
@@ -58,6 +60,8 @@ var MIGRATIONS = [
58
60
  ON conversation_stats(model);
59
61
  CREATE INDEX IF NOT EXISTS idx_conversation_stats_profile
60
62
  ON conversation_stats(profile);
63
+ CREATE INDEX IF NOT EXISTS idx_conversation_stats_workspace_status_updated
64
+ ON conversation_stats(workspace_id, status, updated_at);
61
65
  `
62
66
  },
63
67
  {
@@ -234,6 +238,7 @@ async function listConversationStatsPage(paths, input) {
234
238
  try {
235
239
  const conditions = ["status = ?"];
236
240
  const params = [status];
241
+ appendWorkspaceFilter(conditions, params, input.workspace);
237
242
  if (input.cursor) {
238
243
  conditions.push(`(
239
244
  updated_at < ?
@@ -276,7 +281,8 @@ async function searchConversationStatsPage(paths, input) {
276
281
  return listConversationStatsPage(paths, {
277
282
  limit,
278
283
  cursor: input.cursor,
279
- status
284
+ status,
285
+ workspace: input.workspace
280
286
  });
281
287
  }
282
288
  const db = openDatabase(paths);
@@ -286,6 +292,7 @@ async function searchConversationStatsPage(paths, input) {
286
292
  status,
287
293
  `%${escapeSqlLike(query.toLowerCase())}%`
288
294
  ];
295
+ appendWorkspaceFilter(conditions, params, input.workspace);
289
296
  if (input.cursor) {
290
297
  conditions.push(`(
291
298
  updated_at < ?
@@ -818,7 +825,12 @@ function ensureProfileIdentitySchema(db) {
818
825
  "ALTER TABLE conversation_stats ADD COLUMN profile_name_snapshot TEXT;"
819
826
  );
820
827
  }
828
+ if (!conversationColumns.has("workspace_id")) {
829
+ db.exec("ALTER TABLE conversation_stats ADD COLUMN workspace_id TEXT;");
830
+ }
821
831
  db.exec(`
832
+ CREATE INDEX IF NOT EXISTS idx_conversation_stats_workspace_status_updated
833
+ ON conversation_stats(workspace_id, status, updated_at);
822
834
  CREATE INDEX IF NOT EXISTS idx_conversation_stats_profile_uid
823
835
  ON conversation_stats(profile_uid);
824
836
  CREATE INDEX IF NOT EXISTS idx_conversation_stats_profile_name_snapshot
@@ -835,6 +847,7 @@ function conversationStatsUpsertSql() {
835
847
  title,
836
848
  status,
837
849
  hermes_session_id,
850
+ workspace_id,
838
851
  profile_uid,
839
852
  profile_name_snapshot,
840
853
  profile,
@@ -850,12 +863,13 @@ function conversationStatsUpsertSql() {
850
863
  updated_at,
851
864
  deleted_at,
852
865
  stats_updated_at
853
- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
866
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
854
867
  ON CONFLICT(conversation_id) DO UPDATE SET
855
868
  kind = excluded.kind,
856
869
  title = excluded.title,
857
870
  status = excluded.status,
858
871
  hermes_session_id = excluded.hermes_session_id,
872
+ workspace_id = excluded.workspace_id,
859
873
  profile_uid = excluded.profile_uid,
860
874
  profile_name_snapshot = excluded.profile_name_snapshot,
861
875
  profile = excluded.profile,
@@ -880,6 +894,7 @@ function conversationStatsParams(record) {
880
894
  record.title,
881
895
  record.status,
882
896
  record.hermesSessionId,
897
+ record.workspaceId ?? null,
883
898
  record.profileUid ?? null,
884
899
  record.profileNameSnapshot ?? record.profile ?? null,
885
900
  record.profile ?? record.profileNameSnapshot ?? null,
@@ -897,6 +912,17 @@ function conversationStatsParams(record) {
897
912
  record.statsUpdatedAt
898
913
  ];
899
914
  }
915
+ function appendWorkspaceFilter(conditions, params, filter) {
916
+ if (!filter || filter.kind === "all") {
917
+ return;
918
+ }
919
+ if (filter.kind === "default") {
920
+ conditions.push("(workspace_id IS NULL OR workspace_id = '')");
921
+ return;
922
+ }
923
+ conditions.push("workspace_id = ?");
924
+ params.push(filter.workspaceId);
925
+ }
900
926
  function runUsageFactUpsertSql() {
901
927
  return `
902
928
  INSERT INTO run_usage_facts (
@@ -1642,6 +1668,11 @@ var messages = {
1642
1668
  "error.relayChallengeInvalid": "Relay did not return a valid install challenge.",
1643
1669
  "error.relayLinkInvalid": "Relay did not return a valid link_id.",
1644
1670
  "error.relayEmpty": "Relay returned an empty response.",
1671
+ "error.workspaceNameConflict": "A workspace with this name already exists.",
1672
+ "error.workspaceNameRequired": "Workspace name is required.",
1673
+ "error.workspaceNameTooLong": "Workspace name is too long.",
1674
+ "error.workspaceNotFound": "Workspace was not found.",
1675
+ "error.workspaceIdInvalid": "Workspace id is invalid.",
1645
1676
  "error.serverHttp": "HermesPilot Server request failed with HTTP {status}.",
1646
1677
  "error.pairingServerUnreachable": "Could not reach HermesPilot Server while creating the pairing session. Check whether {url} is reachable, then try again. If you use a proxy network, add hermes-server.clawpilot.me and hermes-relay.clawpilot.me to the proxy exclusion list, or temporarily turn off VPN/proxy and retry.",
1647
1678
  "error.pairingRelayUnreachable": "Could not reach Hermes Relay while creating the pairing session. Check whether {url} is reachable, then try again. If you use a proxy network, add hermes-server.clawpilot.me and hermes-relay.clawpilot.me to the proxy exclusion list, or temporarily turn off VPN/proxy and retry.",
@@ -1883,6 +1914,11 @@ var messages = {
1883
1914
  "error.relayChallengeInvalid": "Relay \u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684\u5B89\u88C5\u6311\u6218\u3002",
1884
1915
  "error.relayLinkInvalid": "Relay \u6CA1\u6709\u8FD4\u56DE\u6709\u6548\u7684 link_id\u3002",
1885
1916
  "error.relayEmpty": "Relay \u8FD4\u56DE\u4E86\u7A7A\u54CD\u5E94\u3002",
1917
+ "error.workspaceNameConflict": "\u5DF2\u5B58\u5728\u540C\u540D\u5DE5\u4F5C\u533A\u3002",
1918
+ "error.workspaceNameRequired": "\u8BF7\u8F93\u5165\u5DE5\u4F5C\u533A\u540D\u79F0\u3002",
1919
+ "error.workspaceNameTooLong": "\u5DE5\u4F5C\u533A\u540D\u79F0\u592A\u957F\u3002",
1920
+ "error.workspaceNotFound": "\u5DE5\u4F5C\u533A\u4E0D\u5B58\u5728\u3002",
1921
+ "error.workspaceIdInvalid": "\u5DE5\u4F5C\u533A\u6807\u8BC6\u65E0\u6548\u3002",
1886
1922
  "error.serverHttp": "HermesPilot Server \u8BF7\u6C42\u5931\u8D25\uFF0CHTTP \u72B6\u6001\u7801\uFF1A{status}\u3002",
1887
1923
  "error.pairingServerUnreachable": "\u521B\u5EFA\u914D\u5BF9\u4F1A\u8BDD\u65F6\u65E0\u6CD5\u8FDE\u63A5 HermesPilot Server\u3002\u8BF7\u5148\u786E\u8BA4 {url} \u53EF\u4EE5\u8BBF\u95EE\uFF0C\u7136\u540E\u91CD\u8BD5\u3002\u91CD\u70B9\u63D0\u9192\uFF1A\u5982\u679C\u4F60\u4F7F\u7528\u4E86\u4EE3\u7406\u7F51\u7EDC\uFF0C\u53EF\u4EE5\u628A hermes-server.clawpilot.me \u548C hermes-relay.clawpilot.me \u52A0\u5165\u4EE3\u7406\u6392\u9664\u540D\u5355\uFF0C\u6216\u4E34\u65F6\u5173\u95ED VPN/\u4EE3\u7406\u540E\u518D\u8BD5\u3002",
1888
1924
  "error.pairingRelayUnreachable": "\u521B\u5EFA\u914D\u5BF9\u4F1A\u8BDD\u65F6\u65E0\u6CD5\u8FDE\u63A5 Hermes Relay\u3002\u8BF7\u5148\u786E\u8BA4 {url} \u53EF\u4EE5\u8BBF\u95EE\uFF0C\u7136\u540E\u91CD\u8BD5\u3002\u91CD\u70B9\u63D0\u9192\uFF1A\u5982\u679C\u4F60\u4F7F\u7528\u4E86\u4EE3\u7406\u7F51\u7EDC\uFF0C\u53EF\u4EE5\u628A hermes-server.clawpilot.me \u548C hermes-relay.clawpilot.me \u52A0\u5165\u4EE3\u7406\u6392\u9664\u540D\u5355\uFF0C\u6216\u4E34\u65F6\u5173\u95ED VPN/\u4EE3\u7406\u540E\u518D\u8BD5\u3002",
@@ -1944,6 +1980,21 @@ function translateKnownError(message, language) {
1944
1980
  if (message === "Relay returned an empty response") {
1945
1981
  return translate(language, "error.relayEmpty");
1946
1982
  }
1983
+ if (message === "workspace name already exists") {
1984
+ return translate(language, "error.workspaceNameConflict");
1985
+ }
1986
+ if (message === "workspace name is required") {
1987
+ return translate(language, "error.workspaceNameRequired");
1988
+ }
1989
+ if (message === "workspace name is too long") {
1990
+ return translate(language, "error.workspaceNameTooLong");
1991
+ }
1992
+ if (message === "workspace was not found") {
1993
+ return translate(language, "error.workspaceNotFound");
1994
+ }
1995
+ if (message === "workspace_id is invalid") {
1996
+ return translate(language, "error.workspaceIdInvalid");
1997
+ }
1947
1998
  const portInUse = /^listen EADDRINUSE: address already in use .*:(?<port>\d+)$/u.exec(message);
1948
1999
  if (portInUse?.groups?.port) {
1949
2000
  return translate(language, "error.portInUse", { port: portInUse.groups.port });
@@ -6496,7 +6547,7 @@ async function listCronOutputFiles(profileName, jobId) {
6496
6547
  orderTimeMs: fileStat.mtimeMs
6497
6548
  });
6498
6549
  }
6499
- return files.sort((left, right) => left.orderTimeMs - right.orderTimeMs).map(({ path: path33, mtime }) => ({ path: path33, mtime }));
6550
+ return files.sort((left, right) => left.orderTimeMs - right.orderTimeMs).map(({ path: path34, mtime }) => ({ path: path34, mtime }));
6500
6551
  }
6501
6552
  function readCronOutputTimestamp(fileName) {
6502
6553
  const match = fileName.match(
@@ -6585,7 +6636,7 @@ function isConversationMissingError(error) {
6585
6636
  }
6586
6637
 
6587
6638
  // src/constants.ts
6588
- var LINK_VERSION = "0.7.7-beta.0";
6639
+ var LINK_VERSION = "0.7.7";
6589
6640
  var LINK_COMMAND = "hermeslink";
6590
6641
  var LINK_DEFAULT_PORT = 52379;
6591
6642
  var LINK_RUNTIME_DIR_NAME = ".hermeslink";
@@ -6605,6 +6656,7 @@ function resolveRuntimePaths(homeDir = resolveRuntimeHome()) {
6605
6656
  credentialsFile: path5.join(homeDir, "credentials.json"),
6606
6657
  databaseFile: path5.join(homeDir, "link.db"),
6607
6658
  conversationsDir: path5.join(homeDir, "conversations"),
6659
+ workspacesFile: path5.join(homeDir, "workspaces.json"),
6608
6660
  blobsDir: path5.join(homeDir, "blobs"),
6609
6661
  indexesDir: path5.join(homeDir, "indexes"),
6610
6662
  logsDir: path5.join(homeDir, "logs"),
@@ -7266,7 +7318,14 @@ async function deleteHermesSession(sessionId, profileName = "default") {
7266
7318
  status: readSessionDeleteStatus(output.stdout, output.stderr)
7267
7319
  };
7268
7320
  } catch (error) {
7269
- if (isSessionNotFoundOutput(readExecErrorOutput(error))) {
7321
+ const output = readExecErrorOutput(error);
7322
+ if (isProfileNotFoundOutput(output)) {
7323
+ return {
7324
+ session_id: normalizedSessionId,
7325
+ status: "profile_not_found"
7326
+ };
7327
+ }
7328
+ if (isSessionNotFoundOutput(output)) {
7270
7329
  return {
7271
7330
  session_id: normalizedSessionId,
7272
7331
  status: "not_found"
@@ -7328,6 +7387,9 @@ ${stderr.toString()}`;
7328
7387
  if (isSessionNotFoundOutput(output)) {
7329
7388
  return "not_found";
7330
7389
  }
7390
+ if (isProfileNotFoundOutput(output)) {
7391
+ return "profile_not_found";
7392
+ }
7331
7393
  if (/deleted session\b/i.test(output)) {
7332
7394
  return "deleted";
7333
7395
  }
@@ -7336,6 +7398,9 @@ ${stderr.toString()}`;
7336
7398
  function isSessionNotFoundOutput(output) {
7337
7399
  return /\bsession\b[\s\S]*\bnot found\b/i.test(output);
7338
7400
  }
7401
+ function isProfileNotFoundOutput(output) {
7402
+ return /\bprofile\b[\s\S]*\bdoes not exist\b/i.test(output);
7403
+ }
7339
7404
  function readExecErrorOutput(error) {
7340
7405
  if (typeof error !== "object" || error === null) {
7341
7406
  return "";
@@ -8641,6 +8706,61 @@ async function respondTuiGatewayApproval(input) {
8641
8706
  );
8642
8707
  return { resolved: readNumber2(result, "resolved") ?? 0 };
8643
8708
  }
8709
+ async function setTuiGatewaySessionArchived(input) {
8710
+ const sessionId = input.sessionId.trim();
8711
+ if (!sessionId) {
8712
+ throw new LinkHttpError(
8713
+ 400,
8714
+ "hermes_session_id_required",
8715
+ "Hermes session id is required"
8716
+ );
8717
+ }
8718
+ if (sessionId.startsWith("hp_")) {
8719
+ return;
8720
+ }
8721
+ const profileName = normalizeProfileName2(input.profileName);
8722
+ const backend = await ensureTuiGatewayBackend({
8723
+ profileName,
8724
+ paths: input.paths,
8725
+ logger: input.logger
8726
+ });
8727
+ const response = await fetch(
8728
+ `${backend.baseUrl}/api/sessions/${encodeURIComponent(sessionId)}`,
8729
+ {
8730
+ method: "PATCH",
8731
+ headers: {
8732
+ "content-type": "application/json",
8733
+ "X-Hermes-Session-Token": backend.token
8734
+ },
8735
+ body: JSON.stringify({ archived: input.archived })
8736
+ }
8737
+ ).catch((error) => {
8738
+ throw new LinkHttpError(
8739
+ 502,
8740
+ "hermes_session_archive_failed",
8741
+ error instanceof Error ? error.message : "Hermes session archive request failed"
8742
+ );
8743
+ });
8744
+ touchBackend(profileName);
8745
+ if (response.ok) {
8746
+ return;
8747
+ }
8748
+ const detail = await response.text().catch(() => "");
8749
+ if (response.status === 404) {
8750
+ void input.logger?.debug("hermes_session_archive_missing", {
8751
+ profile: profileName,
8752
+ session_id: sessionId,
8753
+ archived: input.archived,
8754
+ detail
8755
+ });
8756
+ return;
8757
+ }
8758
+ throw new LinkHttpError(
8759
+ 502,
8760
+ "hermes_session_archive_failed",
8761
+ `Hermes session archive request failed with HTTP ${response.status}${detail ? `: ${detail}` : ""}`
8762
+ );
8763
+ }
8644
8764
  async function readTuiGatewayStatus(input = {}) {
8645
8765
  const profile = normalizeProfileName2(input.profileName);
8646
8766
  const client = clients.get(profile);
@@ -9850,6 +9970,7 @@ function toStatsIndexRecord(manifest, stats = manifest.stats ?? buildConversatio
9850
9970
  title: manifest.title,
9851
9971
  status: manifest.status,
9852
9972
  hermesSessionId: manifest.hermes_session_id,
9973
+ workspaceId: manifest.workspace_id ?? null,
9853
9974
  profileUid: stats.profile_uid ?? null,
9854
9975
  profileNameSnapshot: stats.profile_name_snapshot ?? stats.profile ?? null,
9855
9976
  profile: stats.profile ?? stats.profile_name_snapshot ?? null,
@@ -9999,8 +10120,204 @@ function firstRecord(...values) {
9999
10120
  return {};
10000
10121
  }
10001
10122
 
10002
- // src/conversations/blob-store.ts
10123
+ // src/conversations/workspaces.ts
10003
10124
  import { randomUUID as randomUUID4 } from "crypto";
10125
+ var MAX_WORKSPACE_NAME_LENGTH = 40;
10126
+ var DEFAULT_WORKSPACE_ICON = "grid";
10127
+ var WORKSPACE_ICON_KEYS = /* @__PURE__ */ new Set([
10128
+ "grid",
10129
+ "folder",
10130
+ "briefcase",
10131
+ "code",
10132
+ "cube",
10133
+ "rocket",
10134
+ "albums",
10135
+ "bookmark",
10136
+ "heart",
10137
+ "library",
10138
+ "planet",
10139
+ "school"
10140
+ ]);
10141
+ async function listWorkspaces(paths) {
10142
+ const store = await readWorkspaceStore(paths);
10143
+ return sortWorkspaces(store.workspaces);
10144
+ }
10145
+ async function createWorkspace(paths, input) {
10146
+ const name = normalizeWorkspaceName(input.name);
10147
+ const icon = normalizeWorkspaceIcon(input.icon);
10148
+ let created;
10149
+ await updateWorkspaceStore(paths, (store) => {
10150
+ assertUniqueWorkspaceName(store.workspaces, name);
10151
+ const now = (/* @__PURE__ */ new Date()).toISOString();
10152
+ created = {
10153
+ id: `ws_${randomUUID4().replaceAll("-", "")}`,
10154
+ name,
10155
+ icon,
10156
+ created_at: now,
10157
+ updated_at: now
10158
+ };
10159
+ return {
10160
+ ...store,
10161
+ workspaces: [...store.workspaces, created]
10162
+ };
10163
+ });
10164
+ return created;
10165
+ }
10166
+ async function renameWorkspace(paths, workspaceId, input) {
10167
+ const id = normalizeWorkspaceId(workspaceId);
10168
+ const name = normalizeWorkspaceName(input.name);
10169
+ const icon = normalizeWorkspaceIcon(input.icon);
10170
+ let renamed;
10171
+ await updateWorkspaceStore(paths, (store) => {
10172
+ assertUniqueWorkspaceName(store.workspaces, name, id);
10173
+ const index = store.workspaces.findIndex((workspace) => workspace.id === id);
10174
+ if (index < 0) {
10175
+ throw workspaceNotFound();
10176
+ }
10177
+ const next = [...store.workspaces];
10178
+ renamed = {
10179
+ ...next[index],
10180
+ name,
10181
+ icon,
10182
+ updated_at: (/* @__PURE__ */ new Date()).toISOString()
10183
+ };
10184
+ next[index] = renamed;
10185
+ return { ...store, workspaces: next };
10186
+ });
10187
+ return renamed;
10188
+ }
10189
+ async function deleteWorkspace(paths, workspaceId) {
10190
+ const id = normalizeWorkspaceId(workspaceId);
10191
+ let deleted;
10192
+ await updateWorkspaceStore(paths, (store) => {
10193
+ const next = store.workspaces.filter((workspace) => {
10194
+ if (workspace.id === id) {
10195
+ deleted = workspace;
10196
+ return false;
10197
+ }
10198
+ return true;
10199
+ });
10200
+ if (!deleted) {
10201
+ throw workspaceNotFound();
10202
+ }
10203
+ return { ...store, workspaces: next };
10204
+ });
10205
+ return deleted;
10206
+ }
10207
+ async function assertWorkspaceExists(paths, workspaceId) {
10208
+ const id = normalizeOptionalWorkspaceId(workspaceId);
10209
+ if (!id) {
10210
+ return void 0;
10211
+ }
10212
+ const store = await readWorkspaceStore(paths);
10213
+ if (!store.workspaces.some((workspace) => workspace.id === id)) {
10214
+ throw workspaceNotFound();
10215
+ }
10216
+ return id;
10217
+ }
10218
+ function normalizeOptionalWorkspaceId(value) {
10219
+ if (value == null) {
10220
+ return void 0;
10221
+ }
10222
+ const normalized = value.trim();
10223
+ if (!normalized || normalized === "default") {
10224
+ return void 0;
10225
+ }
10226
+ return normalizeWorkspaceId(normalized);
10227
+ }
10228
+ function normalizeWorkspaceId(value) {
10229
+ const normalized = value.trim();
10230
+ if (!/^ws_[a-zA-Z0-9]+$/u.test(normalized)) {
10231
+ throw new LinkHttpError(400, "workspace_id_invalid", "workspace_id is invalid");
10232
+ }
10233
+ return normalized;
10234
+ }
10235
+ function normalizeWorkspaceName(value) {
10236
+ const normalized = value.replace(/\s+/gu, " ").trim();
10237
+ if (!normalized) {
10238
+ throw new LinkHttpError(
10239
+ 400,
10240
+ "workspace_name_required",
10241
+ "workspace name is required"
10242
+ );
10243
+ }
10244
+ if (Array.from(normalized).length > MAX_WORKSPACE_NAME_LENGTH) {
10245
+ throw new LinkHttpError(
10246
+ 400,
10247
+ "workspace_name_too_long",
10248
+ "workspace name is too long"
10249
+ );
10250
+ }
10251
+ return normalized;
10252
+ }
10253
+ function normalizeWorkspaceIcon(value) {
10254
+ const normalized = value?.trim() ?? "";
10255
+ if (!normalized) {
10256
+ return DEFAULT_WORKSPACE_ICON;
10257
+ }
10258
+ return WORKSPACE_ICON_KEYS.has(normalized) ? normalized : DEFAULT_WORKSPACE_ICON;
10259
+ }
10260
+ function assertUniqueWorkspaceName(workspaces, name, exceptWorkspaceId) {
10261
+ const normalized = name.toLocaleLowerCase();
10262
+ if (workspaces.some(
10263
+ (workspace) => workspace.id !== exceptWorkspaceId && workspace.name.toLocaleLowerCase() === normalized
10264
+ )) {
10265
+ throw new LinkHttpError(
10266
+ 409,
10267
+ "workspace_name_conflict",
10268
+ "workspace name already exists"
10269
+ );
10270
+ }
10271
+ }
10272
+ async function readWorkspaceStore(paths) {
10273
+ return normalizeWorkspaceStore(await readJsonFile(paths.workspacesFile));
10274
+ }
10275
+ async function updateWorkspaceStore(paths, update) {
10276
+ return updateJsonFile(
10277
+ paths.workspacesFile,
10278
+ (current) => update(normalizeWorkspaceStore(current)),
10279
+ 384
10280
+ );
10281
+ }
10282
+ function normalizeWorkspaceStore(store) {
10283
+ if (!store || !Array.isArray(store.workspaces)) {
10284
+ return { schema_version: 1, workspaces: [] };
10285
+ }
10286
+ return {
10287
+ schema_version: 1,
10288
+ workspaces: store.workspaces.map(normalizeWorkspaceRecord).filter(
10289
+ (workspace) => Boolean(workspace)
10290
+ )
10291
+ };
10292
+ }
10293
+ function normalizeWorkspaceRecord(value) {
10294
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
10295
+ return null;
10296
+ }
10297
+ const record = value;
10298
+ const id = typeof record.id === "string" ? record.id.trim() : "";
10299
+ const name = typeof record.name === "string" ? record.name.trim() : "";
10300
+ const icon = normalizeWorkspaceIcon(
10301
+ typeof record.icon === "string" ? record.icon : void 0
10302
+ );
10303
+ const createdAt = typeof record.created_at === "string" ? record.created_at.trim() : "";
10304
+ const updatedAt = typeof record.updated_at === "string" ? record.updated_at.trim() : "";
10305
+ if (!id || !name || !createdAt || !updatedAt) {
10306
+ return null;
10307
+ }
10308
+ return { id, name, icon, created_at: createdAt, updated_at: updatedAt };
10309
+ }
10310
+ function sortWorkspaces(workspaces) {
10311
+ return [...workspaces].sort(
10312
+ (left, right) => left.created_at.localeCompare(right.created_at) || left.id.localeCompare(right.id)
10313
+ );
10314
+ }
10315
+ function workspaceNotFound() {
10316
+ return new LinkHttpError(404, "workspace_not_found", "workspace was not found");
10317
+ }
10318
+
10319
+ // src/conversations/blob-store.ts
10320
+ import { randomUUID as randomUUID5 } from "crypto";
10004
10321
  import { mkdir as mkdir6, readFile as readFile7, readdir as readdir4, rm as rm3, stat as stat6, writeFile } from "fs/promises";
10005
10322
  import path10 from "path";
10006
10323
 
@@ -10453,7 +10770,7 @@ async function writeConversationBlob(paths, conversationId, input, options) {
10453
10770
  if (input.bytes.byteLength > options.maxBytes) {
10454
10771
  throw new LinkHttpError(413, "blob_too_large", "Blob is too large");
10455
10772
  }
10456
- const id = `blob_${randomUUID4().replaceAll("-", "")}`;
10773
+ const id = `blob_${randomUUID5().replaceAll("-", "")}`;
10457
10774
  const filePath = blobPath(paths, id);
10458
10775
  await mkdir6(path10.dirname(filePath), { recursive: true, mode: 448 });
10459
10776
  await writeFile(filePath, input.bytes, { mode: 384 });
@@ -10592,13 +10909,6 @@ async function listConversationBlobIds(paths, conversationId) {
10592
10909
  }
10593
10910
  return blobIds;
10594
10911
  }
10595
- async function removeConversationFiles(paths, conversationId) {
10596
- assertValidConversationId(conversationId);
10597
- await rm3(path10.join(paths.conversationsDir, conversationId), {
10598
- recursive: true,
10599
- force: true
10600
- });
10601
- }
10602
10912
  function conversationAttachmentsDir(paths, conversationId) {
10603
10913
  assertValidConversationId(conversationId);
10604
10914
  return path10.join(paths.conversationsDir, conversationId, "attachments");
@@ -10828,6 +11138,7 @@ function toSummary(manifest, snapshot, profile) {
10828
11138
  id: manifest.id,
10829
11139
  title: manifest.title,
10830
11140
  title_source: manifest.title_source,
11141
+ workspace_id: manifest.workspace_id ?? null,
10831
11142
  created_at: manifest.created_at,
10832
11143
  updated_at: manifest.updated_at,
10833
11144
  last_event_seq: manifest.last_event_seq,
@@ -10878,7 +11189,7 @@ function isRealtimeRunStatus(status) {
10878
11189
  }
10879
11190
 
10880
11191
  // src/conversations/slash-commands.ts
10881
- import { randomUUID as randomUUID5 } from "crypto";
11192
+ import { randomUUID as randomUUID6 } from "crypto";
10882
11193
  var MODEL_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,127}$/u;
10883
11194
  function isValidModelId(value) {
10884
11195
  return MODEL_ID_PATTERN.test(value);
@@ -10975,7 +11286,7 @@ function parseSlashCommandInput(content, language = "zh-CN") {
10975
11286
  }
10976
11287
  function createSlashCommandUserMessage(input) {
10977
11288
  return {
10978
- id: `msg_${randomUUID5().replaceAll("-", "")}`,
11289
+ id: `msg_${randomUUID6().replaceAll("-", "")}`,
10979
11290
  schema_version: 1,
10980
11291
  conversation_id: input.conversationId,
10981
11292
  role: "user",
@@ -11025,7 +11336,7 @@ function slashHelpMessage(language = "zh-CN") {
11025
11336
  ].join("\n");
11026
11337
  }
11027
11338
  function freshHermesSessionId(conversationId) {
11028
- return `hp_${conversationId}_${randomUUID5().replaceAll("-", "").slice(0, 12)}`;
11339
+ return `hp_${conversationId}_${randomUUID6().replaceAll("-", "").slice(0, 12)}`;
11029
11340
  }
11030
11341
  function nextVerboseMode(current) {
11031
11342
  const modes = [
@@ -11663,7 +11974,7 @@ function safePathSegment(value, fallback) {
11663
11974
  }
11664
11975
 
11665
11976
  // src/conversations/conversation-archive-plans.ts
11666
- import { randomUUID as randomUUID6 } from "crypto";
11977
+ import { randomUUID as randomUUID7 } from "crypto";
11667
11978
  import { mkdir as mkdir8 } from "fs/promises";
11668
11979
  import path12 from "path";
11669
11980
  var PLAN_ID_PATTERN = /^archive_[a-f0-9]{32}$/u;
@@ -11675,7 +11986,7 @@ var ConversationArchivePlanStore = class {
11675
11986
  async create(conversationIds) {
11676
11987
  const now = (/* @__PURE__ */ new Date()).toISOString();
11677
11988
  const plan = {
11678
- id: `archive_${randomUUID6().replaceAll("-", "")}`,
11989
+ id: `archive_${randomUUID7().replaceAll("-", "")}`,
11679
11990
  status: "prepared",
11680
11991
  created_at: now,
11681
11992
  updated_at: now,
@@ -11727,7 +12038,7 @@ function normalizePlanId(planId) {
11727
12038
  }
11728
12039
 
11729
12040
  // src/conversations/conversation-clear-plans.ts
11730
- import { randomUUID as randomUUID7 } from "crypto";
12041
+ import { randomUUID as randomUUID8 } from "crypto";
11731
12042
  import { mkdir as mkdir9 } from "fs/promises";
11732
12043
  import path13 from "path";
11733
12044
  var PLAN_ID_PATTERN2 = /^clear_[a-f0-9]{32}$/u;
@@ -11739,7 +12050,7 @@ var ConversationClearPlanStore = class {
11739
12050
  async create(conversationIds, targetStatus = "active") {
11740
12051
  const now = (/* @__PURE__ */ new Date()).toISOString();
11741
12052
  const plan = {
11742
- id: `clear_${randomUUID7().replaceAll("-", "")}`,
12053
+ id: `clear_${randomUUID8().replaceAll("-", "")}`,
11743
12054
  status: "prepared",
11744
12055
  target_status: targetStatus,
11745
12056
  created_at: now,
@@ -11940,6 +12251,11 @@ function collectHermesSessionDeleteTargets(manifest, snapshot) {
11940
12251
  }
11941
12252
  return targets;
11942
12253
  }
12254
+ function collectHermesSessionMutationTargets(manifest, snapshot) {
12255
+ return collectHermesSessionDeleteTargets(manifest, snapshot).filter(
12256
+ (target) => !target.sessionId.startsWith("hp_")
12257
+ );
12258
+ }
11943
12259
  function arraysEqual(left, right) {
11944
12260
  if (left.length !== right.length) {
11945
12261
  return false;
@@ -12389,6 +12705,7 @@ var ConversationMaintenanceCoordinator = class {
12389
12705
  }
12390
12706
  const archivedAt = (/* @__PURE__ */ new Date()).toISOString();
12391
12707
  const snapshot = await this.deps.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
12708
+ await this.setHermesSessionsArchived(manifest, snapshot, true);
12392
12709
  await this.deps.store.writeManifest({
12393
12710
  ...manifest,
12394
12711
  status: "archived",
@@ -12429,6 +12746,7 @@ var ConversationMaintenanceCoordinator = class {
12429
12746
  }
12430
12747
  const unarchivedAt = (/* @__PURE__ */ new Date()).toISOString();
12431
12748
  const snapshot = await this.deps.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
12749
+ await this.setHermesSessionsArchived(manifest, snapshot, false);
12432
12750
  const next = {
12433
12751
  ...manifest,
12434
12752
  status: "active",
@@ -12532,6 +12850,18 @@ var ConversationMaintenanceCoordinator = class {
12532
12850
  }
12533
12851
  return results;
12534
12852
  }
12853
+ async setHermesSessionsArchived(manifest, snapshot, archived) {
12854
+ const targets = collectHermesSessionMutationTargets(manifest, snapshot);
12855
+ for (const target of targets) {
12856
+ await setTuiGatewaySessionArchived({
12857
+ paths: this.deps.paths,
12858
+ logger: this.deps.logger,
12859
+ profileName: target.profileName,
12860
+ sessionId: target.sessionId,
12861
+ archived
12862
+ });
12863
+ }
12864
+ }
12535
12865
  async pruneConversationBlobReferences(conversationId, blobIds) {
12536
12866
  for (const blobId of blobIds) {
12537
12867
  try {
@@ -13056,7 +13386,7 @@ function stripCompressionTitleSuffix(value) {
13056
13386
  }
13057
13387
 
13058
13388
  // src/conversations/conversation-turns.ts
13059
- import { randomUUID as randomUUID8 } from "crypto";
13389
+ import { randomUUID as randomUUID9 } from "crypto";
13060
13390
  var MESSAGE_ORDER_STEP_MS = 10;
13061
13391
  var ASSISTANT_ORDER_OFFSET_MS = 1;
13062
13392
  function createAgentTurnDraft(input) {
@@ -13305,10 +13635,10 @@ function createAssistantMessage(input) {
13305
13635
  };
13306
13636
  }
13307
13637
  function createMessageId() {
13308
- return `msg_${randomUUID8().replaceAll("-", "")}`;
13638
+ return `msg_${randomUUID9().replaceAll("-", "")}`;
13309
13639
  }
13310
13640
  function createRunId() {
13311
- return `run_${randomUUID8().replaceAll("-", "")}`;
13641
+ return `run_${randomUUID9().replaceAll("-", "")}`;
13312
13642
  }
13313
13643
  function hasActiveOrQueuedRuns(snapshot) {
13314
13644
  return snapshot.runs.some(
@@ -14600,11 +14930,17 @@ var ConversationQueryCoordinator = class {
14600
14930
  status,
14601
14931
  limit,
14602
14932
  cursor,
14603
- fallback: () => this.listConversationPageFromStore({ status, limit, cursor }),
14933
+ fallback: () => this.listConversationPageFromStore({
14934
+ status,
14935
+ limit,
14936
+ cursor,
14937
+ workspace: options.workspace
14938
+ }),
14604
14939
  listPage: (pageCursor) => listConversationStatsPage(this.deps.paths, {
14605
14940
  status,
14606
14941
  limit,
14607
- cursor: pageCursor
14942
+ cursor: pageCursor,
14943
+ workspace: options.workspace
14608
14944
  })
14609
14945
  });
14610
14946
  }
@@ -14693,11 +15029,11 @@ var ConversationQueryCoordinator = class {
14693
15029
  }
14694
15030
  };
14695
15031
  }
14696
- async listConversationsFromStore(status = "active") {
15032
+ async listConversationsFromStore(status = "active", workspace) {
14697
15033
  const summaries = [];
14698
15034
  for (const conversationId of await this.deps.store.listConversationIds()) {
14699
15035
  const manifest = await this.deps.store.readManifest(conversationId).catch(() => null);
14700
- if (!manifest || manifest.status !== status) {
15036
+ if (!manifest || manifest.status !== status || !matchesWorkspaceFilter(manifest, workspace)) {
14701
15037
  continue;
14702
15038
  }
14703
15039
  const snapshot = await this.deps.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
@@ -14709,7 +15045,10 @@ var ConversationQueryCoordinator = class {
14709
15045
  );
14710
15046
  }
14711
15047
  async listConversationPageFromStore(input) {
14712
- const all = await this.listConversationsFromStore(input.status);
15048
+ const all = await this.listConversationsFromStore(
15049
+ input.status,
15050
+ input.workspace
15051
+ );
14713
15052
  const startIndex = input.cursor ? all.findIndex(
14714
15053
  (summary) => isAfterConversationListCursor(summary, input.cursor)
14715
15054
  ) : 0;
@@ -14914,6 +15253,16 @@ function readNonEmptyString(value) {
14914
15253
  function normalizeConversationSearchQuery(value) {
14915
15254
  return typeof value === "string" ? value.trim() : "";
14916
15255
  }
15256
+ function matchesWorkspaceFilter(manifest, filter) {
15257
+ if (!filter || filter.kind === "all") {
15258
+ return true;
15259
+ }
15260
+ const workspaceId = manifest.workspace_id?.trim() ?? "";
15261
+ if (filter.kind === "default") {
15262
+ return workspaceId === "";
15263
+ }
15264
+ return workspaceId === filter.workspaceId;
15265
+ }
14917
15266
  function isAfterConversationListCursor(summary, cursor) {
14918
15267
  return summary.updated_at < cursor.updatedAt || summary.updated_at === cursor.updatedAt && summary.id < cursor.conversationId;
14919
15268
  }
@@ -15106,7 +15455,7 @@ function isNodeError9(error, code) {
15106
15455
  }
15107
15456
 
15108
15457
  // src/conversations/hermes-session-sync.ts
15109
- import { randomUUID as randomUUID9 } from "crypto";
15458
+ import { randomUUID as randomUUID10 } from "crypto";
15110
15459
  import { readdir as readdir7, readFile as readFile10, stat as stat10 } from "fs/promises";
15111
15460
  import path17 from "path";
15112
15461
 
@@ -17068,7 +17417,7 @@ function toLinkMessage(input) {
17068
17417
  const sessionId = readString10(input.message, "session_id") ?? input.sessionId;
17069
17418
  const createdAt = isoFromHermesTime(input.message.timestamp) ?? new Date(Date.now() + input.index).toISOString();
17070
17419
  return {
17071
- id: `msg_${randomUUID9().replaceAll("-", "")}`,
17420
+ id: `msg_${randomUUID10().replaceAll("-", "")}`,
17072
17421
  schema_version: 1,
17073
17422
  conversation_id: input.conversationId,
17074
17423
  role,
@@ -17251,7 +17600,7 @@ async function isFile(filePath) {
17251
17600
  });
17252
17601
  }
17253
17602
  function createConversationId() {
17254
- return `conv_${randomUUID9().replaceAll("-", "")}`;
17603
+ return `conv_${randomUUID10().replaceAll("-", "")}`;
17255
17604
  }
17256
17605
  function isoFromHermesTime(value) {
17257
17606
  const numeric = readNumber3(value);
@@ -17942,10 +18291,10 @@ function parseHermesApiCapabilities(payload) {
17942
18291
  sessionKeyHeader: readString12(features, "session_key_header")
17943
18292
  };
17944
18293
  }
17945
- async function callHermesApi(path33, init, options) {
18294
+ async function callHermesApi(path34, init, options) {
17946
18295
  const method = init.method ?? "GET";
17947
18296
  const startedAt = Date.now();
17948
- void options.logger?.debug("hermes_api_request_started", { method, path: path33 });
18297
+ void options.logger?.debug("hermes_api_request_started", { method, path: path34 });
17949
18298
  const availability = await ensureHermesApiServerAvailable({
17950
18299
  fetchImpl: options.fetchImpl,
17951
18300
  logger: options.logger,
@@ -17954,7 +18303,7 @@ async function callHermesApi(path33, init, options) {
17954
18303
  });
17955
18304
  let config = availability.configResult.apiServer;
17956
18305
  const fetcher = options.fetchImpl ?? fetch;
17957
- const request = () => fetchHermesApi(fetcher, config, path33, init, options);
18306
+ const request = () => fetchHermesApi(fetcher, config, path34, init, options);
17958
18307
  let response;
17959
18308
  try {
17960
18309
  response = await request();
@@ -17962,7 +18311,7 @@ async function callHermesApi(path33, init, options) {
17962
18311
  logHermesApiError(
17963
18312
  options.logger,
17964
18313
  method,
17965
- path33,
18314
+ path34,
17966
18315
  options.profileName,
17967
18316
  startedAt,
17968
18317
  error
@@ -17973,7 +18322,7 @@ async function callHermesApi(path33, init, options) {
17973
18322
  logHermesApiResponse(
17974
18323
  options.logger,
17975
18324
  method,
17976
- path33,
18325
+ path34,
17977
18326
  options.profileName,
17978
18327
  startedAt,
17979
18328
  response
@@ -17982,7 +18331,7 @@ async function callHermesApi(path33, init, options) {
17982
18331
  }
17983
18332
  void options.logger?.warn("hermes_api_request_retrying_after_401", {
17984
18333
  method,
17985
- path: path33,
18334
+ path: path34,
17986
18335
  profile: options.profileName ?? "default",
17987
18336
  port: config.port ?? null,
17988
18337
  duration_ms: Date.now() - startedAt
@@ -18001,7 +18350,7 @@ async function callHermesApi(path33, init, options) {
18001
18350
  logHermesApiError(
18002
18351
  options.logger,
18003
18352
  method,
18004
- path33,
18353
+ path34,
18005
18354
  options.profileName,
18006
18355
  startedAt,
18007
18356
  error
@@ -18011,7 +18360,7 @@ async function callHermesApi(path33, init, options) {
18011
18360
  logHermesApiResponse(
18012
18361
  options.logger,
18013
18362
  method,
18014
- path33,
18363
+ path34,
18015
18364
  options.profileName,
18016
18365
  startedAt,
18017
18366
  response
@@ -18021,7 +18370,7 @@ async function callHermesApi(path33, init, options) {
18021
18370
  }
18022
18371
  void options.logger?.warn("hermes_api_request_repairing_after_401", {
18023
18372
  method,
18024
- path: path33,
18373
+ path: path34,
18025
18374
  profile: options.profileName ?? "default",
18026
18375
  port: config.port ?? null,
18027
18376
  duration_ms: Date.now() - startedAt
@@ -18042,7 +18391,7 @@ async function callHermesApi(path33, init, options) {
18042
18391
  logHermesApiError(
18043
18392
  options.logger,
18044
18393
  method,
18045
- path33,
18394
+ path34,
18046
18395
  options.profileName,
18047
18396
  startedAt,
18048
18397
  error
@@ -18052,21 +18401,21 @@ async function callHermesApi(path33, init, options) {
18052
18401
  logHermesApiResponse(
18053
18402
  options.logger,
18054
18403
  method,
18055
- path33,
18404
+ path34,
18056
18405
  options.profileName,
18057
18406
  startedAt,
18058
18407
  response
18059
18408
  );
18060
18409
  return response;
18061
18410
  }
18062
- async function fetchHermesApi(fetcher, config, path33, init, options) {
18411
+ async function fetchHermesApi(fetcher, config, path34, init, options) {
18063
18412
  const headers = new Headers(init.headers);
18064
18413
  headers.set("accept", headers.get("accept") ?? "application/json");
18065
18414
  if (config.key) {
18066
18415
  headers.set("x-api-key", config.key);
18067
18416
  headers.set("authorization", `Bearer ${config.key}`);
18068
18417
  }
18069
- return await fetcher(`http://127.0.0.1:${config.port}${path33}`, {
18418
+ return await fetcher(`http://127.0.0.1:${config.port}${path34}`, {
18070
18419
  ...init,
18071
18420
  headers
18072
18421
  }).catch((error) => {
@@ -18075,10 +18424,10 @@ async function fetchHermesApi(fetcher, config, path33, init, options) {
18075
18424
  }
18076
18425
  void options.logger?.warn("hermes_api_server_connect_failed", {
18077
18426
  method: String(init.method ?? "GET").toUpperCase(),
18078
- path: path33,
18427
+ path: path34,
18079
18428
  profile: options.profileName ?? "default",
18080
18429
  port: config.port ?? null,
18081
- url: `http://127.0.0.1:${config.port}${path33}`,
18430
+ url: `http://127.0.0.1:${config.port}${path34}`,
18082
18431
  error: error instanceof Error ? error.message : String(error)
18083
18432
  });
18084
18433
  throw new LinkHttpError(
@@ -18088,10 +18437,10 @@ async function fetchHermesApi(fetcher, config, path33, init, options) {
18088
18437
  );
18089
18438
  });
18090
18439
  }
18091
- function logHermesApiResponse(logger, method, path33, profileName, startedAt, response) {
18440
+ function logHermesApiResponse(logger, method, path34, profileName, startedAt, response) {
18092
18441
  const fields = {
18093
18442
  method,
18094
- path: path33,
18443
+ path: path34,
18095
18444
  profile: profileName ?? "default",
18096
18445
  status: response.status,
18097
18446
  duration_ms: Date.now() - startedAt
@@ -18112,10 +18461,10 @@ async function logHermesApiFailureResponse(logger, fields, response) {
18112
18461
  ...upstreamError ? { upstream_error: upstreamError } : {}
18113
18462
  });
18114
18463
  }
18115
- function logHermesApiError(logger, method, path33, profileName, startedAt, error) {
18464
+ function logHermesApiError(logger, method, path34, profileName, startedAt, error) {
18116
18465
  void logger?.warn("hermes_api_request_failed", {
18117
18466
  method,
18118
- path: path33,
18467
+ path: path34,
18119
18468
  profile: profileName ?? "default",
18120
18469
  duration_ms: Date.now() - startedAt,
18121
18470
  ...error instanceof LinkHttpError ? { status: error.status, code: error.code } : {},
@@ -23989,6 +24338,7 @@ function isNodeError17(error, code) {
23989
24338
 
23990
24339
  // src/conversations/conversation-service.ts
23991
24340
  var ALL_CONVERSATION_EVENTS = "conversation:*";
24341
+ var HERMES_ARCHIVE_STATE_SYNC_ID = "hermes-agent-archive-state-v1";
23992
24342
  function isConversationNotificationEvent(event) {
23993
24343
  const type = event.type.toLowerCase();
23994
24344
  return type === "conversation.created" || type === "conversation.updated" || isAlwaysPublishedNotificationEvent(type) || type === "message.created" || type === "message.completed" || type === "message.failed" || type === "run.started" || type === "run.queued" || type === "run.completed" || type === "run.failed" || type === "run.cancelled" || type === "run.canceled" || type === "approval.requested" || readPayloadBool(event.payload, "requires_action") || readPayloadBool(event.payload, "requires_user_action") || readPayloadBool(event.payload, "requires_approval");
@@ -24115,6 +24465,7 @@ var ConversationService = class {
24115
24465
  return manifest;
24116
24466
  }
24117
24467
  const snapshot = await this.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
24468
+ await this.setHermesSessionsArchived(manifest, snapshot, false);
24118
24469
  const unarchivedAt = (/* @__PURE__ */ new Date()).toISOString();
24119
24470
  const next = {
24120
24471
  ...manifest,
@@ -24183,12 +24534,16 @@ var ConversationService = class {
24183
24534
  async createConversation(input = {}) {
24184
24535
  await this.store.ensureConversationsDir();
24185
24536
  const now = (/* @__PURE__ */ new Date()).toISOString();
24186
- const id = `conv_${randomUUID10().replaceAll("-", "")}`;
24537
+ const id = `conv_${randomUUID11().replaceAll("-", "")}`;
24187
24538
  const title = input.title?.trim() || DEFAULT_CONVERSATION_TITLE;
24188
24539
  const profile = await resolveConversationProfileTarget(
24189
24540
  this.paths,
24190
24541
  input.profileName
24191
24542
  );
24543
+ const workspaceId = await assertWorkspaceExists(
24544
+ this.paths,
24545
+ input.workspaceId
24546
+ );
24192
24547
  const manifest = {
24193
24548
  id,
24194
24549
  schema_version: 1,
@@ -24204,6 +24559,7 @@ var ConversationService = class {
24204
24559
  profile: profile.profileName,
24205
24560
  owner_account_id: input.accountId,
24206
24561
  owner_app_instance_id: input.appInstanceId,
24562
+ workspace_id: workspaceId ?? null,
24207
24563
  created_at: now,
24208
24564
  updated_at: now,
24209
24565
  last_event_seq: 0
@@ -24214,6 +24570,7 @@ var ConversationService = class {
24214
24570
  payload: {
24215
24571
  title,
24216
24572
  title_source: manifest.title_source,
24573
+ workspace_id: workspaceId ?? null,
24217
24574
  profile: {
24218
24575
  uid: profile.profileUid,
24219
24576
  name: profile.profileName,
@@ -24368,7 +24725,7 @@ var ConversationService = class {
24368
24725
  updated_at: now
24369
24726
  } : manifest;
24370
24727
  const message = {
24371
- id: `msg_${randomUUID10().replaceAll("-", "")}`,
24728
+ id: `msg_${randomUUID11().replaceAll("-", "")}`,
24372
24729
  schema_version: 1,
24373
24730
  conversation_id: manifest.id,
24374
24731
  role: "assistant",
@@ -24559,6 +24916,7 @@ var ConversationService = class {
24559
24916
  if (result.imported_count > 0 || result.reprojected_count > 0) {
24560
24917
  await this.rebuildStatisticsIndex();
24561
24918
  }
24919
+ await this.syncHermesArchiveStatesBestEffort();
24562
24920
  return result;
24563
24921
  })();
24564
24922
  this.hermesSessionSyncPromise = task;
@@ -25126,6 +25484,136 @@ var ConversationService = class {
25126
25484
  async unarchiveConversation(conversationId) {
25127
25485
  return this.maintenance.unarchiveConversation(conversationId);
25128
25486
  }
25487
+ listWorkspaces() {
25488
+ return listWorkspaces(this.paths);
25489
+ }
25490
+ createWorkspace(input) {
25491
+ return createWorkspace(this.paths, input);
25492
+ }
25493
+ renameWorkspace(workspaceId, input) {
25494
+ return renameWorkspace(this.paths, workspaceId, input);
25495
+ }
25496
+ async deleteWorkspace(workspaceId) {
25497
+ const workspace = await deleteWorkspace(this.paths, workspaceId);
25498
+ const movedConversationCount = await this.clearWorkspaceFromConversations(workspace.id);
25499
+ return { workspace, movedConversationCount };
25500
+ }
25501
+ async setConversationWorkspace(conversationId, workspaceId) {
25502
+ const normalizedWorkspaceId = await assertWorkspaceExists(
25503
+ this.paths,
25504
+ workspaceId
25505
+ );
25506
+ return this.withConversationLock(conversationId, async () => {
25507
+ const manifest = await this.store.readRunnableManifest(conversationId);
25508
+ const currentWorkspaceId = manifest.workspace_id ?? null;
25509
+ const nextWorkspaceId = normalizedWorkspaceId ?? null;
25510
+ const snapshot = await this.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
25511
+ if (currentWorkspaceId !== nextWorkspaceId) {
25512
+ const nextManifest = {
25513
+ ...manifest,
25514
+ workspace_id: nextWorkspaceId
25515
+ };
25516
+ await this.store.writeManifest(nextManifest);
25517
+ await this.persistConversationStatsWithoutManifestRewrite(
25518
+ nextManifest,
25519
+ snapshot
25520
+ );
25521
+ const conversation = await this.queries.summarizeConversation(
25522
+ nextManifest,
25523
+ snapshot
25524
+ );
25525
+ return {
25526
+ conversation_id: conversationId,
25527
+ workspace_id: nextWorkspaceId,
25528
+ conversation
25529
+ };
25530
+ }
25531
+ return {
25532
+ conversation_id: conversationId,
25533
+ workspace_id: currentWorkspaceId,
25534
+ conversation: await this.queries.summarizeConversation(
25535
+ manifest,
25536
+ snapshot
25537
+ )
25538
+ };
25539
+ });
25540
+ }
25541
+ async clearWorkspaceFromConversations(workspaceId) {
25542
+ const normalizedWorkspaceId = normalizeOptionalWorkspaceId(workspaceId);
25543
+ if (!normalizedWorkspaceId) {
25544
+ return 0;
25545
+ }
25546
+ let movedConversationCount = 0;
25547
+ for (const conversationId of await this.store.listConversationIds()) {
25548
+ await this.withConversationLock(conversationId, async () => {
25549
+ const manifest = await this.store.readManifest(conversationId).catch(() => null);
25550
+ if (!manifest || manifest.workspace_id !== normalizedWorkspaceId) {
25551
+ return;
25552
+ }
25553
+ const nextManifest = {
25554
+ ...manifest,
25555
+ workspace_id: null
25556
+ };
25557
+ await this.store.writeManifest(nextManifest);
25558
+ const snapshot = await this.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
25559
+ await this.persistConversationStatsWithoutManifestRewrite(
25560
+ nextManifest,
25561
+ snapshot
25562
+ );
25563
+ movedConversationCount += 1;
25564
+ });
25565
+ }
25566
+ return movedConversationCount;
25567
+ }
25568
+ async syncHermesArchiveStatesBestEffort() {
25569
+ const markerPath = this.hermesArchiveStateSyncMarkerPath();
25570
+ const marker = await readJsonFile(
25571
+ markerPath
25572
+ ).catch(() => null);
25573
+ if (marker?.id === HERMES_ARCHIVE_STATE_SYNC_ID) {
25574
+ return;
25575
+ }
25576
+ let failedCount = 0;
25577
+ for (const conversationId of await this.store.listConversationIds()) {
25578
+ const manifest = await this.store.readManifest(conversationId).catch(() => null);
25579
+ if (!manifest || manifest.status !== "archived") {
25580
+ continue;
25581
+ }
25582
+ const snapshot = await this.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
25583
+ await this.setHermesSessionsArchived(manifest, snapshot, true).catch(
25584
+ (error) => {
25585
+ failedCount += 1;
25586
+ void this.logger.debug("hermes_archive_state_sync_failed", {
25587
+ conversation_id: conversationId,
25588
+ archived: true,
25589
+ error: error instanceof Error ? error.message : String(error)
25590
+ });
25591
+ }
25592
+ );
25593
+ }
25594
+ if (failedCount > 0) {
25595
+ return;
25596
+ }
25597
+ await writeJsonFile(markerPath, {
25598
+ id: HERMES_ARCHIVE_STATE_SYNC_ID,
25599
+ completed_at: (/* @__PURE__ */ new Date()).toISOString()
25600
+ });
25601
+ }
25602
+ async setHermesSessionsArchived(manifest, snapshot, archived) {
25603
+ const targets = collectHermesSessionMutationTargets(manifest, snapshot);
25604
+ for (const target of targets) {
25605
+ await setTuiGatewaySessionArchived({
25606
+ paths: this.paths,
25607
+ logger: this.logger,
25608
+ profileName: target.profileName,
25609
+ sessionId: target.sessionId,
25610
+ archived
25611
+ });
25612
+ }
25613
+ }
25614
+ hermesArchiveStateSyncMarkerPath() {
25615
+ return path24.join(this.paths.indexesDir, "hermes-archive-state-sync.json");
25616
+ }
25129
25617
  prepareClearAllConversationPlan(targetStatus) {
25130
25618
  return this.maintenance.prepareClearAllConversationPlan(targetStatus);
25131
25619
  }
@@ -25162,45 +25650,15 @@ var ConversationService = class {
25162
25650
  if (!manifest || !conversationMatchesProfile(manifest, profileName, profileUid)) {
25163
25651
  continue;
25164
25652
  }
25165
- const deleted = await this.withConversationLock(
25166
- conversationId,
25167
- async () => {
25168
- const lockedManifest = await this.store.readManifest(conversationId).catch(() => null);
25169
- if (!lockedManifest || !conversationMatchesProfile(lockedManifest, profileName, profileUid)) {
25170
- return false;
25171
- }
25172
- this.abortActiveRunsForConversation(conversationId);
25173
- const snapshot = await this.store.readSnapshot(conversationId).catch(() => emptySnapshot2());
25174
- const blobIds = /* @__PURE__ */ new Set([
25175
- ...collectBlobIds(snapshot),
25176
- ...await listConversationBlobIds(this.paths, conversationId).catch(
25177
- () => []
25178
- )
25179
- ]);
25180
- for (const blobId of blobIds) {
25181
- await pruneConversationBlobReference(
25182
- this.paths,
25183
- conversationId,
25184
- blobId
25185
- ).catch((error) => {
25186
- void this.logger.warn("profile_delete_blob_gc_failed", {
25187
- profile: profileName,
25188
- conversation_id: conversationId,
25189
- blob_id: blobId,
25190
- error: error instanceof Error ? error.message : String(error)
25191
- });
25192
- });
25193
- }
25194
- await removeConversationFiles(this.paths, conversationId);
25195
- await removeConversationDeliveryStaging(
25196
- this.paths,
25197
- conversationId
25198
- ).catch(() => void 0);
25199
- return true;
25200
- }
25201
- );
25202
- if (deleted) {
25653
+ try {
25654
+ await this.deleteConversation(conversationId);
25203
25655
  deletedConversationIds.push(conversationId);
25656
+ } catch (error) {
25657
+ void this.logger.warn("profile_delete_conversation_cleanup_failed", {
25658
+ profile: profileName,
25659
+ conversation_id: conversationId,
25660
+ error: error instanceof Error ? error.message : String(error)
25661
+ });
25204
25662
  }
25205
25663
  }
25206
25664
  await deleteConversationStatsForProfile(this.paths, {
@@ -25231,6 +25689,13 @@ var ConversationService = class {
25231
25689
  statsOverride
25232
25690
  );
25233
25691
  }
25692
+ async persistConversationStatsWithoutManifestRewrite(manifest, snapshot) {
25693
+ const stats = manifest.stats ?? buildConversationStats(manifest, snapshot);
25694
+ await upsertConversationStats(
25695
+ this.paths,
25696
+ toStatsIndexRecord({ ...manifest, stats }, stats)
25697
+ );
25698
+ }
25234
25699
  async appendEvent(conversationId, input) {
25235
25700
  const event = await this.store.appendEvent(conversationId, input);
25236
25701
  this.emitter.emit(this.liveEventName(conversationId), event);
@@ -25345,7 +25810,7 @@ function approvalRestartText(language, zh, en) {
25345
25810
  }
25346
25811
 
25347
25812
  // src/security/devices.ts
25348
- import { randomBytes as randomBytes3, randomUUID as randomUUID11, timingSafeEqual, createHash as createHash7 } from "crypto";
25813
+ import { randomBytes as randomBytes3, randomUUID as randomUUID12, timingSafeEqual, createHash as createHash7 } from "crypto";
25349
25814
  var ACCESS_TOKEN_TTL_MS = 15 * 60 * 1e3;
25350
25815
  var REFRESH_TOKEN_TTL_MS = 90 * 24 * 60 * 60 * 1e3;
25351
25816
  var DEVICE_SEEN_WRITE_INTERVAL_MS = 60 * 60 * 1e3;
@@ -25363,7 +25828,7 @@ async function createDeviceSession(input, paths = resolveRuntimePaths()) {
25363
25828
  }
25364
25829
  }
25365
25830
  const device = {
25366
- id: `dev_${randomUUID11().replaceAll("-", "")}`,
25831
+ id: `dev_${randomUUID12().replaceAll("-", "")}`,
25367
25832
  label: normalizeDeviceLabel(input.label),
25368
25833
  platform: normalizeDevicePlatform(input.platform),
25369
25834
  model: normalizeDeviceModel(input.model),
@@ -26240,7 +26705,8 @@ function registerConversationRoutes(router, options) {
26240
26705
  ctx.set("cache-control", "no-store");
26241
26706
  const result = await conversations.listConversationPage({
26242
26707
  limit: readLimit(ctx.query.limit),
26243
- cursor
26708
+ cursor,
26709
+ workspace: readConversationWorkspaceFilter(ctx.query)
26244
26710
  });
26245
26711
  const localized = localizeConversationListPage(result, language);
26246
26712
  ctx.body = {
@@ -26309,7 +26775,8 @@ function registerConversationRoutes(router, options) {
26309
26775
  title: readString19(body, "title") ?? void 0,
26310
26776
  profileName: readOptionalProfileName(body),
26311
26777
  accountId: auth.accountId,
26312
- appInstanceId: auth.appInstanceId
26778
+ appInstanceId: auth.appInstanceId,
26779
+ workspaceId: readConversationWorkspaceId(body)
26313
26780
  }),
26314
26781
  language
26315
26782
  )
@@ -26691,6 +27158,21 @@ function registerConversationRoutes(router, options) {
26691
27158
  };
26692
27159
  }
26693
27160
  );
27161
+ router.patch("/api/v1/conversations/:conversationId/workspace", async (ctx) => {
27162
+ await authenticateRequest(ctx, paths);
27163
+ const language = readPreferredLanguage(ctx);
27164
+ const body = await readJsonBody(ctx.req);
27165
+ ctx.body = {
27166
+ ok: true,
27167
+ ...localizeConversationResult(
27168
+ await conversations.setConversationWorkspace(
27169
+ ctx.params.conversationId,
27170
+ readConversationWorkspaceId(body)
27171
+ ),
27172
+ language
27173
+ )
27174
+ };
27175
+ });
26694
27176
  router.delete("/api/v1/conversations/:conversationId", async (ctx) => {
26695
27177
  const auth = await authenticateRequest(ctx, paths);
26696
27178
  const result = await conversations.deleteConversation(
@@ -26784,6 +27266,25 @@ function readConversationListForce(query) {
26784
27266
  const raw = Array.isArray(query.force) ? query.force[0] : query.force;
26785
27267
  return readBoolean3(raw) === true;
26786
27268
  }
27269
+ function readConversationWorkspaceFilter(query) {
27270
+ const workspace = readQueryString(query.workspace) ?? readQueryString(query.workspace_id);
27271
+ if (!workspace || workspace === "all") {
27272
+ return { kind: "all" };
27273
+ }
27274
+ if (workspace === "default") {
27275
+ return { kind: "default" };
27276
+ }
27277
+ return { kind: "workspace", workspaceId: workspace };
27278
+ }
27279
+ function readConversationWorkspaceId(body) {
27280
+ if (Object.prototype.hasOwnProperty.call(body, "workspace_id") || Object.prototype.hasOwnProperty.call(body, "workspaceId")) {
27281
+ return readString19(body, "workspace_id") ?? readString19(body, "workspaceId");
27282
+ }
27283
+ if (Object.prototype.hasOwnProperty.call(body, "workspace")) {
27284
+ return readString19(body, "workspace");
27285
+ }
27286
+ return void 0;
27287
+ }
26787
27288
  function localizeConversationListPage(page, language) {
26788
27289
  return {
26789
27290
  ...page,
@@ -26997,11 +27498,11 @@ function isSseRequestContext(ctx) {
26997
27498
  }
26998
27499
  return isSseRequestPath(ctx.path) || isActiveSseSocket(ctx.req.socket);
26999
27500
  }
27000
- function isSseRequestPath(path33) {
27001
- if (!path33) {
27501
+ function isSseRequestPath(path34) {
27502
+ if (!path34) {
27002
27503
  return false;
27003
27504
  }
27004
- return path33 === "/api/v1/conversations/events" || path33 === "/api/v1/profile-creation/events" || path33 === "/api/v1/hermes/update/events" || path33 === "/api/v1/link/update/events" || /^\/api\/v1\/conversations\/[^/]+\/events$/u.test(path33) || /^\/api\/v1\/runs\/[^/]+\/events$/u.test(path33);
27505
+ return path34 === "/api/v1/conversations/events" || path34 === "/api/v1/profile-creation/events" || path34 === "/api/v1/hermes/update/events" || path34 === "/api/v1/link/update/events" || /^\/api\/v1\/conversations\/[^/]+\/events$/u.test(path34) || /^\/api\/v1\/runs\/[^/]+\/events$/u.test(path34);
27005
27506
  }
27006
27507
  function isExpectedClientDisconnectError2(error, options = {}) {
27007
27508
  if (!(error instanceof Error)) {
@@ -27833,7 +28334,7 @@ function errorMessage3(error) {
27833
28334
 
27834
28335
  // src/hermes/profile-identity.ts
27835
28336
  import { readFile as readFile16, stat as stat17 } from "fs/promises";
27836
- import path24 from "path";
28337
+ import path25 from "path";
27837
28338
  var MAX_SOUL_MD_LENGTH = 2e4;
27838
28339
  async function readHermesProfileIdentity(profileName, paths) {
27839
28340
  await assertProfileExists3(profileName, paths);
@@ -27890,7 +28391,7 @@ async function assertProfileExists3(profileName, paths) {
27890
28391
  }
27891
28392
  }
27892
28393
  function resolveSoulPath(profileName) {
27893
- return path24.join(resolveHermesProfileDir(profileName), "SOUL.md");
28394
+ return path25.join(resolveHermesProfileDir(profileName), "SOUL.md");
27894
28395
  }
27895
28396
  function isNodeError18(error, code) {
27896
28397
  return error instanceof Error && "code" in error && error.code === code;
@@ -27906,13 +28407,13 @@ import {
27906
28407
  rm as rm7,
27907
28408
  stat as stat19
27908
28409
  } from "fs/promises";
27909
- import path26 from "path";
28410
+ import path27 from "path";
27910
28411
  import YAML5 from "yaml";
27911
28412
 
27912
28413
  // src/hermes/link-skill.ts
27913
28414
  import { readFile as readFile17, stat as stat18 } from "fs/promises";
27914
28415
  import os5 from "os";
27915
- import path25 from "path";
28416
+ import path26 from "path";
27916
28417
  import YAML4 from "yaml";
27917
28418
  var HERMES_LINK_SKILL_ROOT_DIR = "hermes-skills";
27918
28419
  var HERMES_LINK_SKILL_DIR = "hermes-link";
@@ -27997,7 +28498,7 @@ Do not modify Hermes profiles, delete user data, edit config files, or kill proc
27997
28498
  async function ensureHermesLinkSkillInstalledForProfiles(options = {}) {
27998
28499
  const paths = options.paths ?? resolveRuntimePaths();
27999
28500
  const externalDir = resolveHermesLinkSkillExternalDir(paths);
28000
- const skillPath = path25.join(
28501
+ const skillPath = path26.join(
28001
28502
  externalDir,
28002
28503
  HERMES_LINK_SKILL_DIR,
28003
28504
  HERMES_LINK_SKILL_FILE
@@ -28079,7 +28580,7 @@ function withDefaultProfilePlaceholder2(profiles) {
28079
28580
  ];
28080
28581
  }
28081
28582
  function resolveHermesLinkSkillExternalDir(paths = resolveRuntimePaths()) {
28082
- return path25.join(paths.homeDir, HERMES_LINK_SKILL_ROOT_DIR);
28583
+ return path26.join(paths.homeDir, HERMES_LINK_SKILL_ROOT_DIR);
28083
28584
  }
28084
28585
  async function writeHermesLinkSkill(skillPath) {
28085
28586
  const existing = await readFile17(skillPath, "utf8").catch((error) => {
@@ -28174,11 +28675,11 @@ function appendExternalDir(current, externalDir, hermesHome) {
28174
28675
  const seen = new Set(
28175
28676
  entries.map((entry) => resolveExternalDirEntry(entry, hermesHome))
28176
28677
  );
28177
- const normalizedExternalDir = path25.resolve(externalDir);
28678
+ const normalizedExternalDir = path26.resolve(externalDir);
28178
28679
  return seen.has(normalizedExternalDir) ? entries : [...entries, normalizedExternalDir];
28179
28680
  }
28180
28681
  function externalDirsInclude(current, externalDir, hermesHome) {
28181
- const normalizedExternalDir = path25.resolve(externalDir);
28682
+ const normalizedExternalDir = path26.resolve(externalDir);
28182
28683
  return readExternalDirEntries(current).some(
28183
28684
  (entry) => resolveExternalDirEntry(entry, hermesHome) === normalizedExternalDir
28184
28685
  );
@@ -28189,14 +28690,14 @@ function readExternalDirEntries(value) {
28189
28690
  }
28190
28691
  function resolveExternalDirEntry(entry, hermesHome) {
28191
28692
  const expanded = expandHome(expandEnvVars(entry));
28192
- return path25.resolve(path25.isAbsolute(expanded) ? expanded : path25.join(hermesHome, expanded));
28693
+ return path26.resolve(path26.isAbsolute(expanded) ? expanded : path26.join(hermesHome, expanded));
28193
28694
  }
28194
28695
  function expandHome(value) {
28195
28696
  if (value === "~") {
28196
28697
  return os5.homedir();
28197
28698
  }
28198
- if (value.startsWith(`~${path25.sep}`) || value.startsWith("~/")) {
28199
- return path25.join(os5.homedir(), value.slice(2));
28699
+ if (value.startsWith(`~${path26.sep}`) || value.startsWith("~/")) {
28700
+ return path26.join(os5.homedir(), value.slice(2));
28200
28701
  }
28201
28702
  return value;
28202
28703
  }
@@ -28748,7 +29249,7 @@ function collectEnvKeys(value, keys = /* @__PURE__ */ new Set()) {
28748
29249
  return keys;
28749
29250
  }
28750
29251
  async function writeEnvValues(profileName, values) {
28751
- const envPath = path26.join(resolveHermesProfileDir(profileName), ".env");
29252
+ const envPath = path27.join(resolveHermesProfileDir(profileName), ".env");
28752
29253
  const existingRaw = await readFile18(envPath, "utf8").catch((error) => {
28753
29254
  if (isNodeError20(error, "ENOENT")) {
28754
29255
  return "";
@@ -28785,8 +29286,8 @@ async function writeEnvValues(profileName, values) {
28785
29286
  await atomicWriteFilePreservingMetadata(envPath, nextRaw);
28786
29287
  }
28787
29288
  async function copySkills(sourceProfile, targetProfile) {
28788
- const sourceSkills = path26.join(resolveHermesProfileDir(sourceProfile), "skills");
28789
- const targetSkills = path26.join(resolveHermesProfileDir(targetProfile), "skills");
29289
+ const sourceSkills = path27.join(resolveHermesProfileDir(sourceProfile), "skills");
29290
+ const targetSkills = path27.join(resolveHermesProfileDir(targetProfile), "skills");
28790
29291
  if (!await pathExists2(sourceSkills)) {
28791
29292
  return;
28792
29293
  }
@@ -28881,10 +29382,10 @@ async function readProfileCreationLogLines(paths) {
28881
29382
  );
28882
29383
  }
28883
29384
  function profileCreationStatePath(paths) {
28884
- return path26.join(paths.runDir, "profile-create-state.json");
29385
+ return path27.join(paths.runDir, "profile-create-state.json");
28885
29386
  }
28886
29387
  function profileCreationLogPath(paths) {
28887
- return path26.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
29388
+ return path27.join(paths.logsDir, PROFILE_CREATE_LOG_FILE);
28888
29389
  }
28889
29390
  async function clearProfileCreationLogFiles(paths) {
28890
29391
  const primary = profileCreationLogPath(paths);
@@ -29173,7 +29674,7 @@ import {
29173
29674
  readFile as readFile19,
29174
29675
  stat as stat20
29175
29676
  } from "fs/promises";
29176
- import path27 from "path";
29677
+ import path28 from "path";
29177
29678
  import YAML6 from "yaml";
29178
29679
  var ENTRY_DELIMITER = "\n\xA7\n";
29179
29680
  var DEFAULT_MEMORY_LIMIT = 2200;
@@ -29504,7 +30005,7 @@ async function saveProviderSettings(profileName, provider, patch) {
29504
30005
  });
29505
30006
  await patchJsonProviderConfig(
29506
30007
  profileName,
29507
- path27.join("hindsight", "config.json"),
30008
+ path28.join("hindsight", "config.json"),
29508
30009
  {
29509
30010
  mode: patch.mode,
29510
30011
  api_url: patch.apiUrl,
@@ -29707,7 +30208,7 @@ async function patchHermesMemoryLimits(profileName, patch) {
29707
30208
  await atomicWriteFilePreservingMetadata(configPath, document.toString());
29708
30209
  }
29709
30210
  function resolveMemoryDir(profileName) {
29710
- return path27.join(resolveHermesProfileDir(profileName), "memories");
30211
+ return path28.join(resolveHermesProfileDir(profileName), "memories");
29711
30212
  }
29712
30213
  async function readMemoryStore(profileName, target, limits) {
29713
30214
  const filePath = memoryFilePath(profileName, target);
@@ -29768,7 +30269,7 @@ async function writeMemoryEntries(profileName, target, entries) {
29768
30269
  );
29769
30270
  }
29770
30271
  function memoryFilePath(profileName, target) {
29771
- return path27.join(
30272
+ return path28.join(
29772
30273
  resolveMemoryDir(profileName),
29773
30274
  target === "user" ? "USER.md" : "MEMORY.md"
29774
30275
  );
@@ -29828,7 +30329,7 @@ async function readCustomProviderSetupSummary(profileName) {
29828
30329
  configurable: true,
29829
30330
  configured: true,
29830
30331
  configurationIssue: null,
29831
- providerConfigPath: path27.join(
30332
+ providerConfigPath: path28.join(
29832
30333
  resolveHermesProfileDir(profileName),
29833
30334
  "<provider>.json"
29834
30335
  ),
@@ -30222,7 +30723,7 @@ async function readProviderSettings(profileName, provider) {
30222
30723
  stringSetting(
30223
30724
  "dbPath",
30224
30725
  "SQLite \u6570\u636E\u5E93\u8DEF\u5F84",
30225
- config.db_path ?? path27.join(resolveHermesProfileDir(profileName), "memory_store.db")
30726
+ config.db_path ?? path28.join(resolveHermesProfileDir(profileName), "memory_store.db")
30226
30727
  ),
30227
30728
  booleanSetting("autoExtract", "\u4F1A\u8BDD\u7ED3\u675F\u81EA\u52A8\u62BD\u53D6", config.auto_extract ?? false),
30228
30729
  numberSetting("defaultTrust", "\u9ED8\u8BA4\u4FE1\u4EFB\u5206", config.default_trust ?? 0.5),
@@ -30258,7 +30759,7 @@ async function readProviderSettings(profileName, provider) {
30258
30759
  stringSetting(
30259
30760
  "workingDirectory",
30260
30761
  "\u5DE5\u4F5C\u76EE\u5F55",
30261
- path27.join(resolveHermesProfileDir(profileName), "byterover"),
30762
+ path28.join(resolveHermesProfileDir(profileName), "byterover"),
30262
30763
  false
30263
30764
  )
30264
30765
  ];
@@ -30267,16 +30768,16 @@ async function readProviderSettings(profileName, provider) {
30267
30768
  }
30268
30769
  function memoryProviderConfigPath(profileName, provider) {
30269
30770
  if (provider === "honcho") {
30270
- return path27.join(resolveHermesProfileDir(profileName), "honcho.json");
30771
+ return path28.join(resolveHermesProfileDir(profileName), "honcho.json");
30271
30772
  }
30272
30773
  if (provider === "mem0") {
30273
- return path27.join(resolveHermesProfileDir(profileName), "mem0.json");
30774
+ return path28.join(resolveHermesProfileDir(profileName), "mem0.json");
30274
30775
  }
30275
30776
  if (provider === "supermemory") {
30276
- return path27.join(resolveHermesProfileDir(profileName), "supermemory.json");
30777
+ return path28.join(resolveHermesProfileDir(profileName), "supermemory.json");
30277
30778
  }
30278
30779
  if (provider === "hindsight") {
30279
- return path27.join(
30780
+ return path28.join(
30280
30781
  resolveHermesProfileDir(profileName),
30281
30782
  "hindsight",
30282
30783
  "config.json"
@@ -30285,13 +30786,13 @@ function memoryProviderConfigPath(profileName, provider) {
30285
30786
  return null;
30286
30787
  }
30287
30788
  function customProviderConfigPath(profileName, provider) {
30288
- return path27.join(
30789
+ return path28.join(
30289
30790
  resolveHermesProfileDir(profileName),
30290
30791
  `${normalizeCustomProviderId(provider)}.json`
30291
30792
  );
30292
30793
  }
30293
30794
  function customProviderRegistryPath(profileName) {
30294
- return path27.join(
30795
+ return path28.join(
30295
30796
  resolveHermesProfileDir(profileName),
30296
30797
  CUSTOM_PROVIDER_REGISTRY_FILE
30297
30798
  );
@@ -30343,7 +30844,7 @@ async function saveCustomProviderRegistryEntry(profileName, provider) {
30343
30844
  );
30344
30845
  }
30345
30846
  async function discoverUserMemoryProviderDescriptors(profileName) {
30346
- const pluginsDir = path27.join(resolveHermesProfileDir(profileName), "plugins");
30847
+ const pluginsDir = path28.join(resolveHermesProfileDir(profileName), "plugins");
30347
30848
  const entries = await readdir10(pluginsDir, { withFileTypes: true }).catch(
30348
30849
  (error) => {
30349
30850
  if (isNodeError21(error, "ENOENT")) {
@@ -30363,7 +30864,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
30363
30864
  } catch {
30364
30865
  continue;
30365
30866
  }
30366
- const providerDir = path27.join(pluginsDir, entry.name);
30867
+ const providerDir = path28.join(pluginsDir, entry.name);
30367
30868
  if (!await isMemoryProviderPluginDir(providerDir)) {
30368
30869
  continue;
30369
30870
  }
@@ -30377,7 +30878,7 @@ async function discoverUserMemoryProviderDescriptors(profileName) {
30377
30878
  return descriptors;
30378
30879
  }
30379
30880
  async function isUserMemoryProviderInstalled(profileName, provider) {
30380
- const providerDir = path27.join(
30881
+ const providerDir = path28.join(
30381
30882
  resolveHermesProfileDir(profileName),
30382
30883
  "plugins",
30383
30884
  normalizeCustomProviderId(provider)
@@ -30385,7 +30886,7 @@ async function isUserMemoryProviderInstalled(profileName, provider) {
30385
30886
  return isMemoryProviderPluginDir(providerDir);
30386
30887
  }
30387
30888
  async function isMemoryProviderPluginDir(providerDir) {
30388
- const source = await readFile19(path27.join(providerDir, "__init__.py"), "utf8").catch(
30889
+ const source = await readFile19(path28.join(providerDir, "__init__.py"), "utf8").catch(
30389
30890
  (error) => {
30390
30891
  if (isNodeError21(error, "ENOENT")) {
30391
30892
  return "";
@@ -30397,7 +30898,7 @@ async function isMemoryProviderPluginDir(providerDir) {
30397
30898
  return sample.includes("register_memory_provider") || sample.includes("MemoryProvider");
30398
30899
  }
30399
30900
  async function readPluginMetadata(providerDir) {
30400
- const raw = await readFile19(path27.join(providerDir, "plugin.yaml"), "utf8").catch(
30901
+ const raw = await readFile19(path28.join(providerDir, "plugin.yaml"), "utf8").catch(
30401
30902
  (error) => {
30402
30903
  if (isNodeError21(error, "ENOENT")) {
30403
30904
  return "";
@@ -30409,10 +30910,10 @@ async function readPluginMetadata(providerDir) {
30409
30910
  }
30410
30911
  async function resolveByteRoverCli() {
30411
30912
  const candidates = [
30412
- ...(process.env.PATH ?? "").split(path27.delimiter).filter(Boolean).map((dir) => path27.join(dir, "brv")),
30413
- path27.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
30913
+ ...(process.env.PATH ?? "").split(path28.delimiter).filter(Boolean).map((dir) => path28.join(dir, "brv")),
30914
+ path28.join(process.env.HOME ?? "", ".brv-cli", "bin", "brv"),
30414
30915
  "/usr/local/bin/brv",
30415
- path27.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
30916
+ path28.join(process.env.HOME ?? "", ".npm-global", "bin", "brv")
30416
30917
  ].filter(Boolean);
30417
30918
  for (const candidate of candidates) {
30418
30919
  const found = await access3(candidate).then(() => true).catch(() => false);
@@ -30473,7 +30974,7 @@ async function patchHermesMemoryEnv(profileName, patch) {
30473
30974
  if (entries.length === 0) {
30474
30975
  return;
30475
30976
  }
30476
- const envPath = path27.join(resolveHermesProfileDir(profileName), ".env");
30977
+ const envPath = path28.join(resolveHermesProfileDir(profileName), ".env");
30477
30978
  const existingRaw = await readFile19(envPath, "utf8").catch((error) => {
30478
30979
  if (isNodeError21(error, "ENOENT")) {
30479
30980
  return "";
@@ -30644,7 +31145,7 @@ async function readActiveMemoryProvider(profileName) {
30644
31145
  return provider;
30645
31146
  }
30646
31147
  async function patchJsonProviderConfig(profileName, relativePath, patch) {
30647
- const configPath = path27.join(
31148
+ const configPath = path28.join(
30648
31149
  resolveHermesProfileDir(profileName),
30649
31150
  relativePath
30650
31151
  );
@@ -30673,7 +31174,7 @@ async function readJsonObject(filePath) {
30673
31174
  } catch {
30674
31175
  throw new HermesMemoryError(
30675
31176
  "memory_provider_config_invalid",
30676
- `${path27.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
31177
+ `${path28.basename(filePath)} \u4E0D\u662F\u6709\u6548\u7684 JSON \u914D\u7F6E\u6587\u4EF6\u3002`
30677
31178
  );
30678
31179
  }
30679
31180
  }
@@ -31297,7 +31798,7 @@ function toMemoryHttpError(error) {
31297
31798
 
31298
31799
  // src/hermes/skills.ts
31299
31800
  import { readFile as readFile20, readdir as readdir11 } from "fs/promises";
31300
- import path28 from "path";
31801
+ import path29 from "path";
31301
31802
  import YAML7 from "yaml";
31302
31803
  var HermesSkillNotFoundError = class extends Error {
31303
31804
  constructor(skillName) {
@@ -31311,7 +31812,7 @@ var EXCLUDED_SKILL_DIRS = /* @__PURE__ */ new Set([".git", ".github", ".hub"]);
31311
31812
  async function listHermesProfileSkills(profileName, paths = resolveRuntimePaths()) {
31312
31813
  const profile = await readExistingProfile(profileName, paths);
31313
31814
  const profileDir = resolveHermesProfileDir(profile.name);
31314
- const skillsRoot = path28.join(profileDir, "skills");
31815
+ const skillsRoot = path29.join(profileDir, "skills");
31315
31816
  const [skillFiles, disabled, provenance] = await Promise.all([
31316
31817
  findSkillFiles(skillsRoot),
31317
31818
  readDisabledSkillNames(resolveHermesConfigPath(profile.name)),
@@ -31402,7 +31903,7 @@ async function collectSkillFiles(directory, results) {
31402
31903
  if (EXCLUDED_SKILL_DIRS.has(entry.name)) {
31403
31904
  continue;
31404
31905
  }
31405
- const entryPath = path28.join(directory, entry.name);
31906
+ const entryPath = path29.join(directory, entry.name);
31406
31907
  if (entry.isDirectory()) {
31407
31908
  await collectSkillFiles(entryPath, results);
31408
31909
  continue;
@@ -31424,10 +31925,10 @@ async function readSkillMetadata(input) {
31424
31925
  if (raw === null) {
31425
31926
  return null;
31426
31927
  }
31427
- const skillDir = path28.dirname(input.skillFile);
31928
+ const skillDir = path29.dirname(input.skillFile);
31428
31929
  const { frontmatter, body } = parseSkillDocument(raw.slice(0, 4e3));
31429
31930
  const name = normalizeSkillName(
31430
- readString21(frontmatter.name) ?? path28.basename(skillDir)
31931
+ readString21(frontmatter.name) ?? path29.basename(skillDir)
31431
31932
  );
31432
31933
  if (!name) {
31433
31934
  return null;
@@ -31446,7 +31947,7 @@ async function readSkillMetadata(input) {
31446
31947
  enabled: !input.disabled.has(name),
31447
31948
  source: provenance.source,
31448
31949
  trust: provenance.trust,
31449
- relativePath: path28.relative(input.skillsRoot, skillDir)
31950
+ relativePath: path29.relative(input.skillsRoot, skillDir)
31450
31951
  };
31451
31952
  }
31452
31953
  function parseSkillDocument(raw) {
@@ -31467,8 +31968,8 @@ function parseSkillDocument(raw) {
31467
31968
  }
31468
31969
  }
31469
31970
  function categoryFromPath(skillsRoot, skillFile) {
31470
- const relative = path28.relative(skillsRoot, skillFile);
31471
- const parts = relative.split(path28.sep).filter(Boolean);
31971
+ const relative = path29.relative(skillsRoot, skillFile);
31972
+ const parts = relative.split(path29.sep).filter(Boolean);
31472
31973
  return parts.length >= 3 ? parts[0] : null;
31473
31974
  }
31474
31975
  function firstBodyDescription(body) {
@@ -31515,7 +32016,7 @@ async function readSkillProvenance(root) {
31515
32016
  return provenance;
31516
32017
  }
31517
32018
  async function readBundledSkillNames(root) {
31518
- const raw = await readFile20(path28.join(root, ".bundled_manifest"), "utf8").catch(
32019
+ const raw = await readFile20(path29.join(root, ".bundled_manifest"), "utf8").catch(
31519
32020
  (error) => {
31520
32021
  if (isNodeError22(error, "ENOENT")) {
31521
32022
  return "";
@@ -31538,7 +32039,7 @@ async function readBundledSkillNames(root) {
31538
32039
  return names;
31539
32040
  }
31540
32041
  async function readHubInstalledSkills(root) {
31541
- const raw = await readFile20(path28.join(root, ".hub", "lock.json"), "utf8").catch(
32042
+ const raw = await readFile20(path29.join(root, ".hub", "lock.json"), "utf8").catch(
31542
32043
  (error) => {
31543
32044
  if (isNodeError22(error, "ENOENT")) {
31544
32045
  return "";
@@ -32201,7 +32702,7 @@ function readModelList(payload) {
32201
32702
  import { EventEmitter as EventEmitter3 } from "events";
32202
32703
  import { spawn as spawn4 } from "child_process";
32203
32704
  import { mkdir as mkdir13, readFile as readFile21, rm as rm8 } from "fs/promises";
32204
- import path29 from "path";
32705
+ import path30 from "path";
32205
32706
  var SERVER_HERMES_RELEASES_LATEST_PATH = "/api/v1/hermes-agent/releases/latest";
32206
32707
  var RELEASE_CACHE_TTL_MS = 6 * 60 * 60 * 1e3;
32207
32708
  var RELEASE_FETCH_TIMEOUT_MS = 5e3;
@@ -32479,13 +32980,13 @@ async function readUpdateLogLines(paths) {
32479
32980
  );
32480
32981
  }
32481
32982
  function releaseCachePath(paths) {
32482
- return path29.join(paths.indexesDir, "hermes-release-check.json");
32983
+ return path30.join(paths.indexesDir, "hermes-release-check.json");
32483
32984
  }
32484
32985
  function updateStatePath(paths) {
32485
- return path29.join(paths.runDir, "hermes-update-state.json");
32986
+ return path30.join(paths.runDir, "hermes-update-state.json");
32486
32987
  }
32487
32988
  function updateLogPath(paths) {
32488
- return path29.join(paths.logsDir, UPDATE_LOG_FILE);
32989
+ return path30.join(paths.logsDir, UPDATE_LOG_FILE);
32489
32990
  }
32490
32991
  async function clearUpdateLogFiles(paths) {
32491
32992
  const primary = updateLogPath(paths);
@@ -32586,12 +33087,12 @@ function readString22(payload, key) {
32586
33087
  import { spawn as spawn6 } from "child_process";
32587
33088
  import { EventEmitter as EventEmitter4 } from "events";
32588
33089
  import { mkdir as mkdir16, readFile as readFile23, rm as rm11 } from "fs/promises";
32589
- import path31 from "path";
33090
+ import path32 from "path";
32590
33091
 
32591
33092
  // src/daemon/process.ts
32592
33093
  import { spawn as spawn5 } from "child_process";
32593
33094
  import { mkdir as mkdir15, readFile as readFile22, rm as rm10, writeFile as writeFile4 } from "fs/promises";
32594
- import path30 from "path";
33095
+ import path31 from "path";
32595
33096
 
32596
33097
  // src/daemon/service.ts
32597
33098
  import { createServer } from "http";
@@ -34538,7 +35039,7 @@ async function runDaemonSupervisor(paths = resolveRuntimePaths()) {
34538
35039
  await mkdir15(paths.logsDir, { recursive: true, mode: 448 });
34539
35040
  const log = createRotatingTextLogWriter({
34540
35041
  paths,
34541
- fileName: path30.basename(daemonLogFile(paths))
35042
+ fileName: path31.basename(daemonLogFile(paths))
34542
35043
  });
34543
35044
  const scriptPath = currentCliScriptPath();
34544
35045
  const write = (chunk) => {
@@ -34836,7 +35337,7 @@ function terminateChild(child, previousForceKillTimer) {
34836
35337
  }
34837
35338
  }
34838
35339
  function supervisorStopIntentPath(paths) {
34839
- return path30.join(paths.runDir, "supervisor-stop-intent.json");
35340
+ return path31.join(paths.runDir, "supervisor-stop-intent.json");
34840
35341
  }
34841
35342
  async function writeSupervisorStopIntent(paths, pid) {
34842
35343
  await mkdir15(paths.runDir, { recursive: true, mode: 448 });
@@ -35399,7 +35900,7 @@ async function buildOfficialInstallCommand(options, targetVersion) {
35399
35900
  };
35400
35901
  }
35401
35902
  function buildUnixInstallCommand(installerUrl) {
35402
- const nodeBinDir = path31.dirname(process.execPath);
35903
+ const nodeBinDir = path32.dirname(process.execPath);
35403
35904
  const fetchScript = [
35404
35905
  quoteShellToken(process.execPath),
35405
35906
  "--input-type=module",
@@ -35669,10 +36170,10 @@ async function readUpdateLogLines2(paths) {
35669
36170
  );
35670
36171
  }
35671
36172
  function updateStatePath2(paths) {
35672
- return path31.join(paths.runDir, "link-update-state.json");
36173
+ return path32.join(paths.runDir, "link-update-state.json");
35673
36174
  }
35674
36175
  function updateLogPath2(paths) {
35675
- return path31.join(paths.logsDir, UPDATE_LOG_FILE2);
36176
+ return path32.join(paths.logsDir, UPDATE_LOG_FILE2);
35676
36177
  }
35677
36178
  async function clearUpdateLogFiles2(paths) {
35678
36179
  const primary = updateLogPath2(paths);
@@ -35756,7 +36257,7 @@ function readString23(payload, key) {
35756
36257
  }
35757
36258
 
35758
36259
  // src/pairing/pairing.ts
35759
- import path32 from "path";
36260
+ import path33 from "path";
35760
36261
  import { rm as rm12 } from "fs/promises";
35761
36262
 
35762
36263
  // src/relay/bootstrap.ts
@@ -36096,10 +36597,10 @@ async function loadRequiredIdentity2(paths) {
36096
36597
  }
36097
36598
  return identity;
36098
36599
  }
36099
- async function postServerJson(serverBaseUrl, path33, body, options) {
36600
+ async function postServerJson(serverBaseUrl, path34, body, options) {
36100
36601
  let response;
36101
36602
  try {
36102
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path33}`, {
36603
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path34}`, {
36103
36604
  method: "POST",
36104
36605
  headers: {
36105
36606
  accept: "application/json",
@@ -36147,10 +36648,10 @@ function pairingErrorSnapshot(stage, error) {
36147
36648
  occurred_at: (/* @__PURE__ */ new Date()).toISOString()
36148
36649
  };
36149
36650
  }
36150
- async function patchServerJson(serverBaseUrl, path33, token, body, options) {
36651
+ async function patchServerJson(serverBaseUrl, path34, token, body, options) {
36151
36652
  let response;
36152
36653
  try {
36153
- response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path33}`, {
36654
+ response = await fetch(`${serverBaseUrl.replace(/\/+$/u, "")}${path34}`, {
36154
36655
  method: "PATCH",
36155
36656
  headers: {
36156
36657
  accept: "application/json",
@@ -36198,10 +36699,10 @@ function createPairingNetworkError(input) {
36198
36699
  );
36199
36700
  }
36200
36701
  function pairingClaimPath(sessionId, paths) {
36201
- return path32.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
36702
+ return path33.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.claimed.json`);
36202
36703
  }
36203
36704
  function pairingSessionPath(sessionId, paths) {
36204
- return path32.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
36705
+ return path33.join(paths.pairingDir, `${Buffer.from(sessionId).toString("base64url")}.json`);
36205
36706
  }
36206
36707
  function qrPreferredUrls(routes) {
36207
36708
  return routes.preferredUrls.filter((url) => !url.includes("/api/v1/relay/links/")).slice(0, 1);
@@ -36272,6 +36773,7 @@ function registerSystemRoutes(router, options) {
36272
36773
  conversation_queue_limit: MAX_CONVERSATION_QUEUED_RUNS,
36273
36774
  responses_interrupted_previous_response: true,
36274
36775
  conversation_rename: true,
36776
+ conversation_workspaces: true,
36275
36777
  blobs: true,
36276
36778
  devices: true,
36277
36779
  device_delete: true,
@@ -36758,6 +37260,51 @@ function readDeviceModelHeader(ctx) {
36758
37260
  return value ? value.slice(0, 128) : null;
36759
37261
  }
36760
37262
 
37263
+ // src/http/routes/workspaces.ts
37264
+ function registerWorkspaceRoutes(router, options) {
37265
+ const { paths, conversations } = options;
37266
+ router.get("/api/v1/workspaces", async (ctx) => {
37267
+ await authenticateRequest(ctx, paths);
37268
+ ctx.set("cache-control", "no-store");
37269
+ ctx.body = {
37270
+ ok: true,
37271
+ workspaces: await conversations.listWorkspaces()
37272
+ };
37273
+ });
37274
+ router.post("/api/v1/workspaces", async (ctx) => {
37275
+ await authenticateRequest(ctx, paths);
37276
+ const body = await readJsonBody(ctx.req);
37277
+ ctx.status = 201;
37278
+ ctx.body = {
37279
+ ok: true,
37280
+ workspace: await conversations.createWorkspace({
37281
+ name: readString19(body, "name") ?? "",
37282
+ icon: readString19(body, "icon") ?? void 0
37283
+ })
37284
+ };
37285
+ });
37286
+ router.patch("/api/v1/workspaces/:workspaceId", async (ctx) => {
37287
+ await authenticateRequest(ctx, paths);
37288
+ const body = await readJsonBody(ctx.req);
37289
+ ctx.body = {
37290
+ ok: true,
37291
+ workspace: await conversations.renameWorkspace(ctx.params.workspaceId, {
37292
+ name: readString19(body, "name") ?? "",
37293
+ icon: readString19(body, "icon") ?? void 0
37294
+ })
37295
+ };
37296
+ });
37297
+ router.delete("/api/v1/workspaces/:workspaceId", async (ctx) => {
37298
+ await authenticateRequest(ctx, paths);
37299
+ const result = await conversations.deleteWorkspace(ctx.params.workspaceId);
37300
+ ctx.body = {
37301
+ ok: true,
37302
+ workspace: result.workspace,
37303
+ moved_conversation_count: result.movedConversationCount
37304
+ };
37305
+ });
37306
+ }
37307
+
36761
37308
  // src/http/routes/hermes-updates.ts
36762
37309
  function registerHermesUpdateRoutes(router, options) {
36763
37310
  const { paths, logger } = options;
@@ -37447,6 +37994,7 @@ async function createApp(options = {}) {
37447
37994
  conversations,
37448
37995
  syncCronDeliveries
37449
37996
  });
37997
+ registerWorkspaceRoutes(router, { paths, conversations });
37450
37998
  registerConversationRoutes(router, { paths, logger, conversations });
37451
37999
  registerRunRoutes(router, { paths, logger, conversations });
37452
38000
  registerProfileRoutes(router, { paths, logger, conversations });