routstrd 0.2.15 → 0.2.16

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.
@@ -37216,7 +37216,7 @@ var init_dist3 = __esm(() => {
37216
37216
  // src/daemon/index.ts
37217
37217
  init_dist3();
37218
37218
  import { createServer } from "http";
37219
- import { existsSync as existsSync8 } from "fs";
37219
+ import { existsSync as existsSync9 } from "fs";
37220
37220
 
37221
37221
  // src/utils/config.ts
37222
37222
  var HOME = process.env.HOME || process.env.USERPROFILE || "";
@@ -42368,6 +42368,76 @@ Installing routstr configuration in ${configPath}...`);
42368
42368
  }
42369
42369
  }
42370
42370
 
42371
+ // src/integrations/hermes.ts
42372
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
42373
+ import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
42374
+ import { dirname as dirname6 } from "path";
42375
+ async function installHermesIntegration(config, apiKey, integrationConfig) {
42376
+ const { name, configPath } = integrationConfig;
42377
+ logger3.log(`
42378
+ Installing routstr configuration in ${configPath}...`);
42379
+ logger3.log(`Using API key for ${name}`);
42380
+ const baseUrl = getDaemonBaseUrl(config);
42381
+ const baseUrlV1 = `${baseUrl}/v1`;
42382
+ let defaultModel = "minimax-m2.7";
42383
+ try {
42384
+ const data = await callDaemon("/models");
42385
+ const models = data.output?.models || [];
42386
+ if (models.length >= 3) {
42387
+ defaultModel = models[2].id;
42388
+ logger3.log(`Set default model to 3rd available model: ${defaultModel}`);
42389
+ } else if (models.length > 0) {
42390
+ defaultModel = models[0].id;
42391
+ logger3.log(`Only ${models.length} models available, using ${defaultModel} as default.`);
42392
+ } else {
42393
+ logger3.log("No models available from routstr daemon, using fallback default.");
42394
+ }
42395
+ } catch (error) {
42396
+ logger3.error("Failed to fetch models for Hermes integration:", error);
42397
+ logger3.log("Using fallback default model.");
42398
+ }
42399
+ let content2 = "";
42400
+ try {
42401
+ if (existsSync8(configPath)) {
42402
+ content2 = await readFile6(configPath, "utf-8");
42403
+ }
42404
+ } catch (error) {
42405
+ logger3.error(`Error reading ${configPath}, creating new one.`);
42406
+ }
42407
+ content2 = content2.replace(/^model:\n(?: .*\n)*/gm, "");
42408
+ content2 = content2.replace(/^custom_providers:\n(?:- .*\n(?: .*\n)*)*/gm, "");
42409
+ content2 = content2.replace(/\n{3,}/g, `
42410
+
42411
+ `).trim();
42412
+ const urlDisplay = baseUrl.replace(/^https?:\/\//, "");
42413
+ const modelBlock = `model:
42414
+ default: ${defaultModel}
42415
+ provider: custom
42416
+ base_url: ${baseUrlV1}
42417
+ api_key: ${apiKey}`;
42418
+ const providerBlock = `custom_providers:
42419
+ - name: Routstr (${urlDisplay})
42420
+ base_url: ${baseUrlV1}
42421
+ api_key: ${apiKey}
42422
+ model: ${defaultModel}`;
42423
+ const parts = [modelBlock];
42424
+ if (content2) {
42425
+ parts.push(content2);
42426
+ }
42427
+ parts.push(providerBlock);
42428
+ const newContent = parts.join(`
42429
+
42430
+ `) + `
42431
+ `;
42432
+ try {
42433
+ mkdirSync5(dirname6(configPath), { recursive: true });
42434
+ await writeFile6(configPath, newContent);
42435
+ logger3.log(`Successfully updated ${configPath} with routstr settings.`);
42436
+ } catch (error) {
42437
+ logger3.error(`Failed to write to ${configPath}:`, error);
42438
+ }
42439
+ }
42440
+
42371
42441
  // src/integrations/registry.ts
42372
42442
  var CLIENT_CONFIGS = {
42373
42443
  opencode: {
@@ -42389,13 +42459,19 @@ var CLIENT_CONFIGS = {
42389
42459
  clientId: "claude-code",
42390
42460
  name: "Claude Code",
42391
42461
  configPath: join5(process.env.HOME || "", ".claude/settings.json")
42462
+ },
42463
+ hermes: {
42464
+ clientId: "hermes",
42465
+ name: "Hermes",
42466
+ configPath: join5(process.env.HOME || "", ".hermes/config.yaml")
42392
42467
  }
42393
42468
  };
42394
42469
  var CLIENT_INTEGRATIONS = {
42395
42470
  opencode: installOpencodeIntegration,
42396
42471
  "pi-agent": installPiIntegration,
42397
42472
  openclaw: installOpenClawIntegration,
42398
- "claude-code": installClaudeCodeIntegration
42473
+ "claude-code": installClaudeCodeIntegration,
42474
+ hermes: installHermesIntegration
42399
42475
  };
42400
42476
  async function runIntegrationsForClients(clientIds, config) {
42401
42477
  for (const client2 of clientIds) {
@@ -43299,7 +43375,7 @@ async function main() {
43299
43375
  }));
43300
43376
  Bun.write(PID_FILE, String(process.pid));
43301
43377
  try {
43302
- if (existsSync8(SOCKET_PATH)) {
43378
+ if (existsSync9(SOCKET_PATH)) {
43303
43379
  Bun.spawn(["rm", SOCKET_PATH]);
43304
43380
  }
43305
43381
  } catch {
package/dist/index.js CHANGED
@@ -15025,6 +15025,78 @@ Installing routstr configuration in ${configPath}...`);
15025
15025
  }
15026
15026
  }
15027
15027
 
15028
+ // src/integrations/hermes.ts
15029
+ init_logger();
15030
+ init_daemon_client();
15031
+ import { existsSync as existsSync8, mkdirSync as mkdirSync5 } from "fs";
15032
+ import { readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
15033
+ import { dirname as dirname6 } from "path";
15034
+ async function installHermesIntegration(config, apiKey, integrationConfig) {
15035
+ const { name, configPath } = integrationConfig;
15036
+ logger.log(`
15037
+ Installing routstr configuration in ${configPath}...`);
15038
+ logger.log(`Using API key for ${name}`);
15039
+ const baseUrl = getDaemonBaseUrl(config);
15040
+ const baseUrlV1 = `${baseUrl}/v1`;
15041
+ let defaultModel = "minimax-m2.7";
15042
+ try {
15043
+ const data = await callDaemon("/models");
15044
+ const models = data.output?.models || [];
15045
+ if (models.length >= 3) {
15046
+ defaultModel = models[2].id;
15047
+ logger.log(`Set default model to 3rd available model: ${defaultModel}`);
15048
+ } else if (models.length > 0) {
15049
+ defaultModel = models[0].id;
15050
+ logger.log(`Only ${models.length} models available, using ${defaultModel} as default.`);
15051
+ } else {
15052
+ logger.log("No models available from routstr daemon, using fallback default.");
15053
+ }
15054
+ } catch (error) {
15055
+ logger.error("Failed to fetch models for Hermes integration:", error);
15056
+ logger.log("Using fallback default model.");
15057
+ }
15058
+ let content = "";
15059
+ try {
15060
+ if (existsSync8(configPath)) {
15061
+ content = await readFile6(configPath, "utf-8");
15062
+ }
15063
+ } catch (error) {
15064
+ logger.error(`Error reading ${configPath}, creating new one.`);
15065
+ }
15066
+ content = content.replace(/^model:\n(?: .*\n)*/gm, "");
15067
+ content = content.replace(/^custom_providers:\n(?:- .*\n(?: .*\n)*)*/gm, "");
15068
+ content = content.replace(/\n{3,}/g, `
15069
+
15070
+ `).trim();
15071
+ const urlDisplay = baseUrl.replace(/^https?:\/\//, "");
15072
+ const modelBlock = `model:
15073
+ default: ${defaultModel}
15074
+ provider: custom
15075
+ base_url: ${baseUrlV1}
15076
+ api_key: ${apiKey}`;
15077
+ const providerBlock = `custom_providers:
15078
+ - name: Routstr (${urlDisplay})
15079
+ base_url: ${baseUrlV1}
15080
+ api_key: ${apiKey}
15081
+ model: ${defaultModel}`;
15082
+ const parts = [modelBlock];
15083
+ if (content) {
15084
+ parts.push(content);
15085
+ }
15086
+ parts.push(providerBlock);
15087
+ const newContent = parts.join(`
15088
+
15089
+ `) + `
15090
+ `;
15091
+ try {
15092
+ mkdirSync5(dirname6(configPath), { recursive: true });
15093
+ await writeFile6(configPath, newContent);
15094
+ logger.log(`Successfully updated ${configPath} with routstr settings.`);
15095
+ } catch (error) {
15096
+ logger.error(`Failed to write to ${configPath}:`, error);
15097
+ }
15098
+ }
15099
+
15028
15100
  // src/integrations/registry.ts
15029
15101
  var CLIENT_CONFIGS = {
15030
15102
  opencode: {
@@ -15046,13 +15118,19 @@ var CLIENT_CONFIGS = {
15046
15118
  clientId: "claude-code",
15047
15119
  name: "Claude Code",
15048
15120
  configPath: join3(process.env.HOME || "", ".claude/settings.json")
15121
+ },
15122
+ hermes: {
15123
+ clientId: "hermes",
15124
+ name: "Hermes",
15125
+ configPath: join3(process.env.HOME || "", ".hermes/config.yaml")
15049
15126
  }
15050
15127
  };
15051
15128
  var CLIENT_INTEGRATIONS = {
15052
15129
  opencode: installOpencodeIntegration,
15053
15130
  "pi-agent": installPiIntegration,
15054
15131
  openclaw: installOpenClawIntegration,
15055
- "claude-code": installClaudeCodeIntegration
15132
+ "claude-code": installClaudeCodeIntegration,
15133
+ hermes: installHermesIntegration
15056
15134
  };
15057
15135
  async function runIntegrationsForClients(clientIds, config) {
15058
15136
  for (const client of clientIds) {
@@ -15162,6 +15240,8 @@ async function addClientAction(options) {
15162
15240
  integrationKeys.push("pi-agent");
15163
15241
  if (options.claudeCode)
15164
15242
  integrationKeys.push("claude-code");
15243
+ if (options.hermes)
15244
+ integrationKeys.push("hermes");
15165
15245
  if (integrationKeys.length > 0) {
15166
15246
  for (const key of integrationKeys) {
15167
15247
  const integrationFn = CLIENT_INTEGRATIONS[key];
@@ -15190,7 +15270,15 @@ async function addClientAction(options) {
15190
15270
  return;
15191
15271
  }
15192
15272
  if (!options.name) {
15193
- console.error("error: required option '-n, --name <name>' not specified");
15273
+ console.error(`error: either provide a client name or specify an integration flag.
15274
+ `);
15275
+ console.error("Options:");
15276
+ console.error(" -n, --name <name> Client name");
15277
+ console.error(" --opencode Set up OpenCode integration");
15278
+ console.error(" --openclaw Set up OpenClaw integration");
15279
+ console.error(" --pi-agent Set up Pi Agent integration");
15280
+ console.error(" --claude-code Set up Claude Code integration");
15281
+ console.error(" --hermes Set up Hermes integration");
15194
15282
  process.exit(1);
15195
15283
  }
15196
15284
  try {
@@ -15221,7 +15309,7 @@ async function addClientAction(options) {
15221
15309
  // src/cli.ts
15222
15310
  init_config();
15223
15311
  init_logger();
15224
- import { existsSync as existsSync9, mkdirSync as mkdirSync5 } from "fs";
15312
+ import { existsSync as existsSync10, mkdirSync as mkdirSync6 } from "fs";
15225
15313
  import { execSync } from "child_process";
15226
15314
 
15227
15315
  // src/integrations/index.ts
@@ -15245,7 +15333,7 @@ function parseChoice(input) {
15245
15333
  return 1;
15246
15334
  }
15247
15335
  const parsed = Number.parseInt(input, 10);
15248
- if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 5) {
15336
+ if (!Number.isNaN(parsed) && parsed >= 1 && parsed <= 6) {
15249
15337
  return parsed;
15250
15338
  }
15251
15339
  return 1;
@@ -15257,14 +15345,16 @@ Choose an integration to set up:`);
15257
15345
  logger.log("2. OpenClaw");
15258
15346
  logger.log("3. Pi");
15259
15347
  logger.log("4. Claude Code");
15260
- logger.log("5. Skip for now");
15348
+ logger.log("5. Hermes");
15349
+ logger.log("6. Skip for now");
15261
15350
  const answer = await ask("Select integration [1]: ");
15262
15351
  const choice = parseChoice(answer);
15263
15352
  const integrationByChoice = {
15264
15353
  1: "opencode",
15265
15354
  2: "openclaw",
15266
15355
  3: "pi-agent",
15267
- 4: "claude-code"
15356
+ 4: "claude-code",
15357
+ 5: "hermes"
15268
15358
  };
15269
15359
  const key = integrationByChoice[choice];
15270
15360
  if (!key) {
@@ -15272,7 +15362,7 @@ Choose an integration to set up:`);
15272
15362
  return;
15273
15363
  }
15274
15364
  const integrationConfig = CLIENT_CONFIGS[key];
15275
- const { client, created } = await addDaemonClient(integrationConfig.name, integrationConfig.clientId);
15365
+ const { client, created } = await addDaemonClient(integrationConfig.name);
15276
15366
  if (created) {
15277
15367
  logger.log(`Created new API key for ${integrationConfig.name}`);
15278
15368
  } else {
@@ -15292,6 +15382,11 @@ Choose an integration to set up:`);
15292
15382
  }
15293
15383
  if (key === "claude-code") {
15294
15384
  await installClaudeCodeIntegration(config, client.apiKey, integrationConfig);
15385
+ return;
15386
+ }
15387
+ if (key === "hermes") {
15388
+ await installHermesIntegration(config, client.apiKey, integrationConfig);
15389
+ return;
15295
15390
  }
15296
15391
  }
15297
15392
 
@@ -15368,7 +15463,7 @@ init_nip98();
15368
15463
  init_esm();
15369
15464
 
15370
15465
  // src/daemon/wallet/cocod-client.ts
15371
- import { existsSync as existsSync8 } from "fs";
15466
+ import { existsSync as existsSync9 } from "fs";
15372
15467
  init_logger();
15373
15468
  init_process_lock();
15374
15469
  var DEFAULT_CONFIG_DIR = process.env.COCOD_DIR || `${process.env.HOME || process.env.USERPROFILE || ""}/.cocod`;
@@ -15380,7 +15475,7 @@ function resolveCocodExecutable(cocodPath) {
15380
15475
  async function isCocodInstalled(cocodPath) {
15381
15476
  const executable = resolveCocodExecutable(cocodPath);
15382
15477
  if (executable.includes("/")) {
15383
- return existsSync8(executable);
15478
+ return existsSync9(executable);
15384
15479
  }
15385
15480
  try {
15386
15481
  const proc = Bun.spawn({
@@ -15396,7 +15491,7 @@ async function isCocodInstalled(cocodPath) {
15396
15491
  // package.json
15397
15492
  var package_default = {
15398
15493
  name: "routstrd",
15399
- version: "0.2.15",
15494
+ version: "0.2.16",
15400
15495
  module: "src/index.ts",
15401
15496
  type: "module",
15402
15497
  private: false,
@@ -15475,11 +15570,11 @@ async function requireLocalDaemon() {
15475
15570
  }
15476
15571
  async function initDaemon() {
15477
15572
  logger.log("Initializing routstrd...");
15478
- if (!existsSync9(CONFIG_DIR)) {
15479
- mkdirSync5(CONFIG_DIR, { recursive: true });
15573
+ if (!existsSync10(CONFIG_DIR)) {
15574
+ mkdirSync6(CONFIG_DIR, { recursive: true });
15480
15575
  logger.log(`Created config directory: ${CONFIG_DIR}`);
15481
15576
  }
15482
- if (!existsSync9(CONFIG_FILE)) {
15577
+ if (!existsSync10(CONFIG_FILE)) {
15483
15578
  const config2 = {
15484
15579
  ...DEFAULT_CONFIG,
15485
15580
  cocodPath: null
@@ -15603,8 +15698,8 @@ program.command("remote <url>").description("Configure a remote daemon URL").act
15603
15698
  console.error(`Invalid URL: ${url}`);
15604
15699
  process.exit(1);
15605
15700
  }
15606
- if (!existsSync9(CONFIG_DIR)) {
15607
- mkdirSync5(CONFIG_DIR, { recursive: true });
15701
+ if (!existsSync10(CONFIG_DIR)) {
15702
+ mkdirSync6(CONFIG_DIR, { recursive: true });
15608
15703
  }
15609
15704
  const config = await loadConfig();
15610
15705
  const updates = { daemonUrl: url };
@@ -15898,7 +15993,7 @@ clientsCmd.command("list").description("List all clients").action(async () => {
15898
15993
  clientsCmd.command("delete <id>").description("Delete a client by its ID").action(async (id) => {
15899
15994
  await deleteClientAction(id);
15900
15995
  });
15901
- clientsCmd.command("add").description("Add a new client or set up client integrations").option("-n, --name <name>", "Client name").option("--opencode", "Set up OpenCode integration").option("--openclaw", "Set up OpenClaw integration").option("--pi-agent", "Set up Pi Agent integration").option("--claude-code", "Set up Claude Code integration").action(async (options) => {
15996
+ clientsCmd.command("add").description("Add a new client or set up client integrations").option("-n, --name <name>", "Client name").option("--opencode", "Set up OpenCode integration").option("--openclaw", "Set up OpenClaw integration").option("--pi-agent", "Set up Pi Agent integration").option("--claude-code", "Set up Claude Code integration").option("--hermes", "Set up Hermes integration").action(async (options) => {
15902
15997
  await addClientAction(options);
15903
15998
  });
15904
15999
  var npubsCmd = program.command("npubs").description("Manage npubs on the daemon (admin or user roles)");
@@ -15971,16 +16066,21 @@ npubsCmd.command("register").description("Register yourself as the first admin (
15971
16066
  console.log(`Successfully registered as first admin npub: ${userNpub}`);
15972
16067
  }
15973
16068
  });
15974
- npubsCmd.command("add <npub>").description("Add a npub (hex pubkey or npub1...). First becomes admin, subsequent become user.").action(async (npubArg) => {
16069
+ npubsCmd.command("add <npub>").description("Add a npub (hex pubkey or npub1...). Defaults to 'user' role unless --role is specified.").option("-r, --role <role>", "Role for the npub: 'admin' or 'user' (default: 'user')", "user").action(async (npubArg, options) => {
15975
16070
  await ensureDaemonRunning();
15976
16071
  const normalized = normalizeNostrPubkey(npubArg);
15977
16072
  if (!normalized) {
15978
16073
  console.error("Invalid npub value. Use npub1... or 64-char hex pubkey.");
15979
16074
  process.exit(1);
15980
16075
  }
16076
+ if (options.role !== "admin" && options.role !== "user") {
16077
+ console.error("Invalid role. Expected 'admin' or 'user'.");
16078
+ process.exit(1);
16079
+ }
16080
+ const body = { npub: npubFromPubkey(normalized), role: options.role };
15981
16081
  const result = await callDaemon("/npubs", {
15982
16082
  method: "POST",
15983
- body: { npub: npubFromPubkey(normalized) }
16083
+ body
15984
16084
  });
15985
16085
  if (result.error) {
15986
16086
  console.log(result.error);
@@ -15988,10 +16088,36 @@ npubsCmd.command("add <npub>").description("Add a npub (hex pubkey or npub1...).
15988
16088
  }
15989
16089
  const output = result.output;
15990
16090
  if (output?.npub) {
15991
- console.log(`${output.added ? "Added" : "Already configured"} npub: ${output.npub}`);
16091
+ console.log(`${output.added ? "Added" : "Already configured"} npub: ${output.npub} [${output.role ?? "user"}]`);
16092
+ }
16093
+ });
16094
+ npubsCmd.command("update <npub>").description("Update the role of an existing npub (requires admin)").requiredOption("-r, --role <role>", "New role: 'admin' or 'user'").action(async (npubArg, options) => {
16095
+ await ensureDaemonRunning();
16096
+ const normalized = normalizeNostrPubkey(npubArg);
16097
+ if (!normalized) {
16098
+ console.error("Invalid npub value. Use npub1... or 64-char hex pubkey.");
16099
+ process.exit(1);
16100
+ }
16101
+ if (options.role !== "admin" && options.role !== "user") {
16102
+ console.error("Invalid role. Expected 'admin' or 'user'.");
16103
+ process.exit(1);
16104
+ }
16105
+ const result = await callDaemon("/npubs", {
16106
+ method: "PATCH",
16107
+ body: { npub: npubFromPubkey(normalized), role: options.role }
16108
+ });
16109
+ if (result.error) {
16110
+ console.log(result.error);
16111
+ process.exit(1);
16112
+ }
16113
+ const data = result.output ?? result;
16114
+ if (data?.npub) {
16115
+ console.log(`Updated npub ${data.npub} role to '${data.role}'.`);
16116
+ } else {
16117
+ console.log("Npub not found or update failed.");
15992
16118
  }
15993
16119
  });
15994
- npubsCmd.command("delete <npub>").description("Delete an admin npub (hex pubkey or npub1...)").action(async (npubArg) => {
16120
+ npubsCmd.command("delete <npub>").description("Delete an npub (hex pubkey or npub1...)").action(async (npubArg) => {
15995
16121
  await ensureDaemonRunning();
15996
16122
  const normalized = normalizeNostrPubkey(npubArg);
15997
16123
  if (!normalized) {
@@ -16167,7 +16293,7 @@ serviceCmd.command("install").description("Install and start routstrd using PM2
16167
16293
  const path = import.meta.require("path");
16168
16294
  daemonPath = path.join(path.dirname(import.meta.url).replace("file://", ""), "daemon", "index.js");
16169
16295
  }
16170
- if (!existsSync9(daemonPath)) {
16296
+ if (!existsSync10(daemonPath)) {
16171
16297
  console.error(`Could not find daemon at ${daemonPath}. Did you run 'bun run build'?`);
16172
16298
  process.exit(1);
16173
16299
  }
@@ -16305,14 +16431,14 @@ program.command("logs").description("View daemon logs").option("-f, --follow", "
16305
16431
  const yesterday = new Date;
16306
16432
  yesterday.setDate(yesterday.getDate() - 1);
16307
16433
  const yesterdayFile = getLogFileForDate2(yesterday);
16308
- if (!existsSync9(todayFile) && !existsSync9(yesterdayFile)) {
16434
+ if (!existsSync10(todayFile) && !existsSync10(yesterdayFile)) {
16309
16435
  console.log("No log files found. Daemon may not have started yet.");
16310
16436
  console.log(`Logs directory: ${LOGS_DIR2}`);
16311
16437
  process.exit(1);
16312
16438
  }
16313
16439
  const lines = parseInt(options.lines, 10);
16314
16440
  const logFiles = [yesterdayFile, todayFile].filter((file, index, files) => {
16315
- return existsSync9(file) && files.indexOf(file) === index;
16441
+ return existsSync10(file) && files.indexOf(file) === index;
16316
16442
  });
16317
16443
  if (options.follow) {
16318
16444
  const proc2 = Bun.spawn(["tail", "-n", String(lines), "-f", todayFile], {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routstrd",
3
- "version": "0.2.15",
3
+ "version": "0.2.16",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
6
  "private": false,
package/src/cli.ts CHANGED
@@ -820,6 +820,7 @@ clientsCmd
820
820
  .option("--openclaw", "Set up OpenClaw integration")
821
821
  .option("--pi-agent", "Set up Pi Agent integration")
822
822
  .option("--claude-code", "Set up Claude Code integration")
823
+ .option("--hermes", "Set up Hermes integration")
823
824
  .action(
824
825
  async (options: {
825
826
  name?: string;
@@ -827,6 +828,7 @@ clientsCmd
827
828
  openclaw?: boolean;
828
829
  piAgent?: boolean;
829
830
  claudeCode?: boolean;
831
+ hermes?: boolean;
830
832
  }) => {
831
833
  await addClientAction(options);
832
834
  },
@@ -930,35 +932,75 @@ npubsCmd
930
932
 
931
933
  npubsCmd
932
934
  .command("add <npub>")
933
- .description("Add a npub (hex pubkey or npub1...). First becomes admin, subsequent become user.")
934
- .action(async (npubArg: string) => {
935
+ .description("Add a npub (hex pubkey or npub1...). Defaults to 'user' role unless --role is specified.")
936
+ .option("-r, --role <role>", "Role for the npub: 'admin' or 'user' (default: 'user')", "user")
937
+ .action(async (npubArg: string, options: { role: string }) => {
935
938
  await ensureDaemonRunning();
936
939
  const normalized = normalizeNostrPubkey(npubArg);
937
940
  if (!normalized) {
938
941
  console.error("Invalid npub value. Use npub1... or 64-char hex pubkey.");
939
942
  process.exit(1);
940
943
  }
944
+ if (options.role !== "admin" && options.role !== "user") {
945
+ console.error("Invalid role. Expected 'admin' or 'user'.");
946
+ process.exit(1);
947
+ }
948
+ const body: Record<string, string> = { npub: npubFromPubkey(normalized), role: options.role };
941
949
  const result = await callDaemon("/npubs", {
942
950
  method: "POST",
943
- body: { npub: npubFromPubkey(normalized) },
951
+ body,
944
952
  });
945
953
  if (result.error) {
946
954
  console.log(result.error);
947
955
  process.exit(1);
948
956
  }
949
957
  const output = result.output as
950
- | { npub?: string; added?: boolean; error?: string }
958
+ | { npub?: string; role?: string; added?: boolean; error?: string }
951
959
  | undefined;
952
960
  if (output?.npub) {
953
961
  console.log(
954
- `${output.added ? "Added" : "Already configured"} npub: ${output.npub}`,
962
+ `${output.added ? "Added" : "Already configured"} npub: ${output.npub} [${output.role ?? "user"}]`,
955
963
  );
956
964
  }
957
965
  });
958
966
 
967
+ npubsCmd
968
+ .command("update <npub>")
969
+ .description("Update the role of an existing npub (requires admin)")
970
+ .requiredOption("-r, --role <role>", "New role: 'admin' or 'user'")
971
+ .action(async (npubArg: string, options: { role: string }) => {
972
+ await ensureDaemonRunning();
973
+ const normalized = normalizeNostrPubkey(npubArg);
974
+ if (!normalized) {
975
+ console.error("Invalid npub value. Use npub1... or 64-char hex pubkey.");
976
+ process.exit(1);
977
+ }
978
+ if (options.role !== "admin" && options.role !== "user") {
979
+ console.error("Invalid role. Expected 'admin' or 'user'.");
980
+ process.exit(1);
981
+ }
982
+ const result = await callDaemon("/npubs", {
983
+ method: "PATCH",
984
+ body: { npub: npubFromPubkey(normalized), role: options.role },
985
+ });
986
+ if (result.error) {
987
+ console.log(result.error);
988
+ process.exit(1);
989
+ }
990
+ // PATCH /npubs returns { npub, pubkey, role } at the top level, not wrapped in { output }
991
+ const data = (result.output ?? result) as
992
+ | { npub?: string; pubkey?: string; role?: string; error?: string }
993
+ | undefined;
994
+ if (data?.npub) {
995
+ console.log(`Updated npub ${data.npub} role to '${data.role}'.`);
996
+ } else {
997
+ console.log("Npub not found or update failed.");
998
+ }
999
+ });
1000
+
959
1001
  npubsCmd
960
1002
  .command("delete <npub>")
961
- .description("Delete an admin npub (hex pubkey or npub1...)")
1003
+ .description("Delete an npub (hex pubkey or npub1...)")
962
1004
  .action(async (npubArg: string) => {
963
1005
  await ensureDaemonRunning();
964
1006
  const normalized = normalizeNostrPubkey(npubArg);
@@ -0,0 +1,87 @@
1
+ import { existsSync, mkdirSync } from "fs";
2
+ import { readFile, writeFile } from "fs/promises";
3
+ import { dirname } from "path";
4
+ import type { RoutstrdConfig } from "../utils/config";
5
+ import { logger } from "../utils/logger";
6
+ import type { IntegrationConfig, RoutstrModel } from "./registry";
7
+ import { callDaemon, getDaemonBaseUrl } from "../utils/daemon-client";
8
+
9
+ export async function installHermesIntegration(
10
+ config: RoutstrdConfig,
11
+ apiKey: string,
12
+ integrationConfig: IntegrationConfig,
13
+ ): Promise<void> {
14
+ const { name, configPath } = integrationConfig;
15
+
16
+ logger.log(`\nInstalling routstr configuration in ${configPath}...`);
17
+ logger.log(`Using API key for ${name}`);
18
+
19
+ const baseUrl = getDaemonBaseUrl(config);
20
+ const baseUrlV1 = `${baseUrl}/v1`;
21
+
22
+ let defaultModel = "minimax-m2.7";
23
+
24
+ try {
25
+ const data = await callDaemon("/models");
26
+ const models = (data.output as { models: RoutstrModel[] } | undefined)?.models || [];
27
+
28
+ if (models.length >= 3) {
29
+ defaultModel = models[2]!.id;
30
+ logger.log(`Set default model to 3rd available model: ${defaultModel}`);
31
+ } else if (models.length > 0) {
32
+ defaultModel = models[0]!.id;
33
+ logger.log(`Only ${models.length} models available, using ${defaultModel} as default.`);
34
+ } else {
35
+ logger.log("No models available from routstr daemon, using fallback default.");
36
+ }
37
+ } catch (error) {
38
+ logger.error("Failed to fetch models for Hermes integration:", error);
39
+ logger.log("Using fallback default model.");
40
+ }
41
+
42
+ let content = "";
43
+ try {
44
+ if (existsSync(configPath)) {
45
+ content = await readFile(configPath, "utf-8");
46
+ }
47
+ } catch (error) {
48
+ logger.error(`Error reading ${configPath}, creating new one.`);
49
+ }
50
+
51
+ // Remove existing model block
52
+ content = content.replace(/^model:\n(?: .*\n)*/gm, "");
53
+ // Remove existing custom_providers block
54
+ content = content.replace(/^custom_providers:\n(?:- .*\n(?: .*\n)*)*/gm, "");
55
+ // Clean up extra blank lines
56
+ content = content.replace(/\n{3,}/g, "\n\n").trim();
57
+
58
+ const urlDisplay = baseUrl.replace(/^https?:\/\//, "");
59
+
60
+ const modelBlock = `model:
61
+ default: ${defaultModel}
62
+ provider: custom
63
+ base_url: ${baseUrlV1}
64
+ api_key: ${apiKey}`;
65
+
66
+ const providerBlock = `custom_providers:
67
+ - name: Routstr (${urlDisplay})
68
+ base_url: ${baseUrlV1}
69
+ api_key: ${apiKey}
70
+ model: ${defaultModel}`;
71
+
72
+ const parts: string[] = [modelBlock];
73
+ if (content) {
74
+ parts.push(content);
75
+ }
76
+ parts.push(providerBlock);
77
+
78
+ const newContent = parts.join("\n\n") + "\n";
79
+
80
+ try {
81
+ mkdirSync(dirname(configPath), { recursive: true });
82
+ await writeFile(configPath, newContent);
83
+ logger.log(`Successfully updated ${configPath} with routstr settings.`);
84
+ } catch (error) {
85
+ logger.error(`Failed to write to ${configPath}:`, error);
86
+ }
87
+ }
@@ -4,6 +4,7 @@ import { installOpencodeIntegration } from "./opencode";
4
4
  import { installPiIntegration } from "./pi";
5
5
  import { installOpenClawIntegration } from "./openclaw";
6
6
  import { installClaudeCodeIntegration } from "./claudecode";
7
+ import { installHermesIntegration } from "./hermes";
7
8
 
8
9
  export interface IntegrationConfig {
9
10
  clientId: string;
@@ -43,6 +44,11 @@ export const CLIENT_CONFIGS: Record<string, IntegrationConfig> = {
43
44
  name: "Claude Code",
44
45
  configPath: join(process.env.HOME || "", ".claude/settings.json"),
45
46
  },
47
+ hermes: {
48
+ clientId: "hermes",
49
+ name: "Hermes",
50
+ configPath: join(process.env.HOME || "", ".hermes/config.yaml"),
51
+ },
46
52
  };
47
53
 
48
54
  export const CLIENT_INTEGRATIONS: Record<string, IntegrationFn> = {
@@ -50,6 +56,7 @@ export const CLIENT_INTEGRATIONS: Record<string, IntegrationFn> = {
50
56
  "pi-agent": installPiIntegration,
51
57
  openclaw: installOpenClawIntegration,
52
58
  "claude-code": installClaudeCodeIntegration,
59
+ hermes: installHermesIntegration,
53
60
  };
54
61
 
55
62
  export async function runIntegrationsForClients(