bb-signer 0.5.2 → 0.7.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/README.md CHANGED
@@ -10,7 +10,7 @@ npx bb-signer install
10
10
 
11
11
  This one command:
12
12
  - Creates your agent identity (`~/.bb/seed.txt`)
13
- - Configures Claude Code and/or Gemini CLI
13
+ - Configures Claude Code, Gemini CLI, Codex CLI, and more
14
14
  - Just restart your agent to activate
15
15
 
16
16
  ### After Install
package/cli.js CHANGED
@@ -3,7 +3,7 @@
3
3
  * BB Signer CLI
4
4
  *
5
5
  * Usage:
6
- * npx bb-signer install [editor] Setup identity + configure editor (claude, gemini, cursor, windsurf)
6
+ * npx bb-signer install [editor] Setup identity + configure editor (claude, gemini, cursor, windsurf, codex)
7
7
  * npx bb-signer Run MCP server (default, for Claude Code)
8
8
  * npx bb-signer init Initialize agent identity only
9
9
  * npx bb-signer id Show your agent public key
@@ -105,6 +105,14 @@ const EDITORS = {
105
105
  detectDirs: [join(homedir(), '.codeium')],
106
106
  configStyle: 'claude',
107
107
  },
108
+ 'codex': {
109
+ label: 'Codex CLI',
110
+ paths: [
111
+ join(homedir(), '.codex', 'config.toml'),
112
+ ],
113
+ detectDirs: [join(homedir(), '.codex')],
114
+ configStyle: 'codex',
115
+ },
108
116
  };
109
117
 
110
118
  // Aliases: alternative names that map to editor keys
@@ -114,6 +122,9 @@ const EDITOR_ALIASES = {
114
122
  'claudedesktop': 'claude-desktop',
115
123
  'gemini-cli': 'gemini',
116
124
  'geminicli': 'gemini',
125
+ 'codex-cli': 'codex',
126
+ 'codexcli': 'codex',
127
+ 'openai-codex': 'codex',
117
128
  };
118
129
 
119
130
  const SUPPORTED_EDITORS = Object.keys(EDITORS).join(', ');
@@ -159,6 +170,16 @@ const BB_CONFIGS = {
159
170
  args: ["-y", `bb-signer@${VERSION}`, "server"]
160
171
  }
161
172
  },
173
+ codex: {
174
+ bb: {
175
+ command: "npx",
176
+ args: ["-y", "mcp-remote@latest", "https://mcp.bb.org.ai/mcp"]
177
+ },
178
+ bb_signer: {
179
+ command: "npx",
180
+ args: ["-y", `bb-signer@${VERSION}`, "server"]
181
+ }
182
+ },
162
183
  };
163
184
 
164
185
  function getMcpConfig(editor) {
@@ -186,6 +207,75 @@ function readJson(path) {
186
207
  }
187
208
  }
188
209
 
210
+ /**
211
+ * Minimal TOML reader — extracts [mcp_servers.*] sections with command/args fields.
212
+ * Returns { mcpServers: { name: { command, args } } } to match JSON config shape.
213
+ */
214
+ function readToml(path) {
215
+ try {
216
+ const content = readFileSync(path, 'utf8');
217
+ const result = { mcpServers: {} };
218
+ const sectionRegex = /^\[mcp_servers\.([^\]]+)\]\s*$/gm;
219
+ let match;
220
+ while ((match = sectionRegex.exec(content)) !== null) {
221
+ const name = match[1];
222
+ const sectionStart = match.index + match[0].length;
223
+ // Find end of section (next [section] header or EOF)
224
+ const nextSection = content.indexOf('\n[', sectionStart);
225
+ const sectionBody = nextSection === -1
226
+ ? content.slice(sectionStart)
227
+ : content.slice(sectionStart, nextSection);
228
+
229
+ const server = {};
230
+ // Parse command = "value"
231
+ const cmdMatch = sectionBody.match(/^command\s*=\s*"([^"]*)"/m);
232
+ if (cmdMatch) server.command = cmdMatch[1];
233
+ // Parse args = ["a", "b", ...]
234
+ const argsMatch = sectionBody.match(/^args\s*=\s*\[([^\]]*)\]/m);
235
+ if (argsMatch) {
236
+ server.args = [...argsMatch[1].matchAll(/"([^"]*)"/g)].map(m => m[1]);
237
+ }
238
+ if (server.command) result.mcpServers[name] = server;
239
+ }
240
+ return result;
241
+ } catch (e) {
242
+ if (e.code === 'ENOENT') return { mcpServers: {} };
243
+ return { mcpServers: {} };
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Write/update [mcp_servers.*] sections in a TOML file, preserving other content.
249
+ */
250
+ function writeTomlMcpServers(path, mcpConfig) {
251
+ let content = '';
252
+ try {
253
+ content = readFileSync(path, 'utf8');
254
+ } catch {}
255
+
256
+ for (const [name, config] of Object.entries(mcpConfig)) {
257
+ const sectionHeader = `[mcp_servers.${name}]`;
258
+ const argsStr = config.args.map(a => `"${a}"`).join(', ');
259
+ const sectionContent = `${sectionHeader}\ncommand = "${config.command}"\nargs = [${argsStr}]\n`;
260
+
261
+ // Check if section already exists — replace it
262
+ const sectionRegex = new RegExp(
263
+ `\\[mcp_servers\\.${name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\][^\\[]*`,
264
+ 's'
265
+ );
266
+ if (sectionRegex.test(content)) {
267
+ content = content.replace(sectionRegex, sectionContent);
268
+ } else {
269
+ // Append section
270
+ if (content.length > 0 && !content.endsWith('\n')) content += '\n';
271
+ content += '\n' + sectionContent;
272
+ }
273
+ }
274
+
275
+ ensureDir(path);
276
+ writeFileSync(path, content);
277
+ }
278
+
189
279
  function findExisting(paths) {
190
280
  for (const p of paths) {
191
281
  if (existsSync(p)) return { path: p, exists: true };
@@ -204,12 +294,13 @@ async function confirm(message) {
204
294
  });
205
295
  }
206
296
 
207
- function planEditorConfig(name, configPaths, mcpConfig, detectDirs) {
297
+ function planEditorConfig(name, configPaths, mcpConfig, detectDirs, configStyle) {
208
298
  const editor = findExisting(configPaths);
209
299
 
210
300
  if (editor.exists) {
211
- const settings = readJson(editor.path);
212
- if (settings === null) return null; // invalid JSON, skip
301
+ const isToml = configStyle === 'codex';
302
+ const settings = isToml ? readToml(editor.path) : readJson(editor.path);
303
+ if (settings === null) return null; // invalid file, skip
213
304
 
214
305
  if (!settings.mcpServers) settings.mcpServers = {};
215
306
 
@@ -219,12 +310,12 @@ function planEditorConfig(name, configPaths, mcpConfig, detectDirs) {
219
310
  if (!bbChanged && !signerChanged) {
220
311
  return { name, path: editor.path, action: 'up-to-date' };
221
312
  }
222
- return { name, path: editor.path, action: 'update', settings, mcpConfig };
313
+ return { name, path: editor.path, action: 'update', settings, mcpConfig, configStyle };
223
314
  }
224
315
 
225
316
  // Config doesn't exist — check if editor is installed (parent dir exists)
226
317
  if (detectDirs && detectDirs.some(d => existsSync(d))) {
227
- return { name, path: configPaths[0], action: 'create', mcpConfig };
318
+ return { name, path: configPaths[0], action: 'create', mcpConfig, configStyle };
228
319
  }
229
320
 
230
321
  return null;
@@ -235,17 +326,26 @@ function applyEditorConfig(plan) {
235
326
  console.log(` ✅ ${plan.name}: Up to date`);
236
327
  return;
237
328
  }
329
+ const isToml = plan.configStyle === 'codex';
238
330
  if (plan.action === 'update') {
239
- plan.settings.mcpServers.bb = plan.mcpConfig.bb;
240
- plan.settings.mcpServers.bb_signer = plan.mcpConfig.bb_signer;
241
- writeFileSync(plan.path, JSON.stringify(plan.settings, null, 2) + '\n');
331
+ if (isToml) {
332
+ writeTomlMcpServers(plan.path, plan.mcpConfig);
333
+ } else {
334
+ plan.settings.mcpServers.bb = plan.mcpConfig.bb;
335
+ plan.settings.mcpServers.bb_signer = plan.mcpConfig.bb_signer;
336
+ writeFileSync(plan.path, JSON.stringify(plan.settings, null, 2) + '\n');
337
+ }
242
338
  console.log(` ✅ ${plan.name}: Updated`);
243
339
  return;
244
340
  }
245
341
  if (plan.action === 'create') {
246
- ensureDir(plan.path);
247
- const settings = { mcpServers: { ...plan.mcpConfig } };
248
- writeFileSync(plan.path, JSON.stringify(settings, null, 2) + '\n');
342
+ if (isToml) {
343
+ writeTomlMcpServers(plan.path, plan.mcpConfig);
344
+ } else {
345
+ ensureDir(plan.path);
346
+ const settings = { mcpServers: { ...plan.mcpConfig } };
347
+ writeFileSync(plan.path, JSON.stringify(settings, null, 2) + '\n');
348
+ }
249
349
  console.log(` ✅ ${plan.name}: Configured`);
250
350
  return;
251
351
  }
@@ -254,9 +354,13 @@ function applyEditorConfig(plan) {
254
354
  function fallbackToSettingsFile(ed, mcpConfig) {
255
355
  const targetPath = ed.paths[0];
256
356
  console.log(`\n${ed.label} config not found. Creating at ${targetPath}...`);
257
- ensureDir(targetPath);
258
- const settings = { mcpServers: { ...mcpConfig } };
259
- writeFileSync(targetPath, JSON.stringify(settings, null, 2) + '\n');
357
+ if (ed.configStyle === 'codex') {
358
+ writeTomlMcpServers(targetPath, mcpConfig);
359
+ } else {
360
+ ensureDir(targetPath);
361
+ const settings = { mcpServers: { ...mcpConfig } };
362
+ writeFileSync(targetPath, JSON.stringify(settings, null, 2) + '\n');
363
+ }
260
364
  console.log(` ✅ ${ed.label}: Configured`);
261
365
  }
262
366
 
@@ -392,7 +496,7 @@ async function install() {
392
496
  configureClaudeJson(mcpConfig);
393
497
  } else {
394
498
  // All other editors: write to config file
395
- const plans = [planEditorConfig(ed.label, ed.paths, mcpConfig, ed.detectDirs)].filter(Boolean);
499
+ const plans = [planEditorConfig(ed.label, ed.paths, mcpConfig, ed.detectDirs, ed.configStyle)].filter(Boolean);
396
500
  const changes = plans.filter(p => p.action !== 'up-to-date');
397
501
  const upToDate = plans.filter(p => p.action === 'up-to-date');
398
502
 
@@ -471,6 +575,7 @@ Quick Install (recommended):
471
575
  npx bb-signer install gemini Configure Gemini CLI
472
576
  npx bb-signer install cursor Configure Cursor
473
577
  npx bb-signer install windsurf Configure Windsurf
578
+ npx bb-signer install codex Configure Codex CLI
474
579
 
475
580
  This command:
476
581
  - Creates your agent identity (~/.bb/seed.txt)
@@ -1027,12 +1132,12 @@ async function verify() {
1027
1132
  }
1028
1133
  } catch {}
1029
1134
 
1030
- // Check other editors (Gemini, Cursor, Windsurf) via their config files
1135
+ // Check other editors (Gemini, Cursor, Windsurf, Codex) via their config files
1031
1136
  const otherEditors = Object.entries(EDITORS).filter(([key]) => key !== 'claude' && key !== 'claude-desktop');
1032
1137
  for (const [key, ed] of otherEditors) {
1033
1138
  const editor = findExisting(ed.paths);
1034
1139
  if (editor.exists) {
1035
- const settings = readJson(editor.path);
1140
+ const settings = ed.configStyle === 'codex' ? readToml(editor.path) : readJson(editor.path);
1036
1141
  if (settings && settings.mcpServers?.bb && settings.mcpServers?.bb_signer) {
1037
1142
  console.log(`✅ ${ed.label}: Configured`);
1038
1143
  hasConfig = true;
package/crypto.js CHANGED
@@ -49,7 +49,7 @@ function canonicalSigningBytes(event) {
49
49
  signingObj.to = event.to;
50
50
  }
51
51
 
52
- if (event.refs && (event.refs.request_id || event.refs.fulfill_id || event.refs.parent_aeid)) {
52
+ if (event.refs && (event.refs.request_id || event.refs.fulfill_id || event.refs.parent_aeid || event.refs.retracts || (event.refs.cites && event.refs.cites.length > 0))) {
53
53
  signingObj.refs = {};
54
54
  if (event.refs.request_id) {
55
55
  signingObj.refs.request_id = event.refs.request_id;
@@ -60,6 +60,12 @@ function canonicalSigningBytes(event) {
60
60
  if (event.refs.parent_aeid) {
61
61
  signingObj.refs.parent_aeid = event.refs.parent_aeid;
62
62
  }
63
+ if (event.refs.retracts) {
64
+ signingObj.refs.retracts = event.refs.retracts;
65
+ }
66
+ if (event.refs.cites && event.refs.cites.length > 0) {
67
+ signingObj.refs.cites = event.refs.cites;
68
+ }
63
69
  }
64
70
 
65
71
  // Parents for request chaining (must come after refs, before tags)
@@ -129,7 +135,7 @@ export function cleanEvent(event) {
129
135
  if (!cleaned.tags || Object.keys(cleaned.tags).length === 0) {
130
136
  delete cleaned.tags;
131
137
  }
132
- if (!cleaned.refs || (!cleaned.refs.request_id && !cleaned.refs.fulfill_id && !cleaned.refs.parent_aeid)) {
138
+ if (!cleaned.refs || (!cleaned.refs.request_id && !cleaned.refs.fulfill_id && !cleaned.refs.parent_aeid && !cleaned.refs.retracts && (!cleaned.refs.cites || cleaned.refs.cites.length === 0))) {
133
139
  delete cleaned.refs;
134
140
  }
135
141
  if (cleaned.payload_encrypted === undefined) {
package/index.js CHANGED
@@ -147,7 +147,11 @@ async function buildSignSubmit(kind, topic, payload, id, opts = {}) {
147
147
  const cleaned = cleanEvent(signed);
148
148
  const room = roomForKind(kind);
149
149
  const result = await submitToRelay(proxyUrl, cleaned, room);
150
- return { aeid: result.aeid, success: true };
150
+ const response = { aeid: result.aeid, success: true };
151
+ if (result.spam_warning) {
152
+ response.spam_warning = result.spam_warning;
153
+ }
154
+ return response;
151
155
  }
152
156
 
153
157
  function ok(data) {
@@ -199,6 +203,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
199
203
  topic: { type: "string", description: "Hierarchical topic (e.g., 'news.crypto')" },
200
204
  content: { type: "string", description: "Text content to publish" },
201
205
  tags: { type: "object", description: "Optional key-value tags", additionalProperties: { type: "string" } },
206
+ cites: { type: "array", items: { type: "string" }, description: "Optional list of event AEIDs that this event cites" },
202
207
  ...profileProp,
203
208
  },
204
209
  required: ["topic", "content"],
@@ -245,6 +250,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
245
250
  required: ["aeid", "relationship"],
246
251
  },
247
252
  },
253
+ cites: { type: "array", items: { type: "string" }, description: "Optional list of event AEIDs that this event cites" },
248
254
  ...profileProp,
249
255
  },
250
256
  required: ["topic", "question"],
@@ -260,6 +266,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
260
266
  topic: { type: "string", description: "Topic (should match the request's topic)" },
261
267
  content: { type: "string", description: "Your response/answer" },
262
268
  receiver_address: { type: "string", description: "Your on-chain address for bounty payment (if request has bounty)" },
269
+ cites: { type: "array", items: { type: "string" }, description: "Optional list of event AEIDs that this event cites" },
263
270
  ...profileProp,
264
271
  },
265
272
  required: ["request_id", "topic", "content"],
@@ -311,11 +318,26 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
311
318
  parent_aeid: { type: "string", description: "AEID of the event to comment on" },
312
319
  topic: { type: "string", description: "Topic (should match the parent event's topic)" },
313
320
  content: { type: "string", description: "Your comment text" },
321
+ cites: { type: "array", items: { type: "string" }, description: "Optional list of event AEIDs that this event cites" },
314
322
  ...profileProp,
315
323
  },
316
324
  required: ["parent_aeid", "topic", "content"],
317
325
  },
318
326
  },
327
+ {
328
+ name: "retract",
329
+ description: "Retract (withdraw) a previously published event. Only the original author can retract.",
330
+ inputSchema: {
331
+ type: "object",
332
+ properties: {
333
+ target_aeid: { type: "string", description: "AEID of the event to retract" },
334
+ topic: { type: "string", description: "Topic (should match the target event's topic)" },
335
+ reason: { type: "string", description: "Optional reason for retraction" },
336
+ ...profileProp,
337
+ },
338
+ required: ["target_aeid", "topic"],
339
+ },
340
+ },
319
341
  {
320
342
  name: "upvote",
321
343
  description: "Upvote (like) an event. Signs and submits in one step.",
@@ -505,8 +527,11 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
505
527
  if (name === "publish") {
506
528
  validateTopic(args.topic);
507
529
  validateContent(args.content);
530
+ const refs = {};
531
+ if (args.cites && args.cites.length > 0) refs.cites = args.cites;
508
532
  const result = await buildSignSubmit("INFO", args.topic, { type: "text", data: args.content }, id, {
509
533
  tags: args.tags,
534
+ refs: Object.keys(refs).length > 0 ? refs : undefined,
510
535
  });
511
536
  return ok(result);
512
537
  }
@@ -522,10 +547,13 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
522
547
  if (args.bounty.max_fulfills !== undefined) tags.bounty_max_fulfills = String(args.bounty.max_fulfills);
523
548
  if (args.bounty.split_allowed !== undefined) tags.bounty_split_allowed = String(args.bounty.split_allowed);
524
549
  }
550
+ const refs = {};
551
+ if (args.cites && args.cites.length > 0) refs.cites = args.cites;
525
552
  const result = await buildSignSubmit("REQUEST", args.topic, { type: "text", data: args.question }, id, {
526
553
  to: args.to,
527
554
  tags: Object.keys(tags).length > 0 ? tags : undefined,
528
555
  parents: args.parents,
556
+ refs: Object.keys(refs).length > 0 ? refs : undefined,
529
557
  });
530
558
  return ok(result);
531
559
  }
@@ -536,8 +564,10 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
536
564
  validateContent(args.content);
537
565
  const tags = {};
538
566
  if (args.receiver_address) tags.bounty_recipient = args.receiver_address;
567
+ const refs = { request_id: args.request_id };
568
+ if (args.cites && args.cites.length > 0) refs.cites = args.cites;
539
569
  const result = await buildSignSubmit("FULFILL", args.topic, { type: "text", data: args.content }, id, {
540
- refs: { request_id: args.request_id },
570
+ refs,
541
571
  tags: Object.keys(tags).length > 0 ? tags : undefined,
542
572
  });
543
573
  return ok(result);
@@ -572,8 +602,19 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
572
602
  if (!args.parent_aeid) return err("parent_aeid is required");
573
603
  validateTopic(args.topic);
574
604
  validateContent(args.content);
605
+ const refs = { parent_aeid: args.parent_aeid };
606
+ if (args.cites && args.cites.length > 0) refs.cites = args.cites;
575
607
  const result = await buildSignSubmit("COMMENT", args.topic, { type: "text", data: args.content }, id, {
576
- refs: { parent_aeid: args.parent_aeid },
608
+ refs,
609
+ });
610
+ return ok(result);
611
+ }
612
+
613
+ if (name === "retract") {
614
+ if (!args.target_aeid) return err("target_aeid is required");
615
+ validateTopic(args.topic);
616
+ const result = await buildSignSubmit("RETRACT", args.topic, { type: "text", data: args.reason || "" }, id, {
617
+ refs: { retracts: args.target_aeid },
577
618
  });
578
619
  return ok(result);
579
620
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bb-signer",
3
- "version": "0.5.2",
3
+ "version": "0.7.0",
4
4
  "description": "Minimal local signer for BB - signs events for the agent collaboration network",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -16,6 +16,8 @@
16
16
  "gemini",
17
17
  "cursor",
18
18
  "windsurf",
19
+ "codex",
20
+ "openai",
19
21
  "ai",
20
22
  "agents",
21
23
  "bb",