a2acalling 0.6.2 → 0.6.3
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/SKILL.md +7 -5
- package/bin/cli.js +121 -61
- package/package.json +1 -1
- package/src/lib/config.js +147 -1
- package/src/lib/conversations.js +74 -0
- package/src/lib/disclosure.js +3 -43
- package/src/routes/dashboard.js +48 -12
package/SKILL.md
CHANGED
|
@@ -96,7 +96,7 @@ Deterministic onboarding flow (sequential, flags-based):
|
|
|
96
96
|
|
|
97
97
|
1. Background bootstrap (config + disclosure)
|
|
98
98
|
2. Owner dashboard access (local URL + optional Callbook Remote install link)
|
|
99
|
-
3. Set permission tiers: populate
|
|
99
|
+
3. Set permission tiers: populate tier `topics` + `goals` (schema-validated and saved)
|
|
100
100
|
4. Port scan + reverse proxy guidance (if needed for public hostname)
|
|
101
101
|
5. External IP confirmation and public reachability check (public hostname only)
|
|
102
102
|
|
|
@@ -110,12 +110,14 @@ a2a quickstart --port 3001
|
|
|
110
110
|
a2a quickstart --hostname YOUR_DOMAIN:443 --port 3001
|
|
111
111
|
```
|
|
112
112
|
|
|
113
|
-
Quickstart
|
|
113
|
+
Quickstart prints and saves a tier configuration immediately (validated by the config layer). If you want to override the Friends tier topics/interests, rerun with:
|
|
114
114
|
|
|
115
115
|
```bash
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
116
|
+
# Provide topics directly
|
|
117
|
+
a2a quickstart --port 3001 --friends-topics "chat,search,openclaw,a2a"
|
|
118
|
+
|
|
119
|
+
# Or prompt interactively for Friends tier topics
|
|
120
|
+
a2a quickstart --port 3001 --interactive
|
|
119
121
|
```
|
|
120
122
|
|
|
121
123
|
If reverse proxy/ingress is required, Quickstart will stop and ask for explicit confirmation (`--confirm-ingress`).
|
package/bin/cli.js
CHANGED
|
@@ -1105,19 +1105,26 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
1105
1105
|
// ── Step 1: Background bootstrap (config + manifest) ─────────
|
|
1106
1106
|
let contextFiles = {};
|
|
1107
1107
|
let manifest = {};
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1108
|
+
try {
|
|
1109
|
+
contextFiles = disc.readContextFiles(workspaceDir);
|
|
1110
|
+
const forceManifest = Boolean(args.flags.force || args.flags['regen-manifest'] || args.flags.regenManifest);
|
|
1111
|
+
if (forceManifest) {
|
|
1112
|
+
const generated = disc.generateDefaultManifest(contextFiles);
|
|
1113
|
+
disc.saveManifest(generated);
|
|
1114
|
+
manifest = generated;
|
|
1115
|
+
} else {
|
|
1116
|
+
manifest = disc.loadManifest();
|
|
1117
|
+
if (!manifest || Object.keys(manifest).length === 0) {
|
|
1118
|
+
const generated = disc.generateDefaultManifest(contextFiles);
|
|
1119
|
+
disc.saveManifest(generated);
|
|
1120
|
+
manifest = generated;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
} catch (err) {
|
|
1124
|
+
// Non-fatal: onboarding can proceed even if manifest fails.
|
|
1125
|
+
contextFiles = {};
|
|
1126
|
+
manifest = {};
|
|
1127
|
+
}
|
|
1121
1128
|
|
|
1122
1129
|
console.log('\nA2A deterministic onboarding');
|
|
1123
1130
|
console.log('──────────────────────────');
|
|
@@ -1180,32 +1187,84 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
1180
1187
|
}
|
|
1181
1188
|
}
|
|
1182
1189
|
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1190
|
+
// ── Step 3: Permission tiers (topics + goals) ───────────────
|
|
1191
|
+
const onboardingAfterAccess = config.getOnboarding();
|
|
1192
|
+
if (!onboardingAfterAccess.tiers_confirmed) {
|
|
1193
|
+
const recommendations = buildTierRecommendations(contextFiles, manifest);
|
|
1194
|
+
|
|
1195
|
+
const parseFreeTextList = (raw) => {
|
|
1196
|
+
if (raw === undefined || raw === null || raw === true) return [];
|
|
1197
|
+
const text = String(raw || '').trim();
|
|
1198
|
+
if (!text) return [];
|
|
1199
|
+
return text
|
|
1200
|
+
.split(/[\n,]+/g)
|
|
1201
|
+
.map(s => s.trim())
|
|
1202
|
+
.filter(Boolean);
|
|
1203
|
+
};
|
|
1204
|
+
|
|
1205
|
+
const promptLine = async (question) => {
|
|
1206
|
+
const readline = require('readline');
|
|
1207
|
+
return await new Promise(resolve => {
|
|
1208
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1209
|
+
rl.question(question, (answer) => {
|
|
1210
|
+
rl.close();
|
|
1211
|
+
resolve(String(answer || '').trim());
|
|
1212
|
+
});
|
|
1213
|
+
});
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
// Optional owner override: Friends tier topics/interests (most important tier).
|
|
1217
|
+
const interactive = Boolean(
|
|
1218
|
+
args.flags.interactive ||
|
|
1219
|
+
args.flags['ask-friends-topics'] ||
|
|
1220
|
+
args.flags.askFriendsTopics
|
|
1221
|
+
);
|
|
1222
|
+
let friendsTopicsOverride = parseFreeTextList(args.flags['friends-topics'] || args.flags.friendsTopics);
|
|
1223
|
+
const noWorkspaceContext = !contextFiles.user && !contextFiles.heartbeat && !contextFiles.soul &&
|
|
1224
|
+
!contextFiles.memory && !contextFiles.claude;
|
|
1225
|
+
const shouldPromptFriendsTopics = (interactive || noWorkspaceContext) &&
|
|
1226
|
+
friendsTopicsOverride.length === 0 &&
|
|
1227
|
+
process.stdin.isTTY &&
|
|
1228
|
+
process.stdout.isTTY;
|
|
1229
|
+
if (shouldPromptFriendsTopics) {
|
|
1230
|
+
const suggested = (recommendations.friends.topics || []).slice(0, 12).join(', ');
|
|
1231
|
+
const answer = await promptLine(`Friends-tier topics/interests (comma-separated).\nSuggested: ${suggested}\n> `);
|
|
1232
|
+
friendsTopicsOverride = parseFreeTextList(answer);
|
|
1233
|
+
}
|
|
1187
1234
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1235
|
+
if (friendsTopicsOverride.length > 0) {
|
|
1236
|
+
const normalized = uniqueNonEmpty(friendsTopicsOverride.map(slugify).filter(Boolean), 24);
|
|
1237
|
+
recommendations.friends.topics = uniqueNonEmpty(
|
|
1238
|
+
[...(recommendations.public.topics || []), ...normalized],
|
|
1239
|
+
24
|
|
1240
|
+
);
|
|
1241
|
+
recommendations.family.topics = uniqueNonEmpty(
|
|
1242
|
+
[...(recommendations.friends.topics || []), ...(recommendations.family.topics || [])],
|
|
1243
|
+
30
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1191
1246
|
|
|
1192
|
-
|
|
1247
|
+
try {
|
|
1248
|
+
config.setTier('public', recommendations.public);
|
|
1249
|
+
config.setTier('friends', recommendations.friends);
|
|
1250
|
+
config.setTier('family', recommendations.family);
|
|
1251
|
+
} catch (err) {
|
|
1252
|
+
console.error('\n❌ Tier configuration validation failed.');
|
|
1253
|
+
console.error(` ${err.message}`);
|
|
1254
|
+
if (err.hint) {
|
|
1255
|
+
console.error(` Hint: ${err.hint}`);
|
|
1256
|
+
}
|
|
1257
|
+
console.error('');
|
|
1258
|
+
process.exit(1);
|
|
1259
|
+
}
|
|
1193
1260
|
|
|
1194
|
-
|
|
1195
|
-
console.log('3️⃣ Confirm tiers');
|
|
1196
|
-
console.log('Review the topics/goals above. To confirm and continue, run:');
|
|
1197
|
-
console.log(` a2a quickstart --port ${backendPort} --confirm-tiers`);
|
|
1198
|
-
console.log('Optional (remote access):');
|
|
1199
|
-
console.log(` a2a quickstart --hostname YOUR_DOMAIN:443 --port ${backendPort} --confirm-tiers`);
|
|
1200
|
-
console.log('');
|
|
1201
|
-
return;
|
|
1202
|
-
}
|
|
1261
|
+
printTierSummary(recommendations);
|
|
1203
1262
|
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1263
|
+
config.setOnboarding({
|
|
1264
|
+
step: 'tiers',
|
|
1265
|
+
tiers_confirmed: true
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1209
1268
|
|
|
1210
1269
|
// ── Step 4: Port scan + reverse proxy guidance (if needed) ──
|
|
1211
1270
|
console.log('\n4️⃣ Port scan + reverse proxy');
|
|
@@ -1239,13 +1298,13 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
1239
1298
|
console.log(` /api/a2a/* -> http://127.0.0.1:${backendPort}`);
|
|
1240
1299
|
console.log(` /dashboard/* -> http://127.0.0.1:${backendPort}`);
|
|
1241
1300
|
console.log(` /callbook/* -> http://127.0.0.1:${backendPort}`);
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1301
|
+
console.log('');
|
|
1302
|
+
console.log('If you have configured your reverse proxy and want to continue, run:');
|
|
1303
|
+
console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-ingress`);
|
|
1304
|
+
console.log('');
|
|
1305
|
+
if (!args.flags['confirm-ingress']) {
|
|
1306
|
+
return;
|
|
1307
|
+
}
|
|
1249
1308
|
} else {
|
|
1250
1309
|
console.log('✅ No reverse proxy required based on invite host/port.');
|
|
1251
1310
|
}
|
|
@@ -1299,13 +1358,13 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
1299
1358
|
const extPing = await externalPingCheck(expectedPingUrl);
|
|
1300
1359
|
if (extPing.ok) {
|
|
1301
1360
|
console.log(`✅ External ping OK (${extPing.provider})`);
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1361
|
+
} else if (!args.flags['skip-verify']) {
|
|
1362
|
+
console.log('⚠️ External ping FAILED (server may not be publicly reachable yet).');
|
|
1363
|
+
console.log('Fix ingress (DNS/reverse proxy/firewall), then rerun with:');
|
|
1364
|
+
console.log(` a2a quickstart --hostname ${inviteHost} --port ${backendPort} --confirm-ingress`);
|
|
1365
|
+
console.log('');
|
|
1366
|
+
return;
|
|
1367
|
+
} else {
|
|
1309
1368
|
console.log('⚠️ External ping FAILED (skipped via --skip-verify).');
|
|
1310
1369
|
}
|
|
1311
1370
|
}
|
|
@@ -1453,15 +1512,14 @@ https://github.com/onthegonow/a2a_calling`;
|
|
|
1453
1512
|
|
|
1454
1513
|
const contextFiles = readContextFiles(workspaceDir);
|
|
1455
1514
|
// Print what was found
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
};
|
|
1515
|
+
const sources = {
|
|
1516
|
+
'USER.md': contextFiles.user,
|
|
1517
|
+
'HEARTBEAT.md': contextFiles.heartbeat,
|
|
1518
|
+
'SOUL.md': contextFiles.soul,
|
|
1519
|
+
'SKILL.md': contextFiles.skill,
|
|
1520
|
+
'CLAUDE.md': contextFiles.claude,
|
|
1521
|
+
'memory/*.md': contextFiles.memory
|
|
1522
|
+
};
|
|
1465
1523
|
for (const [name, content] of Object.entries(sources)) {
|
|
1466
1524
|
console.log(` ${content ? '\u2705' : '\u274c'} ${name}`);
|
|
1467
1525
|
}
|
|
@@ -1537,14 +1595,16 @@ Server:
|
|
|
1537
1595
|
server Start the A2A server
|
|
1538
1596
|
--port, -p Port to listen on (default: 3001)
|
|
1539
1597
|
|
|
1540
|
-
quickstart
|
|
1598
|
+
quickstart Onboarding (access → tiers → ingress → verify)
|
|
1541
1599
|
--hostname Public hostname for remote access (e.g. myserver.com:443)
|
|
1542
1600
|
--public-port Port to assume when --hostname omits a port (default: 443)
|
|
1543
1601
|
--port A2A server port to run locally (default: 3001)
|
|
1544
|
-
--
|
|
1602
|
+
--friends-topics Override Friends tier topics/interests (comma or newline-separated)
|
|
1603
|
+
--interactive Prompt for Friends tier topics if needed
|
|
1545
1604
|
--confirm-ingress Confirm reverse proxy/ingress is configured and continue
|
|
1546
1605
|
--skip-verify Skip external reachability check (not recommended)
|
|
1547
|
-
--force Reset onboarding
|
|
1606
|
+
--force Reset onboarding + regenerate disclosure manifest
|
|
1607
|
+
--regen-manifest Regenerate disclosure manifest (no onboarding reset)
|
|
1548
1608
|
|
|
1549
1609
|
onboard Generate disclosure manifest from workspace context
|
|
1550
1610
|
--force Re-run even if already onboarded
|
package/package.json
CHANGED
package/src/lib/config.js
CHANGED
|
@@ -15,6 +15,140 @@ const CONFIG_DIR = process.env.A2A_CONFIG_DIR ||
|
|
|
15
15
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'a2a-config.json');
|
|
16
16
|
const logger = createLogger({ component: 'a2a.config' });
|
|
17
17
|
|
|
18
|
+
function isPlainObject(value) {
|
|
19
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function configValidationError(code, message, hint, data) {
|
|
23
|
+
const err = new Error(message);
|
|
24
|
+
err.code = code;
|
|
25
|
+
if (hint) err.hint = hint;
|
|
26
|
+
if (data) err.data = data;
|
|
27
|
+
return err;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sanitizeString(value, maxLength = 200) {
|
|
31
|
+
return String(value || '')
|
|
32
|
+
.replace(/\s+/g, ' ')
|
|
33
|
+
.trim()
|
|
34
|
+
.slice(0, maxLength);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function validateStringArray(value, label, options = {}) {
|
|
38
|
+
const maxItems = Number.isFinite(options.maxItems) ? options.maxItems : 200;
|
|
39
|
+
const itemMaxLength = Number.isFinite(options.itemMaxLength) ? options.itemMaxLength : 160;
|
|
40
|
+
|
|
41
|
+
if (!Array.isArray(value)) {
|
|
42
|
+
throw configValidationError(
|
|
43
|
+
'A2A_CONFIG_INVALID_ARRAY',
|
|
44
|
+
`Invalid ${label}: expected an array of strings`,
|
|
45
|
+
'Pass an array like ["chat","search"].',
|
|
46
|
+
{ label, received_type: typeof value }
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const unique = [];
|
|
51
|
+
const seen = new Set();
|
|
52
|
+
for (const entry of value) {
|
|
53
|
+
if (typeof entry !== 'string') {
|
|
54
|
+
throw configValidationError(
|
|
55
|
+
'A2A_CONFIG_INVALID_ARRAY_ITEM',
|
|
56
|
+
`Invalid ${label}: each item must be a string`,
|
|
57
|
+
'Ensure your tier topics/goals are string arrays.',
|
|
58
|
+
{ label, received_item_type: typeof entry }
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
const cleaned = sanitizeString(entry, itemMaxLength);
|
|
62
|
+
if (!cleaned) continue;
|
|
63
|
+
const key = cleaned.toLowerCase();
|
|
64
|
+
if (seen.has(key)) continue;
|
|
65
|
+
seen.add(key);
|
|
66
|
+
unique.push(cleaned);
|
|
67
|
+
if (unique.length >= maxItems) break;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return unique;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function validateTierPatch(tierName, tierConfig) {
|
|
74
|
+
if (!isPlainObject(tierConfig)) {
|
|
75
|
+
throw configValidationError(
|
|
76
|
+
'A2A_CONFIG_INVALID_TIER_PATCH',
|
|
77
|
+
`Invalid tier config for "${tierName}": expected an object`,
|
|
78
|
+
'Pass an object like { topics: [...], goals: [...] }.',
|
|
79
|
+
{ tier: tierName, received_type: typeof tierConfig }
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const out = {};
|
|
84
|
+
|
|
85
|
+
if (tierConfig.name !== undefined) {
|
|
86
|
+
if (typeof tierConfig.name !== 'string') {
|
|
87
|
+
throw configValidationError(
|
|
88
|
+
'A2A_CONFIG_INVALID_TIER_NAME',
|
|
89
|
+
`Invalid tier name for "${tierName}": expected string`,
|
|
90
|
+
null,
|
|
91
|
+
{ tier: tierName, received_type: typeof tierConfig.name }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
out.name = sanitizeString(tierConfig.name, 120);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (tierConfig.description !== undefined) {
|
|
98
|
+
if (typeof tierConfig.description !== 'string') {
|
|
99
|
+
throw configValidationError(
|
|
100
|
+
'A2A_CONFIG_INVALID_TIER_DESCRIPTION',
|
|
101
|
+
`Invalid tier description for "${tierName}": expected string`,
|
|
102
|
+
null,
|
|
103
|
+
{ tier: tierName, received_type: typeof tierConfig.description }
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
out.description = sanitizeString(tierConfig.description, 300);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (tierConfig.disclosure !== undefined) {
|
|
110
|
+
if (typeof tierConfig.disclosure !== 'string') {
|
|
111
|
+
throw configValidationError(
|
|
112
|
+
'A2A_CONFIG_INVALID_TIER_DISCLOSURE',
|
|
113
|
+
`Invalid tier disclosure for "${tierName}": expected string`,
|
|
114
|
+
null,
|
|
115
|
+
{ tier: tierName, received_type: typeof tierConfig.disclosure }
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
out.disclosure = sanitizeString(tierConfig.disclosure, 40) || 'minimal';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (tierConfig.capabilities !== undefined) {
|
|
122
|
+
out.capabilities = validateStringArray(tierConfig.capabilities, `${tierName}.capabilities`, {
|
|
123
|
+
maxItems: 100,
|
|
124
|
+
itemMaxLength: 120
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (tierConfig.topics !== undefined) {
|
|
129
|
+
out.topics = validateStringArray(tierConfig.topics, `${tierName}.topics`, {
|
|
130
|
+
maxItems: 200,
|
|
131
|
+
itemMaxLength: 160
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (tierConfig.goals !== undefined) {
|
|
136
|
+
out.goals = validateStringArray(tierConfig.goals, `${tierName}.goals`, {
|
|
137
|
+
maxItems: 200,
|
|
138
|
+
itemMaxLength: 160
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (tierConfig.examples !== undefined) {
|
|
143
|
+
out.examples = validateStringArray(tierConfig.examples, `${tierName}.examples`, {
|
|
144
|
+
maxItems: 20,
|
|
145
|
+
itemMaxLength: 120
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
18
152
|
function deepMerge(base, override) {
|
|
19
153
|
const baseIsObject = base && typeof base === 'object' && !Array.isArray(base);
|
|
20
154
|
const overrideIsObject = override && typeof override === 'object' && !Array.isArray(override);
|
|
@@ -206,7 +340,19 @@ class A2AConfig {
|
|
|
206
340
|
}
|
|
207
341
|
|
|
208
342
|
setTier(tierName, tierConfig) {
|
|
209
|
-
|
|
343
|
+
const id = String(tierName || '').trim();
|
|
344
|
+
if (!id) {
|
|
345
|
+
throw configValidationError(
|
|
346
|
+
'A2A_CONFIG_INVALID_TIER_ID',
|
|
347
|
+
'Tier name is required',
|
|
348
|
+
'Use one of: public|friends|family|custom (or a non-empty custom tier id).'
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
const patch = validateTierPatch(id, tierConfig);
|
|
353
|
+
this.config.tiers = isPlainObject(this.config.tiers) ? this.config.tiers : {};
|
|
354
|
+
const existing = isPlainObject(this.config.tiers[id]) ? this.config.tiers[id] : {};
|
|
355
|
+
this.config.tiers[id] = { ...existing, ...patch };
|
|
210
356
|
this._save();
|
|
211
357
|
}
|
|
212
358
|
|
package/src/lib/conversations.js
CHANGED
|
@@ -47,6 +47,7 @@ class ConversationStore {
|
|
|
47
47
|
// Best effort - ignore on platforms without chmod support.
|
|
48
48
|
}
|
|
49
49
|
this._migrate();
|
|
50
|
+
this._ensureLatestSchema(Database);
|
|
50
51
|
return this.db;
|
|
51
52
|
} catch (err) {
|
|
52
53
|
if (err.code === 'MODULE_NOT_FOUND') {
|
|
@@ -128,6 +129,79 @@ class ConversationStore {
|
|
|
128
129
|
`);
|
|
129
130
|
}
|
|
130
131
|
|
|
132
|
+
_ensureLatestSchema(Database) {
|
|
133
|
+
// Prototype-mode stance: do not attempt in-place migrations.
|
|
134
|
+
// If schema is missing required columns, back up and recreate the DB.
|
|
135
|
+
try {
|
|
136
|
+
const info = this.db.prepare('PRAGMA table_info(conversations)').all();
|
|
137
|
+
if (!Array.isArray(info) || info.length === 0) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
const cols = new Set(info.map(row => row && row.name).filter(Boolean));
|
|
141
|
+
const required = [
|
|
142
|
+
'joint_action_items',
|
|
143
|
+
'collaboration_opportunity'
|
|
144
|
+
];
|
|
145
|
+
const missing = required.filter(c => !cols.has(c));
|
|
146
|
+
if (missing.length === 0) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const backupPath = `${this.dbPath}.bak.${Date.now()}`;
|
|
151
|
+
logger.warn('Conversation DB schema mismatch; resetting to latest schema', {
|
|
152
|
+
event: 'conversation_db_schema_reset',
|
|
153
|
+
data: {
|
|
154
|
+
db_path: this.dbPath,
|
|
155
|
+
backup_path: backupPath,
|
|
156
|
+
missing_columns: missing
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
try { this.db.close(); } catch (_) {}
|
|
161
|
+
this.db = null;
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
fs.renameSync(this.dbPath, backupPath);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
try {
|
|
167
|
+
fs.copyFileSync(this.dbPath, backupPath);
|
|
168
|
+
fs.unlinkSync(this.dbPath);
|
|
169
|
+
} catch (err2) {
|
|
170
|
+
// If we can't move the old DB out of the way, keep going without resetting.
|
|
171
|
+
logger.error('Failed to back up conversations DB for schema reset', {
|
|
172
|
+
event: 'conversation_db_schema_reset_backup_failed',
|
|
173
|
+
error: err2,
|
|
174
|
+
error_code: 'CONVERSATION_DB_SCHEMA_RESET_BACKUP_FAILED',
|
|
175
|
+
hint: 'Check file permissions on a2a-conversations.db and ensure the process can rename/unlink it.',
|
|
176
|
+
data: {
|
|
177
|
+
db_path: this.dbPath,
|
|
178
|
+
backup_path: backupPath
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
const reopen = new Database(this.dbPath);
|
|
182
|
+
this.db = reopen;
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this.db = new Database(this.dbPath);
|
|
188
|
+
try {
|
|
189
|
+
fs.chmodSync(this.dbPath, 0o600);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
// Best effort.
|
|
192
|
+
}
|
|
193
|
+
this._migrate();
|
|
194
|
+
} catch (err) {
|
|
195
|
+
// Best effort: leave existing DB in place if schema validation fails unexpectedly.
|
|
196
|
+
logger.error('Conversation DB schema validation failed', {
|
|
197
|
+
event: 'conversation_db_schema_validation_failed',
|
|
198
|
+
error: err,
|
|
199
|
+
error_code: 'CONVERSATION_DB_SCHEMA_VALIDATION_FAILED',
|
|
200
|
+
hint: 'If this persists, delete ~/.config/openclaw/a2a-conversations.db (prototype mode) and restart.'
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
131
205
|
/**
|
|
132
206
|
* Generate a conversation ID (shared between both agents)
|
|
133
207
|
*/
|
package/src/lib/disclosure.js
CHANGED
|
@@ -146,7 +146,7 @@ function generateDefaultManifest(contextFiles = {}) {
|
|
|
146
146
|
const soulContent = contextFiles.soul || '';
|
|
147
147
|
|
|
148
148
|
const hasContent = userContent || heartbeatContent || soulContent ||
|
|
149
|
-
contextFiles.
|
|
149
|
+
contextFiles.memory || contextFiles.claude;
|
|
150
150
|
|
|
151
151
|
if (!hasContent) {
|
|
152
152
|
// Minimal starter manifest
|
|
@@ -263,21 +263,6 @@ function generateDefaultManifest(contextFiles = {}) {
|
|
|
263
263
|
}
|
|
264
264
|
}
|
|
265
265
|
|
|
266
|
-
// Extract capability keywords from SKILL.md and installed skills
|
|
267
|
-
const skillContent = (contextFiles.skill || '') + '\n' + (contextFiles.skills || '');
|
|
268
|
-
if (skillContent.trim()) {
|
|
269
|
-
const capabilityLines = skillContent
|
|
270
|
-
.split('\n')
|
|
271
|
-
.filter(l => l.trim().startsWith('-') || l.trim().startsWith('*'))
|
|
272
|
-
.map(l => l.replace(/^[\s\-\*]+/, '').trim())
|
|
273
|
-
.filter(l => l.length > 5 && l.length < 120)
|
|
274
|
-
.slice(0, 6);
|
|
275
|
-
|
|
276
|
-
capabilityLines.forEach(cap => {
|
|
277
|
-
manifest.topics.public.discuss_freely.push({ topic: cap.slice(0, 60), detail: cap });
|
|
278
|
-
});
|
|
279
|
-
}
|
|
280
|
-
|
|
281
266
|
// Extract topic keywords from memory files
|
|
282
267
|
if (contextFiles.memory) {
|
|
283
268
|
const memoryLines = contextFiles.memory
|
|
@@ -350,7 +335,8 @@ function readContextFiles(workspaceDir) {
|
|
|
350
335
|
heartbeat: read('HEARTBEAT.md'),
|
|
351
336
|
soul: read('SOUL.md'),
|
|
352
337
|
skill: read('SKILL.md'),
|
|
353
|
-
claude: read('CLAUDE.md')
|
|
338
|
+
claude: read('CLAUDE.md'),
|
|
339
|
+
skills: ''
|
|
354
340
|
};
|
|
355
341
|
|
|
356
342
|
// Scan memory/*.md
|
|
@@ -366,32 +352,6 @@ function readContextFiles(workspaceDir) {
|
|
|
366
352
|
} catch (e) {}
|
|
367
353
|
}
|
|
368
354
|
|
|
369
|
-
// Scan installed skills from both OpenClaw and standalone paths
|
|
370
|
-
const homeDir = process.env.HOME || '/tmp';
|
|
371
|
-
const skillsDirs = [
|
|
372
|
-
process.env.OPENCLAW_SKILLS || path.join(homeDir, '.openclaw', 'skills'),
|
|
373
|
-
path.join(CONFIG_DIR, 'skills')
|
|
374
|
-
];
|
|
375
|
-
const skillFragments = [];
|
|
376
|
-
const seenSkillDirs = new Set();
|
|
377
|
-
for (const skillsDir of skillsDirs) {
|
|
378
|
-
if (!fs.existsSync(skillsDir) || seenSkillDirs.has(skillsDir)) continue;
|
|
379
|
-
seenSkillDirs.add(skillsDir);
|
|
380
|
-
try {
|
|
381
|
-
const dirs = fs.readdirSync(skillsDir).filter(d => {
|
|
382
|
-
try { return fs.statSync(path.join(skillsDir, d)).isDirectory(); }
|
|
383
|
-
catch (e) { return false; }
|
|
384
|
-
});
|
|
385
|
-
for (const d of dirs) {
|
|
386
|
-
try {
|
|
387
|
-
const content = fs.readFileSync(path.join(skillsDir, d, 'SKILL.md'), 'utf8');
|
|
388
|
-
if (content) skillFragments.push(content);
|
|
389
|
-
} catch (e) {}
|
|
390
|
-
}
|
|
391
|
-
} catch (e) {}
|
|
392
|
-
}
|
|
393
|
-
result.skills = skillFragments.join('\n---\n');
|
|
394
|
-
|
|
395
355
|
return result;
|
|
396
356
|
}
|
|
397
357
|
|
package/src/routes/dashboard.js
CHANGED
|
@@ -1069,7 +1069,16 @@ function createDashboardApiRouter(options = {}) {
|
|
|
1069
1069
|
if (body.topics !== undefined) update.topics = sanitizeStringArray(body.topics, 200, 160);
|
|
1070
1070
|
if (body.goals !== undefined) update.goals = sanitizeStringArray(body.goals, 200, 160);
|
|
1071
1071
|
|
|
1072
|
-
|
|
1072
|
+
try {
|
|
1073
|
+
context.config.setTier(tierId, update);
|
|
1074
|
+
} catch (err) {
|
|
1075
|
+
return res.status(400).json({
|
|
1076
|
+
success: false,
|
|
1077
|
+
error: 'invalid_tier_config',
|
|
1078
|
+
code: err.code || 'A2A_CONFIG_INVALID_TIER_CONFIG',
|
|
1079
|
+
message: err.message
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1073
1082
|
|
|
1074
1083
|
if (body.manifest) {
|
|
1075
1084
|
const manifest = loadManifest();
|
|
@@ -1099,17 +1108,35 @@ function createDashboardApiRouter(options = {}) {
|
|
|
1099
1108
|
|
|
1100
1109
|
const copyFrom = normalizeTierId(body.copy_from || '');
|
|
1101
1110
|
if (copyFrom && cfg.tiers && cfg.tiers[copyFrom]) {
|
|
1102
|
-
|
|
1111
|
+
try {
|
|
1112
|
+
context.config.setTier(tierId, { ...cfg.tiers[copyFrom] });
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
return res.status(400).json({
|
|
1115
|
+
success: false,
|
|
1116
|
+
error: 'invalid_tier_config',
|
|
1117
|
+
code: err.code || 'A2A_CONFIG_INVALID_TIER_CONFIG',
|
|
1118
|
+
message: err.message
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1103
1121
|
} else {
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1122
|
+
try {
|
|
1123
|
+
context.config.setTier(tierId, {
|
|
1124
|
+
name: sanitizeString(body.name || tierId, 120),
|
|
1125
|
+
description: sanitizeString(body.description || 'Custom tier', 300),
|
|
1126
|
+
capabilities: sanitizeStringArray(body.capabilities || []),
|
|
1127
|
+
topics: sanitizeStringArray(body.topics || []),
|
|
1128
|
+
goals: sanitizeStringArray(body.goals || []),
|
|
1129
|
+
disclosure: sanitizeString(body.disclosure || 'minimal', 40),
|
|
1130
|
+
examples: sanitizeStringArray(body.examples || [], 20, 120)
|
|
1131
|
+
});
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
return res.status(400).json({
|
|
1134
|
+
success: false,
|
|
1135
|
+
error: 'invalid_tier_config',
|
|
1136
|
+
code: err.code || 'A2A_CONFIG_INVALID_TIER_CONFIG',
|
|
1137
|
+
message: err.message
|
|
1138
|
+
});
|
|
1139
|
+
}
|
|
1113
1140
|
}
|
|
1114
1141
|
|
|
1115
1142
|
return res.json({ success: true, tier_id: tierId });
|
|
@@ -1127,7 +1154,16 @@ function createDashboardApiRouter(options = {}) {
|
|
|
1127
1154
|
return res.status(404).json({ success: false, error: 'source_tier_not_found' });
|
|
1128
1155
|
}
|
|
1129
1156
|
|
|
1130
|
-
|
|
1157
|
+
try {
|
|
1158
|
+
context.config.setTier(toTier, { ...cfg.tiers[fromTier] });
|
|
1159
|
+
} catch (err) {
|
|
1160
|
+
return res.status(400).json({
|
|
1161
|
+
success: false,
|
|
1162
|
+
error: 'invalid_tier_config',
|
|
1163
|
+
code: err.code || 'A2A_CONFIG_INVALID_TIER_CONFIG',
|
|
1164
|
+
message: err.message
|
|
1165
|
+
});
|
|
1166
|
+
}
|
|
1131
1167
|
|
|
1132
1168
|
const manifest = loadManifest();
|
|
1133
1169
|
if (manifest.topics && manifest.topics[fromTier]) {
|