joinhive 2.0.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/README.md +74 -0
- package/bin/hive +820 -0
- package/bin/hive-claim-invite.mjs +88 -0
- package/bin/hive-join.mjs +243 -0
- package/bin/hive-keygen.mjs +48 -0
- package/bin/hive-mint.mjs +65 -0
- package/bin/hive-net.mjs +400 -0
- package/bin/hive-wallet.mjs +120 -0
- package/bin/hived.mjs +5 -0
- package/bin/setup-queen.sh +71 -0
- package/daemon/engines/anthropic.mjs +45 -0
- package/daemon/engines/cli.mjs +25 -0
- package/daemon/engines/index.mjs +84 -0
- package/daemon/engines/openai.mjs +42 -0
- package/daemon/fanout.mjs +47 -0
- package/daemon/hived.mjs +782 -0
- package/daemon/relay/client.mjs +196 -0
- package/daemon/relay/cursor.mjs +59 -0
- package/daemon/relay/ws.mjs +100 -0
- package/dev/compose.yml +109 -0
- package/docs/README.md +30 -0
- package/docs/SUMMARY.md +20 -0
- package/docs/a2a-events.md +82 -0
- package/docs/architecture.md +86 -0
- package/docs/cli.md +70 -0
- package/docs/concepts.md +50 -0
- package/docs/contracts.md +85 -0
- package/docs/http-api.md +78 -0
- package/docs/protocols.md +64 -0
- package/docs/quickstart.md +51 -0
- package/docs/security.md +53 -0
- package/docs/self-hosting.md +101 -0
- package/docs/tokenomics.md +63 -0
- package/install-remote.sh +49 -0
- package/join.sh +81 -0
- package/onchain/deploy-v2.sh +82 -0
- package/onchain/deployments.sepolia.json +14 -0
- package/onchain/foundry.toml +11 -0
- package/onchain/migrate-v2.mjs +76 -0
- package/onchain/src/Honey.sol +45 -0
- package/onchain/src/HoneyV2.sol +74 -0
- package/onchain/src/Jelly.sol +19 -0
- package/onchain/src/JellyV2.sol +31 -0
- package/package.json +72 -0
- package/protocols/book-recs.md +11 -0
- package/protocols/email-in-style.md +15 -0
- package/protocols/event-hunt.md +17 -0
- package/protocols/food-order.md +20 -0
- package/protocols/group-diagnosis.md +13 -0
- package/protocols/meta.md +11 -0
- package/protocols/movie-recs.md +17 -0
- package/protocols/predict.md +21 -0
- package/protocols/read-what-others-read.md +14 -0
- package/protocols/session-bounty.md +11 -0
- package/protocols/session-split-pool.md +10 -0
- package/server/Dockerfile +33 -0
- package/server/api.mjs +192 -0
- package/server/join-page.mjs +169 -0
- package/server/keygen-treasury.mjs +33 -0
- package/server/provision.mjs +262 -0
- package/server/rewarder.mjs +369 -0
- package/server/supervisor.mjs +237 -0
- package/server/treasury.mjs +172 -0
- package/shared/config-schema.mjs +94 -0
- package/shared/events.mjs +47 -0
- package/shared/nip-oa.mjs +56 -0
- package/shared/nip98.mjs +41 -0
- package/shared/redact.mjs +20 -0
- package/shared/rewards.json +33 -0
- package/shared/sealed.mjs +50 -0
- package/shared/txqueue.mjs +42 -0
- package/skills/hive-capability-store/SKILL.md +49 -0
- package/skills/hive-data-store/SKILL.md +60 -0
- package/skills/hive-join/SKILL.md +86 -0
- package/skills/hive-object-store/SKILL.md +45 -0
- package/skills/hive-prompt/SKILL.md +54 -0
- package/skills/hive-protocol-author/SKILL.md +92 -0
- package/skills/hive-wallet/SKILL.md +54 -0
- package/watcher/distill.mjs +248 -0
- package/watcher/global.nfh.hive.sync.plist.tmpl +20 -0
- package/watcher/sync.mjs +136 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Legacy `claude -p`-style subprocess engine — laptop back-compat only.
|
|
2
|
+
// Cloud bees never use this (the supervisor's config validation forbids it).
|
|
3
|
+
// Keeps the two quirks the old daemon needed:
|
|
4
|
+
// - strip CLAUDECODE/CLAUDE_CODE_* so a daemon launched from inside a
|
|
5
|
+
// Claude Code session isn't refused as a nested session
|
|
6
|
+
// - close stdin (headless CLIs block on open stdin pipes)
|
|
7
|
+
import { execFile } from 'node:child_process';
|
|
8
|
+
|
|
9
|
+
export const cliCall = (cfg, kind, prompt, tier) => new Promise((resolve) => {
|
|
10
|
+
const env = { ...process.env };
|
|
11
|
+
for (const k of Object.keys(env)) if (k === 'CLAUDECODE' || k.startsWith('CLAUDE_CODE_')) delete env[k];
|
|
12
|
+
let args = cfg.engine_args || [];
|
|
13
|
+
// compute (and settle/offer) use the stronger model, mirroring the old
|
|
14
|
+
// per-call --model override.
|
|
15
|
+
const modelOverride = kind === 'compute' ? cfg.compute_model : null;
|
|
16
|
+
if (modelOverride) {
|
|
17
|
+
const i = args.indexOf('--model');
|
|
18
|
+
args = i >= 0 ? args.map((a, k) => (k === i + 1 ? modelOverride : a)) : [...args, '--model', modelOverride];
|
|
19
|
+
}
|
|
20
|
+
const child = execFile(cfg.engine || 'claude', [...args, prompt], { timeout: Math.max(tier.timeoutMs, 120_000), maxBuffer: 4 * 1024 * 1024, env },
|
|
21
|
+
(err, stdout, stderr) => resolve(err
|
|
22
|
+
? `engine-error: exit=${err.code ?? err.signal} stderr=${String(stderr).slice(0, 400)}`
|
|
23
|
+
: stdout.toString().trim()));
|
|
24
|
+
child.stdin?.end();
|
|
25
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// daemon/engines — provider-agnostic LLM engine.
|
|
2
|
+
//
|
|
3
|
+
// Replaces the `claude -p` subprocess (which billed the operator's personal
|
|
4
|
+
// Claude Code subscription via the macOS Keychain and cannot exist on a
|
|
5
|
+
// server). Each bee thinks with ITS MEMBER'S OWN API KEY.
|
|
6
|
+
//
|
|
7
|
+
// Contract (the daemon's bail logic depends on it — keep exact):
|
|
8
|
+
// - extract/compute/selftest resolve a STRING, never throw
|
|
9
|
+
// - failures resolve to a string starting with "engine-error: "
|
|
10
|
+
// - `echo` provider returns "echo-engine: <first 120 chars>" (test seam)
|
|
11
|
+
//
|
|
12
|
+
// Usage accounting: every API call appends one JSONL line to
|
|
13
|
+
// $HIVE_HOME/usage.jsonl: {ts, kind, provider, model, tokens_in, tokens_out, ms, ok}.
|
|
14
|
+
import { appendFileSync } from 'node:fs';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
import { anthropicCall } from './anthropic.mjs';
|
|
17
|
+
import { openaiCall } from './openai.mjs';
|
|
18
|
+
import { cliCall } from './cli.mjs';
|
|
19
|
+
|
|
20
|
+
const TIER = { extract: { timeoutMs: 30_000, maxTokens: 400 }, compute: { timeoutMs: 90_000, maxTokens: 1200 }, selftest: { timeoutMs: 20_000, maxTokens: 16 } };
|
|
21
|
+
const RETRIABLE = new Set([408, 409, 429, 500, 502, 503, 504, 529]);
|
|
22
|
+
|
|
23
|
+
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
24
|
+
|
|
25
|
+
// One attempt loop shared by both HTTP providers. call(model, prompt, opts)
|
|
26
|
+
// must return {ok, text, status?, retryAfterMs?, tokens_in?, tokens_out?, error?}.
|
|
27
|
+
const withRetry = async (call, model, prompt, opts) => {
|
|
28
|
+
let last = null;
|
|
29
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
30
|
+
last = await call(model, prompt, opts);
|
|
31
|
+
if (last.ok) return last;
|
|
32
|
+
const retriable = last.status ? RETRIABLE.has(last.status) : last.network === true;
|
|
33
|
+
if (!retriable || attempt === 2) return last;
|
|
34
|
+
await sleep(last.retryAfterMs ?? (1000 * (attempt + 1) + Math.floor(Math.random() * 400)));
|
|
35
|
+
}
|
|
36
|
+
return last;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const createEngine = (cfg, secrets = {}, { home, log = () => {} } = {}) => {
|
|
40
|
+
const provider = cfg.provider || 'cli';
|
|
41
|
+
|
|
42
|
+
const record = (kind, model, res, ms) => {
|
|
43
|
+
if (!home) return;
|
|
44
|
+
try {
|
|
45
|
+
appendFileSync(join(home, 'usage.jsonl'), JSON.stringify({
|
|
46
|
+
ts: Math.floor(Date.now() / 1000), kind, provider, model,
|
|
47
|
+
tokens_in: res.tokens_in ?? null, tokens_out: res.tokens_out ?? null,
|
|
48
|
+
ms, ok: !!res.ok,
|
|
49
|
+
}) + '\n');
|
|
50
|
+
} catch {}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const run = async (kind, prompt) => {
|
|
54
|
+
const tier = TIER[kind] || TIER.compute;
|
|
55
|
+
if (provider === 'echo') return 'echo-engine: ' + prompt.slice(0, 120);
|
|
56
|
+
if (provider === 'cli') return cliCall(cfg, kind, prompt, tier);
|
|
57
|
+
|
|
58
|
+
const model = kind === 'extract' || kind === 'selftest' ? cfg.model_extract : cfg.model_compute;
|
|
59
|
+
if (!model) return `engine-error: no model configured for ${kind} (provider ${provider})`;
|
|
60
|
+
const apiKey = secrets.llm_api_key;
|
|
61
|
+
if (!apiKey) return 'engine-error: no LLM API key configured for this bee';
|
|
62
|
+
|
|
63
|
+
const call = provider === 'anthropic' ? anthropicCall : openaiCall;
|
|
64
|
+
const started = Date.now();
|
|
65
|
+
const res = await withRetry(call, model, prompt, { apiKey, baseUrl: cfg.base_url, timeoutMs: tier.timeoutMs, maxTokens: kind === 'selftest' ? TIER.selftest.maxTokens : tier.maxTokens });
|
|
66
|
+
record(kind === 'selftest' ? 'selftest' : kind, model, res, Date.now() - started);
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const detail = res.error || `http=${res.status}`;
|
|
69
|
+
log(`engine ${provider}/${model} failed: ${String(detail).slice(0, 160)}`);
|
|
70
|
+
return `engine-error: ${String(detail).slice(0, 300)}`;
|
|
71
|
+
}
|
|
72
|
+
return (res.text || '').trim();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
extract: (prompt) => run('extract', prompt),
|
|
77
|
+
compute: (prompt) => run('compute', prompt),
|
|
78
|
+
selftest: async () => {
|
|
79
|
+
if (provider === 'echo') return 'echo-engine: OK';
|
|
80
|
+
const out = await run('selftest', 'Reply with exactly: OK');
|
|
81
|
+
return out;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// OpenAI-compatible /chat/completions via native fetch. One file covers three
|
|
2
|
+
// providers by base_url: OpenAI, OpenRouter, and Hermes (Nous Research).
|
|
3
|
+
//
|
|
4
|
+
// Token-limit param drift: OpenAI's current models want max_completion_tokens;
|
|
5
|
+
// OpenRouter/Hermes (and most compatible shims) take max_tokens. Send the one
|
|
6
|
+
// that matches the base_url.
|
|
7
|
+
|
|
8
|
+
export const openaiCall = async (model, prompt, { apiKey, baseUrl, timeoutMs, maxTokens }) => {
|
|
9
|
+
const base = String(baseUrl || 'https://api.openai.com/v1').replace(/\/$/, '');
|
|
10
|
+
const isOpenAI = /api\.openai\.com/.test(base);
|
|
11
|
+
const ctl = new AbortController();
|
|
12
|
+
const t = setTimeout(() => ctl.abort(), timeoutMs);
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(`${base}/chat/completions`, {
|
|
15
|
+
method: 'POST',
|
|
16
|
+
signal: ctl.signal,
|
|
17
|
+
headers: {
|
|
18
|
+
'content-type': 'application/json',
|
|
19
|
+
authorization: `Bearer ${apiKey}`,
|
|
20
|
+
},
|
|
21
|
+
body: JSON.stringify({
|
|
22
|
+
model,
|
|
23
|
+
messages: [{ role: 'user', content: prompt }],
|
|
24
|
+
...(isOpenAI ? { max_completion_tokens: maxTokens } : { max_tokens: maxTokens }),
|
|
25
|
+
}),
|
|
26
|
+
});
|
|
27
|
+
const retryAfter = Number(res.headers.get('retry-after'));
|
|
28
|
+
if (!res.ok) {
|
|
29
|
+
let msg = `${isOpenAI ? 'openai' : 'openai-compat'} http=${res.status}`;
|
|
30
|
+
try { const j = await res.json(); msg += ` ${String(j?.error?.message || j?.error || '').slice(0, 160)}`; } catch {}
|
|
31
|
+
return { ok: false, status: res.status, error: msg.trim(), retryAfterMs: Number.isFinite(retryAfter) ? retryAfter * 1000 : undefined };
|
|
32
|
+
}
|
|
33
|
+
const j = await res.json();
|
|
34
|
+
const text = j.choices?.[0]?.message?.content ?? '';
|
|
35
|
+
return { ok: true, text: typeof text === 'string' ? text : '', tokens_in: j.usage?.prompt_tokens ?? null, tokens_out: j.usage?.completion_tokens ?? null };
|
|
36
|
+
} catch (e) {
|
|
37
|
+
const aborted = e?.name === 'AbortError';
|
|
38
|
+
return { ok: false, network: !aborted, error: aborted ? `timeout after ${timeoutMs}ms (${base})` : `network: ${String(e?.message || e).slice(0, 160)} (${base})` };
|
|
39
|
+
} finally {
|
|
40
|
+
clearTimeout(t);
|
|
41
|
+
}
|
|
42
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// daemon/fanout — who answers a broadcast intent when 15 bees all see it.
|
|
2
|
+
//
|
|
3
|
+
// Without this, every bee answers every intent: O(N) result spam per ask and
|
|
4
|
+
// N engine bills for one question. Policy:
|
|
5
|
+
// 1. Your OWN bee always serves you (beneficiary === owner_pubkey).
|
|
6
|
+
// 2. Otherwise a bee is eligible only if a protocol matched OR the intent
|
|
7
|
+
// overlaps its profile's interests (whole-token match, words > 3 chars).
|
|
8
|
+
// 3. Among eligible bees, a deterministic election picks top_k: rank =
|
|
9
|
+
// sha256(intent_event_id + bee_pubkey). No coordination messages, stable
|
|
10
|
+
// under retries, uniformly random per intent. Needs the bee roster —
|
|
11
|
+
// supervisor-written registry.json (HIVE_REGISTRY). Solo laptop
|
|
12
|
+
// endpoints have no registry and stay always-eligible (old behavior).
|
|
13
|
+
// Local caps (results/tick, results/day) are enforced by the caller.
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
|
|
17
|
+
const sha = (s) => createHash('sha256').update(s).digest('hex');
|
|
18
|
+
|
|
19
|
+
// Registry shape: { [nostr_pubkey]: {name, evm?, is_bee?, bee_of?} }
|
|
20
|
+
export const loadRoster = (registryPath) => {
|
|
21
|
+
if (!registryPath) return null;
|
|
22
|
+
try {
|
|
23
|
+
const reg = JSON.parse(readFileSync(registryPath, 'utf8'));
|
|
24
|
+
const bees = Object.entries(reg).filter(([, v]) => v && v.is_bee).map(([pk]) => pk);
|
|
25
|
+
return bees.length ? bees : null;
|
|
26
|
+
} catch { return null; }
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const STOPWORDS = new Set(['what', 'this', 'that', 'with', 'from', 'about', 'have', 'want', 'need', 'like', 'find', 'some', 'should', 'would', 'could', 'recommend', 'recommendations']);
|
|
30
|
+
|
|
31
|
+
export const profileOverlap = (intent, profileText) => {
|
|
32
|
+
const words = String(intent).toLowerCase().split(/[^a-z0-9]+/).filter((w) => w.length > 3 && !STOPWORDS.has(w));
|
|
33
|
+
if (!words.length) return false;
|
|
34
|
+
const prof = ` ${String(profileText).toLowerCase()} `;
|
|
35
|
+
return words.some((w) => prof.includes(` ${w} `) || prof.includes(`${w},`) || prof.includes(`${w}.`));
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// -> {respond: bool, reason: string}
|
|
39
|
+
export const shouldAnswer = ({ intentEventId, intent, beneficiary, selfPubkey, ownerPubkey, matchedProtocols, profileText, topK, roster }) => {
|
|
40
|
+
if (beneficiary === ownerPubkey && ownerPubkey) return { respond: true, reason: 'own-owner' };
|
|
41
|
+
const eligible = (matchedProtocols && matchedProtocols.length > 0) || profileOverlap(intent, profileText || '');
|
|
42
|
+
if (!eligible) return { respond: false, reason: 'not-eligible' };
|
|
43
|
+
if (!roster || roster.length <= topK) return { respond: true, reason: 'eligible' };
|
|
44
|
+
const ranked = [...roster].sort((a, b) => sha(intentEventId + a).localeCompare(sha(intentEventId + b)));
|
|
45
|
+
const inTopK = ranked.slice(0, topK).includes(selfPubkey);
|
|
46
|
+
return { respond: inTopK, reason: inTopK ? 'elected' : 'not-elected' };
|
|
47
|
+
};
|