@aliyunrds/ctxdb 1.0.1-beta.2 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli/main.js CHANGED
@@ -2,18 +2,16 @@
2
2
  import {
3
3
  DEFAULT_FILE_INGEST_TIMEOUT_MS,
4
4
  DEFAULT_INGEST_TIMEOUT_MS,
5
- compactChunk,
6
5
  compactKbQueryResponse,
7
6
  createKb,
8
7
  getDocument,
9
8
  listDocuments,
10
9
  listKnowledgeBases,
11
- minimalChunk,
12
10
  minimalKbQueryResponse,
13
11
  pollIngest,
14
12
  uploadFile,
15
13
  uploadText
16
- } from "../chunk-6S5RJYBC.js";
14
+ } from "../chunk-UH7AJF6F.js";
17
15
  import {
18
16
  DEFAULT_BASE_URL,
19
17
  DEFAULT_USER_ID,
@@ -31,7 +29,7 @@ import {
31
29
  removeAgent,
32
30
  save,
33
31
  writeInstalledPkgVersion
34
- } from "../chunk-TKMIWM6Q.js";
32
+ } from "../chunk-6FZL67GH.js";
35
33
 
36
34
  // src/cli/util.ts
37
35
  var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
@@ -42,6 +40,9 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
42
40
  // setup / init
43
41
  "no-install-skill",
44
42
  "no-validate",
43
+ // update / upgrade
44
+ "self-update",
45
+ "no-self-update",
45
46
  // uninstall
46
47
  "purge-config",
47
48
  "purge-logs",
@@ -174,6 +175,10 @@ function formatStatusResult(r) {
174
175
  ["connected", r.connected],
175
176
  ["version", r.version]
176
177
  ];
178
+ if (r.hooks_node !== void 0) {
179
+ display.push(["hooks_node", r.hooks_node]);
180
+ display.push(["hooks_node_ok", r.hooks_node_ok]);
181
+ }
177
182
  if (r.ping_error) display.push(["ping_error", r.ping_error]);
178
183
  const maxKey = Math.max(...display.map(([k]) => k.length));
179
184
  for (const [k, v] of display) {
@@ -240,6 +245,7 @@ import { homedir } from "os";
240
245
  import { join, dirname, resolve } from "path";
241
246
  import { fileURLToPath } from "url";
242
247
  import { execFileSync } from "child_process";
248
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
243
249
  var UNINSTALL_NPM_HINT = "To also remove the npm binaries: `npm uninstall -g @aliyunrds/ctxdb @aliyunrds/ctxdb-shared`";
244
250
  var UNINSTALL_ORDER_HINT = "When you eventually want to uninstall: run `ctxdb uninstall --purge-all` FIRST, then `npm uninstall -g @aliyunrds/ctxdb @aliyunrds/ctxdb-shared`. (npm 7+ removed uninstall lifecycle hooks, so the order matters \u2014 otherwise agent settings.json hooks + ~/.ctxdb/ residue stay behind.)";
245
251
  function skillInstallRoot(agent) {
@@ -254,12 +260,14 @@ function skillInstallRoot(agent) {
254
260
  return join(homedir(), ".claude", "skills");
255
261
  case "opencode":
256
262
  return join(homedir(), ".config", "opencode", "skills");
263
+ case "hermes":
264
+ return join(homedir(), ".hermes", "skills");
257
265
  }
258
266
  }
259
267
  var SKILL_DIRS = ["contextdb-memory", "contextdb-knowledge"];
260
268
  var LEGACY_SKILL_DIRS = ["ctxdb"];
261
269
  function agentSupportsHooks(agent) {
262
- if (agent === "opencode") return false;
270
+ if (agent === "opencode" || agent === "hermes") return false;
263
271
  return isBuiltinAgent(agent);
264
272
  }
265
273
  function hookConfigPath(agent) {
@@ -273,6 +281,7 @@ function hookConfigPath(agent) {
273
281
  case "claude":
274
282
  return join(homedir(), ".claude", "settings.json");
275
283
  case "opencode":
284
+ case "hermes":
276
285
  return null;
277
286
  }
278
287
  }
@@ -352,6 +361,21 @@ async function runSetup(options) {
352
361
  }
353
362
  if (installSkill && isBuiltinAgent(agent)) {
354
363
  const installRoot = skillInstallRoot(agent);
364
+ for (const legacy of LEGACY_SKILL_DIRS) {
365
+ const legacyDir = join(installRoot, legacy);
366
+ if (!existsSync(legacyDir)) continue;
367
+ try {
368
+ rmSync(legacyDir, { recursive: true, force: true });
369
+ steps.push({ step: "remove-legacy-skill", ok: true, detail: legacyDir });
370
+ } catch (err) {
371
+ steps.push({
372
+ step: "remove-legacy-skill",
373
+ ok: false,
374
+ detail: `${legacyDir}: ${err?.message ?? String(err)}`
375
+ });
376
+ return { ok: false, steps };
377
+ }
378
+ }
355
379
  const skillResult = installSkillsTo(installRoot, agent);
356
380
  steps.push(...skillResult.steps);
357
381
  if (!skillResult.ok) return { ok: false, steps };
@@ -438,6 +462,76 @@ async function runSetup(options) {
438
462
  }
439
463
  }
440
464
  }
465
+ if (agent === "hermes") {
466
+ const hookPaths = locateHookPaths();
467
+ const hermesPreHook = hookPaths?.hermesPreLlmCall ?? null;
468
+ const hermesPostHook = hookPaths?.hermesPostLlmCall ?? null;
469
+ steps.push({
470
+ step: "locate-hermes-pre-hook",
471
+ ok: hermesPreHook !== null,
472
+ detail: hermesPreHook ?? "could not locate hermes-pre-llm-call in dist/ or src/"
473
+ });
474
+ steps.push({
475
+ step: "locate-hermes-post-hook",
476
+ ok: hermesPostHook !== null,
477
+ detail: hermesPostHook ?? "could not locate hermes-post-llm-call in dist/ or src/"
478
+ });
479
+ if (!hermesPreHook || !hermesPostHook) return { ok: false, steps };
480
+ const nodePath = process.execPath;
481
+ const nodeCheck = verifyNodeBinary(nodePath);
482
+ steps.push({
483
+ step: "resolve-node",
484
+ ok: nodeCheck.ok,
485
+ detail: nodeCheck.ok ? `${nodePath} (${nodeCheck.version})` : `node not executable: ${nodeCheck.error}`
486
+ });
487
+ if (!nodeCheck.ok) return { ok: false, steps };
488
+ try {
489
+ backupHermesConfig();
490
+ steps.push({ step: "backup-hermes-config", ok: true });
491
+ const removed = writeHermesHooksConfig(hermesPreHook, hermesPostHook, nodePath);
492
+ steps.push({
493
+ step: "append-hermes-hook",
494
+ ok: true,
495
+ detail: `pre_llm_call + post_llm_call written${removed > 0 ? `, replaced ${removed} stale` : ""}`
496
+ });
497
+ } catch (err) {
498
+ steps.push({
499
+ step: "hermes-config-update",
500
+ ok: false,
501
+ detail: err?.message ?? String(err)
502
+ });
503
+ return { ok: false, steps };
504
+ }
505
+ const chmodErrors = [];
506
+ for (const hookPath of [hermesPreHook, hermesPostHook]) {
507
+ try {
508
+ chmodSync(hookPath, 493);
509
+ } catch (err) {
510
+ chmodErrors.push(`${hookPath}: ${err?.message ?? String(err)}`);
511
+ }
512
+ }
513
+ steps.push({
514
+ step: "chmod-hermes-hook",
515
+ ok: true,
516
+ detail: chmodErrors.length > 0 ? `skipped: ${chmodErrors.join("; ")}` : void 0
517
+ });
518
+ try {
519
+ const allowlist = checkHermesHookAllowlist(
520
+ hermesCtxdbEntriesByEvent(readHermesConfig())
521
+ );
522
+ steps.push({
523
+ step: "check-hermes-hook-allowlist",
524
+ ok: allowlist.ok,
525
+ detail: allowlist.detail
526
+ });
527
+ } catch (err) {
528
+ steps.push({
529
+ step: "check-hermes-hook-allowlist",
530
+ ok: false,
531
+ detail: `${err?.message ?? String(err)}. ${hermesHookApprovalHint()}`
532
+ });
533
+ }
534
+ }
441
535
  if (agent === "opencode") {
442
536
  const absEntry = resolveOpencodePluginEntry();
443
537
  if (!absEntry) {
@@ -493,6 +587,11 @@ async function runSetup(options) {
493
587
  "Restart OpenCode to load the plugin. `ctxdb upgrade` auto-refreshes the shim after npm updates; only re-run setup if you move ctxdb's install dir."
494
588
  );
495
589
  }
590
+ if (ok && agent === "hermes") {
591
+ hints.unshift(
592
+ `Hermes shell hooks: ${hermesConfigPath()} \u2192 pre_llm_call + post_llm_call`
593
+ );
594
+ }
496
595
  return { ok, steps, hints: hints.length > 0 ? hints : void 0 };
497
596
  }
498
597
  function runRemove(agent, options = {}) {
@@ -626,6 +725,42 @@ function runRemove(agent, options = {}) {
626
725
  }
627
726
  }
628
727
  }
728
+ if (agent === "hermes") {
729
+ try {
730
+ backupHermesConfig();
731
+ const removed = removeHermesHooksConfig();
732
+ steps.push({
733
+ step: "remove-hermes-hooks",
734
+ ok: true,
735
+ detail: removed > 0 ? `removed ${removed} entries` : "nothing to remove"
736
+ });
737
+ } catch (err) {
738
+ steps.push({
739
+ step: "remove-hermes-hooks",
740
+ ok: false,
741
+ detail: err?.message ?? String(err)
742
+ });
743
+ return { ok: false, steps };
744
+ }
745
+ const root = skillInstallRoot(agent);
746
+ const dirs = [
747
+ ...SKILL_DIRS.map((d) => join(root, d)),
748
+ ...LEGACY_SKILL_DIRS.map((d) => join(root, d))
749
+ ];
750
+ for (const p of dirs) {
751
+ if (!existsSync(p)) continue;
752
+ try {
753
+ rmSync(p, { recursive: true, force: true });
754
+ steps.push({ step: "remove-skill-dir", ok: true, detail: p });
755
+ } catch (err) {
756
+ steps.push({
757
+ step: "remove-skill-dir",
758
+ ok: false,
759
+ detail: `${p}: ${err?.message ?? String(err)}`
760
+ });
761
+ }
762
+ }
763
+ }
629
764
  try {
630
765
  const r = removeAgent(agent, void 0, {
631
766
  keepEmptyShell: !!options.keepConfigShell
@@ -659,7 +794,14 @@ function runRemove(agent, options = {}) {
659
794
  return { ok: steps.every((s) => s.ok), steps };
660
795
  }
661
796
  function checkAgentHome(agent) {
662
- if (agent === "opencode") {
797
+ if (agent === "hermes" && process.platform === "win32") {
798
+ return {
799
+ step: "check-agent-home",
800
+ ok: false,
801
+ detail: "hermes setup currently supports macOS/Linux only; Windows Hermes will be handled separately"
802
+ };
803
+ }
804
+ if (agent === "opencode" || agent === "hermes") {
663
805
  const primary = agentHomeDir(agent);
664
806
  try {
665
807
  mkdirSync(primary, { recursive: true });
@@ -782,6 +924,7 @@ function verifyNodeBinary(nodePath) {
782
924
  }
783
925
  function checkHookNodePaths(agent) {
784
926
  if (agent === "opencode") return checkOpencodeShimHealth();
927
+ if (agent === "hermes") return checkHermesHookHealth();
785
928
  const empty = {
786
929
  installed: false,
787
930
  nodePaths: [],
@@ -986,12 +1129,16 @@ function hookPathsFromDir(dir, ext) {
986
1129
  const stp = join(dir, `stop.${ext}`);
987
1130
  const ss = join(dir, `session-start.${ext}`);
988
1131
  const ptu = join(dir, `pre-tool-use.${ext}`);
1132
+ const hpl = join(dir, `hermes-pre-llm-call.${ext}`);
1133
+ const hpost = join(dir, `hermes-post-llm-call.${ext}`);
989
1134
  if (existsSync(ups) && existsSync(stp) && existsSync(ss)) {
990
1135
  return {
991
1136
  userPromptSubmit: ups,
992
1137
  stop: stp,
993
1138
  sessionStart: ss,
994
- preToolUse: existsSync(ptu) ? ptu : void 0
1139
+ preToolUse: existsSync(ptu) ? ptu : void 0,
1140
+ hermesPreLlmCall: existsSync(hpl) ? hpl : void 0,
1141
+ hermesPostLlmCall: existsSync(hpost) ? hpost : void 0
995
1142
  };
996
1143
  }
997
1144
  return null;
@@ -1194,6 +1341,242 @@ function entryIsCtxdb(entry) {
1194
1341
  (h) => h?.type === "command" && typeof h.command === "string" && (h.command.includes("@aliyunrds/ctxdb-qoder") || h.command.includes("@aliyunrds/ctxdb") || h.command.includes("ctxdb-qoder") || h.command.includes("rds-ctxdb") || h.command.includes("qoder-ctxdb"))
1195
1342
  );
1196
1343
  }
1344
+ var HERMES_PRE_LLM_CALL_EVENT = "pre_llm_call";
1345
+ var HERMES_POST_LLM_CALL_EVENT = "post_llm_call";
1346
+ var HERMES_HOOK_EVENTS = [
1347
+ HERMES_PRE_LLM_CALL_EVENT,
1348
+ HERMES_POST_LLM_CALL_EVENT
1349
+ ];
1350
+ var HERMES_HOOK_TIMEOUT_SECONDS = 60;
1351
+ var HERMES_ALLOWLIST_FILENAME = "shell-hooks-allowlist.json";
1352
+ function hermesConfigPath() {
1353
+ return join(homedir(), ".hermes", "config.yaml");
1354
+ }
1355
+ function hermesAllowlistPath() {
1356
+ return join(dirname(hermesConfigPath()), HERMES_ALLOWLIST_FILENAME);
1357
+ }
1358
+ function hermesHookApprovalHint(events = HERMES_HOOK_EVENTS) {
1359
+ const hookNames = events.map((event) => `\`${event}\``).join(" and ");
1360
+ return `Approve the Hermes ctxdb hooks ${hookNames}: run \`hermes --accept-hooks chat\` once. This command opens an interactive chat; after Hermes starts, exit the chat, then rerun \`ctxdb setup --agent hermes\`.`;
1361
+ }
1362
+ function backupHermesConfig() {
1363
+ backupSettingsFile(hermesConfigPath());
1364
+ }
1365
+ function readHermesConfig() {
1366
+ const path = hermesConfigPath();
1367
+ if (!existsSync(path)) return {};
1368
+ const raw = readFileSync(path, "utf-8");
1369
+ if (!raw.trim()) return {};
1370
+ const parsed = parseYaml(raw);
1371
+ if (parsed === null || parsed === void 0) return {};
1372
+ if (typeof parsed !== "object" || Array.isArray(parsed)) {
1373
+ throw new Error(`${path}: expected a YAML mapping at the top level`);
1374
+ }
1375
+ return parsed;
1376
+ }
1377
+ function writeHermesConfig(data) {
1378
+ const path = hermesConfigPath();
1379
+ mkdirSync(dirname(path), { recursive: true });
1380
+ writeFileSync(path, stringifyYaml(data, { lineWidth: 0 }), "utf-8");
1381
+ }
1382
+ function hermesHooksMap(data) {
1383
+ if (!data.hooks || typeof data.hooks !== "object" || Array.isArray(data.hooks)) {
1384
+ data.hooks = {};
1385
+ }
1386
+ return data.hooks;
1387
+ }
1388
+ function quoteHermesCommandPart(value) {
1389
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1390
+ }
1391
+ function hermesHookCommand(scriptPath, nodePath) {
1392
+ return `${quoteHermesCommandPart(nodePath)} ${quoteHermesCommandPart(scriptPath)} --agent=hermes`;
1393
+ }
1394
+ function hermesEntryIsCtxdb(entry) {
1395
+ if (!entry || typeof entry !== "object") return false;
1396
+ if (entry[ENTRY_MARKER_KEY] === ENTRY_MARKER_VALUE) return true;
1397
+ if (typeof entry.command === "string" && (entry.command.includes("hermes-pre-llm-call") || entry.command.includes("hermes-post-llm-call")) && entry.command.includes("--agent=hermes")) {
1398
+ return true;
1399
+ }
1400
+ return false;
1401
+ }
1402
+ function hermesCtxdbEntriesByEvent(data) {
1403
+ const hooks = data.hooks && typeof data.hooks === "object" && !Array.isArray(data.hooks) ? data.hooks : {};
1404
+ const entriesByEvent = /* @__PURE__ */ new Map();
1405
+ for (const event of HERMES_HOOK_EVENTS) {
1406
+ const entries = Array.isArray(hooks[event]) ? hooks[event].filter(hermesEntryIsCtxdb) : [];
1407
+ entriesByEvent.set(event, entries);
1408
+ }
1409
+ return entriesByEvent;
1410
+ }
1411
+ function checkHermesHookAllowlist(entriesByEvent) {
1412
+ const path = hermesAllowlistPath();
1413
+ let approvals = [];
1414
+ let readError = null;
1415
+ if (existsSync(path)) {
1416
+ try {
1417
+ const data = JSON.parse(readFileSync(path, "utf-8"));
1418
+ if (!Array.isArray(data?.approvals)) {
1419
+ readError = "expected an approvals array";
1420
+ } else {
1421
+ approvals = data.approvals;
1422
+ }
1423
+ } catch (err) {
1424
+ readError = err?.message ?? String(err);
1425
+ }
1426
+ }
1427
+ const missingEvents = HERMES_HOOK_EVENTS.filter((event) => {
1428
+ const entries = entriesByEvent.get(event) ?? [];
1429
+ return entries.length === 0 || entries.some((entry) => {
1430
+ if (typeof entry?.command !== "string") return true;
1431
+ return !approvals.some(
1432
+ (approval) => approval && typeof approval === "object" && approval.event === event && approval.command === entry.command
1433
+ );
1434
+ });
1435
+ });
1436
+ if (missingEvents.length === 0) {
1437
+ return {
1438
+ ok: true,
1439
+ detail: `pre_llm_call + post_llm_call approved in ${path}`
1440
+ };
1441
+ }
1442
+ const reason = readError ? `${path} is invalid (${readError})` : `${missingEvents.join(", ")} not allowlisted in ${path}`;
1443
+ return {
1444
+ ok: false,
1445
+ detail: `${reason}. ${hermesHookApprovalHint(missingEvents)}`
1446
+ };
1447
+ }
1448
+ function stripHermesHooks(data) {
1449
+ const hooks = hermesHooksMap(data);
1450
+ let removed = 0;
1451
+ for (const event of HERMES_HOOK_EVENTS) {
1452
+ const arr = hooks[event];
1453
+ if (!Array.isArray(arr)) continue;
1454
+ const before = arr.length;
1455
+ hooks[event] = arr.filter((entry) => !hermesEntryIsCtxdb(entry));
1456
+ removed += before - hooks[event].length;
1457
+ }
1458
+ return removed;
1459
+ }
1460
+ function writeHermesHooksConfig(preScriptPath, postScriptPath, nodePath) {
1461
+ const data = readHermesConfig();
1462
+ const removed = stripHermesHooks(data);
1463
+ const hooks = hermesHooksMap(data);
1464
+ const scripts = /* @__PURE__ */ new Map([
1465
+ [HERMES_PRE_LLM_CALL_EVENT, preScriptPath],
1466
+ [HERMES_POST_LLM_CALL_EVENT, postScriptPath]
1467
+ ]);
1468
+ for (const event of HERMES_HOOK_EVENTS) {
1469
+ if (!Array.isArray(hooks[event])) hooks[event] = [];
1470
+ hooks[event].push({
1471
+ command: hermesHookCommand(scripts.get(event), nodePath),
1472
+ timeout: HERMES_HOOK_TIMEOUT_SECONDS,
1473
+ [ENTRY_MARKER_KEY]: ENTRY_MARKER_VALUE,
1474
+ [ENTRY_AGENT_KEY]: "hermes"
1475
+ });
1476
+ }
1477
+ writeHermesConfig(data);
1478
+ return removed;
1479
+ }
1480
+ function removeHermesHooksConfig() {
1481
+ const path = hermesConfigPath();
1482
+ if (!existsSync(path)) return 0;
1483
+ const data = readHermesConfig();
1484
+ const removed = stripHermesHooks(data);
1485
+ if (removed > 0) writeHermesConfig(data);
1486
+ return removed;
1487
+ }
1488
+ function firstCommandToken(command) {
1489
+ const s = command.trim();
1490
+ if (!s) return null;
1491
+ const quote = s[0];
1492
+ if (quote === "'" || quote === '"') {
1493
+ const end = s.indexOf(quote, 1);
1494
+ if (end > 1) return s.slice(1, end);
1495
+ }
1496
+ return s.split(/\s+/, 1)[0] ?? null;
1497
+ }
1498
+ function checkHermesHookHealth() {
1499
+ const path = hermesConfigPath();
1500
+ const empty = {
1501
+ installed: false,
1502
+ nodePaths: [],
1503
+ nodeOk: true,
1504
+ detail: "no hermes hooks installed"
1505
+ };
1506
+ if (!existsSync(path)) return empty;
1507
+ let data;
1508
+ try {
1509
+ data = readHermesConfig();
1510
+ } catch {
1511
+ return empty;
1512
+ }
1513
+ const entriesByEvent = hermesCtxdbEntriesByEvent(data);
1514
+ const installedEvents = HERMES_HOOK_EVENTS.filter(
1515
+ (event) => (entriesByEvent.get(event)?.length ?? 0) > 0
1516
+ );
1517
+ if (installedEvents.length === 0) return empty;
1518
+ const missingEvents = HERMES_HOOK_EVENTS.filter(
1519
+ (event) => (entriesByEvent.get(event)?.length ?? 0) === 0
1520
+ );
1521
+ if (missingEvents.length > 0) {
1522
+ return {
1523
+ installed: true,
1524
+ nodePaths: [],
1525
+ nodeOk: false,
1526
+ detail: `missing ctxdb Hermes hooks: ${missingEvents.join(", ")} \u2014 re-run \`ctxdb setup --agent hermes\``
1527
+ };
1528
+ }
1529
+ const nodePaths = /* @__PURE__ */ new Set();
1530
+ for (const event of HERMES_HOOK_EVENTS) {
1531
+ for (const entry of entriesByEvent.get(event) ?? []) {
1532
+ if (typeof entry.command !== "string") continue;
1533
+ const nodePath = firstCommandToken(entry.command);
1534
+ if (nodePath && (nodePath.includes("/") || nodePath.includes("\\"))) {
1535
+ nodePaths.add(nodePath);
1536
+ }
1537
+ }
1538
+ }
1539
+ if (nodePaths.size === 0) {
1540
+ const allowlist2 = checkHermesHookAllowlist(entriesByEvent);
1541
+ return {
1542
+ installed: true,
1543
+ nodePaths: [],
1544
+ nodeOk: allowlist2.ok,
1545
+ detail: allowlist2.ok ? "hermes pre_llm_call + post_llm_call hooks installed; allowlisted" : allowlist2.detail
1546
+ };
1547
+ }
1548
+ const paths = [...nodePaths];
1549
+ const bad = [];
1550
+ let goodVersion = "";
1551
+ for (const p of paths) {
1552
+ const v = verifyNodeBinary(p);
1553
+ if (!v.ok) bad.push(`${p}: ${v.error}`);
1554
+ else if (!goodVersion) goodVersion = v.version ?? "";
1555
+ }
1556
+ if (bad.length > 0) {
1557
+ return {
1558
+ installed: true,
1559
+ nodePaths: paths,
1560
+ nodeOk: false,
1561
+ detail: `node not executable: ${bad.join("; ")} \u2014 re-run \`ctxdb setup --agent hermes\``
1562
+ };
1563
+ }
1564
+ const allowlist = checkHermesHookAllowlist(entriesByEvent);
1565
+ if (!allowlist.ok) {
1566
+ return {
1567
+ installed: true,
1568
+ nodePaths: paths,
1569
+ nodeOk: false,
1570
+ detail: allowlist.detail
1571
+ };
1572
+ }
1573
+ return {
1574
+ installed: true,
1575
+ nodePaths: paths,
1576
+ nodeOk: true,
1577
+ detail: `hermes pre_llm_call + post_llm_call hooks: ${paths[0]} (${goodVersion}); allowlisted`
1578
+ };
1579
+ }
1197
1580
  function codexConfigTomlPath() {
1198
1581
  return join(homedir(), ".codex", "config.toml");
1199
1582
  }
@@ -1390,7 +1773,7 @@ function describeCodexFeatureMutation(status2, createdNew) {
1390
1773
  return "updated ~/.codex/config.toml";
1391
1774
  }
1392
1775
  }
1393
- function runUpgrade(options = {}) {
1776
+ async function runUpgrade(options = {}) {
1394
1777
  const agents = options.agent ? [options.agent] : configuredAgents(options.configPath).filter(isBuiltinAgent);
1395
1778
  if (agents.length === 0) {
1396
1779
  return {
@@ -1411,113 +1794,15 @@ function runUpgrade(options = {}) {
1411
1794
  }
1412
1795
  const results = {};
1413
1796
  for (const agent of agents) {
1414
- results[agent] = upgradeOneAgent(agent);
1415
- }
1416
- const anyOk = Object.values(results).some((r) => r.ok);
1417
- return { ok: anyOk, agents: results };
1418
- }
1419
- function upgradeOneAgent(agent) {
1420
- const steps = [];
1421
- const homeCheck = checkAgentHome(agent);
1422
- steps.push(homeCheck);
1423
- if (!homeCheck.ok) {
1424
- return { ok: true, steps, hints: [`${agent}: skipped \u2014 agent home not found`] };
1425
- }
1426
- const root = skillInstallRoot(agent);
1427
- for (const legacy of LEGACY_SKILL_DIRS) {
1428
- const legacyDir = join(root, legacy);
1429
- if (!existsSync(legacyDir)) continue;
1430
- try {
1431
- rmSync(legacyDir, { recursive: true, force: true });
1432
- steps.push({ step: "remove-legacy-skill", ok: true, detail: legacyDir });
1433
- } catch {
1434
- }
1435
- }
1436
- const skillResult = installSkillsTo(root, agent);
1437
- steps.push(...skillResult.steps);
1438
- if (!skillResult.ok) return { ok: false, steps };
1439
- if (agentSupportsHooks(agent)) {
1440
- const hookPaths = locateHookPaths();
1441
- if (!hookPaths) {
1442
- steps.push({ step: "locate-hooks", ok: false, detail: "hook scripts not found in dist/ or src/" });
1443
- return { ok: false, steps };
1444
- }
1445
- const nodePath = process.execPath;
1446
- const nodeCheck = verifyNodeBinary(nodePath);
1447
- if (!nodeCheck.ok) {
1448
- steps.push({ step: "resolve-node", ok: false, detail: `node not executable: ${nodeCheck.error}` });
1449
- return { ok: false, steps };
1450
- }
1451
- try {
1452
- backupSettingsJson(agent);
1453
- const removed = stripCtxdbHooks(agent);
1454
- appendHooks(agent, hookPaths);
1455
- steps.push({
1456
- step: "refresh-hooks",
1457
- ok: true,
1458
- detail: `${HOOK_EVENTS.length} hooks written (node: ${nodePath})${removed > 0 ? `, replaced ${removed} stale` : ""}`
1459
- });
1460
- } catch (err) {
1461
- steps.push({ step: "refresh-hooks", ok: false, detail: err?.message ?? String(err) });
1462
- return { ok: false, steps };
1463
- }
1464
- try {
1465
- chmodSync(hookPaths.userPromptSubmit, 493);
1466
- chmodSync(hookPaths.stop, 493);
1467
- chmodSync(hookPaths.sessionStart, 493);
1468
- if (hookPaths.preToolUse && existsSync(hookPaths.preToolUse)) {
1469
- chmodSync(hookPaths.preToolUse, 493);
1470
- }
1471
- } catch {
1472
- }
1473
- if (agent === "codex") {
1474
- steps.push(enableCodexHooksFeature());
1475
- }
1476
- const primaryHome = agentHomeDir(agent);
1477
- const variantHomes = agentHomeDirs(agent).filter((d) => d !== primaryHome && existsSync(d));
1478
- for (const vHome of variantHomes) {
1479
- const tag = vHome.split("/").pop() ?? vHome;
1480
- const vSkillRoot = join(vHome, "skills");
1481
- const vr = installSkillsTo(vSkillRoot, agent);
1482
- steps.push(...vr.steps.map((s) => ({ ...s, step: `${tag}:${s.step}` })));
1483
- try {
1484
- const vPath = join(vHome, "settings.json");
1485
- backupSettingsFile(vPath);
1486
- const vRemoved = stripCtxdbHooksFromFile(vPath);
1487
- appendHooksToFile(vPath, agent, hookPaths);
1488
- steps.push({
1489
- step: `${tag}:refresh-hooks`,
1490
- ok: true,
1491
- detail: vRemoved > 0 ? `replaced ${vRemoved} stale` : void 0
1492
- });
1493
- } catch (err) {
1494
- steps.push({ step: `${tag}:refresh-hooks`, ok: false, detail: err?.message ?? String(err) });
1495
- }
1496
- }
1497
- }
1498
- if (agent === "opencode") {
1499
- const absEntry = resolveOpencodePluginEntry();
1500
- if (!absEntry) {
1501
- steps.push({
1502
- step: "refresh-opencode-shim",
1503
- ok: false,
1504
- detail: "could not resolve plugin entry under @aliyunrds/ctxdb/dist/opencode"
1505
- });
1506
- return { ok: false, steps };
1507
- }
1508
- try {
1509
- writeOpencodeShim(absEntry);
1510
- steps.push({
1511
- step: "refresh-opencode-shim",
1512
- ok: true,
1513
- detail: `${opencodeShimPath()} \u2192 ${absEntry}`
1514
- });
1515
- } catch (err) {
1516
- steps.push({ step: "refresh-opencode-shim", ok: false, detail: err?.message ?? String(err) });
1517
- return { ok: false, steps };
1518
- }
1797
+ results[agent] = await runSetup({
1798
+ agent,
1799
+ // Updating local package artifacts must not depend on service
1800
+ // connectivity. Users can run `ctxdb ping` separately.
1801
+ validate: false
1802
+ });
1519
1803
  }
1520
- return { ok: steps.every((s) => s.ok), steps };
1804
+ const allOk = Object.values(results).every((r) => r.ok);
1805
+ return { ok: allOk, agents: results };
1521
1806
  }
1522
1807
 
1523
1808
  // src/cli/top.ts
@@ -1577,7 +1862,7 @@ async function ping(args) {
1577
1862
  var HELP = `ctxdb setup \u2014 configure an agent
1578
1863
 
1579
1864
  USAGE
1580
- ctxdb setup [--agent <qoder|qoderwork|codex|claude|opencode>]
1865
+ ctxdb setup [--agent <qoder|qoderwork|codex|claude|opencode|hermes>]
1581
1866
  [--api-key=K] [--base-url=URL] [--user-id=ID]
1582
1867
  [--no-install-skill] [--no-validate] [--json]
1583
1868
 
@@ -1586,9 +1871,13 @@ BEHAVIOR
1586
1871
  no hooks or skills installed).
1587
1872
  With --agent Writes agent config + installs hooks + skills for the
1588
1873
  specified agent harness.
1874
+ Hermes Also verifies ~/.hermes/shell-hooks-allowlist.json.
1875
+ Approve pre_llm_call + post_llm_call with
1876
+ \`hermes --accept-hooks chat\`. It opens interactive chat;
1877
+ exit after Hermes starts, then rerun ctxdb setup.
1589
1878
 
1590
1879
  FLAGS
1591
- --agent <name> Target agent (qoder|qoderwork|codex|claude|opencode)
1880
+ --agent <name> Target agent (qoder|qoderwork|codex|claude|opencode|hermes)
1592
1881
  --api-key <key> API key (required on first setup)
1593
1882
  --base-url <url> Server URL (default: https://context-database.aliyuncs.com)
1594
1883
  --user-id <id> User bucket (default: "default")
@@ -1615,7 +1904,7 @@ async function setup(args) {
1615
1904
  const json = !!args.flags.json;
1616
1905
  if (agent === "default" && !json && !configuredAgents().includes("default")) {
1617
1906
  process.stderr.write(
1618
- `hint: creating CLI-only "default" agent (no hooks/skills). Use --agent <claude|qoder|qoderwork|codex|opencode> for full setup.
1907
+ `hint: creating CLI-only "default" agent (no hooks/skills). Use --agent <claude|qoder|qoderwork|codex|opencode|hermes> for full setup.
1619
1908
  `
1620
1909
  );
1621
1910
  }
@@ -1647,11 +1936,11 @@ ${dim("codex hooks \u914D\u7F6E\u540E\uFF0C\u9700\u8981\u91CD\u542F Codex \u5BF9
1647
1936
  var HELP2 = `ctxdb uninstall \u2014 remove hooks + skills
1648
1937
 
1649
1938
  USAGE
1650
- ctxdb uninstall [--agent <qoder|qoderwork|codex|claude|opencode>] [--purge-config] [--purge-logs] [--purge-all] [--json]
1939
+ ctxdb uninstall [--agent <qoder|qoderwork|codex|claude|opencode|hermes>] [--purge-config] [--purge-logs] [--purge-all] [--json]
1651
1940
 
1652
1941
  DEFAULT BEHAVIOR (no --agent, no purge flags)
1653
1942
  Strips ctxdb hook entries and skill directories for ALL agents (qoder,
1654
- qoderwork, codex, claude, opencode). Keeps ~/.ctxdb/ctxdb.json (credentials) and ~/.ctxdb/logs/
1943
+ qoderwork, codex, claude, opencode, hermes). Keeps ~/.ctxdb/ctxdb.json (credentials) and ~/.ctxdb/logs/
1655
1944
  so you can re-run \`ctxdb setup\` later without losing configuration.
1656
1945
 
1657
1946
  WITH --agent <name>
@@ -1659,7 +1948,7 @@ WITH --agent <name>
1659
1948
  Other agents remain untouched.
1660
1949
 
1661
1950
  FLAGS
1662
- --agent <name> Target a single agent (qoder|qoderwork|codex|claude|opencode)
1951
+ --agent <name> Target a single agent (qoder|qoderwork|codex|claude|opencode|hermes)
1663
1952
  --purge-config Also delete ~/.ctxdb/ctxdb.json
1664
1953
  --purge-logs Also delete ~/.ctxdb/logs/
1665
1954
  --purge-all Delete everything under ~/.ctxdb/
@@ -1760,8 +2049,8 @@ function isNewerVersion(candidate, current) {
1760
2049
  if (cMin !== uMin) return cMin > uMin;
1761
2050
  return cPat > uPat;
1762
2051
  }
1763
- function runSelfUpdate(currentVersion, passthroughArgs = []) {
1764
- const method = detectInstallMethod();
2052
+ function runSelfUpdate(currentVersion, passthroughArgs = [], options = {}) {
2053
+ const method = options.installMethod ?? detectInstallMethod();
1765
2054
  if (method !== "npm") {
1766
2055
  return {
1767
2056
  ok: false,
@@ -1772,9 +2061,12 @@ function runSelfUpdate(currentVersion, passthroughArgs = []) {
1772
2061
  }
1773
2062
  const check = checkLatestVersion(currentVersion);
1774
2063
  if (check.error) {
1775
- process.stderr.write(`ctxdb: npm view failed (${check.error}), skipping self-update
1776
- `);
1777
- return { ok: true, updated: false, fromVersion: currentVersion };
2064
+ return {
2065
+ ok: false,
2066
+ updated: false,
2067
+ fromVersion: currentVersion,
2068
+ error: `npm view failed: ${check.error}`
2069
+ };
1778
2070
  }
1779
2071
  if (!check.isNewer) {
1780
2072
  process.stderr.write(`ctxdb: package already up to date (v${currentVersion})
@@ -1796,10 +2088,10 @@ function runSelfUpdate(currentVersion, passthroughArgs = []) {
1796
2088
  error: spawnFailureMessage(install, "npm install -g")
1797
2089
  };
1798
2090
  }
1799
- process.stderr.write(`ctxdb: package updated to v${check.latest}, refreshing skills/hooks...
2091
+ process.stderr.write(`ctxdb: package updated to v${check.latest}, running setup...
1800
2092
  `);
1801
- const upgradeArgs = ["upgrade", ...passthroughArgs];
1802
- const refresh = spawnSync(ctxdbCommand(), upgradeArgs, {
2093
+ const setupArgs = ["update", "--no-self-update", ...passthroughArgs];
2094
+ const refresh = spawnSync(ctxdbCommand(), setupArgs, {
1803
2095
  stdio: "inherit",
1804
2096
  encoding: "utf-8",
1805
2097
  shell: process.platform === "win32"
@@ -1809,7 +2101,8 @@ function runSelfUpdate(currentVersion, passthroughArgs = []) {
1809
2101
  updated: true,
1810
2102
  fromVersion: currentVersion,
1811
2103
  toVersion: check.latest,
1812
- error: refresh.status !== 0 ? spawnFailureMessage(refresh, "ctxdb upgrade") : void 0
2104
+ error: refresh.status !== 0 ? spawnFailureMessage(refresh, "ctxdb setup") : void 0,
2105
+ failureHandled: refresh.status !== null && refresh.status !== 0
1813
2106
  };
1814
2107
  }
1815
2108
 
@@ -1817,58 +2110,68 @@ function runSelfUpdate(currentVersion, passthroughArgs = []) {
1817
2110
  var HELP3 = `ctxdb upgrade \u2014 refresh skills + hooks
1818
2111
 
1819
2112
  USAGE
1820
- ctxdb upgrade [--agent <qoder|qoderwork|codex|claude|opencode>] [--self-update] [--json]
2113
+ ctxdb upgrade [--agent <qoder|qoderwork|codex|claude|opencode|hermes>] [--no-self-update] [--json]
1821
2114
 
1822
2115
  BEHAVIOR
1823
- Refreshes skill files and hook entries for configured agents without
1824
- touching credentials. Designed for the post-npm-update path.
2116
+ Pulls the latest @aliyunrds/ctxdb package from npm, then re-runs setup
2117
+ for configured agents using their existing credentials.
1825
2118
 
1826
- Without --agent: upgrades all configured built-in agents.
1827
- With --agent: upgrades only the specified agent.
2119
+ Without --agent: sets up all configured built-in agents.
2120
+ With --agent: sets up only the specified agent.
1828
2121
 
1829
2122
  FLAGS
1830
- --agent <name> Target a single agent (qoder|qoderwork|codex|claude|opencode)
1831
- --self-update Also pull latest package from npm before upgrading
2123
+ --agent <name> Target a single agent (qoder|qoderwork|codex|claude|opencode|hermes)
2124
+ --self-update Pull latest package from npm (enabled by default)
2125
+ --no-self-update Skip the npm package update and only re-run setup
1832
2126
  --json Machine-readable JSON output
1833
2127
  `;
1834
- function upgrade(args) {
2128
+ var MANUAL_UPDATE_HINT = "Automatic update failed. Please run manually:\n npm install -g @aliyunrds/ctxdb\n ctxdb setup";
2129
+ function printUpdateFailure(error, json) {
2130
+ if (json) {
2131
+ process.stdout.write(JSON.stringify({ ok: false, error, hint: MANUAL_UPDATE_HINT }) + "\n");
2132
+ return;
2133
+ }
2134
+ process.stderr.write(`ctxdb: automatic update/setup failed: ${error}
2135
+ ${MANUAL_UPDATE_HINT}
2136
+ `);
2137
+ }
2138
+ async function upgrade(args) {
1835
2139
  if (args.flags.help || args.flags.h) {
1836
2140
  process.stdout.write(HELP3);
1837
2141
  return 0;
1838
2142
  }
1839
- const selfUpdate = !!args.flags["self-update"];
2143
+ if (args.flags["self-update"] && args.flags["no-self-update"]) {
2144
+ process.stderr.write("--self-update and --no-self-update cannot be used together\n");
2145
+ return 2;
2146
+ }
2147
+ const agentFlag = args.flags.agent;
2148
+ if (agentFlag && agentFlag !== "default" && !isBuiltinAgent(agentFlag)) {
2149
+ process.stderr.write(
2150
+ `unknown --agent: ${agentFlag} (expected one of ${SUPPORTED_AGENTS.join(" / ")})
2151
+ `
2152
+ );
2153
+ return 2;
2154
+ }
2155
+ const selfUpdate = !args.flags["no-self-update"];
1840
2156
  if (selfUpdate) {
1841
2157
  const passthrough = [];
1842
- const agentFlag2 = args.flags.agent;
1843
- if (agentFlag2) passthrough.push("--agent", agentFlag2);
2158
+ if (agentFlag) passthrough.push("--agent", agentFlag);
1844
2159
  if (args.flags.json) passthrough.push("--json");
1845
2160
  const result2 = runSelfUpdate(PACKAGE_VERSION, passthrough);
1846
2161
  if (!result2.ok) {
1847
- if (args.flags.json) {
1848
- process.stdout.write(JSON.stringify({ ok: false, error: result2.error }) + "\n");
1849
- } else {
1850
- process.stderr.write(`ctxdb: self-update failed: ${result2.error}
1851
- `);
1852
- }
2162
+ if (result2.failureHandled) return 1;
2163
+ printUpdateFailure(result2.error ?? "unknown error", !!args.flags.json);
1853
2164
  return 1;
1854
2165
  }
1855
2166
  if (result2.updated) {
1856
2167
  return 0;
1857
2168
  }
1858
2169
  }
1859
- const agentFlag = args.flags.agent;
1860
2170
  if (agentFlag === "default") {
1861
2171
  process.stderr.write("nothing to upgrade for default agent (no hooks or skills)\n");
1862
2172
  return 0;
1863
2173
  }
1864
- if (agentFlag && !isBuiltinAgent(agentFlag)) {
1865
- process.stderr.write(
1866
- `unknown --agent: ${agentFlag} (expected one of ${SUPPORTED_AGENTS.join(" / ")})
1867
- `
1868
- );
1869
- return 2;
1870
- }
1871
- const result = runUpgrade({ agent: agentFlag });
2174
+ const result = await runUpgrade({ agent: agentFlag });
1872
2175
  if (result.ok) {
1873
2176
  try {
1874
2177
  writeInstalledPkgVersion(PACKAGE_VERSION);
@@ -1876,12 +2179,22 @@ function upgrade(args) {
1876
2179
  }
1877
2180
  }
1878
2181
  const json = !!args.flags.json;
2182
+ const detail = result.hints?.[0] ?? "ctxdb setup failed";
2183
+ if (!result.ok && json) {
2184
+ process.stdout.write(
2185
+ JSON.stringify({ ...result, error: detail, hint: MANUAL_UPDATE_HINT }, null, 2) + "\n"
2186
+ );
2187
+ return 1;
2188
+ }
1879
2189
  printResult(result, json);
1880
2190
  if (!json && result.hints) {
1881
2191
  for (const h of result.hints) process.stderr.write(`
1882
2192
  ${h}
1883
2193
  `);
1884
2194
  }
2195
+ if (!result.ok) {
2196
+ printUpdateFailure(detail, false);
2197
+ }
1885
2198
  return result.ok ? 0 : 1;
1886
2199
  }
1887
2200
 
@@ -2000,12 +2313,13 @@ function projectKnowledgeChunks(resp, flags) {
2000
2313
  const r = resp;
2001
2314
  const k = r.knowledge;
2002
2315
  if (!k || typeof k !== "object") return resp;
2003
- const chunks = k.chunks;
2316
+ const knowledge = k;
2317
+ const chunks = knowledge.chunks;
2004
2318
  if (!Array.isArray(chunks)) return resp;
2005
- const project = flags.verbose ? compactChunk : minimalChunk;
2319
+ const normalized = flags.verbose ? compactKbQueryResponse(knowledge) : minimalKbQueryResponse(knowledge);
2006
2320
  return {
2007
2321
  ...r,
2008
- knowledge: { ...k, chunks: chunks.map(project) }
2322
+ knowledge: { ...knowledge, ...normalized }
2009
2323
  };
2010
2324
  }
2011
2325
  async function memoryList(args) {
@@ -2194,7 +2508,7 @@ USAGE
2194
2508
  ctxdb <command> [args] [--flags]
2195
2509
 
2196
2510
  COMMANDS
2197
- setup [--agent <qoder|qoderwork|codex|claude|opencode>]
2511
+ setup [--agent <qoder|qoderwork|codex|claude|opencode|hermes>]
2198
2512
  [--api-key=K] [--base-url=URL] [--user-id=ID]
2199
2513
  [--no-install-skill] [--no-validate] [--json]
2200
2514
  Without --agent \u2192 writes agents.default (CLI-only, no hooks/skills)
@@ -2203,6 +2517,9 @@ COMMANDS
2203
2517
  Codex \u2192 writes agents.codex config + ~/.codex/hooks.json hooks + skill
2204
2518
  Claude \u2192 writes agents.claude config + ~/.claude/settings.json hooks + skill
2205
2519
  OpenCode \u2192 writes agents.opencode config + ~/.config/opencode/plugins/ctxdb.ts shim + skill
2520
+ Hermes \u2192 writes agents.hermes config + ~/.hermes/config.yaml pre/post_llm_call hooks + skill
2521
+ Approve pre_llm_call + post_llm_call with \`hermes --accept-hooks chat\`
2522
+ (opens interactive chat; exit after Hermes starts)
2206
2523
  status [--agent <name>] [--json]
2207
2524
  ping [--agent <name>] [--json]
2208
2525
  uninstall [--agent <name>] [--purge-config] [--purge-logs] [--purge-all] [--json]
@@ -2213,15 +2530,16 @@ COMMANDS
2213
2530
  --purge-logs also deletes ~/.ctxdb/logs/.
2214
2531
  --purge-all deletes everything under ~/.ctxdb/
2215
2532
  (does not run npm uninstall).
2216
- upgrade|update [--agent <name>] [--self-update] [--json]
2217
- Refresh skills + hooks. --self-update also
2218
- pulls latest package from npm.
2533
+ upgrade|update [--agent <name>] [--no-self-update] [--json]
2534
+ Pull latest package from npm, then re-run
2535
+ setup. Self-update is enabled by default.
2219
2536
 
2220
2537
  memory add <text> [--agent=<name>] [--user-id=...] [--metadata=K1=V1,K2=V2] [--no-infer]
2221
2538
  memory search <query> [--agent=<name>] [--top-k=10] [--threshold=0.4] [--knowledge]
2222
2539
  [--verbose] [--raw]
2223
2540
  --knowledge enables KB chunks alongside memory.
2224
2541
  Default chunk view: {content, score} only.
2542
+ Graph synthesis moves to non-citable graph_context.
2225
2543
  --verbose adds doc_name/kb_id/doc_id/tags.
2226
2544
  --raw ships server response verbatim.
2227
2545
  memory list [--agent=<name>] [--page-size=100] [--category=...]
@@ -2241,6 +2559,7 @@ COMMANDS
2241
2559
  [--verbose] [--raw]
2242
2560
  Standalone KB recall (independent of memory).
2243
2561
  Default: minimal {content, score} per chunk.
2562
+ Graph synthesis moves to non-citable graph_context.
2244
2563
  --verbose: compact view adds doc_name/kb_id/
2245
2564
  doc_id?/tags? for citation.
2246
2565
  --raw: server response verbatim (curl parity).
@@ -2263,7 +2582,7 @@ HOOK ENV VARS:
2263
2582
  CTXDB_SKIP_HOOKS=TRUE Hook/plugin entrypoints exit immediately.
2264
2583
  Direct ctxdb CLI commands are unchanged.
2265
2584
 
2266
- CONFIG: ~/.ctxdb/ctxdb.json (agents.default / agents.qoder / agents.qoderwork / agents.codex / agents.claude / agents.opencode)
2585
+ CONFIG: ~/.ctxdb/ctxdb.json (agents.default / agents.qoder / agents.qoderwork / agents.codex / agents.claude / agents.opencode / agents.hermes)
2267
2586
  LOGS: ~/.ctxdb/logs/ctxdb.log
2268
2587
  `;
2269
2588
  var ROUTES = {
@@ -2341,7 +2660,7 @@ async function main(argv = process.argv.slice(2)) {
2341
2660
  }
2342
2661
  main().then(
2343
2662
  (code) => {
2344
- if (typeof code === "number") process.exit(code);
2663
+ if (typeof code === "number") process.exitCode = code;
2345
2664
  },
2346
2665
  (err) => {
2347
2666
  process.stderr.write(`error: ${err?.message ?? err}
@@ -2349,7 +2668,7 @@ main().then(
2349
2668
  if (process.env.RDS_CTXDB_DEBUG === "1" && err?.stack) {
2350
2669
  process.stderr.write(err.stack + "\n");
2351
2670
  }
2352
- process.exit(1);
2671
+ process.exitCode = 1;
2353
2672
  }
2354
2673
  );
2355
2674
  export {