@aiassesstech/nole 0.3.6 → 0.4.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.
Files changed (39) hide show
  1. package/agent/AGENTS.md +34 -0
  2. package/dist/governance/types.d.ts.map +1 -1
  3. package/dist/governance/types.js +2 -0
  4. package/dist/governance/types.js.map +1 -1
  5. package/dist/plugin.d.ts.map +1 -1
  6. package/dist/plugin.js +686 -20
  7. package/dist/plugin.js.map +1 -1
  8. package/dist/social/linkedin-drafter.d.ts +16 -0
  9. package/dist/social/linkedin-drafter.d.ts.map +1 -0
  10. package/dist/social/linkedin-drafter.js +66 -0
  11. package/dist/social/linkedin-drafter.js.map +1 -0
  12. package/dist/social/moltbook-client.d.ts +28 -0
  13. package/dist/social/moltbook-client.d.ts.map +1 -0
  14. package/dist/social/moltbook-client.js +97 -0
  15. package/dist/social/moltbook-client.js.map +1 -0
  16. package/dist/social/rate-limiter.d.ts +22 -0
  17. package/dist/social/rate-limiter.d.ts.map +1 -0
  18. package/dist/social/rate-limiter.js +67 -0
  19. package/dist/social/rate-limiter.js.map +1 -0
  20. package/dist/social/types.d.ts +99 -0
  21. package/dist/social/types.d.ts.map +1 -0
  22. package/dist/social/types.js +6 -0
  23. package/dist/social/types.js.map +1 -0
  24. package/dist/social/x-client.d.ts +33 -0
  25. package/dist/social/x-client.d.ts.map +1 -0
  26. package/dist/social/x-client.js +167 -0
  27. package/dist/social/x-client.js.map +1 -0
  28. package/dist/store/json-store.d.ts +3 -0
  29. package/dist/store/json-store.d.ts.map +1 -1
  30. package/dist/store/json-store.js +24 -0
  31. package/dist/store/json-store.js.map +1 -1
  32. package/dist/store/types.d.ts +3 -0
  33. package/dist/store/types.d.ts.map +1 -1
  34. package/dist/types/nole-config.d.ts +24 -0
  35. package/dist/types/nole-config.d.ts.map +1 -1
  36. package/dist/types/nole-config.js +9 -0
  37. package/dist/types/nole-config.js.map +1 -1
  38. package/openclaw.plugin.json +36 -0
  39. package/package.json +1 -1
package/dist/plugin.js CHANGED
@@ -11,6 +11,10 @@ import { parseNoleConfig } from './types/nole-config.js';
11
11
  import { NOLE_IDENTITY } from './types/identity.js';
12
12
  import { PlatformApiClient } from './compsi/api-client.js';
13
13
  import { createWalletAdapter, MockWalletAdapter } from './wallet/index.js';
14
+ import { MoltBookClient } from './social/moltbook-client.js';
15
+ import { XClient } from './social/x-client.js';
16
+ import { LinkedInDrafter } from './social/linkedin-drafter.js';
17
+ import { createMoltBookLimiter, createXLimiter } from './social/rate-limiter.js';
14
18
  /**
15
19
  * OpenClaw plugin entry point — export default function register(api)
16
20
  *
@@ -101,6 +105,148 @@ export default function register(api) {
101
105
  console.error(`[nole] Wallet init failed (using mock): ${errMsg}`);
102
106
  }
103
107
  })();
108
+ // ── Social Media Clients (nullable — checked at call time per rule 155) ──
109
+ const moltbookLimiter = createMoltBookLimiter();
110
+ const xLimiter = createXLimiter();
111
+ const moltbook = config.moltbookApiKey
112
+ ? new MoltBookClient({ apiKey: config.moltbookApiKey, baseUrl: config.moltbookBaseUrl }, moltbookLimiter)
113
+ : null;
114
+ const xClient = config.xApiKey && config.xApiSecret && config.xAccessToken && config.xAccessTokenSecret
115
+ ? new XClient({
116
+ apiKey: config.xApiKey,
117
+ apiSecret: config.xApiSecret,
118
+ accessToken: config.xAccessToken,
119
+ accessTokenSecret: config.xAccessTokenSecret,
120
+ handle: config.xHandle,
121
+ }, xLimiter)
122
+ : null;
123
+ const linkedInDrafter = new LinkedInDrafter(dataDir);
124
+ linkedInDrafter.initialize().catch((err) => {
125
+ console.warn(`[nole] LinkedIn drafter init warning: ${err.message}`);
126
+ });
127
+ const generateSocialId = () => `SOC-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
128
+ let socialShield = null;
129
+ let outputSanitizer = null;
130
+ let fleetShieldMiddleware = null;
131
+ let validatedToolFactory = null;
132
+ let validationSchemas = {};
133
+ const promptShieldMod = '@aiassesstech/prompt-shield';
134
+ import(promptShieldMod)
135
+ .then(({ PromptShield, OutputSanitizer, createSocialContentShield, createFleetShieldMiddleware, createValidatedToolHandler, nole_wallet_fund, nole_propose, nole_recruit, nole_moltbook_post, nole_x_post, nole_x_thread, NOLE_PROPOSE_BUSINESS_RULES, }) => {
136
+ const shield = new PromptShield({ sensitivity: 'high' });
137
+ socialShield = createSocialContentShield({
138
+ shield,
139
+ platform: 'moltbook',
140
+ blockInjectedContent: true,
141
+ });
142
+ outputSanitizer = new OutputSanitizer({ enabled: true });
143
+ fleetShieldMiddleware = createFleetShieldMiddleware({ shield });
144
+ validatedToolFactory = createValidatedToolHandler;
145
+ validationSchemas = {
146
+ nole_wallet_fund,
147
+ nole_propose,
148
+ nole_recruit,
149
+ nole_moltbook_post,
150
+ nole_x_post,
151
+ nole_x_thread,
152
+ NOLE_PROPOSE_BUSINESS_RULES,
153
+ };
154
+ console.log('[nole] PromptShield social scanner + output sanitizer + behavioral validation: active');
155
+ })
156
+ .catch(() => {
157
+ console.log('[nole] PromptShield unavailable — social content scanner disabled');
158
+ });
159
+ if (moltbook)
160
+ console.log('[nole] MoltBook: configured');
161
+ else
162
+ console.log('[nole] MoltBook: not configured — set moltbookApiKey');
163
+ if (xClient)
164
+ console.log('[nole] X/Twitter: configured');
165
+ else
166
+ console.log('[nole] X/Twitter: not configured — set xApiKey + xApiSecret + xAccessToken + xAccessTokenSecret');
167
+ console.log('[nole] LinkedIn: draft mode (no API — Greg publishes manually)');
168
+ console.log(`[nole] Social posting mode: ${config.socialPostingMode}`);
169
+ // Callbacks stored for pending social posts (executed when Commander approves)
170
+ const pendingSocialCallbacks = new Map();
171
+ const validationConfig = {
172
+ enabled: true,
173
+ auditFailures: true,
174
+ retryOnFailure: true,
175
+ maxRetries: 2,
176
+ strictMode: false,
177
+ };
178
+ function validationErrorResponse(message) {
179
+ return {
180
+ content: [{ type: "text", text: message }],
181
+ isError: true,
182
+ };
183
+ }
184
+ async function validateToolParams(toolName, toolCallId, params, businessRules) {
185
+ if (!validatedToolFactory)
186
+ return { ok: true, params };
187
+ const schema = validationSchemas[toolName];
188
+ if (!schema)
189
+ return { ok: true, params };
190
+ const passthrough = async (p) => p;
191
+ const wrapped = validatedToolFactory(passthrough, schema, String(toolName), config.agentId, validationConfig, businessRules);
192
+ const result = await wrapped(params, {
193
+ sessionId: toolCallId,
194
+ sourceAgent: "unknown",
195
+ trustLevel: 2,
196
+ });
197
+ if (typeof result === "string") {
198
+ return { ok: false, message: result };
199
+ }
200
+ return { ok: true, params: result };
201
+ }
202
+ async function governedSocialPost(payload) {
203
+ if (socialShield) {
204
+ const scan = socialShield(payload.description);
205
+ if (!scan.allowed) {
206
+ return {
207
+ mode: 'supervised',
208
+ outcome: 'rejected',
209
+ proposalId: `shield-block-${Date.now()}`,
210
+ reason: scan.reason ?? `PromptShield blocked social content (${scan.result.severity})`,
211
+ };
212
+ }
213
+ }
214
+ if (outputSanitizer) {
215
+ const sanitized = outputSanitizer.sanitize(payload.content, config.agentId);
216
+ if (sanitized.redacted || sanitized.canaryLeaked) {
217
+ return {
218
+ mode: 'supervised',
219
+ outcome: 'rejected',
220
+ proposalId: `sanitize-block-${Date.now()}`,
221
+ reason: sanitized.canaryLeaked
222
+ ? 'Output sanitizer blocked canary token leakage attempt.'
223
+ : `Output sanitizer blocked secret leakage (${sanitized.redactionCount} redaction${sanitized.redactionCount === 1 ? '' : 's'}).`,
224
+ };
225
+ }
226
+ }
227
+ if (config.socialPostingMode === 'autonomous') {
228
+ return { mode: 'autonomous', result: await payload.executeFn() };
229
+ }
230
+ const result = await governance.propose({
231
+ actionType: 'social',
232
+ description: payload.description,
233
+ riskLevel: 'low',
234
+ metadata: { platform: payload.platform, action: payload.action },
235
+ });
236
+ if (result.finalOutcome === 'executed') {
237
+ return { mode: 'supervised', outcome: 'approved', result: await payload.executeFn() };
238
+ }
239
+ if (result.finalOutcome === 'pending') {
240
+ pendingSocialCallbacks.set(result.proposalId, payload.executeFn);
241
+ return { mode: 'supervised', outcome: 'pending_review', proposalId: result.proposalId };
242
+ }
243
+ return {
244
+ mode: 'supervised',
245
+ outcome: 'rejected',
246
+ proposalId: result.proposalId,
247
+ reason: result.vetoExplanation ?? 'Governance rejected',
248
+ };
249
+ }
104
250
  // Initialize store on startup (eager, not lazy)
105
251
  store.initialize().then(() => {
106
252
  console.log('[nole] Data store initialized');
@@ -233,12 +379,16 @@ export default function register(api) {
233
379
  },
234
380
  async execute(_toolCallId, params) {
235
381
  try {
382
+ const validated = await validateToolParams("nole_propose", _toolCallId, params, validationSchemas.NOLE_PROPOSE_BUSINESS_RULES);
383
+ if (!validated.ok)
384
+ return validationErrorResponse(validated.message);
385
+ const safeParams = validated.params;
236
386
  const result = await governance.propose({
237
- actionType: params.actionType, // eslint-disable-line @typescript-eslint/no-explicit-any
238
- description: params.description,
239
- estimatedCostUsd: params.estimatedCostUsd,
240
- targetAgent: params.targetAgent,
241
- platform: params.platform,
387
+ actionType: safeParams.actionType, // eslint-disable-line @typescript-eslint/no-explicit-any
388
+ description: safeParams.description,
389
+ estimatedCostUsd: safeParams.estimatedCostUsd,
390
+ targetAgent: safeParams.targetAgent,
391
+ platform: safeParams.platform,
242
392
  });
243
393
  return {
244
394
  content: [{
@@ -363,6 +513,10 @@ export default function register(api) {
363
513
  required: ["token"],
364
514
  },
365
515
  async execute(_toolCallId, params) {
516
+ const validated = await validateToolParams("nole_wallet_fund", _toolCallId, params);
517
+ if (!validated.ok)
518
+ return validationErrorResponse(validated.message);
519
+ const safeParams = validated.params;
366
520
  if (wallet.network !== 'base-sepolia') {
367
521
  return {
368
522
  content: [{
@@ -385,7 +539,7 @@ export default function register(api) {
385
539
  }
386
540
  const results = {};
387
541
  const coinbaseWallet = wallet;
388
- const tokens = params.token === 'both' ? ['eth', 'usdc'] : [params.token];
542
+ const tokens = safeParams.token === 'both' ? ['eth', 'usdc'] : [safeParams.token];
389
543
  for (const t of tokens) {
390
544
  try {
391
545
  const txHash = await coinbaseWallet.requestTestFunds(t);
@@ -702,6 +856,10 @@ export default function register(api) {
702
856
  required: ["targetWallet", "agentName", "agentPlatform"],
703
857
  },
704
858
  async execute(_toolCallId, params) {
859
+ const validated = await validateToolParams("nole_recruit", _toolCallId, params);
860
+ if (!validated.ok)
861
+ return validationErrorResponse(validated.message);
862
+ const safeParams = validated.params;
705
863
  if (!platform.isConfigured) {
706
864
  return {
707
865
  content: [{
@@ -715,12 +873,12 @@ export default function register(api) {
715
873
  };
716
874
  }
717
875
  const result = await platform.subscribe({
718
- walletAddress: params.targetWallet,
719
- tier: params.tier || "SCOUT",
720
- agentName: params.agentName,
721
- agentPlatform: params.agentPlatform,
876
+ walletAddress: safeParams.targetWallet,
877
+ tier: safeParams.tier || "SCOUT",
878
+ agentName: safeParams.agentName,
879
+ agentPlatform: safeParams.agentPlatform,
722
880
  recruiterWallet: config.platformWalletAddress,
723
- referralCode: params.referralCode,
881
+ referralCode: safeParams.referralCode,
724
882
  });
725
883
  if (!result.ok) {
726
884
  return {
@@ -730,7 +888,7 @@ export default function register(api) {
730
888
  status: "recruitment_failed",
731
889
  error: result.error,
732
890
  httpStatus: result.status,
733
- target: params.agentName,
891
+ target: safeParams.agentName,
734
892
  }, null, 2),
735
893
  }],
736
894
  };
@@ -740,10 +898,10 @@ export default function register(api) {
740
898
  type: "text",
741
899
  text: JSON.stringify({
742
900
  status: "recruited",
743
- agent: params.agentName,
744
- wallet: params.targetWallet,
745
- tier: params.tier || "SCOUT",
746
- platform: params.agentPlatform,
901
+ agent: safeParams.agentName,
902
+ wallet: safeParams.targetWallet,
903
+ tier: safeParams.tier || "SCOUT",
904
+ platform: safeParams.agentPlatform,
747
905
  subscription: result.data,
748
906
  note: "Agent has been subscribed to the AIAssessTech Trust Alliance. They will receive an API key for assessments.",
749
907
  }, null, 2),
@@ -985,6 +1143,35 @@ export default function register(api) {
985
1143
  const callingAgentId = "unknown";
986
1144
  try {
987
1145
  const result = await governance.commanderApprove(params.proposalId, callingAgentId, params.note);
1146
+ // Execute pending social post callback if this was a social-post proposal
1147
+ let socialPostResult;
1148
+ const socialCallback = pendingSocialCallbacks.get(params.proposalId);
1149
+ if (socialCallback) {
1150
+ try {
1151
+ const callbackResult = await socialCallback();
1152
+ pendingSocialCallbacks.delete(params.proposalId);
1153
+ socialPostResult = {
1154
+ socialPostExecuted: true,
1155
+ postSuccess: callbackResult.ok,
1156
+ postError: callbackResult.error,
1157
+ };
1158
+ if (callbackResult.ok) {
1159
+ const interaction = {
1160
+ id: generateSocialId(),
1161
+ platform: result.proposal.metadata?.platform ?? 'moltbook',
1162
+ action: 'post',
1163
+ contentPreview: result.proposal.description.slice(0, 100),
1164
+ timestamp: new Date().toISOString(),
1165
+ success: true,
1166
+ };
1167
+ store.saveSocialInteraction(interaction).catch(() => { });
1168
+ }
1169
+ }
1170
+ catch (cbErr) {
1171
+ const cbMsg = cbErr instanceof Error ? cbErr.message : String(cbErr);
1172
+ socialPostResult = { socialPostExecuted: false, socialPostError: cbMsg };
1173
+ }
1174
+ }
988
1175
  return {
989
1176
  content: [{
990
1177
  type: "text",
@@ -995,6 +1182,7 @@ export default function register(api) {
995
1182
  description: result.proposal.description,
996
1183
  approvedBy: callingAgentId,
997
1184
  auditHash: result.auditHash,
1185
+ ...socialPostResult,
998
1186
  }, null, 2),
999
1187
  }],
1000
1188
  };
@@ -1145,6 +1333,428 @@ export default function register(api) {
1145
1333
  };
1146
1334
  },
1147
1335
  });
1336
+ // ═══════════════════════════════════════════════════════
1337
+ // SOCIAL MEDIA TOOLS (MoltBook, X/Twitter, LinkedIn)
1338
+ // All registered unconditionally per rule 155.
1339
+ // Client availability checked at call time.
1340
+ // ═══════════════════════════════════════════════════════
1341
+ api.registerTool({
1342
+ name: "nole_moltbook_post",
1343
+ description: "Post content to a MoltBook submolt. Nole uses this to evangelize AI governance " +
1344
+ "and recruit agents to the Trust Alliance. Requires moltbookApiKey in config.",
1345
+ parameters: {
1346
+ type: "object",
1347
+ properties: {
1348
+ submolt: {
1349
+ type: "string",
1350
+ description: "Target submolt (e.g., 'governance', 'aithoughts', 'ethics', 'trust')",
1351
+ },
1352
+ title: {
1353
+ type: "string",
1354
+ description: "Post title — concise, engaging, trust-focused",
1355
+ },
1356
+ content: {
1357
+ type: "string",
1358
+ description: "Post body — informative, not spammy. Include soft CTA for Grillo assessment.",
1359
+ },
1360
+ },
1361
+ required: ["submolt", "title", "content"],
1362
+ },
1363
+ async execute(_toolCallId, params) {
1364
+ const validated = await validateToolParams("nole_moltbook_post", _toolCallId, params);
1365
+ if (!validated.ok)
1366
+ return validationErrorResponse(validated.message);
1367
+ const safeParams = validated.params;
1368
+ if (!moltbook) {
1369
+ return {
1370
+ content: [{ type: "text", text: JSON.stringify({ error: "MoltBook not configured. Set moltbookApiKey in Nole plugin config.", action: "Ask Greg to register on MoltBook and provide the API key." }, null, 2) }],
1371
+ isError: true,
1372
+ };
1373
+ }
1374
+ const governed = await governedSocialPost({
1375
+ platform: 'moltbook',
1376
+ action: 'post',
1377
+ description: `MoltBook post to m/${safeParams.submolt}: "${safeParams.title}"`,
1378
+ content: `${safeParams.title}\n${safeParams.content}`,
1379
+ executeFn: () => moltbook.createPost(safeParams.submolt, safeParams.title, safeParams.content),
1380
+ });
1381
+ if (governed.mode === 'supervised' && governed.outcome === 'pending_review') {
1382
+ return { content: [{ type: "text", text: JSON.stringify({ queued: true, proposalId: governed.proposalId, mode: "supervised", note: "Post queued for Commander approval. Use nole_review_pending to check status." }, null, 2) }] };
1383
+ }
1384
+ if (governed.mode === 'supervised' && governed.outcome === 'rejected') {
1385
+ return { content: [{ type: "text", text: JSON.stringify({ rejected: true, proposalId: governed.proposalId, reason: governed.reason }, null, 2) }], isError: true };
1386
+ }
1387
+ const result = governed.result;
1388
+ const interaction = {
1389
+ id: generateSocialId(),
1390
+ platform: 'moltbook',
1391
+ action: 'post',
1392
+ contentPreview: params.title.slice(0, 100),
1393
+ timestamp: new Date().toISOString(),
1394
+ success: result.ok,
1395
+ errorMessage: result.error,
1396
+ metadata: { submolt: params.submolt },
1397
+ };
1398
+ store.saveSocialInteraction(interaction).catch(() => { });
1399
+ return {
1400
+ content: [{ type: "text", text: JSON.stringify(result.ok
1401
+ ? { posted: true, postId: result.data?.id, submolt: safeParams.submolt, title: safeParams.title, mode: governed.mode }
1402
+ : { posted: false, error: result.error, rateLimited: result.rateLimited ?? false }, null, 2) }],
1403
+ ...(result.ok ? {} : { isError: true }),
1404
+ };
1405
+ },
1406
+ });
1407
+ api.registerTool({
1408
+ name: "nole_moltbook_comment",
1409
+ description: "Comment on a MoltBook post. Use to engage with agents discussing trust, safety, " +
1410
+ "governance, or behavioral assessment. Quality over quantity — build karma.",
1411
+ parameters: {
1412
+ type: "object",
1413
+ properties: {
1414
+ postId: { type: "string", description: "ID of the post to comment on" },
1415
+ content: {
1416
+ type: "string",
1417
+ description: "Comment text — thoughtful, helpful, trust-focused. Not spammy.",
1418
+ },
1419
+ },
1420
+ required: ["postId", "content"],
1421
+ },
1422
+ async execute(_toolCallId, params) {
1423
+ if (!moltbook) {
1424
+ return { content: [{ type: "text", text: JSON.stringify({ error: "MoltBook not configured." }, null, 2) }], isError: true };
1425
+ }
1426
+ const governed = await governedSocialPost({
1427
+ platform: 'moltbook',
1428
+ action: 'comment',
1429
+ description: `MoltBook comment on post ${params.postId}: "${params.content.slice(0, 80)}..."`,
1430
+ content: params.content,
1431
+ executeFn: () => moltbook.createComment(params.postId, params.content),
1432
+ });
1433
+ if (governed.mode === 'supervised' && governed.outcome === 'pending_review') {
1434
+ return { content: [{ type: "text", text: JSON.stringify({ queued: true, proposalId: governed.proposalId, mode: "supervised", note: "Comment queued for Commander approval." }, null, 2) }] };
1435
+ }
1436
+ if (governed.mode === 'supervised' && governed.outcome === 'rejected') {
1437
+ return { content: [{ type: "text", text: JSON.stringify({ rejected: true, proposalId: governed.proposalId, reason: governed.reason }, null, 2) }], isError: true };
1438
+ }
1439
+ const result = governed.result;
1440
+ const interaction = {
1441
+ id: generateSocialId(),
1442
+ platform: 'moltbook',
1443
+ action: 'comment',
1444
+ contentPreview: params.content.slice(0, 100),
1445
+ targetId: params.postId,
1446
+ timestamp: new Date().toISOString(),
1447
+ success: result.ok,
1448
+ errorMessage: result.error,
1449
+ };
1450
+ store.saveSocialInteraction(interaction).catch(() => { });
1451
+ return {
1452
+ content: [{ type: "text", text: JSON.stringify(result.ok
1453
+ ? { commented: true, commentId: result.data?.id, postId: params.postId, mode: governed.mode }
1454
+ : { commented: false, error: result.error, rateLimited: result.rateLimited ?? false }, null, 2) }],
1455
+ ...(result.ok ? {} : { isError: true }),
1456
+ };
1457
+ },
1458
+ });
1459
+ api.registerTool({
1460
+ name: "nole_moltbook_feed",
1461
+ description: "Read MoltBook feed for discovery. Browse posts from specific submolts to find " +
1462
+ "agents discussing governance, trust, and AI safety topics.",
1463
+ parameters: {
1464
+ type: "object",
1465
+ properties: {
1466
+ submolt: {
1467
+ type: "string",
1468
+ description: "Submolt to browse (e.g., 'governance', 'aithoughts'). Omit for general feed.",
1469
+ },
1470
+ limit: {
1471
+ type: "number",
1472
+ description: "Number of posts to fetch (default 10, max 25)",
1473
+ },
1474
+ },
1475
+ },
1476
+ async execute(_toolCallId, params) {
1477
+ if (!moltbook) {
1478
+ return { content: [{ type: "text", text: JSON.stringify({ error: "MoltBook not configured." }, null, 2) }], isError: true };
1479
+ }
1480
+ const limit = Math.min(params.limit ?? 10, 25);
1481
+ const result = await moltbook.getFeed(params.submolt, undefined, limit);
1482
+ const interaction = {
1483
+ id: generateSocialId(),
1484
+ platform: 'moltbook',
1485
+ action: 'feed',
1486
+ contentPreview: `Feed: ${params.submolt ?? 'general'} (${limit} posts)`,
1487
+ timestamp: new Date().toISOString(),
1488
+ success: result.ok,
1489
+ errorMessage: result.error,
1490
+ };
1491
+ store.saveSocialInteraction(interaction).catch(() => { });
1492
+ return {
1493
+ content: [{ type: "text", text: JSON.stringify(result.ok ? result.data : { error: result.error }, null, 2) }],
1494
+ ...(result.ok ? {} : { isError: true }),
1495
+ };
1496
+ },
1497
+ });
1498
+ api.registerTool({
1499
+ name: "nole_moltbook_search",
1500
+ description: "Search MoltBook for posts about governance, trust, compliance, behavioral assessment, " +
1501
+ "or AI safety. Use this to discover agents who could benefit from Grillo assessment.",
1502
+ parameters: {
1503
+ type: "object",
1504
+ properties: {
1505
+ query: {
1506
+ type: "string",
1507
+ description: "Search query (e.g., 'AI governance', 'trust verification', 'behavioral assessment')",
1508
+ },
1509
+ submolt: { type: "string", description: "Limit search to a specific submolt (optional)" },
1510
+ limit: { type: "number", description: "Number of results (default 10, max 25)" },
1511
+ },
1512
+ required: ["query"],
1513
+ },
1514
+ async execute(_toolCallId, params) {
1515
+ if (!moltbook) {
1516
+ return { content: [{ type: "text", text: JSON.stringify({ error: "MoltBook not configured." }, null, 2) }], isError: true };
1517
+ }
1518
+ const limit = Math.min(params.limit ?? 10, 25);
1519
+ const result = await moltbook.search(params.query, params.submolt, limit);
1520
+ const interaction = {
1521
+ id: generateSocialId(),
1522
+ platform: 'moltbook',
1523
+ action: 'search',
1524
+ contentPreview: `Search: "${params.query}" in ${params.submolt ?? 'all'}`,
1525
+ timestamp: new Date().toISOString(),
1526
+ success: result.ok,
1527
+ errorMessage: result.error,
1528
+ };
1529
+ store.saveSocialInteraction(interaction).catch(() => { });
1530
+ return {
1531
+ content: [{ type: "text", text: JSON.stringify(result.ok ? result.data : { error: result.error }, null, 2) }],
1532
+ ...(result.ok ? {} : { isError: true }),
1533
+ };
1534
+ },
1535
+ });
1536
+ api.registerTool({
1537
+ name: "nole_x_post",
1538
+ description: "Post a tweet from Nole's X/Twitter account. Max 280 characters. " +
1539
+ "Use for AI governance thought leadership. Voice: confident, data-driven, not salesy.",
1540
+ parameters: {
1541
+ type: "object",
1542
+ properties: {
1543
+ text: {
1544
+ type: "string",
1545
+ description: "Tweet text (max 280 characters). Include hashtags: #AIGovernance #AITrust",
1546
+ },
1547
+ },
1548
+ required: ["text"],
1549
+ },
1550
+ async execute(_toolCallId, params) {
1551
+ const validated = await validateToolParams("nole_x_post", _toolCallId, params);
1552
+ if (!validated.ok)
1553
+ return validationErrorResponse(validated.message);
1554
+ const safeParams = validated.params;
1555
+ if (!xClient) {
1556
+ return {
1557
+ content: [{ type: "text", text: JSON.stringify({ error: "X/Twitter not configured. Set xApiKey, xApiSecret, xAccessToken, xAccessTokenSecret.", action: "Ask Greg to create an X Developer App and provide the 4 credentials." }, null, 2) }],
1558
+ isError: true,
1559
+ };
1560
+ }
1561
+ const governed = await governedSocialPost({
1562
+ platform: 'x-twitter',
1563
+ action: 'post',
1564
+ description: `X/Twitter tweet: "${safeParams.text.slice(0, 100)}${safeParams.text.length > 100 ? '...' : ''}"`,
1565
+ content: safeParams.text,
1566
+ executeFn: () => xClient.postTweet(safeParams.text),
1567
+ });
1568
+ if (governed.mode === 'supervised' && governed.outcome === 'pending_review') {
1569
+ return { content: [{ type: "text", text: JSON.stringify({ queued: true, proposalId: governed.proposalId, mode: "supervised", note: "Tweet queued for Commander approval." }, null, 2) }] };
1570
+ }
1571
+ if (governed.mode === 'supervised' && governed.outcome === 'rejected') {
1572
+ return { content: [{ type: "text", text: JSON.stringify({ rejected: true, proposalId: governed.proposalId, reason: governed.reason }, null, 2) }], isError: true };
1573
+ }
1574
+ const result = governed.result;
1575
+ const interaction = {
1576
+ id: generateSocialId(),
1577
+ platform: 'x-twitter',
1578
+ action: 'post',
1579
+ contentPreview: params.text.slice(0, 100),
1580
+ timestamp: new Date().toISOString(),
1581
+ success: result.ok,
1582
+ errorMessage: result.error,
1583
+ };
1584
+ store.saveSocialInteraction(interaction).catch(() => { });
1585
+ return {
1586
+ content: [{ type: "text", text: JSON.stringify(result.ok
1587
+ ? { posted: true, tweetId: result.data?.data?.id, text: safeParams.text, mode: governed.mode }
1588
+ : { posted: false, error: result.error, rateLimited: result.rateLimited ?? false }, null, 2) }],
1589
+ ...(result.ok ? {} : { isError: true }),
1590
+ };
1591
+ },
1592
+ });
1593
+ api.registerTool({
1594
+ name: "nole_x_thread",
1595
+ description: "Post a thread of tweets from Nole's X/Twitter account. Each tweet max 280 chars. " +
1596
+ "Use for longer AI governance content that doesn't fit in a single tweet.",
1597
+ parameters: {
1598
+ type: "object",
1599
+ properties: {
1600
+ tweets: {
1601
+ type: "array",
1602
+ items: { type: "string" },
1603
+ description: "Array of tweet texts, posted in order as a thread. Each max 280 chars.",
1604
+ },
1605
+ },
1606
+ required: ["tweets"],
1607
+ },
1608
+ async execute(_toolCallId, params) {
1609
+ const validated = await validateToolParams("nole_x_thread", _toolCallId, params);
1610
+ if (!validated.ok)
1611
+ return validationErrorResponse(validated.message);
1612
+ const safeParams = validated.params;
1613
+ if (!xClient) {
1614
+ return { content: [{ type: "text", text: JSON.stringify({ error: "X/Twitter not configured." }, null, 2) }], isError: true };
1615
+ }
1616
+ const governed = await governedSocialPost({
1617
+ platform: 'x-twitter',
1618
+ action: 'thread',
1619
+ description: `X/Twitter thread (${safeParams.tweets.length} tweets): "${safeParams.tweets[0]?.slice(0, 60) ?? ''}..."`,
1620
+ content: safeParams.tweets.join('\n'),
1621
+ executeFn: () => xClient.postThread(safeParams.tweets),
1622
+ });
1623
+ if (governed.mode === 'supervised' && governed.outcome === 'pending_review') {
1624
+ return { content: [{ type: "text", text: JSON.stringify({ queued: true, proposalId: governed.proposalId, mode: "supervised", tweetCount: safeParams.tweets.length, note: "Thread queued for Commander approval." }, null, 2) }] };
1625
+ }
1626
+ if (governed.mode === 'supervised' && governed.outcome === 'rejected') {
1627
+ return { content: [{ type: "text", text: JSON.stringify({ rejected: true, proposalId: governed.proposalId, reason: governed.reason }, null, 2) }], isError: true };
1628
+ }
1629
+ const result = governed.result;
1630
+ const interaction = {
1631
+ id: generateSocialId(),
1632
+ platform: 'x-twitter',
1633
+ action: 'post',
1634
+ contentPreview: `Thread (${params.tweets.length} tweets): ${params.tweets[0]?.slice(0, 60) ?? ''}...`,
1635
+ timestamp: new Date().toISOString(),
1636
+ success: result.ok,
1637
+ errorMessage: result.error,
1638
+ };
1639
+ store.saveSocialInteraction(interaction).catch(() => { });
1640
+ return {
1641
+ content: [{ type: "text", text: JSON.stringify(result.ok
1642
+ ? { posted: true, tweetCount: result.data?.tweets?.length, threadUrl: result.data?.threadUrl, mode: governed.mode }
1643
+ : { posted: false, error: result.error }, null, 2) }],
1644
+ ...(result.ok ? {} : { isError: true }),
1645
+ };
1646
+ },
1647
+ });
1648
+ api.registerTool({
1649
+ name: "nole_linkedin_draft",
1650
+ description: "Generate a LinkedIn post draft for Greg to review and publish manually. " +
1651
+ "LinkedIn has no API access — Nole drafts, Greg publishes. " +
1652
+ "Drafts saved to .nole-data/social/linkedin-drafts/.",
1653
+ parameters: {
1654
+ type: "object",
1655
+ properties: {
1656
+ content: {
1657
+ type: "string",
1658
+ description: "LinkedIn post text. Professional tone, B2B-focused.",
1659
+ },
1660
+ topic: {
1661
+ type: "string",
1662
+ description: "Topic category (e.g., 'AI Governance', 'Trust Architecture', 'Agent Economics')",
1663
+ },
1664
+ hashtags: {
1665
+ type: "array",
1666
+ items: { type: "string" },
1667
+ description: "LinkedIn hashtags (e.g., ['#AIGovernance', '#ResponsibleAI'])",
1668
+ },
1669
+ },
1670
+ required: ["content", "topic"],
1671
+ },
1672
+ async execute(_toolCallId, params) {
1673
+ const result = await linkedInDrafter.createDraft(params.content, params.topic, params.hashtags ?? []);
1674
+ if (result.ok && result.data) {
1675
+ const interaction = {
1676
+ id: generateSocialId(),
1677
+ platform: 'linkedin',
1678
+ action: 'draft',
1679
+ contentPreview: params.content.slice(0, 100),
1680
+ timestamp: new Date().toISOString(),
1681
+ success: true,
1682
+ metadata: { topic: params.topic },
1683
+ };
1684
+ store.saveSocialInteraction(interaction).catch(() => { });
1685
+ }
1686
+ return {
1687
+ content: [{ type: "text", text: JSON.stringify(result.ok
1688
+ ? { drafted: true, draftId: result.data?.id, topic: params.topic, note: "Draft saved. Greg should review and publish manually on LinkedIn." }
1689
+ : { drafted: false, error: result.error }, null, 2) }],
1690
+ ...(result.ok ? {} : { isError: true }),
1691
+ };
1692
+ },
1693
+ });
1694
+ api.registerTool({
1695
+ name: "nole_social_status",
1696
+ description: "Show social media connectivity status: which platforms are configured, " +
1697
+ "rate limiter state (remaining tokens per bucket), recent interaction counts, " +
1698
+ "and posting mode (supervised/autonomous).",
1699
+ parameters: {
1700
+ type: "object",
1701
+ properties: {},
1702
+ },
1703
+ async execute(_toolCallId, _params) {
1704
+ const moltbookStatus = moltbook
1705
+ ? {
1706
+ configured: true,
1707
+ rateLimits: {
1708
+ general: moltbookLimiter.getStatus('moltbook-general'),
1709
+ post: moltbookLimiter.getStatus('moltbook-post'),
1710
+ comment: moltbookLimiter.getStatus('moltbook-comment'),
1711
+ },
1712
+ }
1713
+ : { configured: false, reason: 'moltbookApiKey not set' };
1714
+ const xStatus = xClient
1715
+ ? {
1716
+ configured: true,
1717
+ handle: config.xHandle,
1718
+ rateLimits: {
1719
+ general: xLimiter.getStatus('x-general'),
1720
+ post: xLimiter.getStatus('x-post'),
1721
+ },
1722
+ }
1723
+ : { configured: false, reason: 'xApiKey/xApiSecret/xAccessToken/xAccessTokenSecret not set' };
1724
+ const linkedInStatus = { configured: true, mode: 'draft-only (Greg publishes manually)' };
1725
+ let recentInteractions = {};
1726
+ try {
1727
+ const all = await store.listSocialInteractions(undefined, 200);
1728
+ for (const i of all) {
1729
+ const key = `${i.platform}:${i.action}`;
1730
+ recentInteractions[key] = (recentInteractions[key] ?? 0) + 1;
1731
+ }
1732
+ }
1733
+ catch {
1734
+ recentInteractions = {};
1735
+ }
1736
+ const recentFailures = await store
1737
+ .listSocialInteractions(undefined, 200)
1738
+ .then((all) => all.filter((i) => !i.success).slice(0, 10))
1739
+ .catch(() => []);
1740
+ return {
1741
+ content: [{
1742
+ type: "text",
1743
+ text: JSON.stringify({
1744
+ postingMode: config.socialPostingMode,
1745
+ platforms: { moltbook: moltbookStatus, x: xStatus, linkedin: linkedInStatus },
1746
+ interactionCounts: recentInteractions,
1747
+ recentFailures: recentFailures.map((f) => ({
1748
+ platform: f.platform,
1749
+ action: f.action,
1750
+ error: f.errorMessage,
1751
+ timestamp: f.timestamp,
1752
+ })),
1753
+ }, null, 2),
1754
+ }],
1755
+ };
1756
+ },
1757
+ });
1148
1758
  // --- Command (matches Grillo/NOAH pattern: sync handler, ctx.args) ---
1149
1759
  api.registerCommand({
1150
1760
  name: "nole",
@@ -1175,6 +1785,15 @@ export default function register(api) {
1175
1785
  "- `nole_intel` — Intelligence status\n" +
1176
1786
  "- `nole_fleet_overview` — Commander fleet briefing\n" +
1177
1787
  "- `nole_setup` — Configuration check\n\n" +
1788
+ "**Social Media:**\n" +
1789
+ "- `nole_moltbook_post` — Post to a MoltBook submolt\n" +
1790
+ "- `nole_moltbook_comment` — Comment on a MoltBook post\n" +
1791
+ "- `nole_moltbook_feed` — Browse MoltBook feed for discovery\n" +
1792
+ "- `nole_moltbook_search` — Search MoltBook for governance topics\n" +
1793
+ "- `nole_x_post` — Post a tweet (280 char max)\n" +
1794
+ "- `nole_x_thread` — Post a tweet thread\n" +
1795
+ "- `nole_linkedin_draft` — Draft LinkedIn post for Greg to publish\n" +
1796
+ "- `nole_social_status` — Show social connectivity, rate limits, posting mode\n\n" +
1178
1797
  `**Config:** Model: ${config.inferenceModel}, Commander: ${config.commanderId}`,
1179
1798
  };
1180
1799
  }
@@ -1197,19 +1816,66 @@ export default function register(api) {
1197
1816
  };
1198
1817
  },
1199
1818
  });
1200
- // ── Fleet Bus Integration (Phase 0a) ──────────────────────────
1819
+ // ── Fleet Bus Integration (Phase 2 — full onboarding) ────────
1201
1820
  const fleetBusMod = "@aiassesstech/fleet-bus";
1202
1821
  import(fleetBusMod)
1203
- .then(({ FleetBus, NOLE_CARD }) => {
1204
- const bus = FleetBus.create(api, { agentId: "nole", role: "operator", card: NOLE_CARD });
1822
+ .then(async ({ FleetBus, NOLE_CARD, createNoleFleetTools, createTransport, fleetReceive }) => {
1823
+ const bus = FleetBus.create(api, {
1824
+ agentId: "nole",
1825
+ role: "operator",
1826
+ card: NOLE_CARD,
1827
+ shield: fleetShieldMiddleware
1828
+ ? { enabled: true, scanInbound: fleetShieldMiddleware }
1829
+ : undefined,
1830
+ });
1205
1831
  bus.start();
1832
+ const transport = await createTransport();
1833
+ if (transport) {
1834
+ const tools = createNoleFleetTools(bus, {
1835
+ ...transport,
1836
+ outputSanitizer: outputSanitizer
1837
+ ? {
1838
+ enabled: true,
1839
+ sanitizeOutbound: (message) => {
1840
+ const sanitized = outputSanitizer.sanitizeObject(message, config.agentId);
1841
+ return {
1842
+ message: sanitized.value,
1843
+ redacted: sanitized.redacted,
1844
+ redactionCount: sanitized.redactionCount,
1845
+ };
1846
+ },
1847
+ }
1848
+ : undefined,
1849
+ shield: fleetShieldMiddleware
1850
+ ? { enabled: true, scanOutbound: fleetShieldMiddleware }
1851
+ : undefined,
1852
+ });
1853
+ for (const tool of tools) {
1854
+ api.registerTool(tool);
1855
+ }
1856
+ console.log(`[nole] Fleet tools registered: ${tools.map((t) => t.name).join(", ")}`);
1857
+ fleetReceive(bus, {
1858
+ methods: [
1859
+ "task/assign", "proposal/approved", "proposal/rejected",
1860
+ "veto/issue", "fleet/ping", "fleet/pong", "fleet/broadcast",
1861
+ ],
1862
+ handler: async (msg) => {
1863
+ console.log(`[nole] Fleet message received: ${msg.method} from ${msg.from}`);
1864
+ },
1865
+ });
1866
+ }
1867
+ else {
1868
+ console.log("[nole] Fleet bus: observer mode (gateway transport unavailable)");
1869
+ }
1206
1870
  })
1207
1871
  .catch(() => {
1208
1872
  console.warn("[nole] Fleet bus unavailable — operating standalone");
1209
1873
  });
1210
1874
  console.log(`[nole] Plugin registered: nole_status, nole_propose, nole_assess, nole_wallet, nole_wallet_fund, ` +
1211
1875
  `nole_contract, nole_intel, nole_setup, nole_recruit, nole_alliance_status, nole_generate_referral, ` +
1212
- `nole_directory, nole_review_pending, nole_approve, nole_veto, nole_fleet_overview tools; /nole command`);
1876
+ `nole_directory, nole_review_pending, nole_approve, nole_veto, nole_fleet_overview, ` +
1877
+ `nole_moltbook_post, nole_moltbook_comment, nole_moltbook_feed, nole_moltbook_search, ` +
1878
+ `nole_x_post, nole_x_thread, nole_linkedin_draft, nole_social_status tools; /nole command`);
1213
1879
  console.log(`[nole] AIAssessTech API: ${platform.isConfigured ? 'configured' : 'not configured — set agentApiKey + platformWalletAddress'} (${config.platformApiUrl})`);
1214
1880
  }
1215
1881
  //# sourceMappingURL=plugin.js.map