claude-flow 3.46.1 → 3.47.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-flow",
3
- "version": "3.46.1",
3
+ "version": "3.47.0",
4
4
  "workspaces": [
5
5
  "v3/@claude-flow/codex",
6
6
  "v3/@claude-flow/mcp",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "generation": 6,
4
- "generatedAt": "2026-09-26T23:25:25.296Z",
5
- "gitSha": "a5adb5aa",
4
+ "generatedAt": "2026-09-27T19:39:37.000Z",
5
+ "gitSha": "09595b70",
6
6
  "catalog": {
7
7
  "agents": 167,
8
8
  "tools": 418,
@@ -13,6 +13,8 @@ async function run(ctx, tool, args, title) {
13
13
  // CLI flag --gateway takes precedence over the RUFLO_X_GATEWAY_URL env var (ADR-125).
14
14
  const data = await callMCPTool(tool, { gatewayUrl: ctx.flags.gateway, ...args });
15
15
  printJsonOrTable(ctx, data, title);
16
+ if (data && typeof data === 'object' && (('ok' in data && data.ok === false) || ('degraded' in data && data.degraded === true)))
17
+ return { success: false, exitCode: 1, data };
16
18
  return { success: true, data };
17
19
  }
18
20
  catch (e) {
@@ -28,8 +30,8 @@ export const federationCommand = {
28
30
  { name: 'gateway', description: 'Gateway base URL (takes precedence over RUFLO_X_GATEWAY_URL; default https://x.ruv.io)', type: 'string' },
29
31
  ],
30
32
  subcommands: [
31
- { name: 'join', description: 'Join the open swarm with YOUR OWN key using an invite code (generates ~/.ruflo/nostr.key if absent, claims via NIP-98, verifies via NIP-42)',
32
- options: [{ name: 'code', description: 'Invite code (v2.…) — a bearer secret, keep it private', type: 'string', required: true }],
33
+ { name: 'join', description: 'Join the open swarm with YOUR OWN key without an invite on open relays (generates ~/.ruflo/nostr.key if absent, registers via NIP-98, verifies via NIP-42)',
34
+ options: [{ name: 'code', description: 'Invite code (v2.…) — a bearer secret, keep it private', type: 'string' }],
33
35
  action: (ctx) => run(ctx, 'x_federation_join', { code: ctx.flags.code }, 'Join federation') },
34
36
  { name: 'sync', description: 'Fetch recent verified swarm messages',
35
37
  options: [{ name: 'since', description: 'Look-back seconds (default 3600)', type: 'number' }, { name: 'limit', description: 'Max messages', type: 'number' }, { name: 'type', description: 'Filter by message type', type: 'string' }],
@@ -1,8 +1,8 @@
1
1
  /**
2
- * Self-service join for the open swarm federation — the user-facing invite path.
2
+ * Self-service federation join with a local key: public registration or a private invite.
3
3
  *
4
4
  * Decentralized by design: the user generates/holds THEIR OWN Nostr key locally,
5
- * redeems an invite code with a NIP-98-signed claim (no admin in the loop), proves
5
+ * registers with a NIP-98-signed request (no admin in the user flow), proves
6
6
  * membership with NIP-42, and announces themselves. The gateway never signs for them.
7
7
  *
8
8
  * `nostr-tools` is an optional dependency (secp256k1/Schnorr is not in node:crypto):
@@ -36,49 +36,92 @@ export function nip98Header(nt, sk, url, method, body) {
36
36
  export async function verifyMembership(nt, sk, relayWs) {
37
37
  const { default: WebSocket } = await import('ws');
38
38
  return new Promise((resolve) => {
39
- const ws = new WebSocket(relayWs, { perMessageDeflate: false });
40
- let done = false;
39
+ const ws = new WebSocket(relayWs, { perMessageDeflate: false, maxPayload: 64 * 1024 });
40
+ let done = false, authId;
41
41
  const fin = (r) => { if (done)
42
- return; done = true; try {
42
+ return; done = true; clearTimeout(timer); try {
43
43
  ws.close();
44
44
  }
45
45
  catch { /* */ } resolve(r); };
46
+ const timer = setTimeout(() => fin({ ok: false, reason: 'timeout' }), 15000);
46
47
  ws.on('message', (d) => {
47
- const m = JSON.parse(d.toString());
48
- if (m[0] === 'AUTH' && typeof m[1] === 'string')
49
- ws.send(JSON.stringify(['AUTH', nt.finalizeEvent({ kind: 22242, created_at: Math.floor(Date.now() / 1000), tags: [['relay', relayWs], ['challenge', m[1]]], content: '' }, sk)]));
50
- else if (m[0] === 'OK')
51
- fin({ ok: !!m[2], reason: m[3] });
48
+ let m;
49
+ try {
50
+ m = JSON.parse(d.toString());
51
+ }
52
+ catch {
53
+ return;
54
+ }
55
+ if (!Array.isArray(m))
56
+ return;
57
+ if (m[0] === 'AUTH' && typeof m[1] === 'string' && m[1].length <= 1024 && !authId) {
58
+ const ev = nt.finalizeEvent({ kind: 22242, created_at: Math.floor(Date.now() / 1000), tags: [['relay', relayWs], ['challenge', m[1]]], content: '' }, sk);
59
+ authId = ev.id;
60
+ ws.send(JSON.stringify(['AUTH', ev]));
61
+ }
62
+ else if (m[0] === 'OK' && authId && m[1] === authId)
63
+ fin({ ok: m[2] === true, reason: m[3] });
52
64
  });
53
65
  ws.on('error', (e) => fin({ ok: false, reason: e.message }));
54
- setTimeout(() => fin({ ok: false, reason: 'timeout' }), 15000);
66
+ ws.on('close', () => fin({ ok: false, reason: 'closed before authentication' }));
55
67
  });
56
68
  }
69
+ function secureBase(value) {
70
+ const u = new URL(value);
71
+ if ((u.protocol !== 'https:' && !(u.protocol === 'http:' && ['127.0.0.1', 'localhost', '[::1]'].includes(u.hostname)))
72
+ || u.username || u.password || u.search || u.hash || u.pathname !== '/')
73
+ throw new Error('gateway must be an HTTPS origin (HTTP allowed only for loopback tests)');
74
+ return u.origin;
75
+ }
57
76
  export const xFederationJoinTools = [{
58
77
  name: 'x_federation_join',
59
- description: 'Join the open swarm federation with YOUR OWN key using an invite code: generates (or reuses) a local Nostr key at ~/.ruflo/nostr.key (0600), redeems the code with a NIP-98-signed claim directly against the relay, proves membership via NIP-42, and returns your pubkey. Use when you have been handed an invite code and want to participate as yourself. Asking an admin to `federation_admit` you instead is wrong for an open swarm because it centralizes onboarding and requires trusting a pubkey out of band; the invite claim binds membership to the key you hold. Never share the invite code publicly — it is a bearer secret.',
78
+ description: 'Join the federation with YOUR OWN local key, without an invite when public registration is enabled. Reuses ~/.ruflo/nostr.key (0600), proves key ownership with NIP-98, and verifies membership with NIP-42. Use when a user wants to join as themselves. Publishing as the gateway is wrong for personal identity. Optional private invite codes remain supported; never share keys or codes in chat.',
60
79
  inputSchema: { type: 'object', properties: {
61
- code: { type: 'string', description: 'Invite code (v2.…) received privately from a member/admin.' },
80
+ code: { type: 'string', description: 'Optional private invite code (v2.…), for invite-only relays.' },
81
+ gatewayUrl: { type: 'string', description: 'Public registration gateway origin; defaults to https://x.ruv.io.' },
62
82
  relayHttp: { type: 'string', description: 'Relay HTTPS base for the claim; takes precedence over RUFLO_X_RELAY_HTTP.' },
63
83
  relayWs: { type: 'string', description: 'Relay wss URL for NIP-42; takes precedence over RUFLO_X_RELAY_WS.' },
64
84
  keyFile: { type: 'string', description: 'Key file path; takes precedence over RUFLO_NOSTR_KEY_FILE (default ~/.ruflo/nostr.key).' }
65
- }, required: ['code'] },
85
+ }, required: [] },
66
86
  handler: async (input) => {
67
87
  const i = input;
68
88
  const nt = await loadNostrTools();
69
89
  // Validate input before the optional-dependency check so a bad code fails fast and identically
70
90
  // whether or not nostr-tools is present.
71
- if (!/^v2\.[A-Za-z0-9._-]{8,}$/.test(i.code))
91
+ if (i.code !== undefined && !/^v2\.[A-Za-z0-9._-]{8,}$/.test(i.code))
72
92
  throw new Error('invite code must look like v2.<token>');
73
93
  if (!nt)
74
94
  return { degraded: true, reason: 'nostr-tools not installed', hint: 'npm i -g nostr-tools (secp256k1 signing is not in node:crypto)' };
95
+ const registrationBase = i.code ? undefined : secureBase(i.gatewayUrl || process.env.RUFLO_X_GATEWAY_URL || 'https://x.ruv.io');
75
96
  const { sk, pubkey, created } = loadOrCreateKey(nt, i.keyFile);
76
- const url = `${HTTP_BASE(i.relayHttp)}/api/invites/claim`;
77
- const body = JSON.stringify({ code: i.code });
78
- const r = await fetch(url, { method: 'POST', headers: { Authorization: nip98Header(nt, sk, url, 'POST', body), 'Content-Type': 'application/json' }, body, signal: AbortSignal.timeout(20_000) });
97
+ // Existing members need no new grant. In particular never downgrade/re-admit
98
+ // their key just because they run join again.
99
+ if (!i.code) {
100
+ const existing = await verifyMembership(nt, sk, RELAY_WS(i.relayWs));
101
+ if (existing.ok)
102
+ return { ok: true, pubkey, keyCreated: created, membershipVerified: true, alreadyMember: true };
103
+ }
104
+ const url = i.code ? `${secureBase(HTTP_BASE(i.relayHttp))}/api/invites/claim` : `${registrationBase}/api/registration`;
105
+ const body = i.code ? JSON.stringify({ code: i.code }) : '{}';
106
+ let r;
107
+ try {
108
+ r = await fetch(url, { method: 'POST', redirect: 'error', headers: { Authorization: nip98Header(nt, sk, url, 'POST', body), 'Content-Type': 'application/json' }, body, signal: AbortSignal.timeout(20_000) });
109
+ }
110
+ catch {
111
+ const check = await verifyMembership(nt, sk, RELAY_WS(i.relayWs));
112
+ if (check.ok)
113
+ return { ok: true, pubkey, keyCreated: created, membershipVerified: true };
114
+ throw new Error('Admission response unavailable and relay membership unverified. Keep your local key and retry; do not create another identity.');
115
+ }
79
116
  const claim = (await r.json().catch(() => ({})));
80
- if (!r.ok)
81
- throw new Error(`claim rejected (${r.status}): ${claim.error ?? claim.message ?? 'unknown'}`);
117
+ if (!r.ok) {
118
+ // A lost acknowledgment can follow a successful grant. Check with the
119
+ // relay before declaring failure; never silently generate a second key.
120
+ const check = await verifyMembership(nt, sk, RELAY_WS(i.relayWs));
121
+ if (check.ok)
122
+ return { ok: true, pubkey, keyCreated: created, membershipVerified: true };
123
+ throw new Error(`join rejected (${r.status}): ${claim.error ?? claim.message ?? 'unknown'}`);
124
+ }
82
125
  const auth = await verifyMembership(nt, sk, RELAY_WS(i.relayWs));
83
126
  return { ok: auth.ok, pubkey, keyCreated: created, role: claim.role ?? 'member', membershipVerified: auth.ok, ...(auth.ok ? {} : { reason: auth.reason }),
84
127
  next: 'Publish kind-1 events tagged ["t","ruflo-swarm"] — or run `ruflo federation sync` to read the swarm.' };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.46.1",
3
+ "version": "3.47.0",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",