2020117-agent 0.6.3 → 0.6.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.
- package/dist/agent.js +104 -9
- package/dist/lnurl.d.ts +14 -0
- package/dist/lnurl.js +41 -0
- package/package.json +2 -2
package/dist/agent.js
CHANGED
|
@@ -80,9 +80,10 @@ for (const arg of process.argv.slice(2)) {
|
|
|
80
80
|
}
|
|
81
81
|
}
|
|
82
82
|
import { randomBytes } from 'crypto';
|
|
83
|
+
import { createConnection } from 'net';
|
|
83
84
|
import { SwarmNode, topicFromKind } from './swarm.js';
|
|
84
85
|
import { createProcessor } from './processor.js';
|
|
85
|
-
import { generateInvoice } from './
|
|
86
|
+
import { generateInvoice } from './lnurl.js';
|
|
86
87
|
import { generateKeypair, loadSovereignKeys, saveSovereignKeys, loadAgentName, signEvent, signEventWithPow, nip44Encrypt, nip44Decrypt, pubkeyFromPrivkey, RelayPool, } from './nostr.js';
|
|
87
88
|
import { parseNwcUri, nwcGetBalance, nwcPayLightningAddress } from './nwc.js';
|
|
88
89
|
import { readFileSync } from 'fs';
|
|
@@ -742,6 +743,80 @@ async function delegateNostr(label, kind, input, bidSats, provider) {
|
|
|
742
743
|
});
|
|
743
744
|
});
|
|
744
745
|
}
|
|
746
|
+
/**
|
|
747
|
+
* Inject think:false into Ollama chat/generate requests.
|
|
748
|
+
* Suppresses reasoning tokens on models like qwen3 that enable thinking by default.
|
|
749
|
+
*/
|
|
750
|
+
function injectThinkFalse(rawBytes) {
|
|
751
|
+
const raw = rawBytes.toString();
|
|
752
|
+
const sep = raw.indexOf('\r\n\r\n');
|
|
753
|
+
if (sep === -1)
|
|
754
|
+
return rawBytes;
|
|
755
|
+
const header = raw.slice(0, sep + 4);
|
|
756
|
+
const body = raw.slice(sep + 4);
|
|
757
|
+
if (!header.includes('POST') || (!header.includes('/api/generate') && !header.includes('/api/chat')))
|
|
758
|
+
return rawBytes;
|
|
759
|
+
try {
|
|
760
|
+
const json = JSON.parse(body);
|
|
761
|
+
if (json.think !== undefined)
|
|
762
|
+
return rawBytes;
|
|
763
|
+
json.think = false;
|
|
764
|
+
const newBody = JSON.stringify(json);
|
|
765
|
+
const newHeader = header.replace(/Content-Length:\s*\d+/i, `Content-Length: ${Buffer.byteLength(newBody)}`);
|
|
766
|
+
return Buffer.from(newHeader + newBody);
|
|
767
|
+
}
|
|
768
|
+
catch {
|
|
769
|
+
return rawBytes;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* After session payment, switch this socket to a raw TCP pipe to the HTTP backend.
|
|
774
|
+
* The customer then has direct HTTP API access (Ollama, SD-WebUI, etc.) with true streaming.
|
|
775
|
+
* This removes SwarmNode's JSON framing from the socket and replaces it with a bidirectional pipe.
|
|
776
|
+
*/
|
|
777
|
+
function startTcpProxy(session, node, processorUrl, label) {
|
|
778
|
+
let addr;
|
|
779
|
+
try {
|
|
780
|
+
const u = new URL(processorUrl);
|
|
781
|
+
addr = { host: u.hostname, port: Number(u.port) || (u.protocol === 'https:' ? 443 : 80) };
|
|
782
|
+
}
|
|
783
|
+
catch {
|
|
784
|
+
console.error(`[${label}] Session ${session.sessionId}: invalid backend URL ${processorUrl}`);
|
|
785
|
+
endSession(node, session, label);
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
session.proxyStarted = true;
|
|
789
|
+
const socket = session.socket;
|
|
790
|
+
const backend = createConnection(addr.port, addr.host);
|
|
791
|
+
backend.setKeepAlive(true);
|
|
792
|
+
// Remove SwarmNode's newline-delimited JSON listener so raw HTTP bytes flow through
|
|
793
|
+
socket.removeAllListeners('data');
|
|
794
|
+
socket.on('data', (chunk) => backend.write(injectThinkFalse(chunk)));
|
|
795
|
+
backend.pipe(socket);
|
|
796
|
+
let cleaned = false;
|
|
797
|
+
const cleanup = () => {
|
|
798
|
+
if (cleaned)
|
|
799
|
+
return;
|
|
800
|
+
cleaned = true;
|
|
801
|
+
try {
|
|
802
|
+
socket.destroy();
|
|
803
|
+
}
|
|
804
|
+
catch { }
|
|
805
|
+
try {
|
|
806
|
+
backend.destroy();
|
|
807
|
+
}
|
|
808
|
+
catch { }
|
|
809
|
+
endSession(node, session, label);
|
|
810
|
+
};
|
|
811
|
+
socket.on('close', cleanup);
|
|
812
|
+
socket.on('error', () => cleanup());
|
|
813
|
+
backend.on('close', cleanup);
|
|
814
|
+
backend.on('error', (e) => {
|
|
815
|
+
console.log(`[${label}] Session ${session.sessionId}: backend error: ${e.message}`);
|
|
816
|
+
cleanup();
|
|
817
|
+
});
|
|
818
|
+
console.log(`[${label}] Session ${session.sessionId}: TCP proxy → ${addr.host}:${addr.port}`);
|
|
819
|
+
}
|
|
745
820
|
const activeSessions = new Map();
|
|
746
821
|
// Dedup: track recently seen DVM request event IDs (prevent double-processing from relay + inbox)
|
|
747
822
|
const seenEventIds = new Set();
|
|
@@ -800,7 +875,9 @@ async function startSwarmListener(label) {
|
|
|
800
875
|
}
|
|
801
876
|
// --- Session protocol ---
|
|
802
877
|
if (msg.type === 'session_start') {
|
|
803
|
-
|
|
878
|
+
const processorUrl = process.env.PROCESSOR || '';
|
|
879
|
+
const proxyMode = processorUrl.startsWith('http://') || processorUrl.startsWith('https://');
|
|
880
|
+
if (!proxyMode && !LIGHTNING_ADDRESS) {
|
|
804
881
|
node.send(socket, { type: 'error', id: msg.id, message: 'Provider Lightning Address not configured' });
|
|
805
882
|
return;
|
|
806
883
|
}
|
|
@@ -812,7 +889,8 @@ async function startSwarmListener(label) {
|
|
|
812
889
|
const BILLING_INTERVAL_MIN = 1;
|
|
813
890
|
const billingAmount = satsPerMinute * BILLING_INTERVAL_MIN;
|
|
814
891
|
const sessionId = randomBytes(8).toString('hex');
|
|
815
|
-
|
|
892
|
+
const modeLabel = proxyMode ? `proxy→${processorUrl}` : `${satsPerMinute} sats/min`;
|
|
893
|
+
console.log(`[${label}] Session ${sessionId} from ${tag}: ${modeLabel}`);
|
|
816
894
|
const session = {
|
|
817
895
|
socket,
|
|
818
896
|
peerId,
|
|
@@ -825,6 +903,8 @@ async function startSwarmListener(label) {
|
|
|
825
903
|
billingTimer: null,
|
|
826
904
|
timeoutTimer: null,
|
|
827
905
|
customerPubkey: msg.pubkey || undefined,
|
|
906
|
+
proxyMode,
|
|
907
|
+
proxyStarted: false,
|
|
828
908
|
};
|
|
829
909
|
activeSessions.set(sessionId, session);
|
|
830
910
|
node.send(socket, {
|
|
@@ -835,12 +915,23 @@ async function startSwarmListener(label) {
|
|
|
835
915
|
payment_method: paymentMethod,
|
|
836
916
|
pubkey: state.sovereignKeys?.pubkey,
|
|
837
917
|
});
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
918
|
+
if (proxyMode) {
|
|
919
|
+
if (billingAmount <= 0 || !LIGHTNING_ADDRESS) {
|
|
920
|
+
// Free proxy — open TCP pipe immediately
|
|
921
|
+
startTcpProxy(session, node, processorUrl, label);
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
// Paid proxy — one-time session fee, TCP pipe starts on payment (session_tick_ack)
|
|
925
|
+
await sendBillingTick(node, session, billingAmount, label);
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
else {
|
|
929
|
+
// Per-minute billing for structured request/result sessions
|
|
930
|
+
await sendBillingTick(node, session, billingAmount, label);
|
|
931
|
+
session.billingTimer = setInterval(() => {
|
|
932
|
+
sendBillingTick(node, session, billingAmount, label);
|
|
933
|
+
}, BILLING_INTERVAL_MIN * 60_000);
|
|
934
|
+
}
|
|
844
935
|
return;
|
|
845
936
|
}
|
|
846
937
|
// Customer sent payment (Lightning preimage)
|
|
@@ -854,6 +945,10 @@ async function startSwarmListener(label) {
|
|
|
854
945
|
session.lastPaidAt = Date.now();
|
|
855
946
|
console.log(`[${label}] Session ${session.sessionId}: invoice payment received (+${amount}, total: ${session.totalEarned} sats)`);
|
|
856
947
|
}
|
|
948
|
+
// Proxy mode: switch to raw TCP pipe after first payment confirmed
|
|
949
|
+
if (session.proxyMode && !session.proxyStarted) {
|
|
950
|
+
startTcpProxy(session, node, process.env.PROCESSOR || '', label);
|
|
951
|
+
}
|
|
857
952
|
return;
|
|
858
953
|
}
|
|
859
954
|
if (msg.type === 'session_end') {
|
package/dist/lnurl.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightning payment utilities — invoice generation via LNURL-pay
|
|
3
|
+
*
|
|
4
|
+
* Provider generates invoices from their own Lightning Address.
|
|
5
|
+
* Customer pays invoices via NWC wallet.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a Lightning Address to a bolt11 invoice via LNURL-pay protocol.
|
|
9
|
+
* The provider calls this on their OWN Lightning Address to generate
|
|
10
|
+
* an invoice that pays themselves.
|
|
11
|
+
*
|
|
12
|
+
* Flow: address → .well-known/lnurlp → callback?amount= → bolt11
|
|
13
|
+
*/
|
|
14
|
+
export declare function generateInvoice(lightningAddress: string, amountSats: number): Promise<string>;
|
package/dist/lnurl.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightning payment utilities — invoice generation via LNURL-pay
|
|
3
|
+
*
|
|
4
|
+
* Provider generates invoices from their own Lightning Address.
|
|
5
|
+
* Customer pays invoices via NWC wallet.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Resolve a Lightning Address to a bolt11 invoice via LNURL-pay protocol.
|
|
9
|
+
* The provider calls this on their OWN Lightning Address to generate
|
|
10
|
+
* an invoice that pays themselves.
|
|
11
|
+
*
|
|
12
|
+
* Flow: address → .well-known/lnurlp → callback?amount= → bolt11
|
|
13
|
+
*/
|
|
14
|
+
export async function generateInvoice(lightningAddress, amountSats) {
|
|
15
|
+
const [user, domain] = lightningAddress.split('@');
|
|
16
|
+
if (!user || !domain)
|
|
17
|
+
throw new Error(`Invalid Lightning Address: ${lightningAddress}`);
|
|
18
|
+
// Step 1: Fetch LNURL-pay metadata
|
|
19
|
+
const metaUrl = `https://${domain}/.well-known/lnurlp/${user}`;
|
|
20
|
+
const metaResp = await fetch(metaUrl);
|
|
21
|
+
if (!metaResp.ok)
|
|
22
|
+
throw new Error(`LNURL fetch failed: ${metaResp.status} from ${metaUrl}`);
|
|
23
|
+
const meta = await metaResp.json();
|
|
24
|
+
if (meta.tag !== 'payRequest')
|
|
25
|
+
throw new Error(`Not a LNURL-pay endpoint (tag: ${meta.tag})`);
|
|
26
|
+
const amountMsats = amountSats * 1000;
|
|
27
|
+
if (amountMsats < meta.minSendable)
|
|
28
|
+
throw new Error(`Amount ${amountSats} sats below min ${meta.minSendable / 1000} sats`);
|
|
29
|
+
if (amountMsats > meta.maxSendable)
|
|
30
|
+
throw new Error(`Amount ${amountSats} sats above max ${meta.maxSendable / 1000} sats`);
|
|
31
|
+
// Step 2: Request invoice from callback
|
|
32
|
+
const sep = meta.callback.includes('?') ? '&' : '?';
|
|
33
|
+
const invoiceUrl = `${meta.callback}${sep}amount=${amountMsats}`;
|
|
34
|
+
const invoiceResp = await fetch(invoiceUrl);
|
|
35
|
+
if (!invoiceResp.ok)
|
|
36
|
+
throw new Error(`Invoice request failed: ${invoiceResp.status}`);
|
|
37
|
+
const invoiceData = await invoiceResp.json();
|
|
38
|
+
if (!invoiceData.pr)
|
|
39
|
+
throw new Error(`No invoice returned: ${invoiceData.reason || 'unknown error'}`);
|
|
40
|
+
return invoiceData.pr;
|
|
41
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "2020117-agent",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
4
4
|
"description": "2020117 agent runtime — Nostr-native relay subscription + Hyperswarm P2P + Lightning payments",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"exports": {
|
|
19
19
|
"./processor": "./dist/processor.js",
|
|
20
20
|
"./swarm": "./dist/swarm.js",
|
|
21
|
-
"./lightning": "./dist/
|
|
21
|
+
"./lightning": "./dist/lnurl.js",
|
|
22
22
|
"./nostr": "./dist/nostr.js",
|
|
23
23
|
"./nwc": "./dist/nwc.js"
|
|
24
24
|
},
|