a2acalling 0.6.17 → 0.6.19

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/bin/cli.js CHANGED
@@ -335,9 +335,18 @@ async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
335
335
 
336
336
  // Sync tier config from manifest
337
337
  const manifest = result.manifest;
338
- function flattenTopics(sections) {
338
+
339
+ // Helper to extract topic names from both new and legacy formats
340
+ function getTierTopics(tierData) {
341
+ if (!tierData) return [];
342
+ // New format: topics array
343
+ if (Array.isArray(tierData.topics)) {
344
+ return tierData.topics.map(t => String(t && t.topic || '').trim()).filter(Boolean);
345
+ }
346
+ // Legacy format: lead_with + discuss_freely + deflect
339
347
  const out = [];
340
- for (const section of sections) {
348
+ for (const section of [tierData.lead_with, tierData.discuss_freely, tierData.deflect]) {
349
+ if (!Array.isArray(section)) continue;
341
350
  for (const item of section) {
342
351
  const t = String(item && item.topic || '').trim();
343
352
  if (t && !out.includes(t)) out.push(t);
@@ -346,24 +355,20 @@ async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
346
355
  return out;
347
356
  }
348
357
 
358
+ // Get tiers data (support both new 'tiers' key and legacy 'topics' key)
359
+ const tiersData = manifest.tiers || manifest.topics || {};
360
+
349
361
  try {
350
362
  config.setTier('public', {
351
- topics: flattenTopics([manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect]),
363
+ topics: getTierTopics(tiersData.public),
352
364
  disclosure: 'public'
353
365
  });
354
366
  config.setTier('friends', {
355
- topics: flattenTopics([
356
- manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect,
357
- manifest.topics.friends.lead_with, manifest.topics.friends.discuss_freely, manifest.topics.friends.deflect
358
- ]),
367
+ topics: [...getTierTopics(tiersData.public), ...getTierTopics(tiersData.friends)],
359
368
  disclosure: 'minimal'
360
369
  });
361
370
  config.setTier('family', {
362
- topics: flattenTopics([
363
- manifest.topics.public.lead_with, manifest.topics.public.discuss_freely, manifest.topics.public.deflect,
364
- manifest.topics.friends.lead_with, manifest.topics.friends.discuss_freely, manifest.topics.friends.deflect,
365
- manifest.topics.family.lead_with, manifest.topics.family.discuss_freely, manifest.topics.family.deflect
366
- ]),
371
+ topics: [...getTierTopics(tiersData.public), ...getTierTopics(tiersData.friends), ...getTierTopics(tiersData.family)],
367
372
  disclosure: 'minimal'
368
373
  });
369
374
  } catch (err) {
@@ -382,10 +387,7 @@ async function handleDisclosureSubmit(args, commandLabel = 'onboard') {
382
387
  const hostname = config.getAgent().hostname || process.env.A2A_HOSTNAME || 'localhost';
383
388
  if (args.flags.name) config.setAgent({ name: agentName });
384
389
 
385
- const publicTopics = flattenTopics([
386
- manifest.topics.public.lead_with,
387
- manifest.topics.public.discuss_freely
388
- ]);
390
+ const publicTopics = getTierTopics(tiersData.public);
389
391
 
390
392
  const { token } = store.create({
391
393
  name: agentName,
@@ -1402,10 +1404,37 @@ https://github.com/onthegonow/a2a_calling`;
1402
1404
  if (externalIp) {
1403
1405
  const verifyUrl = `http://${publicHost}/api/a2a/ping`;
1404
1406
  if (serverPort !== 80) {
1405
- console.log(`\n Note: Port ${serverPort} must be reachable externally.`);
1406
- console.log(` Reverse-proxy /api/a2a/* to localhost:${serverPort}, or open the port in your firewall.`);
1407
+ // Check what's using port 80
1408
+ const port80Status = await isPortListening(80, '127.0.0.1', { timeoutMs: 250 });
1409
+
1410
+ console.log(`\n ━━━ Reverse Proxy Setup ━━━`);
1411
+ console.log(` Server running on port ${serverPort}, but external callers expect port 80/443.`);
1412
+ if (port80Status.listening) {
1413
+ console.log(` Port 80: in use (likely nginx, caddy, or another web server)`);
1414
+ } else {
1415
+ console.log(` Port 80: available (but A2A is on ${serverPort})`);
1416
+ }
1417
+ console.log(`\n RECOMMENDED: Configure your web server to proxy A2A requests:\n`);
1418
+ console.log(` For nginx, add to your server block:`);
1419
+ console.log(` location /api/a2a/ {`);
1420
+ console.log(` proxy_pass http://127.0.0.1:${serverPort}/api/a2a/;`);
1421
+ console.log(` proxy_http_version 1.1;`);
1422
+ console.log(` proxy_set_header Host $host;`);
1423
+ console.log(` }`);
1424
+ console.log(`\n For Caddy:`);
1425
+ console.log(` handle /api/a2a/* {`);
1426
+ console.log(` reverse_proxy 127.0.0.1:${serverPort}`);
1427
+ console.log(` }`);
1428
+ console.log(`\n After configuring, update the invite hostname to use port 80:`);
1429
+ console.log(` Current: ${publicHost}`);
1430
+ console.log(` Should be: ${externalIp} (without port, defaults to 80)`);
1431
+ console.log(`\n Or open port ${serverPort} directly in your firewall if no proxy is used.`);
1432
+ console.log(` ━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
1433
+ console.log(`\n ACTION REQUIRED: Ask your owner if they want to configure a reverse proxy.`);
1434
+ console.log(` If they say yes, help them add the nginx/caddy config above.`);
1435
+ console.log(` If they say no or skip, the invite will use port ${serverPort} directly.`);
1407
1436
  }
1408
- console.log(` Verify: curl -s ${verifyUrl}`);
1437
+ console.log(`\n Verify: curl -s ${verifyUrl}`);
1409
1438
  }
1410
1439
 
1411
1440
  // Save server config and advance onboarding state to awaiting_disclosure.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "a2acalling",
3
- "version": "0.6.17",
3
+ "version": "0.6.19",
4
4
  "description": "Agent-to-agent calling for OpenClaw - A2A agent communication",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -32,7 +32,37 @@ function dedupeByTopic(items) {
32
32
  seen.add(topic.toLowerCase());
33
33
  out.push({
34
34
  topic,
35
- detail: normalizeTopic(item && item.detail)
35
+ description: normalizeTopic(item && (item.description || item.detail))
36
+ });
37
+ }
38
+ return out;
39
+ }
40
+
41
+ function dedupeByObjective(items) {
42
+ const seen = new Set();
43
+ const out = [];
44
+ for (const item of items) {
45
+ const objective = normalizeTopic(item && item.objective);
46
+ if (!objective || seen.has(objective.toLowerCase())) continue;
47
+ seen.add(objective.toLowerCase());
48
+ out.push({
49
+ objective,
50
+ description: normalizeTopic(item && item.description)
51
+ });
52
+ }
53
+ return out;
54
+ }
55
+
56
+ function dedupeDoNotDiscuss(items) {
57
+ const seen = new Set();
58
+ const out = [];
59
+ for (const item of items) {
60
+ const topic = normalizeTopic(item && item.topic);
61
+ if (!topic || seen.has(topic.toLowerCase())) continue;
62
+ seen.add(topic.toLowerCase());
63
+ out.push({
64
+ topic,
65
+ reason: normalizeTopic(item && item.reason)
36
66
  });
37
67
  }
38
68
  return out;
@@ -118,11 +148,11 @@ function saveManifest(manifest) {
118
148
  * Get topics for a given tier, merged down the hierarchy.
119
149
  * family gets everything, friends gets friends+public, public gets public only.
120
150
  *
121
- * Returns { lead_with, discuss_freely, deflect, never_disclose }
151
+ * Returns { topics, objectives, do_not_discuss, never_disclose }
122
152
  */
123
153
  function getTopicsForTier(tier) {
124
154
  const manifest = loadManifest();
125
- const topics = manifest.topics || {};
155
+ const tiers = manifest.tiers || manifest.topics || {};
126
156
 
127
157
  const tierIndex = TIER_HIERARCHY.indexOf(tier);
128
158
  if (tierIndex === -1) {
@@ -134,26 +164,42 @@ function getTopicsForTier(tier) {
134
164
  const tiersToMerge = TIER_HIERARCHY.slice(0, tierIndex + 1);
135
165
 
136
166
  const merged = {
137
- lead_with: [],
138
- discuss_freely: [],
139
- deflect: [],
167
+ topics: [],
168
+ objectives: [],
169
+ do_not_discuss: [],
140
170
  never_disclose: manifest.never_disclose || []
141
171
  };
142
172
 
143
173
  for (const t of tiersToMerge) {
144
- const tierTopics = topics[t] || {};
145
- if (tierTopics.lead_with) merged.lead_with.push(...tierTopics.lead_with);
146
- if (tierTopics.discuss_freely) merged.discuss_freely.push(...tierTopics.discuss_freely);
147
- if (tierTopics.deflect) merged.deflect.push(...tierTopics.deflect);
174
+ const tierData = tiers[t] || {};
175
+ // Support both new format (topics/objectives/do_not_discuss) and legacy (lead_with/discuss_freely/deflect)
176
+ if (tierData.topics) {
177
+ merged.topics.push(...tierData.topics);
178
+ } else {
179
+ // Legacy format fallback
180
+ if (tierData.lead_with) merged.topics.push(...tierData.lead_with);
181
+ if (tierData.discuss_freely) merged.topics.push(...tierData.discuss_freely);
182
+ }
183
+ if (tierData.objectives) merged.objectives.push(...tierData.objectives);
184
+ if (tierData.do_not_discuss) {
185
+ merged.do_not_discuss.push(...tierData.do_not_discuss);
186
+ } else if (tierData.deflect) {
187
+ // Legacy format fallback
188
+ merged.do_not_discuss.push(...tierData.deflect.map(d => ({
189
+ topic: d.topic,
190
+ reason: d.detail || d.reason
191
+ })));
192
+ }
148
193
  }
149
194
 
150
- // Deflect items: remove any that already appear in lead_with or discuss_freely
151
- // (higher tiers promote topics from deflect to discuss/lead)
152
- const promoted = new Set([
153
- ...merged.lead_with.map(t => t.topic),
154
- ...merged.discuss_freely.map(t => t.topic)
155
- ]);
156
- merged.deflect = merged.deflect.filter(t => !promoted.has(t.topic));
195
+ // Remove do_not_discuss items that appear in topics (higher tiers promote them)
196
+ const promoted = new Set(merged.topics.map(t => (t.topic || '').toLowerCase()));
197
+ merged.do_not_discuss = merged.do_not_discuss.filter(t => !promoted.has((t.topic || '').toLowerCase()));
198
+
199
+ // Dedupe
200
+ merged.topics = dedupeByTopic(merged.topics);
201
+ merged.objectives = dedupeByObjective(merged.objectives);
202
+ merged.do_not_discuss = dedupeDoNotDiscuss(merged.do_not_discuss);
157
203
 
158
204
  return merged;
159
205
  }
@@ -162,18 +208,32 @@ function getTopicsForTier(tier) {
162
208
  * Format topic lists into readable bullet points for prompt injection.
163
209
  */
164
210
  function formatTopicsForPrompt(tierTopics) {
165
- const formatList = (items) => {
211
+ const formatTopicList = (items) => {
212
+ if (!items || items.length === 0) return ' (none specified)';
213
+ return items.map(item => ` - ${item.topic}: ${item.description || item.detail || ''}`).join('\n');
214
+ };
215
+
216
+ const formatObjectiveList = (items) => {
166
217
  if (!items || items.length === 0) return ' (none specified)';
167
- return items.map(item => ` - ${item.topic}: ${item.detail}`).join('\n');
218
+ return items.map(item => ` - ${item.objective}: ${item.description || ''}`).join('\n');
219
+ };
220
+
221
+ const formatDoNotDiscuss = (items) => {
222
+ if (!items || items.length === 0) return ' (none specified)';
223
+ return items.map(item => ` - ${item.topic}: ${item.reason || ''}`).join('\n');
168
224
  };
169
225
 
170
226
  return {
171
- leadWithTopics: formatList(tierTopics.lead_with),
172
- discussFreelyTopics: formatList(tierTopics.discuss_freely),
173
- deflectTopics: formatList(tierTopics.deflect),
227
+ topics: formatTopicList(tierTopics.topics),
228
+ objectives: formatObjectiveList(tierTopics.objectives),
229
+ doNotDiscuss: formatDoNotDiscuss(tierTopics.do_not_discuss),
174
230
  neverDisclose: tierTopics.never_disclose?.length
175
231
  ? tierTopics.never_disclose.map(item => ` - ${item}`).join('\n')
176
- : ' (none specified)'
232
+ : ' (none specified)',
233
+ // Legacy compatibility
234
+ leadWithTopics: formatTopicList(tierTopics.topics?.slice(0, 3) || tierTopics.lead_with),
235
+ discussFreelyTopics: formatTopicList(tierTopics.topics?.slice(3) || tierTopics.discuss_freely),
236
+ deflectTopics: formatDoNotDiscuss(tierTopics.do_not_discuss || tierTopics.deflect)
177
237
  };
178
238
  }
179
239
 
@@ -226,86 +286,71 @@ function generateDefaultManifest(contextFiles = {}) {
226
286
 
227
287
  if (candidateTopics.length === 0) {
228
288
  return {
229
- version: 1,
289
+ version: 2,
230
290
  generated_at: now,
231
291
  updated_at: now,
232
- topics: {
292
+ tiers: {
233
293
  public: {
234
- lead_with: [{ topic: 'What I do', detail: 'Brief professional description' }],
235
- discuss_freely: [{ topic: 'General interests', detail: 'Non-sensitive topics and hobbies' }],
236
- deflect: [{ topic: 'Personal details', detail: 'Redirect to direct owner contact' }]
294
+ topics: [{ topic: 'What I do', description: 'Brief professional description' }],
295
+ objectives: [{ objective: 'Networking', description: 'Connect with others in the field' }],
296
+ do_not_discuss: [{ topic: 'Personal details', reason: 'Redirect to direct owner contact' }]
237
297
  },
238
- friends: { lead_with: [], discuss_freely: [], deflect: [] },
239
- family: { lead_with: [], discuss_freely: [], deflect: [] }
298
+ friends: { topics: [], objectives: [], do_not_discuss: [] },
299
+ family: { topics: [], objectives: [], do_not_discuss: [] }
240
300
  },
241
301
  never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
242
302
  personality_notes: 'Direct and technical. Prefers depth over breadth.'
243
303
  };
244
304
  }
245
305
 
246
- const publicLead = [];
247
- const publicDiscuss = [];
248
- const publicDeflect = [];
249
- const friendsLead = [];
250
- const friendsDiscuss = [];
251
- const familyDiscuss = [];
306
+ const publicTopics = [];
307
+ const publicObjectives = [];
308
+ const friendsTopics = [];
309
+ const friendsObjectives = [];
310
+ const familyTopics = [];
252
311
 
253
312
  candidateTopics.forEach((entry, index) => {
254
- const topic = truncateAtWordBoundary(entry.topic || '', 60);
255
- const detail = truncateAtWordBoundary(entry.detail || 'Open discussion topic.', 120);
313
+ const topic = truncateAtWordBoundary(entry.topic || '', 160);
314
+ const description = truncateAtWordBoundary(entry.description || entry.detail || 'Open discussion topic.', 500);
256
315
  if (!topic) return;
257
316
 
258
- const node = { topic, detail };
259
- if (index < 2) {
260
- publicLead.push(node);
261
- return;
262
- }
263
- if (index < 6) {
264
- publicDiscuss.push(node);
265
- return;
266
- }
267
- if (index < 8) {
268
- friendsLead.push(node);
317
+ const node = { topic, description };
318
+ if (index < 5) {
319
+ publicTopics.push(node);
269
320
  return;
270
321
  }
271
- if (index < 12) {
272
- friendsDiscuss.push(node);
322
+ if (index < 10) {
323
+ friendsTopics.push(node);
273
324
  return;
274
325
  }
275
- if (index < 14) {
276
- familyDiscuss.push(node);
277
- }
326
+ familyTopics.push(node);
278
327
  });
279
328
 
280
- if (publicLead.length === 0) {
281
- publicLead.push({ topic: 'Open source', detail: 'General product and engineering topics.' });
282
- }
283
- if (publicDiscuss.length === 0) {
284
- publicDiscuss.push({ topic: 'Collaboration', detail: 'Ways to collaborate and support each other.' });
285
- }
286
- if (publicDeflect.length === 0) {
287
- publicDeflect.push({ topic: 'Personal details', detail: 'Redirect to direct owner contact.' });
329
+ if (publicTopics.length === 0) {
330
+ publicTopics.push({ topic: 'Open source', description: 'General product and engineering topics.' });
288
331
  }
289
332
 
290
333
  return {
291
- version: 1,
334
+ version: 2,
292
335
  generated_at: now,
293
336
  updated_at: now,
294
- topics: {
337
+ tiers: {
295
338
  public: {
296
- lead_with: publicLead,
297
- discuss_freely: publicDiscuss,
298
- deflect: publicDeflect
339
+ topics: publicTopics,
340
+ objectives: publicObjectives.length > 0 ? publicObjectives : [
341
+ { objective: 'Grow network', description: 'Connect with others working on similar problems' }
342
+ ],
343
+ do_not_discuss: [{ topic: 'Personal details', reason: 'Redirect to direct owner contact' }]
299
344
  },
300
345
  friends: {
301
- lead_with: friendsLead,
302
- discuss_freely: friendsDiscuss,
303
- deflect: []
346
+ topics: friendsTopics,
347
+ objectives: friendsObjectives,
348
+ do_not_discuss: []
304
349
  },
305
350
  family: {
306
- lead_with: [],
307
- discuss_freely: familyDiscuss,
308
- deflect: []
351
+ topics: familyTopics,
352
+ objectives: [],
353
+ do_not_discuss: []
309
354
  }
310
355
  },
311
356
  never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
@@ -337,16 +382,17 @@ function validateDisclosureSubmission(data) {
337
382
  return { valid: false, manifest: null, errors: ['Submission must be a non-null object'] };
338
383
  }
339
384
 
340
- // Require topics object
341
- if (!data.topics || typeof data.topics !== 'object' || Array.isArray(data.topics)) {
342
- errors.push('Submission must include a "topics" object');
385
+ // Support both new format (tiers) and legacy format (topics)
386
+ const tiersData = data.tiers || data.topics;
387
+ if (!tiersData || typeof tiersData !== 'object' || Array.isArray(tiersData)) {
388
+ errors.push('Submission must include a "tiers" object (or legacy "topics" object)');
343
389
  return { valid: false, manifest: null, errors };
344
390
  }
345
391
 
346
392
  // Require all three tiers
347
393
  for (const tier of TIER_HIERARCHY) {
348
- if (!data.topics[tier] || typeof data.topics[tier] !== 'object') {
349
- errors.push(`Missing required tier: "${tier}" in topics`);
394
+ if (!tiersData[tier] || typeof tiersData[tier] !== 'object') {
395
+ errors.push(`Missing required tier: "${tier}"`);
350
396
  }
351
397
  }
352
398
  if (errors.length > 0) {
@@ -354,42 +400,108 @@ function validateDisclosureSubmission(data) {
354
400
  }
355
401
 
356
402
  // Reject extra tiers beyond the known hierarchy
357
- const extraTiers = Object.keys(data.topics).filter(t => !TIER_HIERARCHY.includes(t));
403
+ const extraTiers = Object.keys(tiersData).filter(t => !TIER_HIERARCHY.includes(t));
358
404
  if (extraTiers.length > 0) {
359
405
  errors.push(`Unknown tiers: ${extraTiers.join(', ')} — only public, friends, family are allowed`);
360
406
  }
361
407
 
362
- // Validate each tier's structure
363
- const requiredLists = ['lead_with', 'discuss_freely', 'deflect'];
364
- const LIST_LIMITS = { lead_with: 10, discuss_freely: 20, deflect: 10 };
408
+ // Detect format: new (topics/objectives/do_not_discuss) or legacy (lead_with/discuss_freely/deflect)
409
+ const isNewFormat = tiersData.public && (
410
+ Array.isArray(tiersData.public.topics) ||
411
+ Array.isArray(tiersData.public.objectives) ||
412
+ Array.isArray(tiersData.public.do_not_discuss)
413
+ );
414
+
415
+ const LIST_LIMITS = { topics: 15, objectives: 8, do_not_discuss: 10 };
416
+
365
417
  for (const tier of TIER_HIERARCHY) {
366
- const tierData = data.topics[tier];
367
- for (const cat of requiredLists) {
368
- if (!Array.isArray(tierData[cat])) {
369
- errors.push(`topics.${tier}.${cat} must be an array`);
370
- continue;
418
+ const tierData = tiersData[tier];
419
+
420
+ if (isNewFormat) {
421
+ // Validate new format: topics, objectives, do_not_discuss
422
+ if (tierData.topics !== undefined) {
423
+ if (!Array.isArray(tierData.topics)) {
424
+ errors.push(`tiers.${tier}.topics must be an array`);
425
+ } else {
426
+ if (tierData.topics.length > LIST_LIMITS.topics) {
427
+ errors.push(`tiers.${tier}.topics has ${tierData.topics.length} items — max ${LIST_LIMITS.topics}`);
428
+ }
429
+ for (let i = 0; i < tierData.topics.length; i++) {
430
+ const item = tierData.topics[i];
431
+ if (!item || typeof item !== 'object' || typeof item.topic !== 'string') {
432
+ errors.push(`tiers.${tier}.topics[${i}]: must have "topic" (string) and "description" (string)`);
433
+ continue;
434
+ }
435
+ if (item.topic.trim().length === 0) {
436
+ errors.push(`tiers.${tier}.topics[${i}].topic must not be empty`);
437
+ }
438
+ if (item.topic.length > 160) {
439
+ errors.push(`tiers.${tier}.topics[${i}]: topic exceeds 160 chars`);
440
+ }
441
+ const desc = item.description || '';
442
+ if (desc.length > 500) {
443
+ errors.push(`tiers.${tier}.topics[${i}]: description exceeds 500 chars`);
444
+ }
445
+ }
446
+ }
371
447
  }
372
- if (tierData[cat].length > LIST_LIMITS[cat]) {
373
- errors.push(`topics.${tier}.${cat} has ${tierData[cat].length} items max ${LIST_LIMITS[cat]}`);
448
+
449
+ if (tierData.objectives !== undefined) {
450
+ if (!Array.isArray(tierData.objectives)) {
451
+ errors.push(`tiers.${tier}.objectives must be an array`);
452
+ } else {
453
+ if (tierData.objectives.length > LIST_LIMITS.objectives) {
454
+ errors.push(`tiers.${tier}.objectives has ${tierData.objectives.length} items — max ${LIST_LIMITS.objectives}`);
455
+ }
456
+ for (let i = 0; i < tierData.objectives.length; i++) {
457
+ const item = tierData.objectives[i];
458
+ if (!item || typeof item !== 'object' || typeof item.objective !== 'string') {
459
+ errors.push(`tiers.${tier}.objectives[${i}]: must have "objective" (string) and "description" (string)`);
460
+ continue;
461
+ }
462
+ if (item.objective.trim().length === 0) {
463
+ errors.push(`tiers.${tier}.objectives[${i}].objective must not be empty`);
464
+ }
465
+ }
466
+ }
374
467
  }
375
- for (let i = 0; i < tierData[cat].length; i++) {
376
- const item = tierData[cat][i];
377
- if (!item || typeof item !== 'object' || typeof item.topic !== 'string' || typeof item.detail !== 'string') {
378
- errors.push(`topics.${tier}.${cat}[${i}]: each topic item must have "topic" (string) and "detail" (string)`);
379
- continue;
468
+
469
+ if (tierData.do_not_discuss !== undefined) {
470
+ if (!Array.isArray(tierData.do_not_discuss)) {
471
+ errors.push(`tiers.${tier}.do_not_discuss must be an array`);
472
+ } else {
473
+ if (tierData.do_not_discuss.length > LIST_LIMITS.do_not_discuss) {
474
+ errors.push(`tiers.${tier}.do_not_discuss has ${tierData.do_not_discuss.length} items — max ${LIST_LIMITS.do_not_discuss}`);
475
+ }
476
+ for (let i = 0; i < tierData.do_not_discuss.length; i++) {
477
+ const item = tierData.do_not_discuss[i];
478
+ if (!item || typeof item !== 'object' || typeof item.topic !== 'string') {
479
+ errors.push(`tiers.${tier}.do_not_discuss[${i}]: must have "topic" (string) and "reason" (string)`);
480
+ }
481
+ }
380
482
  }
381
- if (item.topic.trim().length === 0) {
382
- errors.push(`topics.${tier}.${cat}[${i}].topic must not be empty`);
483
+ }
484
+ } else {
485
+ // Validate legacy format: lead_with, discuss_freely, deflect
486
+ const requiredLists = ['lead_with', 'discuss_freely', 'deflect'];
487
+ const LEGACY_LIMITS = { lead_with: 10, discuss_freely: 20, deflect: 10 };
488
+ for (const cat of requiredLists) {
489
+ if (!Array.isArray(tierData[cat])) {
490
+ errors.push(`topics.${tier}.${cat} must be an array`);
383
491
  continue;
384
492
  }
385
- if (item.topic.length > 160) {
386
- errors.push(`topics.${tier}.${cat}[${i}]: topic exceeds 160 character limit (got ${item.topic.length})`);
493
+ if (tierData[cat].length > LEGACY_LIMITS[cat]) {
494
+ errors.push(`topics.${tier}.${cat} has ${tierData[cat].length} items max ${LEGACY_LIMITS[cat]}`);
387
495
  }
388
- if (item.detail.length > 500) {
389
- errors.push(`topics.${tier}.${cat}[${i}]: detail exceeds 500 character limit (got ${item.detail.length})`);
390
- }
391
- if (isTechnicalContent(item.topic) || isTechnicalContent(item.detail)) {
392
- errors.push(`topics.${tier}.${cat}[${i}]: contains technical content (code, URLs, or markdown formatting) — use plain language`);
496
+ for (let i = 0; i < tierData[cat].length; i++) {
497
+ const item = tierData[cat][i];
498
+ if (!item || typeof item !== 'object' || typeof item.topic !== 'string') {
499
+ errors.push(`topics.${tier}.${cat}[${i}]: must have "topic" (string) and "detail" (string)`);
500
+ continue;
501
+ }
502
+ if (item.topic.trim().length === 0) {
503
+ errors.push(`topics.${tier}.${cat}[${i}].topic must not be empty`);
504
+ }
393
505
  }
394
506
  }
395
507
  }
@@ -426,30 +538,63 @@ function validateDisclosureSubmission(data) {
426
538
  return { valid: false, manifest: null, errors };
427
539
  }
428
540
 
429
- // Rebuild topics from only validated keys to prevent extra properties passing through
430
- const cleanTopics = {};
431
- for (const tier of TIER_HIERARCHY) {
432
- cleanTopics[tier] = {};
433
- for (const cat of ['lead_with', 'discuss_freely', 'deflect']) {
434
- cleanTopics[tier][cat] = (data.topics[tier][cat] || []).map(item => ({
435
- topic: item.topic,
436
- detail: item.detail
437
- }));
541
+ // Rebuild clean structure (isNewFormat already set above)
542
+ const now = new Date().toISOString();
543
+
544
+ if (isNewFormat) {
545
+ // New format: tiers with topics/objectives/do_not_discuss
546
+ const cleanTiers = {};
547
+ for (const tier of TIER_HIERARCHY) {
548
+ cleanTiers[tier] = {
549
+ topics: (tiersData[tier].topics || []).map(item => ({
550
+ topic: item.topic,
551
+ description: item.description || ''
552
+ })),
553
+ objectives: (tiersData[tier].objectives || []).map(item => ({
554
+ objective: item.objective,
555
+ description: item.description || ''
556
+ })),
557
+ do_not_discuss: (tiersData[tier].do_not_discuss || []).map(item => ({
558
+ topic: item.topic,
559
+ reason: item.reason || ''
560
+ }))
561
+ };
438
562
  }
439
- }
440
563
 
441
- // Build valid manifest
442
- const now = new Date().toISOString();
443
- const manifest = {
444
- version: 1,
445
- generated_at: now,
446
- updated_at: now,
447
- topics: cleanTopics,
448
- never_disclose: data.never_disclose || ['API keys', 'Other users\' data', 'Financial figures'],
449
- personality_notes: data.personality_notes || ''
450
- };
564
+ const manifest = {
565
+ version: 2,
566
+ generated_at: now,
567
+ updated_at: now,
568
+ tiers: cleanTiers,
569
+ never_disclose: data.never_disclose || ['API keys', 'Other users\' data', 'Financial figures'],
570
+ personality_notes: data.personality_notes || ''
571
+ };
572
+
573
+ return { valid: true, manifest, errors: [] };
574
+ } else {
575
+ // Legacy format: topics with lead_with/discuss_freely/deflect
576
+ const cleanTopics = {};
577
+ for (const tier of TIER_HIERARCHY) {
578
+ cleanTopics[tier] = {};
579
+ for (const cat of ['lead_with', 'discuss_freely', 'deflect']) {
580
+ cleanTopics[tier][cat] = (tiersData[tier][cat] || []).map(item => ({
581
+ topic: item.topic,
582
+ detail: item.detail || ''
583
+ }));
584
+ }
585
+ }
586
+
587
+ const manifest = {
588
+ version: 1,
589
+ generated_at: now,
590
+ updated_at: now,
591
+ topics: cleanTopics,
592
+ never_disclose: data.never_disclose || ['API keys', 'Other users\' data', 'Financial figures'],
593
+ personality_notes: data.personality_notes || ''
594
+ };
451
595
 
452
- return { valid: true, manifest, errors: [] };
596
+ return { valid: true, manifest, errors: [] };
597
+ }
453
598
  }
454
599
 
455
600
  /**
@@ -520,7 +665,35 @@ function buildExtractionPrompt(availableFiles) {
520
665
  Look for these files in your workspace directory and read the ones that exist. Extract disclosure topics from USER.md and SOUL.md primarily.`;
521
666
  }
522
667
 
523
- const jsonBlock = '```json\n{\n "topics": {\n "public": {\n "lead_with": [\n { "topic": "Short label (max 160 chars)", "detail": "Longer description of the topic" }\n ],\n "discuss_freely": [],\n "deflect": []\n },\n "friends": {\n "lead_with": [],\n "discuss_freely": [],\n "deflect": []\n },\n "family": {\n "lead_with": [],\n "discuss_freely": [],\n "deflect": []\n }\n },\n "never_disclose": ["API keys", "Credentials", "Financial figures"],\n "personality_notes": "Brief description of communication style"\n}\n```';
668
+ const jsonBlock = `\`\`\`json
669
+ {
670
+ "tiers": {
671
+ "public": {
672
+ "topics": [
673
+ { "topic": "Short label (max 160 chars)", "description": "Longer description of the topic" }
674
+ ],
675
+ "objectives": [
676
+ { "objective": "What you want to achieve", "description": "Longer description of this goal" }
677
+ ],
678
+ "do_not_discuss": [
679
+ { "topic": "Topic to avoid", "reason": "Why this should be redirected" }
680
+ ]
681
+ },
682
+ "friends": {
683
+ "topics": [],
684
+ "objectives": [],
685
+ "do_not_discuss": []
686
+ },
687
+ "family": {
688
+ "topics": [],
689
+ "objectives": [],
690
+ "do_not_discuss": []
691
+ }
692
+ },
693
+ "never_disclose": ["API keys", "Credentials", "Financial figures"],
694
+ "personality_notes": "Brief description of communication style"
695
+ }
696
+ \`\`\``;
524
697
 
525
698
  return `## A2A Disclosure Extraction
526
699
 
@@ -530,21 +703,35 @@ ${fileSection}
530
703
 
531
704
  Focus on what the OWNER cares about, works on, and wants to discuss — NOT on agent instructions, code documentation, or operational tasks.
532
705
 
533
- ### What to extract
706
+ ### Tier Inheritance
707
+
708
+ - **public** — base tier, anyone can see these
709
+ - **friends** — inherits all PUBLIC topics/objectives, plus additional friend-only items
710
+ - **family** — inherits all FRIENDS and PUBLIC items, plus additional family-only items
711
+
712
+ Family callers see everything. Friends see friends + public. Public callers see only public.
713
+
714
+ ### What to extract for each tier
534
715
 
535
- For each trust tier, identify topics the owner would want to discuss:
716
+ **topics** Things the owner is interested in or working on:
717
+ - Professional role and expertise
718
+ - Current projects and interests
719
+ - Hobbies and activities
720
+ - Max 8 topics per tier
536
721
 
537
- - **public** — safe for anyone: professional role, public interests, general project descriptions
538
- - **friends** — for trusted contacts: current goals, collaboration interests, values, detailed project work
539
- - **family** — inner circle only: personal interests, private projects, sensitive plans
722
+ **objectives** — What the owner wants to achieve in conversations:
723
+ - Networking goals
724
+ - Collaboration interests
725
+ - Opportunities they're seeking
726
+ - Max 4 objectives per tier
540
727
 
541
- For each tier, categorize topics as:
542
- - **lead_with** proactively bring up (max 3 per tier)
543
- - **discuss_freely** — happy to discuss if asked (max 8 per tier)
544
- - **deflect** — redirect or decline (max 3 per tier)
728
+ **do_not_discuss** Topics to redirect or decline (can be empty):
729
+ - Personal matters (for public tier)
730
+ - Sensitive subjects
731
+ - Max 3 per tier
545
732
 
546
733
  Also identify:
547
- - **never_disclose** — information that should never be shared regardless of tier (API keys, credentials, financial data, etc.)
734
+ - **never_disclose** — information that should NEVER be shared regardless of tier (API keys, credentials, financial data, etc.)
548
735
  - **personality_notes** — a 1-2 sentence description of the owner's communication style
549
736
 
550
737
  ### What NOT to extract
@@ -565,11 +752,11 @@ ${jsonBlock}
565
752
  ### Rules
566
753
 
567
754
  1. Each "topic" string must be a short, human-readable label (max 160 chars)
568
- 2. Each "detail" string explains the topic more fully (max 500 chars)
755
+ 2. Each "description" string explains the topic more fully (max 500 chars)
569
756
  3. Topics should be things a person would discuss, not technical artifacts
570
- 4. Higher tiers (friends, family) inherit lower-tier topics automatically — don't duplicate
757
+ 4. Higher tiers inherit lower-tier items automatically — only add NEW items at each tier
571
758
  5. Present this to the owner for review before submitting
572
- 6. The owner may edit, remove, or add topics before final submission`;
759
+ 6. The owner may edit, remove, or add items before final submission`;
573
760
  }
574
761
 
575
762
  module.exports = {
@@ -1032,9 +1032,14 @@ function createDashboardApiRouter(options = {}) {
1032
1032
  disclosure: configTier.disclosure || 'minimal',
1033
1033
  examples: sanitizeStringArray(configTier.examples || [], 20, 120),
1034
1034
  manifest: {
1035
- lead_with: manifestTier.lead_with || [],
1036
- discuss_freely: manifestTier.discuss_freely || [],
1037
- deflect: manifestTier.deflect || []
1035
+ // Support both new format (topics/objectives/do_not_discuss) and legacy
1036
+ topics: manifestTier.topics || manifestTier.lead_with || [],
1037
+ objectives: manifestTier.objectives || [],
1038
+ do_not_discuss: manifestTier.do_not_discuss || manifestTier.deflect || [],
1039
+ // Legacy fields for backwards compatibility
1040
+ lead_with: manifestTier.lead_with || manifestTier.topics?.slice(0, 3) || [],
1041
+ discuss_freely: manifestTier.discuss_freely || manifestTier.topics?.slice(3) || [],
1042
+ deflect: manifestTier.deflect || manifestTier.do_not_discuss || []
1038
1043
  }
1039
1044
  };
1040
1045
  });
package/src/server.js CHANGED
@@ -184,24 +184,38 @@ function extractSignalPhrases(text, pattern, maxItems = 3) {
184
184
 
185
185
  function collectTopicKeywords(tierTopics) {
186
186
  const keywords = new Set();
187
- const sourceLists = ['lead_with', 'discuss_freely'];
187
+
188
+ // Support both new format (topics) and legacy format (lead_with/discuss_freely)
189
+ const topicsList = tierTopics?.topics || [
190
+ ...(tierTopics?.lead_with || []),
191
+ ...(tierTopics?.discuss_freely || [])
192
+ ];
193
+ const objectivesList = tierTopics?.objectives || [];
194
+
195
+ for (const item of topicsList) {
196
+ for (const part of [item?.topic, item?.description, item?.detail]) {
197
+ if (!part) continue;
198
+ const terms = String(part)
199
+ .toLowerCase()
200
+ .split(/[^a-z0-9]+/)
201
+ .filter(term => term.length >= 4);
202
+ for (const term of terms.slice(0, 6)) {
203
+ keywords.add(term);
204
+ if (keywords.size >= 48) return Array.from(keywords);
205
+ }
206
+ }
207
+ }
188
208
 
189
- for (const listName of sourceLists) {
190
- for (const item of tierTopics?.[listName] || []) {
191
- for (const part of [item?.topic, item?.detail]) {
192
- if (!part) {
193
- continue;
194
- }
195
- const terms = String(part)
196
- .toLowerCase()
197
- .split(/[^a-z0-9]+/)
198
- .filter(term => term.length >= 4);
199
- for (const term of terms.slice(0, 6)) {
200
- keywords.add(term);
201
- if (keywords.size >= 48) {
202
- return Array.from(keywords);
203
- }
204
- }
209
+ for (const item of objectivesList) {
210
+ for (const part of [item?.objective, item?.description]) {
211
+ if (!part) continue;
212
+ const terms = String(part)
213
+ .toLowerCase()
214
+ .split(/[^a-z0-9]+/)
215
+ .filter(term => term.length >= 4);
216
+ for (const term of terms.slice(0, 6)) {
217
+ keywords.add(term);
218
+ if (keywords.size >= 48) return Array.from(keywords);
205
219
  }
206
220
  }
207
221
  }