threadnote 0.7.3 → 0.7.5

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.
@@ -6,7 +6,7 @@
6
6
  <string>io.threadnote.openviking</string>
7
7
  <key>ProgramArguments</key>
8
8
  <array>
9
- <string>openviking-server</string>
9
+ <string>{{OPENVIKING_SERVER_PATH}}</string>
10
10
  <string>--config</string>
11
11
  <string>{{THREADNOTE_HOME}}/ov.conf</string>
12
12
  <string>--host</string>
@@ -3850,6 +3850,27 @@ async function exists(path) {
3850
3850
  return false;
3851
3851
  }
3852
3852
  }
3853
+ async function isExecutable(path) {
3854
+ try {
3855
+ await (0, import_promises.access)(path, import_node_fs.constants.X_OK);
3856
+ return true;
3857
+ } catch (_err) {
3858
+ return false;
3859
+ }
3860
+ }
3861
+ function suggestedShellRc(shellPath, currentPlatform) {
3862
+ const shell = shellPath ?? "";
3863
+ if (shell.endsWith("/zsh")) {
3864
+ return "~/.zshrc";
3865
+ }
3866
+ if (shell.endsWith("/bash")) {
3867
+ return currentPlatform === "darwin" ? "~/.bash_profile" : "~/.bashrc";
3868
+ }
3869
+ if (shell.endsWith("/fish")) {
3870
+ return "~/.config/fish/config.fish";
3871
+ }
3872
+ return "your shell rc";
3873
+ }
3853
3874
  async function isFile(path) {
3854
3875
  try {
3855
3876
  return (await (0, import_promises.stat)(path)).isFile();
@@ -7258,8 +7279,12 @@ function openVikingServerArgs(config) {
7258
7279
  function withIdentity(config, args) {
7259
7280
  return [...args, "--account", config.account, "--user", config.user, "--agent-id", config.agentId];
7260
7281
  }
7261
- function renderTemplate(template, config) {
7262
- return template.replaceAll("{{THREADNOTE_HOME}}", config.agentContextHome).replaceAll("{{OPENVIKING_ACCOUNT}}", config.account).replaceAll("{{OPENVIKING_AGENT_ID}}", config.agentId).replaceAll("{{OPENVIKING_HOST}}", config.host).replaceAll("{{OPENVIKING_PORT}}", String(config.port)).replaceAll("{{OPENVIKING_USER}}", config.user);
7282
+ function renderTemplate(template, config, extras = {}) {
7283
+ let rendered = template.replaceAll("{{THREADNOTE_HOME}}", config.agentContextHome).replaceAll("{{OPENVIKING_ACCOUNT}}", config.account).replaceAll("{{OPENVIKING_AGENT_ID}}", config.agentId).replaceAll("{{OPENVIKING_HOST}}", config.host).replaceAll("{{OPENVIKING_PORT}}", String(config.port)).replaceAll("{{OPENVIKING_USER}}", config.user);
7284
+ for (const [key, value] of Object.entries(extras)) {
7285
+ rendered = rendered.replaceAll(`{{${key}}}`, () => value);
7286
+ }
7287
+ return rendered;
7263
7288
  }
7264
7289
 
7265
7290
  // src/share.ts
@@ -10124,11 +10149,7 @@ async function runPostUpdate(config, options) {
10124
10149
  throw new Error("Provide --from-version and --to-version for post-update.");
10125
10150
  }
10126
10151
  const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
10127
- await ensurePinnedOpenVikingInstalled(config, {
10128
- dryRun: options.dryRun === true,
10129
- interactive,
10130
- yes: options.yes === true
10131
- });
10152
+ await ensurePinnedOpenVikingInstalled(config, { dryRun: options.dryRun === true });
10132
10153
  await runApplicablePostUpdateMigrations(config, {
10133
10154
  dryRun: options.dryRun === true,
10134
10155
  fromVersion: options.fromVersion,
@@ -10141,7 +10162,7 @@ async function runPostUpdate(config, options) {
10141
10162
  async function maybeRunPostUpdateAfterRepair(config, options) {
10142
10163
  const toVersion = await currentPackageVersion();
10143
10164
  const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
10144
- await ensurePinnedOpenVikingInstalled(config, { dryRun: options.dryRun, interactive, yes: false });
10165
+ await ensurePinnedOpenVikingInstalled(config, { dryRun: options.dryRun });
10145
10166
  const state = await readPostUpdateState(config);
10146
10167
  const migrations = await applicablePostUpdateMigrations(config, {
10147
10168
  fromVersion: "0.0.0",
@@ -10184,18 +10205,80 @@ async function ensurePinnedOpenVikingInstalled(config, options) {
10184
10205
  return;
10185
10206
  }
10186
10207
  console.log("");
10187
- console.log(`OpenViking ${installedVersion} is older than the pinned version ${pinned}.`);
10188
- console.log(
10189
- "Upgrading picks up upstream fixes for share-sync reliability (reindex lock acquisition, ov wait timeout)."
10190
- );
10208
+ console.log(`Upgrading OpenViking ${installedVersion} -> ${pinned} (pinned by Threadnote).`);
10209
+ console.log("Picks up upstream fixes for share-sync reliability (reindex lock acquisition, ov wait timeout).");
10210
+ const wasRunning = await isOpenVikingHealthy(config);
10211
+ const usingLaunchd = await isLaunchAgentInstalled();
10191
10212
  const threadnoteCommand = currentThreadnoteCommand() ?? await findExecutable([NPM_PACKAGE_NAME]) ?? NPM_PACKAGE_NAME;
10192
- const installArgs = ["install", "--force", "--no-start"];
10193
- const accepted = options.dryRun || options.yes || options.interactive && await confirmPostUpdateMigration(`Upgrade OpenViking to ${pinned} now? [Y/n] `, true);
10194
- if (!accepted) {
10195
- console.log(`Skipped. Run manually later: ${formatMigrationCommand(threadnoteCommand, installArgs)}`);
10213
+ await maybeRun(options.dryRun, threadnoteCommand, ["install", "--force", "--no-start"]);
10214
+ if (options.dryRun) {
10215
+ if (wasRunning || usingLaunchd) {
10216
+ console.log("Would restart OpenViking server so the new binary takes effect.");
10217
+ }
10218
+ return;
10219
+ }
10220
+ if (!wasRunning && !usingLaunchd) {
10196
10221
  return;
10197
10222
  }
10198
- await maybeRun(options.dryRun, threadnoteCommand, installArgs);
10223
+ console.log("Restarting OpenViking server so the new binary takes effect.");
10224
+ if (usingLaunchd) {
10225
+ const launchAgentPath = launchAgentPlistPath();
10226
+ await runCommand("launchctl", ["unload", launchAgentPath], { allowFailure: true });
10227
+ await runCommand("launchctl", ["load", launchAgentPath], { allowFailure: true });
10228
+ } else {
10229
+ await maybeRun(false, threadnoteCommand, ["stop"]);
10230
+ await maybeRun(false, threadnoteCommand, ["start"]);
10231
+ }
10232
+ const healthyAfter = await waitForOpenVikingHealthy(config, 1e4);
10233
+ if (!healthyAfter) {
10234
+ console.log(
10235
+ `Warning: OpenViking did not return to healthy at ${openVikingHealthEndpoint(config)} within 10s after the restart.`
10236
+ );
10237
+ console.log("Check the server log or run: threadnote start");
10238
+ }
10239
+ }
10240
+ function openVikingHealthEndpoint(config) {
10241
+ return `http://${config.host}:${config.port}/health`;
10242
+ }
10243
+ async function isOpenVikingHealthy(config) {
10244
+ const controller = new AbortController();
10245
+ const timer = setTimeout(() => {
10246
+ controller.abort();
10247
+ }, 800);
10248
+ try {
10249
+ const response = await fetch(openVikingHealthEndpoint(config), { signal: controller.signal });
10250
+ return response.ok;
10251
+ } catch (_err) {
10252
+ return false;
10253
+ } finally {
10254
+ clearTimeout(timer);
10255
+ }
10256
+ }
10257
+ async function waitForOpenVikingHealthy(config, timeoutMs) {
10258
+ const deadline = Date.now() + timeoutMs;
10259
+ while (Date.now() < deadline) {
10260
+ if (await isOpenVikingHealthy(config)) {
10261
+ return true;
10262
+ }
10263
+ await new Promise((resolvePromise) => {
10264
+ setTimeout(resolvePromise, 500);
10265
+ });
10266
+ }
10267
+ return isOpenVikingHealthy(config);
10268
+ }
10269
+ function launchAgentPlistPath() {
10270
+ return (0, import_node_path10.join)((0, import_node_os5.homedir)(), "Library", "LaunchAgents", "io.threadnote.openviking.plist");
10271
+ }
10272
+ async function isLaunchAgentInstalled() {
10273
+ if (process.platform !== "darwin") {
10274
+ return false;
10275
+ }
10276
+ try {
10277
+ await (0, import_promises9.access)(launchAgentPlistPath(), import_node_fs5.constants.F_OK);
10278
+ return true;
10279
+ } catch (_err) {
10280
+ return false;
10281
+ }
10199
10282
  }
10200
10283
  async function readOpenVikingCliVersion(ov) {
10201
10284
  const result = await runCommand(ov, ["version"], { allowFailure: true });
@@ -10455,14 +10538,6 @@ async function runtimeThreadnoteBin(runtime) {
10455
10538
  }
10456
10539
  return (0, import_node_path10.join)(process.env.DENO_INSTALL ?? (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".deno"), "bin", NPM_PACKAGE_NAME);
10457
10540
  }
10458
- async function isExecutable(path) {
10459
- try {
10460
- await (0, import_promises9.access)(path, import_node_fs5.constants.X_OK);
10461
- return true;
10462
- } catch (_err) {
10463
- return false;
10464
- }
10465
- }
10466
10541
  function updatePackageCommand(runtime, registry) {
10467
10542
  if (runtime === "npm") {
10468
10543
  return { executable: "npm", args: ["install", "--global", `${NPM_PACKAGE_NAME}@latest`, `--registry=${registry}`] };
@@ -10506,7 +10581,7 @@ async function runDoctor(config, options) {
10506
10581
  checks.push({ name: "platform", status: (0, import_node_os6.platform)() === "darwin" ? "ok" : "warn", detail: (0, import_node_os6.platform)() });
10507
10582
  checks.push(await commandCheck("node", ["--version"]));
10508
10583
  checks.push(await commandCheck("python3", ["--version"]));
10509
- checks.push(await commandCheck("openviking-server", ["--help"]));
10584
+ checks.push(await openVikingServerCheck());
10510
10585
  checks.push(await firstCommandCheck("openviking cli", ["ov", "openviking"], ["--help"]));
10511
10586
  checks.push(await localEmbeddingCheck());
10512
10587
  checks.push(await pythonSystemCertificatesCheck());
@@ -10540,7 +10615,8 @@ async function runInstall(config, options) {
10540
10615
  await ensureDirectory((0, import_node_path11.join)(config.agentContextHome, "mcp"), dryRun);
10541
10616
  await installCommandShim(dryRun);
10542
10617
  await installUserAgentInstructions(dryRun);
10543
- const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
10618
+ const serverPath = await findOpenVikingServer();
10619
+ let serverInstallRan = false;
10544
10620
  if (serverPath) {
10545
10621
  console.log(`OpenViking server already installed: ${serverPath}`);
10546
10622
  const localEmbeddingMissing = await hasLocalEmbeddingDependency(serverPath) === false;
@@ -10548,6 +10624,7 @@ async function runInstall(config, options) {
10548
10624
  if (options.force === true) {
10549
10625
  console.log(`Reinstalling OpenViking at pinned version ${config.openVikingVersion} (--force).`);
10550
10626
  await runInstallCommands(config, options.packageManager, true, dryRun);
10627
+ serverInstallRan = true;
10551
10628
  } else if (localEmbeddingMissing) {
10552
10629
  const repairReasons = [];
10553
10630
  repairReasons.push("local embedding extra is missing");
@@ -10556,6 +10633,7 @@ async function runInstall(config, options) {
10556
10633
  }
10557
10634
  console.log(`OpenViking install needs repair: ${repairReasons.join("; ")}.`);
10558
10635
  await runInstallCommands(config, options.packageManager, true, dryRun);
10636
+ serverInstallRan = true;
10559
10637
  } else if (pythonSystemCertificatesMissing) {
10560
10638
  console.log("OpenViking install needs repair: Python system certificate bridge is missing.");
10561
10639
  const installCommand = await getPythonSystemCertificatesInstallCommand(serverPath);
@@ -10563,6 +10641,11 @@ async function runInstall(config, options) {
10563
10641
  }
10564
10642
  } else {
10565
10643
  await runInstallCommands(config, options.packageManager, false, dryRun);
10644
+ serverInstallRan = true;
10645
+ }
10646
+ const resolvedServerPath = serverInstallRan ? await findOpenVikingServer() : serverPath;
10647
+ if (resolvedServerPath && !dryRun) {
10648
+ await maybePrintOpenVikingPathHint(resolvedServerPath);
10566
10649
  }
10567
10650
  await writeTemplateIfMissing({
10568
10651
  config,
@@ -10699,6 +10782,64 @@ async function repairManifest(config, dryRun) {
10699
10782
  await (0, import_promises11.chmod)(config.manifestPath, 384);
10700
10783
  console.log(`Wrote replacement manifest: ${config.manifestPath}`);
10701
10784
  }
10785
+ async function findOpenVikingServer() {
10786
+ const onPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
10787
+ if (onPath) {
10788
+ return onPath;
10789
+ }
10790
+ for (const candidateDir of await openVikingServerCandidateDirs()) {
10791
+ const candidate = (0, import_node_path11.join)(candidateDir, OPENVIKING_SERVER_COMMAND);
10792
+ if (await isExecutable(candidate)) {
10793
+ return candidate;
10794
+ }
10795
+ }
10796
+ return void 0;
10797
+ }
10798
+ var candidateDirsPromise;
10799
+ async function openVikingServerCandidateDirs() {
10800
+ if (!candidateDirsPromise) {
10801
+ candidateDirsPromise = computeOpenVikingServerCandidateDirs();
10802
+ }
10803
+ return candidateDirsPromise;
10804
+ }
10805
+ async function computeOpenVikingServerCandidateDirs() {
10806
+ const dirs = [];
10807
+ const uv = await findExecutable(["uv"]);
10808
+ if (uv) {
10809
+ const result = await runCommand(uv, ["tool", "dir", "--bin"], { allowFailure: true });
10810
+ if (result.exitCode === 0) {
10811
+ const dir = result.stdout.trim();
10812
+ if (dir) {
10813
+ dirs.push(dir);
10814
+ }
10815
+ }
10816
+ }
10817
+ if (process.env.UV_TOOL_BIN_DIR) {
10818
+ dirs.push(process.env.UV_TOOL_BIN_DIR);
10819
+ }
10820
+ dirs.push(expandPath("~/.local/bin"));
10821
+ return Array.from(new Set(dirs));
10822
+ }
10823
+ async function requireOpenVikingServer() {
10824
+ const resolved = await findOpenVikingServer();
10825
+ if (!resolved) {
10826
+ throw new Error(
10827
+ `${OPENVIKING_SERVER_COMMAND} was not found in PATH, uv tool bin dir, $UV_TOOL_BIN_DIR, or ~/.local/bin. Run \`threadnote install\` first.`
10828
+ );
10829
+ }
10830
+ return resolved;
10831
+ }
10832
+ async function maybePrintOpenVikingPathHint(serverPath) {
10833
+ const onPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
10834
+ if (onPath) {
10835
+ return;
10836
+ }
10837
+ const binDir = (0, import_node_path11.dirname)(serverPath);
10838
+ const rcHint = suggestedShellRc(process.env.SHELL, (0, import_node_os6.platform)());
10839
+ console.log(
10840
+ `Note: ${serverPath} is installed but ${binDir} is not on this shell's PATH. Add \`export PATH="${binDir}:$PATH"\` to ${rcHint} so other tools can find openviking-server.`
10841
+ );
10842
+ }
10702
10843
  async function repairServerHealth(config, dryRun) {
10703
10844
  const existingHealth = await readOpenVikingHealthIfAvailable(config, 800);
10704
10845
  if (existingHealth) {
@@ -10719,7 +10860,7 @@ async function runStart(config, options) {
10719
10860
  await installLaunchAgent(config, options.dryRun === true);
10720
10861
  return;
10721
10862
  }
10722
- const server = options.dryRun === true ? await findExecutable([OPENVIKING_SERVER_COMMAND]) ?? OPENVIKING_SERVER_COMMAND : await requiredExecutable(OPENVIKING_SERVER_COMMAND);
10863
+ const server = options.dryRun === true ? await findOpenVikingServer() ?? OPENVIKING_SERVER_COMMAND : await requireOpenVikingServer();
10723
10864
  const args = openVikingServerArgs(config);
10724
10865
  if (options.dryRun === true) {
10725
10866
  console.log(formatShellCommand(server, args));
@@ -10813,6 +10954,21 @@ async function commandCheck(name, args) {
10813
10954
  detail: firstLine(result.stdout || result.stderr) || executable
10814
10955
  };
10815
10956
  }
10957
+ async function openVikingServerCheck() {
10958
+ const name = OPENVIKING_SERVER_COMMAND;
10959
+ const executable = await findOpenVikingServer();
10960
+ if (!executable) {
10961
+ return { name, status: "fail", detail: "missing; install will fetch it via uv or pipx" };
10962
+ }
10963
+ const result = await runCommand(executable, ["--help"], { allowFailure: true });
10964
+ const onPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
10965
+ const detail = onPath ? executable : `${executable} (found outside PATH; add ${(0, import_node_path11.dirname)(executable)} to PATH)`;
10966
+ return {
10967
+ name,
10968
+ status: result.exitCode === 0 ? "ok" : "warn",
10969
+ detail: result.exitCode === 0 ? detail : firstLine(result.stderr || result.stdout) || detail
10970
+ };
10971
+ }
10816
10972
  async function commandPresenceCheck(name, args) {
10817
10973
  const executable = await findExecutable([name]);
10818
10974
  if (!executable) {
@@ -10841,7 +10997,7 @@ async function firstCommandCheck(name, commands, args) {
10841
10997
  return { name, status: "fail", detail: `none found: ${commands.join(", ")}` };
10842
10998
  }
10843
10999
  async function localEmbeddingCheck() {
10844
- const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
11000
+ const serverPath = await findOpenVikingServer();
10845
11001
  if (!serverPath) {
10846
11002
  return { name: "local embedding extra", status: "warn", detail: "openviking-server missing" };
10847
11003
  }
@@ -10860,7 +11016,7 @@ async function localEmbeddingCheck() {
10860
11016
  };
10861
11017
  }
10862
11018
  async function pythonSystemCertificatesCheck() {
10863
- const serverPath = await findExecutable([OPENVIKING_SERVER_COMMAND]);
11019
+ const serverPath = await findOpenVikingServer();
10864
11020
  if (!serverPath) {
10865
11021
  return { name: "python system certs", status: "warn", detail: "openviking-server missing" };
10866
11022
  }
@@ -11041,6 +11197,7 @@ async function installUv() {
11041
11197
  console.log(result.stdout.trim());
11042
11198
  }
11043
11199
  if (await findExecutable(["uv"])) {
11200
+ candidateDirsPromise = void 0;
11044
11201
  return true;
11045
11202
  }
11046
11203
  } else {
@@ -11057,6 +11214,7 @@ async function installUv() {
11057
11214
  console.log(result.stdout.trim());
11058
11215
  }
11059
11216
  if (await findExecutable(["uv"])) {
11217
+ candidateDirsPromise = void 0;
11060
11218
  return true;
11061
11219
  }
11062
11220
  console.warn(
@@ -11361,10 +11519,20 @@ async function installLaunchAgent(config, dryRun) {
11361
11519
  if ((0, import_node_os6.platform)() !== "darwin") {
11362
11520
  throw new Error("launchd autostart is only supported on macOS.");
11363
11521
  }
11522
+ const resolvedServer = await findOpenVikingServer();
11523
+ if (!resolvedServer && !dryRun) {
11524
+ throw new Error(
11525
+ `Cannot install LaunchAgent: ${OPENVIKING_SERVER_COMMAND} was not found in PATH, uv tool bin dir, $UV_TOOL_BIN_DIR, or ~/.local/bin. Run \`threadnote install\` first.`
11526
+ );
11527
+ }
11364
11528
  const source = (0, import_node_path11.join)(toolRoot(), "config", "launchd", `${LAUNCHD_LABEL}.plist.template`);
11365
11529
  const destination = expandPath(`~/Library/LaunchAgents/${LAUNCHD_LABEL}.plist`);
11366
- const rendered = renderTemplate(await (0, import_promises11.readFile)(source, "utf8"), config);
11530
+ const rendered = renderTemplate(await (0, import_promises11.readFile)(source, "utf8"), config, {
11531
+ OPENVIKING_SERVER_PATH: resolvedServer ?? OPENVIKING_SERVER_COMMAND
11532
+ });
11367
11533
  if (dryRun) {
11534
+ const resolutionDetail = resolvedServer ?? `<not found; would use bare \`${OPENVIKING_SERVER_COMMAND}\`>`;
11535
+ console.log(`Resolved openviking-server: ${resolutionDetail}`);
11368
11536
  console.log(`Would write ${destination}`);
11369
11537
  console.log(`Would run: launchctl unload ${destination}`);
11370
11538
  console.log(`Would run: launchctl load ${destination}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "threadnote",
3
- "version": "0.7.3",
3
+ "version": "0.7.5",
4
4
  "type": "commonjs",
5
5
  "main": "dist/threadnote.cjs",
6
6
  "description": "Shared local context and handoffs for development agents",
@@ -43,18 +43,21 @@
43
43
  "clean": "rm -rf dist",
44
44
  "dev": "tsx src/threadnote.ts",
45
45
  "dev:mcp-server": "tsx src/mcp_server.ts",
46
- "lint": "eslint \"src/**/*.ts\"",
47
- "lint:fix": "eslint \"src/**/*.ts\" --fix",
46
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
47
+ "lint:fix": "eslint \"src/**/*.ts\" \"test/**/*.ts\" --fix",
48
48
  "prepare": "husky",
49
- "precommit": "npm run lint && npm run prettier:check",
49
+ "precommit": "npm run lint && npm run prettier:check && npm run test",
50
50
  "prepack": "npm run build",
51
51
  "prettier:check": "prettier --check \"**/*.{ts,js,cjs,json,md,yaml,yml}\"",
52
52
  "prettier:write": "prettier --write \"**/*.{ts,js,cjs,json,md,yaml,yml}\"",
53
+ "test": "vitest run",
54
+ "test:watch": "vitest",
55
+ "test:coverage": "vitest run --coverage",
53
56
  "threadnote": "npm run build --silent && node ./bin/threadnote.cjs",
54
57
  "doctor": "npm run build --silent && node ./bin/threadnote.cjs doctor",
55
58
  "mcp-server": "npm run build --silent && node ./bin/threadnote-mcp-server.cjs",
56
59
  "pack:dry-run": "npm pack --dry-run",
57
- "typecheck": "tsc --noEmit"
60
+ "typecheck": "tsc --noEmit && tsc -p test/tsconfig.json --noEmit"
58
61
  },
59
62
  "engines": {
60
63
  "node": ">=20"
@@ -64,6 +67,7 @@
64
67
  "@modelcontextprotocol/sdk": "^1.29.0",
65
68
  "@types/js-yaml": "^4.0.9",
66
69
  "@types/node": "^25.6.0",
70
+ "@vitest/coverage-v8": "^4.1.7",
67
71
  "commander": "^14.0.3",
68
72
  "esbuild": "^0.27.7",
69
73
  "eslint": "^10.3.0",
@@ -74,6 +78,7 @@
74
78
  "tsx": "^4.21.0",
75
79
  "typescript": "^6.0.3",
76
80
  "typescript-eslint": "^8.59.2",
81
+ "vitest": "^4.1.7",
77
82
  "zod": "^4.4.3"
78
83
  }
79
84
  }