routstrd 0.3.1 → 0.3.2
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/bun.lock +2 -2
- package/dist/daemon/index.js +1643 -2700
- package/dist/index.js +46 -14
- package/package.json +2 -2
- package/say_hi.sh +32 -0
- package/src/cli.ts +15 -2
- package/src/daemon/models.ts +55 -0
- package/src/daemon/types.ts +6 -0
- package/src/tui/usage/data.ts +6 -2
- package/src/tui/usage/render.ts +20 -5
- package/src/utils/clients.ts +3 -4
- package/src/utils/config.ts +3 -0
- package/new-task.md +0 -60
package/dist/index.js
CHANGED
|
@@ -13943,10 +13943,14 @@ function getTodayStart() {
|
|
|
13943
13943
|
return new Date(now2.getFullYear(), now2.getMonth(), now2.getDate()).getTime();
|
|
13944
13944
|
}
|
|
13945
13945
|
function formatDate(timestamp) {
|
|
13946
|
-
|
|
13946
|
+
const d = new Date(timestamp);
|
|
13947
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
13948
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
13947
13949
|
}
|
|
13948
13950
|
function formatTime(timestamp) {
|
|
13949
|
-
|
|
13951
|
+
const d = new Date(timestamp);
|
|
13952
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
13953
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
13950
13954
|
}
|
|
13951
13955
|
function formatNumber(n) {
|
|
13952
13956
|
if (n >= 1e6)
|
|
@@ -14737,19 +14741,34 @@ function renderRecent(stats, width) {
|
|
|
14737
14741
|
if (recentEntries.length === 0)
|
|
14738
14742
|
return renderBox(["No recent entries"], width, "Recent Requests");
|
|
14739
14743
|
const clientCol = 14;
|
|
14744
|
+
const tokensCol = 18;
|
|
14745
|
+
const costCol = 18;
|
|
14746
|
+
const providerCol = Math.max(16, width - 4 - 10 - 18 - tokensCol - costCol - clientCol - 5);
|
|
14747
|
+
const msatsToSats = (msats) => typeof msats === "number" ? msats / 1000 : 0;
|
|
14740
14748
|
const lines = [];
|
|
14741
|
-
lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"MODEL".padEnd(18)} ${"
|
|
14749
|
+
lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"MODEL".padEnd(18)} ${"I/CR/CW/O".padEnd(tokensCol)} ${"I/O/T in sats".padEnd(costCol)} ${"BASE:PROVIDER".padEnd(providerCol)} ${"CLIENT".slice(0, clientCol)}${COLORS.reset}`);
|
|
14742
14750
|
lines.push(COLORS.dim + "\u2500".repeat(width - 4) + COLORS.reset);
|
|
14743
14751
|
for (const entry of recentEntries) {
|
|
14744
14752
|
const time = formatTime(entry.timestamp).slice(0, 8);
|
|
14745
14753
|
const model = entry.modelId.slice(0, 18).padEnd(18);
|
|
14746
|
-
const tokens =
|
|
14747
|
-
|
|
14748
|
-
|
|
14754
|
+
const tokens = [
|
|
14755
|
+
entry.promptTokens,
|
|
14756
|
+
entry.cacheReadInputTokens || 0,
|
|
14757
|
+
entry.cacheCreationInputTokens || 0,
|
|
14758
|
+
entry.completionTokens
|
|
14759
|
+
].map(formatNumber).join("/");
|
|
14760
|
+
const totalSats = typeof entry.totalMsats === "number" ? entry.totalMsats / 1000 : entry.satsCost;
|
|
14761
|
+
const cost = [
|
|
14762
|
+
formatCost(msatsToSats(entry.inputMsats)),
|
|
14763
|
+
formatCost(msatsToSats(entry.outputMsats)),
|
|
14764
|
+
formatCost(totalSats)
|
|
14765
|
+
].join("/");
|
|
14766
|
+
const baseUrl = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "");
|
|
14767
|
+
const provider = `${baseUrl}:${entry.provider || "unknown"}`.slice(0, providerCol).padEnd(providerCol);
|
|
14749
14768
|
const clientName = (entry.client || "unknown").slice(0, clientCol - 1);
|
|
14750
14769
|
const clientColor = CLIENT_COLORS[entry.client || "unknown"] || CLIENT_COLORS.default || COLORS.white;
|
|
14751
14770
|
const modelColor = MODEL_COLORS[entry.modelId] || MODEL_COLORS.default;
|
|
14752
|
-
lines.push(`${COLORS.dim}${time}${COLORS.reset} ${modelColor}${model}${COLORS.reset} ${tokens.padEnd(
|
|
14771
|
+
lines.push(`${COLORS.dim}${time}${COLORS.reset} ${modelColor}${model}${COLORS.reset} ${tokens.padEnd(tokensCol)} ${COLORS.green}${cost.padEnd(costCol)}${COLORS.reset} ${COLORS.dim}${provider}${COLORS.reset} ${clientColor}${clientName}${COLORS.reset}`);
|
|
14753
14772
|
}
|
|
14754
14773
|
return renderBox(lines, width, `Recent Requests (${stats.entries.length} shown)`);
|
|
14755
14774
|
}
|
|
@@ -15440,7 +15459,7 @@ async function runIntegrationsForClients(clientIds, config) {
|
|
|
15440
15459
|
|
|
15441
15460
|
// src/utils/clients.ts
|
|
15442
15461
|
async function getClientsList() {
|
|
15443
|
-
const result = await
|
|
15462
|
+
const result = await callDaemon("/clients");
|
|
15444
15463
|
const clients = result.output?.clients;
|
|
15445
15464
|
if (!clients) {
|
|
15446
15465
|
return [];
|
|
@@ -15469,7 +15488,7 @@ async function addDaemonClient(name) {
|
|
|
15469
15488
|
};
|
|
15470
15489
|
return { client, created: false };
|
|
15471
15490
|
}
|
|
15472
|
-
const result = await
|
|
15491
|
+
const result = await callDaemon("/clients/add", {
|
|
15473
15492
|
method: "POST",
|
|
15474
15493
|
body: { name, id: derivedId }
|
|
15475
15494
|
});
|
|
@@ -15507,7 +15526,7 @@ async function listClientsAction() {
|
|
|
15507
15526
|
}
|
|
15508
15527
|
async function deleteClientAction(id) {
|
|
15509
15528
|
await ensureDaemonRunning();
|
|
15510
|
-
const result = await
|
|
15529
|
+
const result = await callDaemon("/clients/delete", {
|
|
15511
15530
|
method: "POST",
|
|
15512
15531
|
body: { id }
|
|
15513
15532
|
});
|
|
@@ -15783,7 +15802,7 @@ async function isCocodInstalled(cocodPath) {
|
|
|
15783
15802
|
// package.json
|
|
15784
15803
|
var package_default = {
|
|
15785
15804
|
name: "routstrd",
|
|
15786
|
-
version: "0.3.
|
|
15805
|
+
version: "0.3.2",
|
|
15787
15806
|
module: "src/index.ts",
|
|
15788
15807
|
type: "module",
|
|
15789
15808
|
private: false,
|
|
@@ -15807,7 +15826,7 @@ var package_default = {
|
|
|
15807
15826
|
},
|
|
15808
15827
|
dependencies: {
|
|
15809
15828
|
"@cashu/cashu-ts": "^4.3.0",
|
|
15810
|
-
"@routstr/sdk": "^0.3.
|
|
15829
|
+
"@routstr/sdk": "^0.3.10",
|
|
15811
15830
|
"applesauce-core": "^5.1.0",
|
|
15812
15831
|
"applesauce-relay": "^5.1.0",
|
|
15813
15832
|
"applesauce-wallet-connect": "^6.0.0",
|
|
@@ -15876,6 +15895,18 @@ async function initDaemon() {
|
|
|
15876
15895
|
console.log(`Created config file: ${CONFIG_FILE}`);
|
|
15877
15896
|
}
|
|
15878
15897
|
const config = await loadConfig();
|
|
15898
|
+
if (!config.nsec) {
|
|
15899
|
+
const secretKey = generateSecretKey();
|
|
15900
|
+
const nsec = nip19_exports.nsecEncode(secretKey);
|
|
15901
|
+
const npub = npubFromSecretKey(secretKey);
|
|
15902
|
+
config.nsec = nsec;
|
|
15903
|
+
await Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
15904
|
+
console.log(`
|
|
15905
|
+
A new Nostr identity has been generated for authentication.`);
|
|
15906
|
+
console.log(`Your npub: ${npub}`);
|
|
15907
|
+
console.log(`You can view it in the config file at: ${CONFIG_FILE}
|
|
15908
|
+
`);
|
|
15909
|
+
}
|
|
15879
15910
|
if (!await isCocodInstalled(config.cocodPath)) {
|
|
15880
15911
|
if (config.cocodPath) {
|
|
15881
15912
|
logger.error(`Configured cocod executable was not found: ${config.cocodPath}`);
|
|
@@ -16249,9 +16280,10 @@ program.command("usage").description("Show recent usage logs and total sats cost
|
|
|
16249
16280
|
});
|
|
16250
16281
|
});
|
|
16251
16282
|
var providersCmd = program.command("providers").description("List and manage providers");
|
|
16252
|
-
providersCmd.command("list").description("List all providers with their enabled/disabled status").action(async () => {
|
|
16283
|
+
providersCmd.command("list").description("List all providers with their enabled/disabled status").option("--refresh", "Force re-fetch all Nostr events and refresh models from all enabled providers", false).action(async (options) => {
|
|
16253
16284
|
await ensureDaemonRunning();
|
|
16254
|
-
const
|
|
16285
|
+
const query = options.refresh ? "/providers?refresh=true" : "/providers";
|
|
16286
|
+
const result = await callDaemon(query);
|
|
16255
16287
|
if (result.error) {
|
|
16256
16288
|
console.log(result.error);
|
|
16257
16289
|
process.exit(1);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "routstrd",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"module": "src/index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
26
|
"@cashu/cashu-ts": "^4.3.0",
|
|
27
|
-
"@routstr/sdk": "^0.3.
|
|
27
|
+
"@routstr/sdk": "^0.3.10",
|
|
28
28
|
"applesauce-core": "^5.1.0",
|
|
29
29
|
"applesauce-relay": "^5.1.0",
|
|
30
30
|
"applesauce-wallet-connect": "^6.0.0",
|
package/say_hi.sh
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
LANGUAGES=(
|
|
5
|
+
"Mandarin Chinese"
|
|
6
|
+
"Spanish"
|
|
7
|
+
"English"
|
|
8
|
+
"Hindi"
|
|
9
|
+
"Arabic"
|
|
10
|
+
"Portuguese"
|
|
11
|
+
"Bengali"
|
|
12
|
+
"Russian"
|
|
13
|
+
"Japanese"
|
|
14
|
+
"German"
|
|
15
|
+
"French"
|
|
16
|
+
"Italian"
|
|
17
|
+
"Korean"
|
|
18
|
+
"Turkish"
|
|
19
|
+
"Vietnamese"
|
|
20
|
+
"Tamil"
|
|
21
|
+
"Polish"
|
|
22
|
+
"Dutch"
|
|
23
|
+
"Swahili"
|
|
24
|
+
"Thai"
|
|
25
|
+
"Greek"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
for lang in "${LANGUAGES[@]}"; do
|
|
29
|
+
echo "=== $lang ==="
|
|
30
|
+
pi -p -c "in $lang"
|
|
31
|
+
echo
|
|
32
|
+
done
|
package/src/cli.ts
CHANGED
|
@@ -134,6 +134,17 @@ async function initDaemon(): Promise<void> {
|
|
|
134
134
|
|
|
135
135
|
const config = await loadConfig();
|
|
136
136
|
|
|
137
|
+
if (!config.nsec) {
|
|
138
|
+
const secretKey = generateSecretKey();
|
|
139
|
+
const nsec = nip19.nsecEncode(secretKey);
|
|
140
|
+
const npub = npubFromSecretKey(secretKey);
|
|
141
|
+
config.nsec = nsec;
|
|
142
|
+
await Bun.write(CONFIG_FILE, JSON.stringify(config, null, 2));
|
|
143
|
+
console.log("\nA new Nostr identity has been generated for authentication.");
|
|
144
|
+
console.log(`Your npub: ${npub}`);
|
|
145
|
+
console.log(`You can view it in the config file at: ${CONFIG_FILE}\n`);
|
|
146
|
+
}
|
|
147
|
+
|
|
137
148
|
if (!(await isCocodInstalled(config.cocodPath))) {
|
|
138
149
|
if (config.cocodPath) {
|
|
139
150
|
logger.error(
|
|
@@ -732,10 +743,12 @@ const providersCmd = program
|
|
|
732
743
|
providersCmd
|
|
733
744
|
.command("list")
|
|
734
745
|
.description("List all providers with their enabled/disabled status")
|
|
735
|
-
.
|
|
746
|
+
.option("--refresh", "Force re-fetch all Nostr events and refresh models from all enabled providers", false)
|
|
747
|
+
.action(async (options: { refresh: boolean }) => {
|
|
736
748
|
await ensureDaemonRunning();
|
|
737
749
|
|
|
738
|
-
const
|
|
750
|
+
const query = options.refresh ? "/providers?refresh=true" : "/providers";
|
|
751
|
+
const result = await callDaemon(query);
|
|
739
752
|
if (result.error) {
|
|
740
753
|
console.log(result.error);
|
|
741
754
|
process.exit(1);
|
package/src/daemon/models.ts
CHANGED
|
@@ -120,9 +120,64 @@ export function createModelService(modelManager: ModelManager, store: SdkStore)
|
|
|
120
120
|
};
|
|
121
121
|
};
|
|
122
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Force-refresh everything: re-fetch Nostr provider discovery events,
|
|
125
|
+
* routstr21 model list, Nostr review events, and models from all enabled
|
|
126
|
+
* providers. Syncs the discovered provider list into the store.
|
|
127
|
+
*/
|
|
128
|
+
const refreshProvidersAndModels = async (): Promise<void> => {
|
|
129
|
+
// Reset the bootstrap promise so we don't reuse cached results
|
|
130
|
+
providerBootstrapPromise = null;
|
|
131
|
+
|
|
132
|
+
console.log("Force-refreshing providers from Nostr...");
|
|
133
|
+
|
|
134
|
+
// Force-refresh provider discovery from Nostr (kind 38421)
|
|
135
|
+
const providers = await modelManager.bootstrapProviders(false, true);
|
|
136
|
+
console.log(`Discovered ${providers.length} providers from Nostr`);
|
|
137
|
+
|
|
138
|
+
// Force-refresh routstr21 models from Nostr (kind 38423)
|
|
139
|
+
const routstr21ModelIds = await modelManager.fetchRoutstr21Models(true);
|
|
140
|
+
console.log(`Fetched ${routstr21ModelIds.length} routstr21 model IDs from Nostr`);
|
|
141
|
+
|
|
142
|
+
// Force-refresh models from all providers
|
|
143
|
+
const models = await modelManager.fetchModels(providers, true);
|
|
144
|
+
console.log(`Fetched ${models.length} models from ${providers.length} providers`);
|
|
145
|
+
|
|
146
|
+
// Sync review events from Nostr (kind 38425) and apply disabled status
|
|
147
|
+
const reviewedDisabled = await modelManager.syncReviewedProvidersFromNostr(
|
|
148
|
+
providers,
|
|
149
|
+
undefined,
|
|
150
|
+
true,
|
|
151
|
+
);
|
|
152
|
+
if (reviewedDisabled.length > 0) {
|
|
153
|
+
console.log(
|
|
154
|
+
`Review sync disabled ${reviewedDisabled.length} provider(s): ${reviewedDisabled.join(", ")}`,
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Sync discovered providers into the store
|
|
159
|
+
const { baseUrlsList, setBaseUrlsList, disabledProviders, setDisabledProviders } =
|
|
160
|
+
store.getState() as any;
|
|
161
|
+
|
|
162
|
+
// Replace baseUrlsList with the fresh provider list
|
|
163
|
+
setBaseUrlsList(providers);
|
|
164
|
+
|
|
165
|
+
// Merge review-disabled providers into the store's disabled list
|
|
166
|
+
const existingDisabled = new Set(disabledProviders || []);
|
|
167
|
+
for (const url of reviewedDisabled) {
|
|
168
|
+
existingDisabled.add(url);
|
|
169
|
+
}
|
|
170
|
+
setDisabledProviders([...existingDisabled]);
|
|
171
|
+
|
|
172
|
+
console.log(
|
|
173
|
+
`Provider refresh complete: ${providers.length} total, ${existingDisabled.size} disabled`,
|
|
174
|
+
);
|
|
175
|
+
};
|
|
176
|
+
|
|
123
177
|
return {
|
|
124
178
|
ensureProvidersBootstrapped,
|
|
125
179
|
getRoutstr21Models,
|
|
126
180
|
getModelProviders,
|
|
181
|
+
refreshProvidersAndModels,
|
|
127
182
|
};
|
|
128
183
|
}
|
package/src/daemon/types.ts
CHANGED
|
@@ -11,6 +11,12 @@ export type UsageData = {
|
|
|
11
11
|
totalTokens: number;
|
|
12
12
|
cost: number;
|
|
13
13
|
satsCost: number;
|
|
14
|
+
provider?: string;
|
|
15
|
+
inputMsats?: number;
|
|
16
|
+
outputMsats?: number;
|
|
17
|
+
totalMsats?: number;
|
|
18
|
+
cacheReadInputTokens?: number;
|
|
19
|
+
cacheCreationInputTokens?: number;
|
|
14
20
|
};
|
|
15
21
|
|
|
16
22
|
export type UsageTrackingEntry = UsageData & {
|
package/src/tui/usage/data.ts
CHANGED
|
@@ -118,11 +118,15 @@ export function getTodayStart(): number {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
export function formatDate(timestamp: number): string {
|
|
121
|
-
|
|
121
|
+
const d = new Date(timestamp);
|
|
122
|
+
const p = (n: number) => String(n).padStart(2, "0");
|
|
123
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}`;
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
export function formatTime(timestamp: number): string {
|
|
125
|
-
|
|
127
|
+
const d = new Date(timestamp);
|
|
128
|
+
const p = (n: number) => String(n).padStart(2, "0");
|
|
129
|
+
return `${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}`;
|
|
126
130
|
}
|
|
127
131
|
|
|
128
132
|
export function formatNumber(n: number): string {
|
package/src/tui/usage/render.ts
CHANGED
|
@@ -609,20 +609,35 @@ export function renderRecent(stats: UsageStats, width: number): string {
|
|
|
609
609
|
if (recentEntries.length === 0) return renderBox(["No recent entries"], width, "Recent Requests");
|
|
610
610
|
|
|
611
611
|
const clientCol = 14;
|
|
612
|
+
const tokensCol = 18;
|
|
613
|
+
const costCol = 18;
|
|
614
|
+
const providerCol = Math.max(16, width - 4 - 10 - 18 - tokensCol - costCol - clientCol - 5);
|
|
615
|
+
const msatsToSats = (msats?: number) => typeof msats === "number" ? msats / 1000 : 0;
|
|
612
616
|
const lines: string[] = [];
|
|
613
|
-
lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"MODEL".padEnd(18)} ${"
|
|
617
|
+
lines.push(`${COLORS.bold}${"TIME".padEnd(10)} ${"MODEL".padEnd(18)} ${"I/CR/CW/O".padEnd(tokensCol)} ${"I/O/T in sats".padEnd(costCol)} ${"BASE:PROVIDER".padEnd(providerCol)} ${"CLIENT".slice(0, clientCol)}${COLORS.reset}`);
|
|
614
618
|
lines.push(COLORS.dim + "─".repeat(width - 4) + COLORS.reset);
|
|
615
619
|
|
|
616
620
|
for (const entry of recentEntries) {
|
|
617
621
|
const time = formatTime(entry.timestamp).slice(0, 8);
|
|
618
622
|
const model = entry.modelId.slice(0, 18).padEnd(18);
|
|
619
|
-
const tokens =
|
|
620
|
-
|
|
621
|
-
|
|
623
|
+
const tokens = [
|
|
624
|
+
entry.promptTokens,
|
|
625
|
+
entry.cacheReadInputTokens || 0,
|
|
626
|
+
entry.cacheCreationInputTokens || 0,
|
|
627
|
+
entry.completionTokens,
|
|
628
|
+
].map(formatNumber).join("/");
|
|
629
|
+
const totalSats = typeof entry.totalMsats === "number" ? entry.totalMsats / 1000 : entry.satsCost;
|
|
630
|
+
const cost = [
|
|
631
|
+
formatCost(msatsToSats(entry.inputMsats)),
|
|
632
|
+
formatCost(msatsToSats(entry.outputMsats)),
|
|
633
|
+
formatCost(totalSats),
|
|
634
|
+
].join("/");
|
|
635
|
+
const baseUrl = (entry.baseUrl || "unknown").replace("https://", "").replace("http://", "");
|
|
636
|
+
const provider = `${baseUrl}:${entry.provider || "unknown"}`.slice(0, providerCol).padEnd(providerCol);
|
|
622
637
|
const clientName = (entry.client || "unknown").slice(0, clientCol - 1);
|
|
623
638
|
const clientColor = CLIENT_COLORS[entry.client || "unknown"] || CLIENT_COLORS.default || COLORS.white;
|
|
624
639
|
const modelColor = MODEL_COLORS[entry.modelId] || MODEL_COLORS.default;
|
|
625
|
-
lines.push(`${COLORS.dim}${time}${COLORS.reset} ${modelColor}${model}${COLORS.reset} ${tokens.padEnd(
|
|
640
|
+
lines.push(`${COLORS.dim}${time}${COLORS.reset} ${modelColor}${model}${COLORS.reset} ${tokens.padEnd(tokensCol)} ${COLORS.green}${cost.padEnd(costCol)}${COLORS.reset} ${COLORS.dim}${provider}${COLORS.reset} ${clientColor}${clientName}${COLORS.reset}`);
|
|
626
641
|
}
|
|
627
642
|
|
|
628
643
|
return renderBox(lines, width, `Recent Requests (${stats.entries.length} shown)`);
|
package/src/utils/clients.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import {
|
|
2
|
-
callAuth,
|
|
3
2
|
callDaemon,
|
|
4
3
|
loadConfig,
|
|
5
4
|
getDaemonBaseUrl,
|
|
@@ -57,7 +56,7 @@ export function getClientsFromStore(store: { getState(): any }): ClientEntry[] {
|
|
|
57
56
|
* Use this when running remotely (CLI in remote mode).
|
|
58
57
|
*/
|
|
59
58
|
export async function getClientsList(): Promise<ClientEntry[]> {
|
|
60
|
-
const result = await
|
|
59
|
+
const result = await callDaemon("/clients");
|
|
61
60
|
const clients = (
|
|
62
61
|
result.output as
|
|
63
62
|
| {
|
|
@@ -108,7 +107,7 @@ export async function addDaemonClient(
|
|
|
108
107
|
return { client, created: false };
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
const result = await
|
|
110
|
+
const result = await callDaemon("/clients/add", {
|
|
112
111
|
method: "POST",
|
|
113
112
|
body: { name, id: derivedId },
|
|
114
113
|
});
|
|
@@ -160,7 +159,7 @@ export async function listClientsAction(): Promise<void> {
|
|
|
160
159
|
export async function deleteClientAction(id: string): Promise<void> {
|
|
161
160
|
await ensureDaemonRunning();
|
|
162
161
|
|
|
163
|
-
const result = await
|
|
162
|
+
const result = await callDaemon("/clients/delete", {
|
|
164
163
|
method: "POST",
|
|
165
164
|
body: { id },
|
|
166
165
|
});
|
package/src/utils/config.ts
CHANGED
|
@@ -41,6 +41,9 @@ export interface RoutstrdConfig {
|
|
|
41
41
|
nsec?: string;
|
|
42
42
|
/** Nostr hex pubkey for routstr review/model events (kind 38425/38423). */
|
|
43
43
|
routstrPubkey?: string;
|
|
44
|
+
/** Nostr relay URLs for provider/model discovery (kinds 38421/38423/38425).
|
|
45
|
+
* When unset, each method uses its own built-in defaults. */
|
|
46
|
+
relays?: string[];
|
|
44
47
|
/** NWC integration configuration */
|
|
45
48
|
nwc?: NwcConfig;
|
|
46
49
|
}
|
package/new-task.md
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
# Hot-reload NWC connection (remove restart requirement)
|
|
2
|
-
|
|
3
|
-
## Problem
|
|
4
|
-
|
|
5
|
-
`nwc connect` and `nwc disconnect` require a daemon restart to take effect. The
|
|
6
|
-
CLI writes the new connection string to disk, but the daemon never re-reads it.
|
|
7
|
-
|
|
8
|
-
## Current flow
|
|
9
|
-
|
|
10
|
-
1. **Startup** — `createWalletAdapter` reads `options.nwcConnectionString` and
|
|
11
|
-
creates a `WalletConnect` (relay pool + NWC wallet service) once:
|
|
12
|
-
```ts
|
|
13
|
-
// src/daemon/wallet/index.ts:70-78
|
|
14
|
-
if (options.nwcConnectionString) {
|
|
15
|
-
wallet = WalletConnect.fromConnectURI(options.nwcConnectionString, { pool });
|
|
16
|
-
}
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
2. **CLI `nwc connect`** — HTTP handler at `POST /nwc/connect` only calls
|
|
20
|
-
`saveDaemonConfig(config)`. The running `WalletConnect` instance is untouched.
|
|
21
|
-
|
|
22
|
-
3. **Result** — the old connection (or lack thereof) stays active until restart.
|
|
23
|
-
|
|
24
|
-
## What needs to happen
|
|
25
|
-
|
|
26
|
-
Provide a way for the daemon to tear down the old `WalletConnect` and create a
|
|
27
|
-
new one from the updated connection string, triggered by the HTTP handler,
|
|
28
|
-
**without restarting the process**.
|
|
29
|
-
|
|
30
|
-
### Key pieces
|
|
31
|
-
|
|
32
|
-
- **`src/daemon/wallet/index.ts`** — `createWalletAdapter` needs to expose a
|
|
33
|
-
`reconnect(connectionString: string)` method that:
|
|
34
|
-
- Closes the old relay pool / WalletConnect
|
|
35
|
-
- Creates a new `WalletConnect` from the new connection string
|
|
36
|
-
- Updates the adapter's internal `wallet` reference
|
|
37
|
-
- If connection string is empty/undefined, disconnects and sets `wallet` to
|
|
38
|
-
`undefined`
|
|
39
|
-
|
|
40
|
-
- **`src/daemon/http/index.ts`** — `POST /nwc/connect` and `POST /nwc/disconnect`
|
|
41
|
-
handlers should call `reconnect()` after saving config, and drop the "Restart
|
|
42
|
-
daemon" message.
|
|
43
|
-
|
|
44
|
-
- **`src/daemon/index.ts`** — may need to store the `reconnect` function and pass
|
|
45
|
-
it through `DaemonDeps` to the HTTP handler.
|
|
46
|
-
|
|
47
|
-
- **`src/cli.ts`** — remove `console.log("\nRun 'routstrd restart' to connect...")`
|
|
48
|
-
from the connect/disconnect commands.
|
|
49
|
-
|
|
50
|
-
### Considerations
|
|
51
|
-
|
|
52
|
-
- The `WalletConnect` instance is referenced in the auto-refill loop (passed to
|
|
53
|
-
`startAutoRefillLoop`). After a reconnect, the auto-refill loop's captured
|
|
54
|
-
`wallet` reference would be stale. The loop already reads config from disk
|
|
55
|
-
each cycle, so it should also reference the latest `wallet` instance.
|
|
56
|
-
- Could make the auto-refill loop accept a `getWallet: () => WalletConnect |
|
|
57
|
-
undefined` getter, same pattern as the config getter.
|
|
58
|
-
- Relay pool cleanup — `applesauce-wallet-connect` / `applesauce-relay` may need
|
|
59
|
-
explicit `pool.close()` or `pool.destroy()` to free connections.
|
|
60
|
-
- Edge case: a refill is in progress when reconnect is triggered.
|