@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/dist/bridge.d.ts +46 -0
  3. package/dist/bridge.d.ts.map +1 -0
  4. package/dist/bridge.js +542 -0
  5. package/dist/chain.d.ts +496 -0
  6. package/dist/chain.d.ts.map +1 -0
  7. package/dist/chain.js +763 -0
  8. package/dist/credit.d.ts +35 -0
  9. package/dist/credit.d.ts.map +1 -0
  10. package/dist/credit.js +112 -0
  11. package/dist/dial.d.ts +43 -0
  12. package/dist/dial.d.ts.map +1 -0
  13. package/dist/dial.js +266 -0
  14. package/dist/directory.d.ts +39 -0
  15. package/dist/directory.d.ts.map +1 -0
  16. package/dist/directory.js +149 -0
  17. package/dist/discovery.d.ts +20 -0
  18. package/dist/discovery.d.ts.map +1 -0
  19. package/dist/discovery.js +58 -0
  20. package/dist/hello.d.ts +14 -0
  21. package/dist/hello.d.ts.map +1 -0
  22. package/dist/hello.js +42 -0
  23. package/dist/index.d.ts +32 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +32 -0
  26. package/dist/keys.d.ts +30 -0
  27. package/dist/keys.d.ts.map +1 -0
  28. package/dist/keys.js +126 -0
  29. package/dist/llmBridge.d.ts +34 -0
  30. package/dist/llmBridge.d.ts.map +1 -0
  31. package/dist/llmBridge.js +151 -0
  32. package/dist/lp.d.ts +31 -0
  33. package/dist/lp.d.ts.map +1 -0
  34. package/dist/lp.js +116 -0
  35. package/dist/modeManager.d.ts +30 -0
  36. package/dist/modeManager.d.ts.map +1 -0
  37. package/dist/modeManager.js +81 -0
  38. package/dist/netconfig.d.ts +11 -0
  39. package/dist/netconfig.d.ts.map +1 -0
  40. package/dist/netconfig.js +39 -0
  41. package/dist/offer.d.ts +70 -0
  42. package/dist/offer.d.ts.map +1 -0
  43. package/dist/offer.js +84 -0
  44. package/dist/ollama.d.ts +53 -0
  45. package/dist/ollama.d.ts.map +1 -0
  46. package/dist/ollama.js +162 -0
  47. package/dist/p2pNode.d.ts +50 -0
  48. package/dist/p2pNode.d.ts.map +1 -0
  49. package/dist/p2pNode.js +450 -0
  50. package/dist/paywall.d.ts +30 -0
  51. package/dist/paywall.d.ts.map +1 -0
  52. package/dist/paywall.js +84 -0
  53. package/dist/publishOptIn.d.ts +15 -0
  54. package/dist/publishOptIn.d.ts.map +1 -0
  55. package/dist/publishOptIn.js +34 -0
  56. package/dist/receipts.d.ts +61 -0
  57. package/dist/receipts.d.ts.map +1 -0
  58. package/dist/receipts.js +135 -0
  59. package/dist/registry.d.ts +19 -0
  60. package/dist/registry.d.ts.map +1 -0
  61. package/dist/registry.js +50 -0
  62. package/dist/rpc.d.ts +24 -0
  63. package/dist/rpc.d.ts.map +1 -0
  64. package/dist/rpc.js +143 -0
  65. package/dist/status.d.ts +56 -0
  66. package/dist/status.d.ts.map +1 -0
  67. package/dist/status.js +354 -0
  68. package/dist/userSession.d.ts +64 -0
  69. package/dist/userSession.d.ts.map +1 -0
  70. package/dist/userSession.js +125 -0
  71. package/dist/workerSession.d.ts +67 -0
  72. package/dist/workerSession.d.ts.map +1 -0
  73. package/dist/workerSession.js +65 -0
  74. package/package.json +56 -3
  75. 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
@@ -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
@@ -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