routstrd 0.2.14 → 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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "routstrd",
3
- "version": "0.2.14",
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,19 +828,25 @@ 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
  },
833
835
  );
834
836
 
835
- // Npubs - manage admin npubs
837
+ // Npubs - manage npubs (admin and user roles)
836
838
  const npubsCmd = program
837
839
  .command("npubs")
838
- .description("Manage admin npubs on the daemon");
840
+ .description("Manage npubs on the daemon (admin or user roles)");
841
+
842
+ type NpubEntry = {
843
+ npub: string;
844
+ role: string;
845
+ };
839
846
 
840
847
  npubsCmd
841
848
  .command("list")
842
- .description("List configured admin npubs")
849
+ .description("List configured npubs with their roles")
843
850
  .action(async () => {
844
851
  await ensureDaemonRunning();
845
852
  const config = await loadConfig();
@@ -850,20 +857,20 @@ npubsCmd
850
857
  process.exit(1);
851
858
  }
852
859
  // Handle both wrapped { output: { npubs } } and direct { npubs } response formats
853
- const data = (result.output as { npubs?: string[] } | undefined)?.npubs
860
+ const data = (result.output as { npubs?: NpubEntry[] } | undefined)?.npubs
854
861
  ? result.output
855
862
  : result;
856
- const npubs = (data as { npubs?: string[] } | undefined)?.npubs ?? [];
863
+ const npubs = (data as { npubs?: NpubEntry[] } | undefined)?.npubs ?? [];
857
864
  if (npubs.length === 0) {
858
865
  console.log("No admin npubs configured. Run 'routstrd npubs register' to register yourself as the first admin.");
859
866
  return;
860
867
  }
861
- console.log(`Admin npubs (${npubs.length}):`);
868
+ console.log(`Npubs (${npubs.length}):`);
862
869
  let found = false;
863
- for (const npub of npubs) {
864
- const marker = npub === userNpub ? " → you" : "";
865
- if (npub === userNpub) found = true;
866
- console.log(`- ${npub}${marker}`);
870
+ for (const entry of npubs) {
871
+ const marker = entry.npub === userNpub ? " → you" : "";
872
+ if (entry.npub === userNpub) found = true;
873
+ console.log(`- ${entry.npub} [${entry.role}]${marker}`);
867
874
  }
868
875
  if (userNpub && !found) {
869
876
  console.log("");
@@ -892,10 +899,10 @@ npubsCmd
892
899
  console.log(result.error);
893
900
  process.exit(1);
894
901
  }
895
- const data = (result.output as { npubs?: string[] } | undefined)?.npubs
902
+ const data = (result.output as { npubs?: NpubEntry[] } | undefined)?.npubs
896
903
  ? result.output
897
904
  : result;
898
- const npubs = (data as { npubs?: string[] } | undefined)?.npubs ?? [];
905
+ const npubs = (data as { npubs?: NpubEntry[] } | undefined)?.npubs ?? [];
899
906
  if (npubs.length > 0) {
900
907
  console.log(`Admin npubs already configured (${npubs.length}). Ask your admin to add your npub. \n Your npub: ${userNpub}`);
901
908
  return;
@@ -925,35 +932,75 @@ npubsCmd
925
932
 
926
933
  npubsCmd
927
934
  .command("add <npub>")
928
- .description("Add an admin npub (hex pubkey or npub1...)")
929
- .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 }) => {
930
938
  await ensureDaemonRunning();
931
939
  const normalized = normalizeNostrPubkey(npubArg);
932
940
  if (!normalized) {
933
941
  console.error("Invalid npub value. Use npub1... or 64-char hex pubkey.");
934
942
  process.exit(1);
935
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 };
936
949
  const result = await callDaemon("/npubs", {
937
950
  method: "POST",
938
- body: { npub: npubFromPubkey(normalized) },
951
+ body,
939
952
  });
940
953
  if (result.error) {
941
954
  console.log(result.error);
942
955
  process.exit(1);
943
956
  }
944
957
  const output = result.output as
945
- | { npub?: string; added?: boolean; error?: string }
958
+ | { npub?: string; role?: string; added?: boolean; error?: string }
946
959
  | undefined;
947
960
  if (output?.npub) {
948
961
  console.log(
949
- `${output.added ? "Added" : "Already configured"} admin npub: ${output.npub}`,
962
+ `${output.added ? "Added" : "Already configured"} npub: ${output.npub} [${output.role ?? "user"}]`,
950
963
  );
951
964
  }
952
965
  });
953
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
+
954
1001
  npubsCmd
955
1002
  .command("delete <npub>")
956
- .description("Delete an admin npub (hex pubkey or npub1...)")
1003
+ .description("Delete an npub (hex pubkey or npub1...)")
957
1004
  .action(async (npubArg: string) => {
958
1005
  await ensureDaemonRunning();
959
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(
@@ -187,6 +187,7 @@ export interface AddClientOptions {
187
187
  openclaw?: boolean;
188
188
  piAgent?: boolean;
189
189
  claudeCode?: boolean;
190
+ hermes?: boolean;
190
191
  }
191
192
 
192
193
  export async function addClientAction(options: AddClientOptions): Promise<void> {
@@ -198,6 +199,7 @@ export async function addClientAction(options: AddClientOptions): Promise<void>
198
199
  if (options.openclaw) integrationKeys.push("openclaw");
199
200
  if (options.piAgent) integrationKeys.push("pi-agent");
200
201
  if (options.claudeCode) integrationKeys.push("claude-code");
202
+ if (options.hermes) integrationKeys.push("hermes");
201
203
 
202
204
  if (integrationKeys.length > 0) {
203
205
  for (const key of integrationKeys) {
@@ -233,9 +235,14 @@ export async function addClientAction(options: AddClientOptions): Promise<void>
233
235
  }
234
236
 
235
237
  if (!options.name) {
236
- console.error(
237
- "error: required option '-n, --name <name>' not specified",
238
- );
238
+ console.error("error: either provide a client name or specify an integration flag.\n");
239
+ console.error("Options:");
240
+ console.error(" -n, --name <name> Client name");
241
+ console.error(" --opencode Set up OpenCode integration");
242
+ console.error(" --openclaw Set up OpenClaw integration");
243
+ console.error(" --pi-agent Set up Pi Agent integration");
244
+ console.error(" --claude-code Set up Claude Code integration");
245
+ console.error(" --hermes Set up Hermes integration");
239
246
  process.exit(1);
240
247
  }
241
248
 
@@ -37,7 +37,7 @@ export function getDaemonBaseUrl(config: RoutstrdConfig): string {
37
37
 
38
38
  export async function callDaemon(
39
39
  path: string,
40
- options: { method?: "GET" | "POST" | "DELETE"; body?: object } = {},
40
+ options: { method?: "GET" | "POST" | "PATCH" | "DELETE"; body?: object } = {},
41
41
  ): Promise<CommandResponse> {
42
42
  const { method = "GET", body } = options;
43
43
  const config = await loadConfig();