@genkaku/protocol 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/dist/bridge.d.ts +46 -0
- package/dist/bridge.d.ts.map +1 -0
- package/dist/bridge.js +542 -0
- package/dist/chain.d.ts +496 -0
- package/dist/chain.d.ts.map +1 -0
- package/dist/chain.js +763 -0
- package/dist/credit.d.ts +35 -0
- package/dist/credit.d.ts.map +1 -0
- package/dist/credit.js +112 -0
- package/dist/dial.d.ts +43 -0
- package/dist/dial.d.ts.map +1 -0
- package/dist/dial.js +266 -0
- package/dist/directory.d.ts +39 -0
- package/dist/directory.d.ts.map +1 -0
- package/dist/directory.js +149 -0
- package/dist/discovery.d.ts +20 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +58 -0
- package/dist/hello.d.ts +14 -0
- package/dist/hello.d.ts.map +1 -0
- package/dist/hello.js +42 -0
- package/dist/index.d.ts +32 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +32 -0
- package/dist/keys.d.ts +30 -0
- package/dist/keys.d.ts.map +1 -0
- package/dist/keys.js +126 -0
- package/dist/llmBridge.d.ts +34 -0
- package/dist/llmBridge.d.ts.map +1 -0
- package/dist/llmBridge.js +151 -0
- package/dist/lp.d.ts +31 -0
- package/dist/lp.d.ts.map +1 -0
- package/dist/lp.js +116 -0
- package/dist/modeManager.d.ts +30 -0
- package/dist/modeManager.d.ts.map +1 -0
- package/dist/modeManager.js +81 -0
- package/dist/netconfig.d.ts +11 -0
- package/dist/netconfig.d.ts.map +1 -0
- package/dist/netconfig.js +39 -0
- package/dist/offer.d.ts +70 -0
- package/dist/offer.d.ts.map +1 -0
- package/dist/offer.js +84 -0
- package/dist/ollama.d.ts +53 -0
- package/dist/ollama.d.ts.map +1 -0
- package/dist/ollama.js +162 -0
- package/dist/p2pNode.d.ts +50 -0
- package/dist/p2pNode.d.ts.map +1 -0
- package/dist/p2pNode.js +450 -0
- package/dist/paywall.d.ts +30 -0
- package/dist/paywall.d.ts.map +1 -0
- package/dist/paywall.js +84 -0
- package/dist/publishOptIn.d.ts +15 -0
- package/dist/publishOptIn.d.ts.map +1 -0
- package/dist/publishOptIn.js +34 -0
- package/dist/receipts.d.ts +61 -0
- package/dist/receipts.d.ts.map +1 -0
- package/dist/receipts.js +135 -0
- package/dist/registry.d.ts +19 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/registry.js +50 -0
- package/dist/rpc.d.ts +24 -0
- package/dist/rpc.d.ts.map +1 -0
- package/dist/rpc.js +143 -0
- package/dist/status.d.ts +56 -0
- package/dist/status.d.ts.map +1 -0
- package/dist/status.js +354 -0
- package/dist/userSession.d.ts +64 -0
- package/dist/userSession.d.ts.map +1 -0
- package/dist/userSession.js +125 -0
- package/dist/workerSession.d.ts +67 -0
- package/dist/workerSession.d.ts.map +1 -0
- package/dist/workerSession.js +65 -0
- package/package.json +56 -3
- package/README.md +0 -3
package/dist/lp.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// GENKAKU: LP-JSON wire primitives (protocol v0).
|
|
2
|
+
// 4-byte big-endian length prefix + UTF-8 JSON. Frame codec is streamable:
|
|
3
|
+
// sessions START before the peer finishes writing (tokens keep flowing after
|
|
4
|
+
// the open envelope), so reads must increment — never drain-to-EOS.
|
|
5
|
+
// A sink may only be attached ONCE per libp2p stream; all writes to one stream
|
|
6
|
+
// go through a WriteQueue whose async generator never ends until .end().
|
|
7
|
+
export function lpEncode(obj) {
|
|
8
|
+
const body = new TextEncoder().encode(JSON.stringify(obj) + '\n');
|
|
9
|
+
const head = new Uint8Array(4);
|
|
10
|
+
new DataView(head.buffer).setUint32(0, body.byteLength);
|
|
11
|
+
const out = new Uint8Array(head.length + body.length);
|
|
12
|
+
out.set(head);
|
|
13
|
+
out.set(body, head.length);
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
16
|
+
function toU8(c) {
|
|
17
|
+
if (c instanceof Uint8Array) {
|
|
18
|
+
return c;
|
|
19
|
+
}
|
|
20
|
+
return c.subarray();
|
|
21
|
+
}
|
|
22
|
+
function parseFrames(buf) {
|
|
23
|
+
const dv = new DataView(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
24
|
+
const frames = [];
|
|
25
|
+
let p = 0;
|
|
26
|
+
while (p + 4 <= buf.length) {
|
|
27
|
+
const len = dv.getUint32(p);
|
|
28
|
+
if (len === 0 || p + 4 + len > buf.length) {
|
|
29
|
+
break;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
frames.push(JSON.parse(new TextDecoder().decode(buf.subarray(p + 4, p + 4 + len))));
|
|
33
|
+
}
|
|
34
|
+
catch { /* tolerate */ }
|
|
35
|
+
p += 4 + len;
|
|
36
|
+
}
|
|
37
|
+
return { frames, consumed: p };
|
|
38
|
+
}
|
|
39
|
+
/** Parse every COMPLETE frame in a byte blob (tests / whole-stream reads). */
|
|
40
|
+
export function lpFrames(chunks) {
|
|
41
|
+
const parts = [];
|
|
42
|
+
let total = 0;
|
|
43
|
+
for (const c of chunks) {
|
|
44
|
+
const u = toU8(c);
|
|
45
|
+
parts.push(u);
|
|
46
|
+
total += u.length;
|
|
47
|
+
}
|
|
48
|
+
const buf = new Uint8Array(total);
|
|
49
|
+
let off = 0;
|
|
50
|
+
for (const u of parts) {
|
|
51
|
+
buf.set(u, off);
|
|
52
|
+
off += u.length;
|
|
53
|
+
}
|
|
54
|
+
return parseFrames(buf).frames;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Incremental frame reader — feeds whatever bytes arrive and pops complete
|
|
58
|
+
* frames WITHOUT waiting for stream end.
|
|
59
|
+
*/
|
|
60
|
+
export class FrameReader {
|
|
61
|
+
buf = new Uint8Array(0);
|
|
62
|
+
push(chunk) {
|
|
63
|
+
const u = toU8(chunk);
|
|
64
|
+
const merged = new Uint8Array(this.buf.length + u.length);
|
|
65
|
+
merged.set(this.buf);
|
|
66
|
+
merged.set(u, this.buf.length);
|
|
67
|
+
this.buf = merged;
|
|
68
|
+
}
|
|
69
|
+
/** next complete frame, or undefined while the current one is still partial */
|
|
70
|
+
next() {
|
|
71
|
+
const { frames, consumed } = parseFrames(this.buf);
|
|
72
|
+
if (frames.length === 0) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
this.buf = this.buf.subarray(consumed);
|
|
76
|
+
return frames[0];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* One durable write side per stream: `sink(queue.source())` once, then push
|
|
81
|
+
* frames as they happen; the write half stays open until .end().
|
|
82
|
+
*/
|
|
83
|
+
export class WriteQueue {
|
|
84
|
+
items = [];
|
|
85
|
+
waiters = [];
|
|
86
|
+
/** end() has been called — readers will see EOF after draining the items */
|
|
87
|
+
ended = false;
|
|
88
|
+
push(b) {
|
|
89
|
+
if (this.ended) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
this.items.push(b);
|
|
93
|
+
this.drain();
|
|
94
|
+
}
|
|
95
|
+
end() {
|
|
96
|
+
this.ended = true;
|
|
97
|
+
this.drain();
|
|
98
|
+
}
|
|
99
|
+
drain() {
|
|
100
|
+
for (const w of this.waiters.splice(0)) {
|
|
101
|
+
w();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
async *source() {
|
|
105
|
+
while (true) {
|
|
106
|
+
while (this.items.length > 0) {
|
|
107
|
+
yield this.items.shift();
|
|
108
|
+
}
|
|
109
|
+
if (this.ended) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
await new Promise(resolve => this.waiters.push(resolve));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=lp.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export type RoleMode = 'user' | 'localPlusSharing' | 'workerDedicated';
|
|
2
|
+
export type ModeState = {
|
|
3
|
+
mode: RoleMode;
|
|
4
|
+
/** a live remote session consumes another peer's worker this instant */
|
|
5
|
+
remoteUserActive: boolean;
|
|
6
|
+
/** this machine exposes /genkaku/* worker protocols on its libp2p node */
|
|
7
|
+
sharingActive: boolean;
|
|
8
|
+
};
|
|
9
|
+
export declare class ModeError extends Error {
|
|
10
|
+
readonly code: string;
|
|
11
|
+
constructor(msg: string, code: string);
|
|
12
|
+
}
|
|
13
|
+
export declare class ModeManager {
|
|
14
|
+
private readonly onTransition?;
|
|
15
|
+
private state;
|
|
16
|
+
constructor(onTransition?: ((from: RoleMode, to: RoleMode) => void) | undefined);
|
|
17
|
+
current(): Readonly<ModeState>;
|
|
18
|
+
/** Enter a role. `remoteUserActive` hints whether a live remote session should
|
|
19
|
+
* carry over (only meaningful user→user). */
|
|
20
|
+
switch(to: RoleMode, opts?: {
|
|
21
|
+
remoteUserActive?: boolean;
|
|
22
|
+
}): ModeState;
|
|
23
|
+
/** Sidecar calls at transport start (user mode only; D-block is enforced here). */
|
|
24
|
+
beginRemoteSession(): void;
|
|
25
|
+
endRemoteSession(): void;
|
|
26
|
+
/** Sidecar guard before installing /genkaku/* protocol handlers. */
|
|
27
|
+
installWorkerListeners(): 'installed' | 'already';
|
|
28
|
+
removeWorkerListeners(): void;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=modeManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"modeManager.d.ts","sourceRoot":"","sources":["../src/modeManager.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,kBAAkB,GAAG,iBAAiB,CAAA;AAEtE,MAAM,MAAM,SAAS,GAAG;IACtB,IAAI,EAAE,QAAQ,CAAA;IACd,wEAAwE;IACxE,gBAAgB,EAAE,OAAO,CAAA;IACzB,0EAA0E;IAC1E,aAAa,EAAE,OAAO,CAAA;CACvB,CAAA;AAED,qBAAa,SAAU,SAAQ,KAAK;aACO,IAAI,EAAE,MAAM;gBAAzC,GAAG,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAGtD;AAED,qBAAa,WAAW;IAGV,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAC;IAF1C,OAAO,CAAC,KAAK,CAAW;gBAEK,YAAY,CAAC,GAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,KAAK,IAAI,aAAA;IAIlF,OAAO,IAAI,QAAQ,CAAC,SAAS,CAAC;IAI9B;kDAC8C;IAC9C,MAAM,CAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,GAAE;QAAE,gBAAgB,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,SAAS;IAkB1E,mFAAmF;IACnF,kBAAkB,IAAI,IAAI;IAY1B,gBAAgB,IAAI,IAAI;IAIxB,oEAAoE;IACpE,sBAAsB,IAAI,WAAW,GAAG,SAAS;IAgBjD,qBAAqB,IAAI,IAAI;CAO9B"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// GENKAKU: role mode manager — the D-block enforcement lives HERE and in the
|
|
2
|
+
// sidecar (never both a remote-user dialer and a worker listener on one machine).
|
|
3
|
+
// Spec: docs/genkaku/P4-ADAPTATION.md §2; user-locked rule 2026-11-06.
|
|
4
|
+
//
|
|
5
|
+
// Semantics:
|
|
6
|
+
// · "remote session" = this machine CONSUMES someone else's worker stream
|
|
7
|
+
// · "sharing" = this machine SERVES its local LLM over libp2p
|
|
8
|
+
// · A user : may hold remote sessions, never shares
|
|
9
|
+
// · B localPlusSharing: owners use their LOCAL model + share idle capacity
|
|
10
|
+
// ("local use" is not a remote session — no counterparty, no metering)
|
|
11
|
+
// · C workerDedicated : pure serving
|
|
12
|
+
// · D : remote-consumption + sharing simultaneously — banned
|
|
13
|
+
export class ModeError extends Error {
|
|
14
|
+
code;
|
|
15
|
+
constructor(msg, code) {
|
|
16
|
+
super(`[genkaku:${code}] ${msg}`);
|
|
17
|
+
this.code = code;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class ModeManager {
|
|
21
|
+
onTransition;
|
|
22
|
+
state;
|
|
23
|
+
constructor(onTransition) {
|
|
24
|
+
this.onTransition = onTransition;
|
|
25
|
+
this.state = { mode: 'localPlusSharing', remoteUserActive: false, sharingActive: false };
|
|
26
|
+
}
|
|
27
|
+
current() {
|
|
28
|
+
return { ...this.state };
|
|
29
|
+
}
|
|
30
|
+
/** Enter a role. `remoteUserActive` hints whether a live remote session should
|
|
31
|
+
* carry over (only meaningful user→user). */
|
|
32
|
+
switch(to, opts = {}) {
|
|
33
|
+
if (this.state.mode === 'user' && this.state.remoteUserActive && to !== 'user') {
|
|
34
|
+
throw new ModeError('open remote sessions must end before leaving user mode', 'remote-must-drain');
|
|
35
|
+
}
|
|
36
|
+
if (this.state.sharingActive && to === 'user') {
|
|
37
|
+
throw new ModeError('stop worker listening before becoming a remote user', 'sharing-must-drain');
|
|
38
|
+
}
|
|
39
|
+
const sharing = to !== 'user';
|
|
40
|
+
const remote = to === 'user' ? Boolean(opts.remoteUserActive) : false;
|
|
41
|
+
const before = this.state.mode;
|
|
42
|
+
this.state = { mode: to, remoteUserActive: remote, sharingActive: sharing };
|
|
43
|
+
this.onTransition?.(before, to);
|
|
44
|
+
return { ...this.state };
|
|
45
|
+
}
|
|
46
|
+
/** Sidecar calls at transport start (user mode only; D-block is enforced here). */
|
|
47
|
+
beginRemoteSession() {
|
|
48
|
+
if (this.state.mode !== 'user') {
|
|
49
|
+
throw new ModeError('remote consumption is only allowed in dedicated user mode (D-block)', 'd-block');
|
|
50
|
+
}
|
|
51
|
+
if (this.state.sharingActive) {
|
|
52
|
+
throw new ModeError('cannot consume a remote worker while sharing (D-block)', 'd-block');
|
|
53
|
+
}
|
|
54
|
+
this.state.remoteUserActive = true;
|
|
55
|
+
}
|
|
56
|
+
endRemoteSession() {
|
|
57
|
+
this.state.remoteUserActive = false;
|
|
58
|
+
}
|
|
59
|
+
/** Sidecar guard before installing /genkaku/* protocol handlers. */
|
|
60
|
+
installWorkerListeners() {
|
|
61
|
+
if (this.state.mode === 'user') {
|
|
62
|
+
throw new ModeError('worker listeners forbidden in user mode (D-block)', 'd-block');
|
|
63
|
+
}
|
|
64
|
+
if (this.state.remoteUserActive) {
|
|
65
|
+
throw new ModeError('still draining a remote session before installing listeners', 'remote-must-drain');
|
|
66
|
+
}
|
|
67
|
+
if (this.state.sharingActive) {
|
|
68
|
+
return 'already';
|
|
69
|
+
}
|
|
70
|
+
this.state.sharingActive = true;
|
|
71
|
+
this.onTransition?.('user', this.state.mode);
|
|
72
|
+
return 'installed';
|
|
73
|
+
}
|
|
74
|
+
removeWorkerListeners() {
|
|
75
|
+
if (this.state.remoteUserActive) {
|
|
76
|
+
throw new ModeError('still draining a remote session before removing listeners', 'remote-must-drain');
|
|
77
|
+
}
|
|
78
|
+
this.state.sharingActive = false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=modeManager.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type NetEnvironment = 'base-sepolia' | 'base';
|
|
2
|
+
export declare const CHAIN_ID: Record<NetEnvironment, 84532 | 8453>;
|
|
3
|
+
export declare const CONTRACTS: Record<NetEnvironment, {
|
|
4
|
+
escrow: string;
|
|
5
|
+
registry: string;
|
|
6
|
+
usdc: string;
|
|
7
|
+
}>;
|
|
8
|
+
export declare const TREASURY = "0x639a2B713041e15B0c5C18710536b661d5265b48";
|
|
9
|
+
export declare const HARDCODED_SEEDS: string[];
|
|
10
|
+
export declare const DEFAULT_RECEIPT_STEP_TOKENS = 10;
|
|
11
|
+
//# sourceMappingURL=netconfig.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"netconfig.d.ts","sourceRoot":"","sources":["../src/netconfig.ts"],"names":[],"mappings":"AAIA,MAAM,MAAM,cAAc,GAAG,cAAc,GAAG,MAAM,CAAA;AAEpD,eAAO,MAAM,QAAQ,EAAE,MAAM,CAAC,cAAc,EAAE,KAAK,GAAG,IAAI,CAGzD,CAAA;AAED,eAAO,MAAM,SAAS,EAAE,MAAM,CAAC,cAAc,EAAE;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAWhG,CAAA;AAED,eAAO,MAAM,QAAQ,+CAA+C,CAAA;AAWpE,eAAO,MAAM,eAAe,EAAE,MAAM,EAQhC,CAAA;AAGJ,eAAO,MAAM,2BAA2B,KAAK,CAAA"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// GENKAKU: network constants for the desktop client (P4).
|
|
2
|
+
// Chain/data-model follows protocol/GENKAKU-P1.md + GENKAKU-P2.md.
|
|
3
|
+
// Testnet values are verified on-chain (deployed 2026-11-06, contracts/README.md).
|
|
4
|
+
export const CHAIN_ID = {
|
|
5
|
+
'base-sepolia': 84532,
|
|
6
|
+
base: 8453,
|
|
7
|
+
};
|
|
8
|
+
export const CONTRACTS = {
|
|
9
|
+
'base-sepolia': {
|
|
10
|
+
escrow: '0x8175df796A659681009BeDa4609a3cFe9d281D82',
|
|
11
|
+
registry: '0xFbE9FF497dC24bD69d13E4201418938Df7435Ff6', // v0.2 — on-chain directory (P4.9a)
|
|
12
|
+
usdc: '0xEb33DFC0424810039337a38510ef522e9E7F9727', // MockUSDC — test only
|
|
13
|
+
},
|
|
14
|
+
base: {
|
|
15
|
+
escrow: '', // deploy at mainnet launch
|
|
16
|
+
registry: '',
|
|
17
|
+
usdc: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', // canonical USDC — verify vs Circle
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
export const TREASURY = '0x639a2B713041e15B0c5C18710536b661d5265b48';
|
|
21
|
+
// Discovery (P4.9): the worker directory lives on Base — see directory.ts and
|
|
22
|
+
// rpc.ts. There is NO baked bootstrap seed: genkaku.app is a static download
|
|
23
|
+
// page, and neither a user nor a worker may depend on infrastructure we run.
|
|
24
|
+
//
|
|
25
|
+
// `GENKAKU_BOOTSTRAP_SEEDS` (comma-separated multiaddrs) is a DEVELOPMENT
|
|
26
|
+
// escape hatch for a LAN bring-up or a private deployment; it is empty in
|
|
27
|
+
// shipped builds and is never required for discovery to work.
|
|
28
|
+
const BAKED_SEEDS = [];
|
|
29
|
+
export const HARDCODED_SEEDS = (() => {
|
|
30
|
+
const fromEnv = (process.env.GENKAKU_BOOTSTRAP_SEEDS ?? '')
|
|
31
|
+
.split(',')
|
|
32
|
+
.map(s => s.trim())
|
|
33
|
+
.filter(s => s !== '');
|
|
34
|
+
return (fromEnv.length > 0 ? fromEnv : BAKED_SEEDS)
|
|
35
|
+
.filter(s => !s.includes('PLACEHOLDER'));
|
|
36
|
+
})();
|
|
37
|
+
// Metered receipts: default k = 10 tokens per signed ledger increment (P2 §3).
|
|
38
|
+
export const DEFAULT_RECEIPT_STEP_TOKENS = 10;
|
|
39
|
+
//# sourceMappingURL=netconfig.js.map
|
package/dist/offer.d.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
export type Offer = {
|
|
2
|
+
proto: 'offers/0';
|
|
3
|
+
workerName: string;
|
|
4
|
+
model: string;
|
|
5
|
+
/** 6-decimal USDC per hour of usage, as bigint */
|
|
6
|
+
pricePerHour: bigint;
|
|
7
|
+
/** coarse, self-reported (privacy: never street-level) */
|
|
8
|
+
city?: string;
|
|
9
|
+
/** 0–100; v0 local heuristic, chain-verified in P4.4 */
|
|
10
|
+
creditScore: number;
|
|
11
|
+
/** completed sessions; join into creditScore on-chain later */
|
|
12
|
+
sessionsServed: number;
|
|
13
|
+
/** where creditScore came from: chain-derived (P4.9h) or the worker's own
|
|
14
|
+
* claim. The UI should only show a chain score as a score. */
|
|
15
|
+
creditSource?: 'chain' | 'self';
|
|
16
|
+
/** measured RTT to this worker at discovery time (ms) */
|
|
17
|
+
latencyMs?: number;
|
|
18
|
+
/** parallel sessions the worker accepts */
|
|
19
|
+
maxConcurrent?: number;
|
|
20
|
+
/** true when `workerWallet` was proven by the P4.9e hello handshake rather
|
|
21
|
+
* than self-reported. Priced sessions should require this. */
|
|
22
|
+
walletVerified?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* The worker's EVM wallet (escrow settle recipient). The client REPLACES the
|
|
25
|
+
* self-reported value with the wallet proven by the P4.9e hello handshake and
|
|
26
|
+
* sets `walletVerified`; an offer whose claim contradicts its proof is
|
|
27
|
+
* dropped before it is ever listed.
|
|
28
|
+
*/
|
|
29
|
+
workerWallet?: string;
|
|
30
|
+
};
|
|
31
|
+
/** JSON-safe shape used over /genkaku/offers/0 frames. */
|
|
32
|
+
export type OfferWire = Omit<Offer, 'pricePerHour'> & {
|
|
33
|
+
pricePerHour: string;
|
|
34
|
+
};
|
|
35
|
+
export declare function toWire(offer: Offer): OfferWire;
|
|
36
|
+
export declare function fromWire(wire: OfferWire): Offer;
|
|
37
|
+
export declare function ceilDiv(a: bigint, b: bigint): bigint;
|
|
38
|
+
/**
|
|
39
|
+
* Cumulative billed amount for a session. `unitMs` must divide 3600000
|
|
40
|
+
* (v0 default 60000 → 60 units/hour). Rate per unit is rounded UP once per
|
|
41
|
+
* hour — never per tick — so the user-perceived hourly price never exceeds
|
|
42
|
+
* the quoted pricePerHour, and the worker's per-tick exposure stays bounded.
|
|
43
|
+
*/
|
|
44
|
+
export declare function cumulativeBilled(pricePerHour: bigint, unitMs: number, elapsedMs: number): bigint;
|
|
45
|
+
export type OfferSortKey = 'creditScore' | 'pricePerHour' | 'workerName' | 'model' | 'city' | 'latencyMs';
|
|
46
|
+
/** One offer row with the address it arrived from. */
|
|
47
|
+
export type AddressedOffer = {
|
|
48
|
+
addr: string;
|
|
49
|
+
offer: Offer;
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Collapse the same worker offer reached through several multiaddrs. A stable
|
|
53
|
+
* peer can be announced over IPv4, IPv6 and relay addresses; the board should
|
|
54
|
+
* list the offer once, not once per address.
|
|
55
|
+
* @param rows - flattened offer rows in discovery order.
|
|
56
|
+
* @returns rows with duplicate (wallet-or-address, model, price) entries removed.
|
|
57
|
+
*/
|
|
58
|
+
export declare function dedupeOffers(rows: readonly AddressedOffer[]): AddressedOffer[];
|
|
59
|
+
/** One-key sort with stable tiebreakers (name asc, then model asc). */
|
|
60
|
+
export declare function sortOffers(offers: Offer[], key?: OfferSortKey, dir?: 'asc' | 'desc'): Offer[];
|
|
61
|
+
export type OfferFilter = {
|
|
62
|
+
model?: string;
|
|
63
|
+
maxPricePerHour?: bigint;
|
|
64
|
+
minCredit?: number;
|
|
65
|
+
city?: string;
|
|
66
|
+
/** case-insensitive substring across workerName/model/city */
|
|
67
|
+
query?: string;
|
|
68
|
+
};
|
|
69
|
+
export declare function filterOffers(offers: Offer[], f: OfferFilter): Offer[];
|
|
70
|
+
//# sourceMappingURL=offer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"offer.d.ts","sourceRoot":"","sources":["../src/offer.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,KAAK,GAAG;IAClB,KAAK,EAAE,UAAU,CAAA;IACjB,UAAU,EAAE,MAAM,CAAA;IAClB,KAAK,EAAE,MAAM,CAAA;IACb,kDAAkD;IAClD,YAAY,EAAE,MAAM,CAAA;IACpB,0DAA0D;IAC1D,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,wDAAwD;IACxD,WAAW,EAAE,MAAM,CAAA;IACnB,+DAA+D;IAC/D,cAAc,EAAE,MAAM,CAAA;IACtB;mEAC+D;IAC/D,YAAY,CAAC,EAAE,OAAO,GAAG,MAAM,CAAA;IAC/B,yDAAyD;IACzD,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,2CAA2C;IAC3C,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB;mEAC+D;IAC/D,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;;;OAKG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;CACtB,CAAA;AAED,0DAA0D;AAC1D,MAAM,MAAM,SAAS,GAAG,IAAI,CAAC,KAAK,EAAE,cAAc,CAAC,GAAG;IAAE,YAAY,EAAE,MAAM,CAAA;CAAE,CAAA;AAE9E,wBAAgB,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,SAAS,CAE9C;AAED,wBAAgB,QAAQ,CAAC,IAAI,EAAE,SAAS,GAAG,KAAK,CAE/C;AAED,wBAAgB,OAAO,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAIpD;AAID;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,MAAM,CAahG;AAED,MAAM,MAAM,YAAY,GAAG,aAAa,GAAG,cAAc,GAAG,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,WAAW,CAAA;AAWzG,sDAAsD;AACtD,MAAM,MAAM,cAAc,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,KAAK,CAAA;CAAE,CAAA;AAE3D;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,SAAS,cAAc,EAAE,GAAG,cAAc,EAAE,CAW9E;AAED,uEAAuE;AACvE,wBAAgB,UAAU,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,GAAG,GAAE,YAA4B,EAAE,GAAG,GAAE,KAAK,GAAG,MAAe,GAAG,KAAK,EAAE,CAOpH;AAED,MAAM,MAAM,WAAW,GAAG;IACxB,KAAK,CAAC,EAAE,MAAM,CAAA;IACd,eAAe,CAAC,EAAE,MAAM,CAAA;IACxB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,EAAE,WAAW,GAAG,KAAK,EAAE,CASrE"}
|
package/dist/offer.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// GENKAKU: worker offer board (P4.3 marketplace).
|
|
2
|
+
// A worker publishes one Offer row per (model, price) it chooses to serve.
|
|
3
|
+
// Price is FREELY set by the worker and quoted PER HOUR of user usage time
|
|
4
|
+
// (MockUSDC 6 decimals in testnet; canonical USDC 6 decimals in prod). Billing
|
|
5
|
+
// accrues per STARTED MINUTE: worker's max unbilled exposure = one minute.
|
|
6
|
+
// Credit score: v0 is a local heuristic (sessions served); P4.4 upgrades it to
|
|
7
|
+
// the on-chain settled-claim ratio from JobEscrow/WorkerRegistry events.
|
|
8
|
+
export function toWire(offer) {
|
|
9
|
+
return { ...offer, pricePerHour: offer.pricePerHour.toString() };
|
|
10
|
+
}
|
|
11
|
+
export function fromWire(wire) {
|
|
12
|
+
return { ...wire, pricePerHour: BigInt(wire.pricePerHour), proto: 'offers/0' };
|
|
13
|
+
}
|
|
14
|
+
export function ceilDiv(a, b) {
|
|
15
|
+
if (b <= 0n) {
|
|
16
|
+
throw new Error('[genkaku:pricing] divisor must be positive');
|
|
17
|
+
}
|
|
18
|
+
return (a + b - 1n) / b;
|
|
19
|
+
}
|
|
20
|
+
const MS_PER_HOUR = 3600000n;
|
|
21
|
+
/**
|
|
22
|
+
* Cumulative billed amount for a session. `unitMs` must divide 3600000
|
|
23
|
+
* (v0 default 60000 → 60 units/hour). Rate per unit is rounded UP once per
|
|
24
|
+
* hour — never per tick — so the user-perceived hourly price never exceeds
|
|
25
|
+
* the quoted pricePerHour, and the worker's per-tick exposure stays bounded.
|
|
26
|
+
*/
|
|
27
|
+
export function cumulativeBilled(pricePerHour, unitMs, elapsedMs) {
|
|
28
|
+
if (unitMs <= 0) {
|
|
29
|
+
throw new Error('[genkaku:pricing] unitMs must be positive');
|
|
30
|
+
}
|
|
31
|
+
const unit = BigInt(unitMs);
|
|
32
|
+
if (MS_PER_HOUR % unit !== 0n) {
|
|
33
|
+
throw new Error('[genkaku:pricing] unitMs must divide 3600000');
|
|
34
|
+
}
|
|
35
|
+
const units = Math.ceil(elapsedMs / unitMs);
|
|
36
|
+
if (units <= 0) {
|
|
37
|
+
return 0n;
|
|
38
|
+
}
|
|
39
|
+
return BigInt(units) * ceilDiv(pricePerHour, MS_PER_HOUR / unit);
|
|
40
|
+
}
|
|
41
|
+
const comparators = {
|
|
42
|
+
creditScore: (a, b) => a.creditScore - b.creditScore,
|
|
43
|
+
pricePerHour: (a, b) => (a.pricePerHour <= b.pricePerHour ? -1 : 1),
|
|
44
|
+
workerName: (a, b) => a.workerName.localeCompare(b.workerName),
|
|
45
|
+
model: (a, b) => a.model.localeCompare(b.model),
|
|
46
|
+
city: (a, b) => (a.city ?? '').localeCompare(b.city ?? ''),
|
|
47
|
+
latencyMs: (a, b) => (a.latencyMs ?? Number.POSITIVE_INFINITY) - (b.latencyMs ?? Number.POSITIVE_INFINITY),
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* Collapse the same worker offer reached through several multiaddrs. A stable
|
|
51
|
+
* peer can be announced over IPv4, IPv6 and relay addresses; the board should
|
|
52
|
+
* list the offer once, not once per address.
|
|
53
|
+
* @param rows - flattened offer rows in discovery order.
|
|
54
|
+
* @returns rows with duplicate (wallet-or-address, model, price) entries removed.
|
|
55
|
+
*/
|
|
56
|
+
export function dedupeOffers(rows) {
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
const out = [];
|
|
59
|
+
for (const row of rows) {
|
|
60
|
+
const worker = row.offer.workerWallet ?? row.addr;
|
|
61
|
+
const key = `${worker}|${row.offer.model}|${row.offer.pricePerHour.toString()}`;
|
|
62
|
+
if (seen.has(key))
|
|
63
|
+
continue;
|
|
64
|
+
seen.add(key);
|
|
65
|
+
out.push(row);
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
/** One-key sort with stable tiebreakers (name asc, then model asc). */
|
|
70
|
+
export function sortOffers(offers, key = 'creditScore', dir = 'desc') {
|
|
71
|
+
const mul = dir === 'desc' ? -1 : 1;
|
|
72
|
+
return [...offers].sort((a, b) => mul * comparators[key](a, b)
|
|
73
|
+
|| comparators.workerName(a, b)
|
|
74
|
+
|| comparators.model(a, b));
|
|
75
|
+
}
|
|
76
|
+
export function filterOffers(offers, f) {
|
|
77
|
+
const q = f.query?.toLowerCase();
|
|
78
|
+
return offers.filter(o => (!f.model || o.model === f.model)
|
|
79
|
+
&& (!f.minCredit || o.creditScore >= f.minCredit)
|
|
80
|
+
&& (!f.city || o.city === f.city)
|
|
81
|
+
&& (!q || `${o.workerName} ${o.model} ${o.city ?? ''}`.toLowerCase().includes(q))
|
|
82
|
+
&& (!f.maxPricePerHour || o.pricePerHour <= f.maxPricePerHour));
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=offer.js.map
|
package/dist/ollama.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ModelInfo, type PullProgress } from './llmBridge.ts';
|
|
2
|
+
/** Facts the settings page renders before it offers a next step. */
|
|
3
|
+
export type OllamaState = {
|
|
4
|
+
/** an `ollama` executable was found on this machine */
|
|
5
|
+
installed: boolean;
|
|
6
|
+
/** the local HTTP API answers right now */
|
|
7
|
+
running: boolean;
|
|
8
|
+
/** version reported by `GET /api/version`, when running */
|
|
9
|
+
version?: string;
|
|
10
|
+
/** base URL the plugin uses */
|
|
11
|
+
host: string;
|
|
12
|
+
/** official installer page for this platform */
|
|
13
|
+
downloadUrl: string;
|
|
14
|
+
/** installed models, empty when the service is not running */
|
|
15
|
+
models: ModelInfo[];
|
|
16
|
+
/** last detection or start failure, for the onboarding hint */
|
|
17
|
+
error?: string;
|
|
18
|
+
};
|
|
19
|
+
/** Official installer page per platform. */
|
|
20
|
+
export declare function ollamaDownloadUrl(platform?: NodeJS.Platform): string;
|
|
21
|
+
/**
|
|
22
|
+
* Locate an `ollama` executable without trusting the GUI process PATH.
|
|
23
|
+
*
|
|
24
|
+
* A packaged macOS app inherits a minimal PATH, so the Homebrew and app-bundle
|
|
25
|
+
* locations are probed directly; on Windows the per-user installer path is the
|
|
26
|
+
* usual one.
|
|
27
|
+
* @param platform - target platform, injectable for tests.
|
|
28
|
+
* @param environment - environment to read PATH from.
|
|
29
|
+
* @returns absolute executable path, or undefined when Ollama is absent.
|
|
30
|
+
*/
|
|
31
|
+
export declare function locateOllama(platform?: NodeJS.Platform, environment?: NodeJS.ProcessEnv): string | undefined;
|
|
32
|
+
export type OllamaSupervisor = {
|
|
33
|
+
/** Detect the binary, probe the service and list models. */
|
|
34
|
+
state: () => Promise<OllamaState>;
|
|
35
|
+
/** Start `ollama serve` when installed but stopped; idempotent. */
|
|
36
|
+
ensureRunning: () => Promise<OllamaState>;
|
|
37
|
+
/** Download a registry model, forwarding progress frames. */
|
|
38
|
+
pull: (model: string, onProgress?: (p: PullProgress) => void, signal?: AbortSignal) => Promise<{
|
|
39
|
+
status: string;
|
|
40
|
+
}>;
|
|
41
|
+
/** Remove a locally installed model. */
|
|
42
|
+
remove: (model: string) => Promise<void>;
|
|
43
|
+
/** Stop the child this supervisor started, if any. */
|
|
44
|
+
dispose: () => void;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Build the worker-side Ollama supervisor.
|
|
48
|
+
* @param host - local Ollama base URL.
|
|
49
|
+
* @param log - diagnostic line sink.
|
|
50
|
+
* @returns supervisor over the local install.
|
|
51
|
+
*/
|
|
52
|
+
export declare function createOllamaSupervisor(host: string, log: (line: string) => void): OllamaSupervisor;
|
|
53
|
+
//# sourceMappingURL=ollama.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ollama.d.ts","sourceRoot":"","sources":["../src/ollama.ts"],"names":[],"mappings":"AAWA,OAAO,EAAe,KAAK,SAAS,EAAE,KAAK,YAAY,EAAE,MAAM,gBAAgB,CAAA;AAE/E,oEAAoE;AACpE,MAAM,MAAM,WAAW,GAAG;IACxB,uDAAuD;IACvD,SAAS,EAAE,OAAO,CAAA;IAClB,2CAA2C;IAC3C,OAAO,EAAE,OAAO,CAAA;IAChB,2DAA2D;IAC3D,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAA;IACZ,gDAAgD;IAChD,WAAW,EAAE,MAAM,CAAA;IACnB,8DAA8D;IAC9D,MAAM,EAAE,SAAS,EAAE,CAAA;IACnB,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,4CAA4C;AAC5C,wBAAgB,iBAAiB,CAAC,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,MAAM,CAItF;AAED;;;;;;;;;GASG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,GAAE,MAAM,CAAC,QAA2B,EAC5C,WAAW,GAAE,MAAM,CAAC,UAAwB,GAC3C,MAAM,GAAG,SAAS,CAuBpB;AASD,MAAM,MAAM,gBAAgB,GAAG;IAC7B,4DAA4D;IAC5D,KAAK,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,CAAA;IACjC,mEAAmE;IACnE,aAAa,EAAE,MAAM,OAAO,CAAC,WAAW,CAAC,CAAA;IACzC,6DAA6D;IAC7D,IAAI,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,YAAY,KAAK,IAAI,EAAE,MAAM,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC;QAAE,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC,CAAA;IAClH,wCAAwC;IACxC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAA;IACxC,sDAAsD;IACtD,OAAO,EAAE,MAAM,IAAI,CAAA;CACpB,CAAA;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,GAAG,gBAAgB,CA0FlG"}
|
package/dist/ollama.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// GENKAKU: local Ollama lifecycle for worker onboarding (P4.7b).
|
|
2
|
+
//
|
|
3
|
+
// The desktop ships no multi-GB runtime. A worker either already runs Ollama
|
|
4
|
+
// or is guided to install it; this module only detects, starts and talks to
|
|
5
|
+
// the local HTTP API. Model management lives in llmBridge.ollamaAdmin.
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
import { existsSync } from 'node:fs';
|
|
8
|
+
import { homedir } from 'node:os';
|
|
9
|
+
import { delimiter, join } from 'node:path';
|
|
10
|
+
import { ollamaAdmin } from "./llmBridge.js";
|
|
11
|
+
/** Official installer page per platform. */
|
|
12
|
+
export function ollamaDownloadUrl(platform = process.platform) {
|
|
13
|
+
if (platform === 'darwin')
|
|
14
|
+
return 'https://ollama.com/download/mac';
|
|
15
|
+
if (platform === 'win32')
|
|
16
|
+
return 'https://ollama.com/download/windows';
|
|
17
|
+
return 'https://ollama.com/download/linux';
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Locate an `ollama` executable without trusting the GUI process PATH.
|
|
21
|
+
*
|
|
22
|
+
* A packaged macOS app inherits a minimal PATH, so the Homebrew and app-bundle
|
|
23
|
+
* locations are probed directly; on Windows the per-user installer path is the
|
|
24
|
+
* usual one.
|
|
25
|
+
* @param platform - target platform, injectable for tests.
|
|
26
|
+
* @param environment - environment to read PATH from.
|
|
27
|
+
* @returns absolute executable path, or undefined when Ollama is absent.
|
|
28
|
+
*/
|
|
29
|
+
export function locateOllama(platform = process.platform, environment = process.env) {
|
|
30
|
+
const executable = platform === 'win32' ? 'ollama.exe' : 'ollama';
|
|
31
|
+
// Scan PATH directly: a packaged GUI app cannot rely on `which`/`where`
|
|
32
|
+
// because the inherited PATH is minimal and those tools may be unresolvable.
|
|
33
|
+
for (const dir of (environment.PATH ?? '').split(delimiter)) {
|
|
34
|
+
if (dir === '')
|
|
35
|
+
continue;
|
|
36
|
+
const candidate = join(dir, executable);
|
|
37
|
+
if (existsSync(candidate))
|
|
38
|
+
return candidate;
|
|
39
|
+
}
|
|
40
|
+
const candidates = platform === 'darwin'
|
|
41
|
+
? [
|
|
42
|
+
'/usr/local/bin/ollama',
|
|
43
|
+
'/opt/homebrew/bin/ollama',
|
|
44
|
+
'/Applications/Ollama.app/Contents/Resources/ollama',
|
|
45
|
+
join(homedir(), 'Applications/Ollama.app/Contents/Resources/ollama'),
|
|
46
|
+
]
|
|
47
|
+
: platform === 'win32'
|
|
48
|
+
? [
|
|
49
|
+
join(environment.LOCALAPPDATA ?? join(homedir(), 'AppData/Local'), 'Programs/Ollama/ollama.exe'),
|
|
50
|
+
join(environment.ProgramFiles ?? 'C:/Program Files', 'Ollama/ollama.exe'),
|
|
51
|
+
]
|
|
52
|
+
: ['/usr/local/bin/ollama', '/usr/bin/ollama', '/snap/bin/ollama'];
|
|
53
|
+
return candidates.find(candidate => existsSync(candidate));
|
|
54
|
+
}
|
|
55
|
+
/** Read one JSON document from the local API with a bounded wait. */
|
|
56
|
+
async function getJson(url, timeoutMs = 1500) {
|
|
57
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
58
|
+
if (!response.ok)
|
|
59
|
+
throw new Error(`${url} refused (${response.status})`);
|
|
60
|
+
return await response.json();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Build the worker-side Ollama supervisor.
|
|
64
|
+
* @param host - local Ollama base URL.
|
|
65
|
+
* @param log - diagnostic line sink.
|
|
66
|
+
* @returns supervisor over the local install.
|
|
67
|
+
*/
|
|
68
|
+
export function createOllamaSupervisor(host, log) {
|
|
69
|
+
const admin = ollamaAdmin(host);
|
|
70
|
+
const base = host.replace(/\/$/u, '');
|
|
71
|
+
let child;
|
|
72
|
+
let starting;
|
|
73
|
+
const running = async () => {
|
|
74
|
+
try {
|
|
75
|
+
const versionDoc = await getJson(`${base}/api/version`);
|
|
76
|
+
const models = await admin.list();
|
|
77
|
+
return {
|
|
78
|
+
running: true,
|
|
79
|
+
...(typeof versionDoc.version === 'string' ? { version: versionDoc.version } : {}),
|
|
80
|
+
models,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
return { running: false, models: [], error: error instanceof Error ? error.message : String(error) };
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const state = async () => {
|
|
88
|
+
const probe = await running();
|
|
89
|
+
const binary = locateOllama();
|
|
90
|
+
if (probe.running) {
|
|
91
|
+
return {
|
|
92
|
+
installed: true,
|
|
93
|
+
running: true,
|
|
94
|
+
...(probe.version === undefined ? {} : { version: probe.version }),
|
|
95
|
+
host, downloadUrl: ollamaDownloadUrl(), models: probe.models,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
installed: binary !== undefined,
|
|
100
|
+
running: false,
|
|
101
|
+
host,
|
|
102
|
+
downloadUrl: ollamaDownloadUrl(),
|
|
103
|
+
models: [],
|
|
104
|
+
...(binary === undefined
|
|
105
|
+
? { error: 'ollama executable not found' }
|
|
106
|
+
: probe.error === undefined ? {} : { error: probe.error }),
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
const start = async () => {
|
|
110
|
+
const binary = locateOllama();
|
|
111
|
+
if (binary === undefined)
|
|
112
|
+
return await state();
|
|
113
|
+
const url = new URL(base);
|
|
114
|
+
log(`ollama: starting ${binary} serve (OLLAMA_HOST=${url.host})`);
|
|
115
|
+
const spawned = spawn(binary, ['serve'], {
|
|
116
|
+
env: { ...process.env, OLLAMA_HOST: url.host },
|
|
117
|
+
stdio: 'ignore',
|
|
118
|
+
detached: false,
|
|
119
|
+
});
|
|
120
|
+
child = spawned;
|
|
121
|
+
spawned.once('error', (error) => { log(`ollama spawn failed (${error.message})`); });
|
|
122
|
+
spawned.once('exit', (code, signal) => {
|
|
123
|
+
if (child === spawned)
|
|
124
|
+
child = undefined;
|
|
125
|
+
log(`ollama exited (${code ?? signal ?? 'unknown'})`);
|
|
126
|
+
});
|
|
127
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
128
|
+
const probe = await running();
|
|
129
|
+
if (probe.running)
|
|
130
|
+
return await state();
|
|
131
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
132
|
+
}
|
|
133
|
+
const current = await state();
|
|
134
|
+
return { ...current, error: current.error ?? 'ollama did not answer after 10s' };
|
|
135
|
+
};
|
|
136
|
+
return {
|
|
137
|
+
state,
|
|
138
|
+
ensureRunning: async () => {
|
|
139
|
+
const current = await state();
|
|
140
|
+
if (current.running || !current.installed)
|
|
141
|
+
return current;
|
|
142
|
+
starting ??= start().finally(() => { starting = undefined; });
|
|
143
|
+
return await starting;
|
|
144
|
+
},
|
|
145
|
+
pull: async (model, onProgress, signal) => await admin.pull(model, onProgress, signal),
|
|
146
|
+
remove: async (model) => {
|
|
147
|
+
const response = await fetch(`${base}/api/delete`, {
|
|
148
|
+
method: 'DELETE',
|
|
149
|
+
headers: { 'content-type': 'application/json' },
|
|
150
|
+
body: JSON.stringify({ model }),
|
|
151
|
+
});
|
|
152
|
+
if (!response.ok)
|
|
153
|
+
throw new Error(`ollama delete refused (${response.status})`);
|
|
154
|
+
},
|
|
155
|
+
dispose: () => {
|
|
156
|
+
if (child !== undefined && !child.killed)
|
|
157
|
+
child.kill();
|
|
158
|
+
child = undefined;
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=ollama.js.map
|