a2acalling 0.1.7 → 0.1.9

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/src/lib/config.js CHANGED
@@ -23,6 +23,8 @@ const DEFAULT_CONFIG = {
23
23
  name: 'Public',
24
24
  description: 'Basic networking - safe for anyone',
25
25
  capabilities: [],
26
+ topics: [],
27
+ goals: [],
26
28
  disclosure: 'minimal',
27
29
  examples: ['calendar availability', 'public social posts', 'general questions']
28
30
  },
@@ -30,6 +32,8 @@ const DEFAULT_CONFIG = {
30
32
  name: 'Friends',
31
33
  description: 'Most capabilities, no sensitive financial data',
32
34
  capabilities: [],
35
+ topics: [],
36
+ goals: [],
33
37
  disclosure: 'public',
34
38
  examples: ['email summaries', 'schedule meetings', 'project discussions']
35
39
  },
@@ -37,6 +41,8 @@ const DEFAULT_CONFIG = {
37
41
  name: 'Private',
38
42
  description: 'Full access - only for you',
39
43
  capabilities: [],
44
+ topics: [],
45
+ goals: [],
40
46
  disclosure: 'public',
41
47
  examples: ['financial data', 'personal notes', 'private conversations']
42
48
  },
@@ -44,6 +50,8 @@ const DEFAULT_CONFIG = {
44
50
  name: 'Custom',
45
51
  description: 'User-defined permissions',
46
52
  capabilities: [],
53
+ topics: [],
54
+ goals: [],
47
55
  disclosure: 'minimal',
48
56
  examples: []
49
57
  }
@@ -0,0 +1,300 @@
1
+ /**
2
+ * Disclosure Manifest System
3
+ *
4
+ * Manages a structured list of topics the owner wants to discuss
5
+ * at each access tier. Stored as JSON at ~/.config/openclaw/a2a-disclosure.json.
6
+ */
7
+
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+
11
+ const CONFIG_DIR = process.env.A2A_CONFIG_DIR ||
12
+ process.env.OPENCLAW_CONFIG_DIR ||
13
+ path.join(process.env.HOME || '/tmp', '.config', 'openclaw');
14
+
15
+ const MANIFEST_FILE = path.join(CONFIG_DIR, 'a2a-disclosure.json');
16
+
17
+ const TIER_HIERARCHY = ['public', 'friends', 'family'];
18
+
19
+ /**
20
+ * Load manifest from disk. Returns {} if not found.
21
+ */
22
+ function loadManifest() {
23
+ try {
24
+ if (fs.existsSync(MANIFEST_FILE)) {
25
+ return JSON.parse(fs.readFileSync(MANIFEST_FILE, 'utf8'));
26
+ }
27
+ } catch (e) {
28
+ console.error('[a2a] Failed to load disclosure manifest:', e.message);
29
+ }
30
+ return {};
31
+ }
32
+
33
+ /**
34
+ * Save manifest to disk.
35
+ */
36
+ function saveManifest(manifest) {
37
+ if (!fs.existsSync(CONFIG_DIR)) {
38
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
39
+ }
40
+ manifest.updated_at = new Date().toISOString();
41
+ const tmpPath = `${MANIFEST_FILE}.tmp`;
42
+ fs.writeFileSync(tmpPath, JSON.stringify(manifest, null, 2));
43
+ fs.renameSync(tmpPath, MANIFEST_FILE);
44
+ }
45
+
46
+ /**
47
+ * Get topics for a given tier, merged down the hierarchy.
48
+ * family gets everything, friends gets friends+public, public gets public only.
49
+ *
50
+ * Returns { lead_with, discuss_freely, deflect, never_disclose }
51
+ */
52
+ function getTopicsForTier(tier) {
53
+ const manifest = loadManifest();
54
+ const topics = manifest.topics || {};
55
+
56
+ const tierIndex = TIER_HIERARCHY.indexOf(tier);
57
+ if (tierIndex === -1) {
58
+ // Unknown tier, treat as public
59
+ return getTopicsForTier('public');
60
+ }
61
+
62
+ // Merge tiers from public up to the requested tier
63
+ const tiersToMerge = TIER_HIERARCHY.slice(0, tierIndex + 1);
64
+
65
+ const merged = {
66
+ lead_with: [],
67
+ discuss_freely: [],
68
+ deflect: [],
69
+ never_disclose: manifest.never_disclose || []
70
+ };
71
+
72
+ for (const t of tiersToMerge) {
73
+ const tierTopics = topics[t] || {};
74
+ if (tierTopics.lead_with) merged.lead_with.push(...tierTopics.lead_with);
75
+ if (tierTopics.discuss_freely) merged.discuss_freely.push(...tierTopics.discuss_freely);
76
+ if (tierTopics.deflect) merged.deflect.push(...tierTopics.deflect);
77
+ }
78
+
79
+ // Deflect items: remove any that already appear in lead_with or discuss_freely
80
+ // (higher tiers promote topics from deflect to discuss/lead)
81
+ const promoted = new Set([
82
+ ...merged.lead_with.map(t => t.topic),
83
+ ...merged.discuss_freely.map(t => t.topic)
84
+ ]);
85
+ merged.deflect = merged.deflect.filter(t => !promoted.has(t.topic));
86
+
87
+ return merged;
88
+ }
89
+
90
+ /**
91
+ * Format topic lists into readable bullet points for prompt injection.
92
+ */
93
+ function formatTopicsForPrompt(tierTopics) {
94
+ const formatList = (items) => {
95
+ if (!items || items.length === 0) return ' (none specified)';
96
+ return items.map(item => ` - ${item.topic}: ${item.detail}`).join('\n');
97
+ };
98
+
99
+ return {
100
+ leadWithTopics: formatList(tierTopics.lead_with),
101
+ discussFreelyTopics: formatList(tierTopics.discuss_freely),
102
+ deflectTopics: formatList(tierTopics.deflect),
103
+ neverDisclose: tierTopics.never_disclose?.length
104
+ ? tierTopics.never_disclose.map(item => ` - ${item}`).join('\n')
105
+ : ' (none specified)'
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Generate a default manifest by reading USER.md, HEARTBEAT.md, SOUL.md
111
+ * from the owner's workspace. Falls back to a minimal starter if files don't exist.
112
+ */
113
+ function generateDefaultManifest(contextFiles = {}) {
114
+ const now = new Date().toISOString();
115
+
116
+ const manifest = {
117
+ version: 1,
118
+ generated_at: now,
119
+ updated_at: now,
120
+ topics: {
121
+ public: { lead_with: [], discuss_freely: [], deflect: [] },
122
+ friends: { lead_with: [], discuss_freely: [], deflect: [] },
123
+ family: { lead_with: [], discuss_freely: [], deflect: [] }
124
+ },
125
+ never_disclose: ['API keys', 'Other users\' data', 'Financial figures'],
126
+ personality_notes: 'Direct and technical. Prefers depth over breadth.'
127
+ };
128
+
129
+ const userContent = contextFiles.user || '';
130
+ const heartbeatContent = contextFiles.heartbeat || '';
131
+ const soulContent = contextFiles.soul || '';
132
+
133
+ const hasContent = userContent || heartbeatContent || soulContent;
134
+
135
+ if (!hasContent) {
136
+ // Minimal starter manifest
137
+ manifest.topics.public.lead_with.push(
138
+ { topic: 'What I do', detail: 'Brief professional description' }
139
+ );
140
+ manifest.topics.public.discuss_freely.push(
141
+ { topic: 'General interests', detail: 'Non-sensitive topics and hobbies' }
142
+ );
143
+ manifest.topics.public.deflect.push(
144
+ { topic: 'Personal details', detail: 'Redirect to direct owner contact' }
145
+ );
146
+ return manifest;
147
+ }
148
+
149
+ // Extract from USER.md
150
+ if (userContent) {
151
+ // Goals/seeking
152
+ const goalsMatch = userContent.match(/##\s*(?:Goals|Current|Seeking|Working On)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
153
+ if (goalsMatch) {
154
+ const goals = goalsMatch[1]
155
+ .split('\n')
156
+ .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
157
+ .map(l => l.replace(/^[\s\-\*]+/, '').trim())
158
+ .filter(Boolean);
159
+
160
+ goals.forEach((goal, i) => {
161
+ if (i < 2) {
162
+ manifest.topics.public.lead_with.push({ topic: goal.slice(0, 60), detail: goal });
163
+ } else {
164
+ manifest.topics.public.discuss_freely.push({ topic: goal.slice(0, 60), detail: goal });
165
+ }
166
+ });
167
+ }
168
+
169
+ // Interests/projects
170
+ const interestsMatch = userContent.match(/##\s*(?:Interests|Projects|Skills)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
171
+ if (interestsMatch) {
172
+ const interests = interestsMatch[1]
173
+ .split('\n')
174
+ .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
175
+ .map(l => l.replace(/^[\s\-\*]+/, '').trim())
176
+ .filter(Boolean);
177
+
178
+ interests.forEach(interest => {
179
+ manifest.topics.public.discuss_freely.push({ topic: interest.slice(0, 60), detail: interest });
180
+ });
181
+ }
182
+
183
+ // Private/personal sections go to friends/family
184
+ const privateMatch = userContent.match(/##\s*(?:Private|Personal|Family)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
185
+ if (privateMatch) {
186
+ const privateItems = privateMatch[1]
187
+ .split('\n')
188
+ .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
189
+ .map(l => l.replace(/^[\s\-\*]+/, '').trim())
190
+ .filter(Boolean);
191
+
192
+ privateItems.forEach(item => {
193
+ manifest.topics.family.discuss_freely.push({ topic: item.slice(0, 60), detail: item });
194
+ });
195
+
196
+ // Deflect these for public
197
+ manifest.topics.public.deflect.push(
198
+ { topic: 'Personal life', detail: 'Redirect — suggest owners connect directly' }
199
+ );
200
+ }
201
+ }
202
+
203
+ // Extract from HEARTBEAT.md (recent activity/status)
204
+ if (heartbeatContent) {
205
+ const recentLines = heartbeatContent
206
+ .split('\n')
207
+ .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
208
+ .map(l => l.replace(/^[\s\-\*]+/, '').trim())
209
+ .filter(Boolean)
210
+ .slice(0, 5);
211
+
212
+ recentLines.forEach((line, i) => {
213
+ if (i < 2) {
214
+ manifest.topics.public.lead_with.push({ topic: line.slice(0, 60), detail: line });
215
+ } else {
216
+ manifest.topics.friends.discuss_freely.push({ topic: line.slice(0, 60), detail: line });
217
+ }
218
+ });
219
+ }
220
+
221
+ // Extract from SOUL.md (personality, values)
222
+ if (soulContent) {
223
+ // Look for personality cues
224
+ const personalityLines = soulContent
225
+ .split('\n')
226
+ .filter(l => l.trim() && !l.startsWith('#'))
227
+ .slice(0, 3)
228
+ .join(' ')
229
+ .trim();
230
+
231
+ if (personalityLines) {
232
+ manifest.personality_notes = personalityLines.slice(0, 300);
233
+ }
234
+
235
+ // Values become friends-tier topics
236
+ const valuesMatch = soulContent.match(/##\s*(?:Values|Beliefs|Principles)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
237
+ if (valuesMatch) {
238
+ const values = valuesMatch[1]
239
+ .split('\n')
240
+ .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
241
+ .map(l => l.replace(/^[\s\-\*]+/, '').trim())
242
+ .filter(Boolean);
243
+
244
+ values.forEach(value => {
245
+ manifest.topics.friends.discuss_freely.push({ topic: value.slice(0, 60), detail: value });
246
+ });
247
+ }
248
+ }
249
+
250
+ // Ensure at least something in each public category
251
+ if (manifest.topics.public.lead_with.length === 0) {
252
+ manifest.topics.public.lead_with.push(
253
+ { topic: 'Current focus', detail: 'Primary work and interests' }
254
+ );
255
+ }
256
+ if (manifest.topics.public.discuss_freely.length === 0) {
257
+ manifest.topics.public.discuss_freely.push(
258
+ { topic: 'General interests', detail: 'Non-sensitive topics' }
259
+ );
260
+ }
261
+ if (manifest.topics.public.deflect.length === 0) {
262
+ manifest.topics.public.deflect.push(
263
+ { topic: 'Private matters', detail: 'Redirect to direct owner contact' }
264
+ );
265
+ }
266
+
267
+ return manifest;
268
+ }
269
+
270
+ /**
271
+ * Read context files from an OpenClaw workspace directory.
272
+ * Returns { user, heartbeat, soul } with file contents or empty strings.
273
+ */
274
+ function readContextFiles(workspaceDir) {
275
+ const read = (filename) => {
276
+ try {
277
+ const filePath = path.join(workspaceDir, filename);
278
+ if (fs.existsSync(filePath)) {
279
+ return fs.readFileSync(filePath, 'utf8');
280
+ }
281
+ } catch (e) {}
282
+ return '';
283
+ };
284
+
285
+ return {
286
+ user: read('USER.md'),
287
+ heartbeat: read('HEARTBEAT.md'),
288
+ soul: read('SOUL.md')
289
+ };
290
+ }
291
+
292
+ module.exports = {
293
+ loadManifest,
294
+ saveManifest,
295
+ getTopicsForTier,
296
+ formatTopicsForPrompt,
297
+ generateDefaultManifest,
298
+ readContextFiles,
299
+ MANIFEST_FILE
300
+ };
@@ -9,8 +9,11 @@ const path = require('path');
9
9
 
10
10
  /**
11
11
  * Load owner context from OpenClaw workspace files
12
+ * @param {string} workspaceDir - Path to workspace
13
+ * @param {Object} options
14
+ * @param {string[]} options.tierGoals - Goals from config tier (takes priority over USER.md)
12
15
  */
13
- function loadOwnerContext(workspaceDir = process.cwd()) {
16
+ function loadOwnerContext(workspaceDir = process.cwd(), options = {}) {
14
17
  const context = {
15
18
  goals: [],
16
19
  interests: [],
@@ -19,18 +22,25 @@ function loadOwnerContext(workspaceDir = process.cwd()) {
19
22
  memory: null
20
23
  };
21
24
 
25
+ // Tier goals from config take priority
26
+ if (options.tierGoals && options.tierGoals.length > 0) {
27
+ context.goals = [...options.tierGoals];
28
+ }
29
+
22
30
  // Load USER.md
23
31
  const userPath = path.join(workspaceDir, 'USER.md');
24
32
  if (fs.existsSync(userPath)) {
25
33
  context.user = fs.readFileSync(userPath, 'utf8');
26
- // Extract goals from USER.md
27
- const goalsMatch = context.user.match(/##\s*(?:Goals|Current|Seeking)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
28
- if (goalsMatch) {
29
- context.goals = goalsMatch[1]
30
- .split('\n')
31
- .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
32
- .map(l => l.replace(/^[\s\-\*]+/, '').trim())
33
- .filter(Boolean);
34
+ // Extract goals from USER.md (fallback if no tier goals from config)
35
+ if (context.goals.length === 0) {
36
+ const goalsMatch = context.user.match(/##\s*(?:Goals|Current|Seeking)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
37
+ if (goalsMatch) {
38
+ context.goals = goalsMatch[1]
39
+ .split('\n')
40
+ .filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
41
+ .map(l => l.replace(/^[\s\-\*]+/, '').trim())
42
+ .filter(Boolean);
43
+ }
34
44
  }
35
45
  // Extract interests
36
46
  const interestsMatch = context.user.match(/##\s*(?:Interests|Projects)[^\n]*\n([\s\S]*?)(?=\n##|$)/i);
@@ -84,7 +94,7 @@ function buildSummaryPrompt(messages, ownerContext, callerInfo = {}) {
84
94
  const goalsSection = ownerContext.goals?.length ? `### Current Goals\n- ${ownerContext.goals.join('\n- ')}` : '';
85
95
  const interestsSection = ownerContext.interests?.length ? `### Interests\n- ${ownerContext.interests.join('\n- ')}` : '';
86
96
 
87
- return `You just finished a A2A agent-to-agent call. Analyze it strategically for your owner.
97
+ return `You just finished an A2A agent-to-agent call. Analyze it strategically for your owner.
88
98
 
89
99
  ## Philosophy
90
100
  A2A is cooperative AND adversarial. Each agent maximizes value for their own owner — but the best outcomes are mutual wins. Your job:
@@ -112,35 +122,32 @@ ${callerInfo.context ? `Context: ${callerInfo.context}` : ''}
112
122
  Analyze as a strategic advisor. Return JSON:
113
123
 
114
124
  {
115
- "summary": "Brief neutral summary (shareable)",
116
-
125
+ "who": "Who called, who they represent, key facts about them",
126
+
127
+ "keyDiscoveries": ["What was learned about the other side — capabilities, interests, blind spots"],
128
+
129
+ "collaborationPotential": {
130
+ "rating": "HIGH | MEDIUM | LOW",
131
+ "opportunities": ["specific opportunities identified"]
132
+ },
133
+
117
134
  "exchange": {
118
135
  "weGot": ["info, commitments, or value we extracted"],
119
136
  "weGave": ["info, compute, or commitments we provided"],
120
- "balance": "favorable | even | unfavorable",
121
- "fair": true
122
- },
123
-
124
- "mutualValue": {
125
- "found": true,
126
- "opportunities": ["potential wins for BOTH sides"],
127
- "alignment": "how this connects to owner's goals"
137
+ "balance": "favorable | even | unfavorable"
128
138
  },
129
-
130
- "actionItems": {
131
- "owner": ["what YOUR OWNER should do"],
132
- "caller": ["what THEY committed to or should do"],
133
- "joint": ["things to do TOGETHER"]
134
- },
135
-
139
+
140
+ "recommendedFollowUp": ["actionable items with specifics"],
141
+
142
+ "assessment": "One-sentence strategic value judgment",
143
+
136
144
  "trust": {
137
145
  "assessment": "appropriate | too_high | too_low",
138
146
  "recommendation": "maintain | increase | decrease | revoke",
139
147
  "pattern": "What's their angle? Genuine partner or extractive?"
140
148
  },
141
-
142
- "ownerBrief": "2-3 sentences: the strategic takeaway for your owner",
143
- "followUp": "concrete next step to advance this relationship (if any)"
149
+
150
+ "ownerBrief": "2-3 sentences: the strategic takeaway for your owner"
144
151
  }
145
152
 
146
153
  Think like a strategic advisor: protect your owner's interests AND find mutual wins.