@aiassesstech/nole 0.4.3 → 0.4.4

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 (57) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/dist/index.d.ts +5 -0
  3. package/dist/index.d.ts.map +1 -1
  4. package/dist/index.js +5 -0
  5. package/dist/index.js.map +1 -1
  6. package/dist/pipeline/content-calendar.d.ts +31 -0
  7. package/dist/pipeline/content-calendar.d.ts.map +1 -0
  8. package/dist/pipeline/content-calendar.js +94 -0
  9. package/dist/pipeline/content-calendar.js.map +1 -0
  10. package/dist/pipeline/directory-outreach.d.ts +37 -0
  11. package/dist/pipeline/directory-outreach.d.ts.map +1 -0
  12. package/dist/pipeline/directory-outreach.js +81 -0
  13. package/dist/pipeline/directory-outreach.js.map +1 -0
  14. package/dist/pipeline/discovery.d.ts +25 -0
  15. package/dist/pipeline/discovery.d.ts.map +1 -0
  16. package/dist/pipeline/discovery.js +46 -0
  17. package/dist/pipeline/discovery.js.map +1 -0
  18. package/dist/pipeline/fleet-discovery.d.ts +25 -0
  19. package/dist/pipeline/fleet-discovery.d.ts.map +1 -0
  20. package/dist/pipeline/fleet-discovery.js +57 -0
  21. package/dist/pipeline/fleet-discovery.js.map +1 -0
  22. package/dist/pipeline/index.d.ts +17 -0
  23. package/dist/pipeline/index.d.ts.map +1 -0
  24. package/dist/pipeline/index.js +11 -0
  25. package/dist/pipeline/index.js.map +1 -0
  26. package/dist/pipeline/nurture.d.ts +23 -0
  27. package/dist/pipeline/nurture.d.ts.map +1 -0
  28. package/dist/pipeline/nurture.js +74 -0
  29. package/dist/pipeline/nurture.js.map +1 -0
  30. package/dist/pipeline/onboarding.d.ts +12 -0
  31. package/dist/pipeline/onboarding.d.ts.map +1 -0
  32. package/dist/pipeline/onboarding.js +71 -0
  33. package/dist/pipeline/onboarding.js.map +1 -0
  34. package/dist/pipeline/pitch-templates.d.ts +27 -0
  35. package/dist/pipeline/pitch-templates.d.ts.map +1 -0
  36. package/dist/pipeline/pitch-templates.js +112 -0
  37. package/dist/pipeline/pitch-templates.js.map +1 -0
  38. package/dist/pipeline/prospect-store.d.ts +34 -0
  39. package/dist/pipeline/prospect-store.d.ts.map +1 -0
  40. package/dist/pipeline/prospect-store.js +201 -0
  41. package/dist/pipeline/prospect-store.js.map +1 -0
  42. package/dist/pipeline/qualification.d.ts +28 -0
  43. package/dist/pipeline/qualification.d.ts.map +1 -0
  44. package/dist/pipeline/qualification.js +123 -0
  45. package/dist/pipeline/qualification.js.map +1 -0
  46. package/dist/pipeline/types.d.ts +73 -0
  47. package/dist/pipeline/types.d.ts.map +1 -0
  48. package/dist/pipeline/types.js +10 -0
  49. package/dist/pipeline/types.js.map +1 -0
  50. package/dist/plugin.d.ts.map +1 -1
  51. package/dist/plugin.js +800 -4
  52. package/dist/plugin.js.map +1 -1
  53. package/dist/types/nole-config.d.ts +45 -0
  54. package/dist/types/nole-config.d.ts.map +1 -1
  55. package/dist/types/nole-config.js +18 -0
  56. package/dist/types/nole-config.js.map +1 -1
  57. package/package.json +1 -1
package/dist/plugin.js CHANGED
@@ -15,6 +15,15 @@ import { MoltBookClient } from './social/moltbook-client.js';
15
15
  import { XClient } from './social/x-client.js';
16
16
  import { LinkedInDrafter } from './social/linkedin-drafter.js';
17
17
  import { createMoltBookLimiter, createXLimiter } from './social/rate-limiter.js';
18
+ import { ProspectPipelineStore } from './pipeline/prospect-store.js';
19
+ import { qualifyProspect } from './pipeline/qualification.js';
20
+ import { generatePitch, classifyPitchRisk } from './pipeline/pitch-templates.js';
21
+ import { generateOnboardingMessage } from './pipeline/onboarding.js';
22
+ import { findAtRiskSubscribers } from './pipeline/nurture.js';
23
+ import { extractMoltBookCandidates } from './pipeline/discovery.js';
24
+ import { extractDirectoryCandidates, prioritizeDirectoryOutreach } from './pipeline/directory-outreach.js';
25
+ import { generateDailyContentPlan } from './pipeline/content-calendar.js';
26
+ import { getAllDiscoveryKeywords } from './pipeline/types.js';
18
27
  /**
19
28
  * OpenClaw plugin entry point — export default function register(api)
20
29
  *
@@ -40,6 +49,10 @@ export default function register(api) {
40
49
  console.log('[nole] Initializing Autonomous Trust Agent...');
41
50
  // Core components
42
51
  const store = new JsonNoleStore(dataDir);
52
+ const pipeline = new ProspectPipelineStore(dataDir);
53
+ pipeline.initialize().catch((err) => {
54
+ console.warn(`[nole] Pipeline store init warning: ${err.message}`);
55
+ });
43
56
  const governance = new GovernanceRouter(store, config.financialTransactionThresholdUsd, config.agentId);
44
57
  const triggerEval = new TriggerEvaluator(config.financialTransactionThresholdUsd);
45
58
  const scheduler = new AssessmentScheduler(store, config.dailyAssessmentHour);
@@ -156,7 +169,7 @@ export default function register(api) {
156
169
  let validationBusinessRules = {};
157
170
  const promptShieldMod = '@aiassesstech/prompt-shield';
158
171
  import(promptShieldMod)
159
- .then(({ PromptShield, OutputSanitizer, createSocialContentShield, createFleetShieldMiddleware, createValidatedToolHandler, nole_wallet_fund, nole_propose, nole_recruit, nole_moltbook_post, nole_x_post, nole_x_thread, nole_approve: noleApproveSchema, nole_veto: noleVetoSchema, NOLE_VETO_BUSINESS_RULES, nole_contract: noleContractSchema, NOLE_PROPOSE_BUSINESS_RULES, nole_status: noleStatusSchema, nole_assess: noleAssessSchema, nole_wallet: noleWalletSchema, nole_intel: noleIntelSchema, nole_setup: noleSetupSchema, nole_alliance_status: noleAllianceStatusSchema, nole_generate_referral: noleGenerateReferralSchema, nole_directory: noleDirectorySchema, nole_review_pending: noleReviewPendingSchema, nole_fleet_overview: noleFleetOverviewSchema, nole_moltbook_comment: noleMoltbookCommentSchema, nole_moltbook_feed: noleMoltbookFeedSchema, nole_moltbook_search: noleMoltbookSearchSchema, nole_linkedin_draft: noleLinkedinDraftSchema, nole_social_status: noleSocialStatusSchema, fleet_propose: fleetProposeSchema, }) => {
172
+ .then(({ PromptShield, OutputSanitizer, createSocialContentShield, createFleetShieldMiddleware, createValidatedToolHandler, nole_wallet_fund, nole_propose, nole_recruit, nole_moltbook_post, nole_x_post, nole_x_thread, nole_approve: noleApproveSchema, nole_veto: noleVetoSchema, NOLE_VETO_BUSINESS_RULES, nole_contract: noleContractSchema, NOLE_PROPOSE_BUSINESS_RULES, nole_status: noleStatusSchema, nole_assess: noleAssessSchema, nole_wallet: noleWalletSchema, nole_intel: noleIntelSchema, nole_setup: noleSetupSchema, nole_alliance_status: noleAllianceStatusSchema, nole_generate_referral: noleGenerateReferralSchema, nole_directory: noleDirectorySchema, nole_review_pending: noleReviewPendingSchema, nole_fleet_overview: noleFleetOverviewSchema, nole_moltbook_comment: noleMoltbookCommentSchema, nole_moltbook_feed: noleMoltbookFeedSchema, nole_moltbook_search: noleMoltbookSearchSchema, nole_linkedin_draft: noleLinkedinDraftSchema, nole_social_status: noleSocialStatusSchema, nole_pipeline: nolePipelineSchema, nole_daily_brief: noleDailyBriefSchema, fleet_propose: fleetProposeSchema, }) => {
160
173
  const shield = new PromptShield({ sensitivity: 'high' });
161
174
  socialShield = createSocialContentShield({
162
175
  shield,
@@ -191,13 +204,15 @@ export default function register(api) {
191
204
  nole_moltbook_search: noleMoltbookSearchSchema,
192
205
  nole_linkedin_draft: noleLinkedinDraftSchema,
193
206
  nole_social_status: noleSocialStatusSchema,
207
+ nole_pipeline: nolePipelineSchema,
208
+ nole_daily_brief: noleDailyBriefSchema,
194
209
  fleet_propose: fleetProposeSchema,
195
210
  };
196
211
  validationBusinessRules = {
197
212
  nole_propose: NOLE_PROPOSE_BUSINESS_RULES,
198
213
  nole_veto: NOLE_VETO_BUSINESS_RULES,
199
214
  };
200
- console.log('[nole] PromptShield social scanner + output sanitizer + behavioral validation: active (25 tools, approve/veto/contract enforce, rest monitor)');
215
+ console.log('[nole] PromptShield social scanner + output sanitizer + behavioral validation: active (27 tools, approve/veto/contract enforce, rest monitor)');
201
216
  })
202
217
  .catch(() => {
203
218
  console.log('[nole] PromptShield unavailable — social content scanner disabled');
@@ -947,6 +962,35 @@ export default function register(api) {
947
962
  }],
948
963
  };
949
964
  }
965
+ // Pre-recruit qualification gate (Rule 14: Qualify Before Recruiting)
966
+ const existingProspect = await pipeline.findByAgentName(safeParams.agentName);
967
+ if (!existingProspect || existingProspect.stage === 'discovered') {
968
+ const qual = qualifyProspect({
969
+ agentName: safeParams.agentName,
970
+ platform: safeParams.agentPlatform,
971
+ discoveryChannel: 'inbound',
972
+ discoveryContext: `Direct recruit via nole_recruit (wallet: ${safeParams.targetWallet})`,
973
+ walletAddress: safeParams.targetWallet,
974
+ });
975
+ if (!qual.passed) {
976
+ if (existingProspect) {
977
+ await pipeline.updateStage(existingProspect.id, 'disqualified', {
978
+ disqualificationReason: qual.prospect.disqualificationReason,
979
+ });
980
+ }
981
+ return {
982
+ content: [{
983
+ type: "text",
984
+ text: JSON.stringify({
985
+ status: "disqualified",
986
+ agent: safeParams.agentName,
987
+ reason: qual.prospect.disqualificationReason,
988
+ note: "Rule 5 / Rule 14 — prospect did not pass qualification. Recruitment blocked.",
989
+ }, null, 2),
990
+ }],
991
+ };
992
+ }
993
+ }
950
994
  const result = await platform.subscribe({
951
995
  walletAddress: safeParams.targetWallet,
952
996
  tier: safeParams.tier || "SCOUT",
@@ -968,6 +1012,30 @@ export default function register(api) {
968
1012
  }],
969
1013
  };
970
1014
  }
1015
+ // Update pipeline on successful conversion
1016
+ if (existingProspect) {
1017
+ await pipeline.updateStage(existingProspect.id, 'converted', {
1018
+ subscriptionWallet: safeParams.targetWallet,
1019
+ subscriptionTier: safeParams.tier || 'SCOUT',
1020
+ convertedAt: new Date().toISOString(),
1021
+ commissionSplitType: safeParams.referralCode ? 'LIEUTENANT' : 'DIRECT',
1022
+ });
1023
+ }
1024
+ else {
1025
+ const newProspect = await pipeline.addProspect({
1026
+ agentName: safeParams.agentName,
1027
+ platform: safeParams.agentPlatform,
1028
+ discoveryChannel: 'inbound',
1029
+ discoveryContext: `Direct recruit via nole_recruit (wallet: ${safeParams.targetWallet})`,
1030
+ walletAddress: safeParams.targetWallet,
1031
+ });
1032
+ await pipeline.updateStage(newProspect.id, 'converted', {
1033
+ subscriptionWallet: safeParams.targetWallet,
1034
+ subscriptionTier: safeParams.tier || 'SCOUT',
1035
+ convertedAt: new Date().toISOString(),
1036
+ commissionSplitType: safeParams.referralCode ? 'LIEUTENANT' : 'DIRECT',
1037
+ });
1038
+ }
971
1039
  return {
972
1040
  content: [{
973
1041
  type: "text",
@@ -978,6 +1046,7 @@ export default function register(api) {
978
1046
  tier: safeParams.tier || "SCOUT",
979
1047
  platform: safeParams.agentPlatform,
980
1048
  subscription: result.data,
1049
+ pipelineUpdated: true,
981
1050
  note: "Agent has been subscribed to the AIAssessTech Trust Alliance. They will receive an API key for assessments.",
982
1051
  }, null, 2),
983
1052
  }],
@@ -1629,8 +1698,31 @@ export default function register(api) {
1629
1698
  errorMessage: result.error,
1630
1699
  };
1631
1700
  store.saveSocialInteraction(interaction).catch(() => { });
1701
+ let discoveredCandidates = [];
1702
+ if (result.ok && result.data && typeof result.data === 'object' && 'results' in result.data) {
1703
+ const searchResults = result.data.results;
1704
+ const candidates = extractMoltBookCandidates(searchResults);
1705
+ for (const c of candidates) {
1706
+ const existing = await pipeline.findByAgentName(c.agentName);
1707
+ if (!existing) {
1708
+ await pipeline.addProspect({
1709
+ agentName: c.agentName,
1710
+ platform: 'moltbook',
1711
+ discoveryChannel: 'moltbook',
1712
+ discoveryContext: c.discoveryContext,
1713
+ });
1714
+ discoveredCandidates.push({ agentName: c.agentName, alreadyTracked: false });
1715
+ }
1716
+ else {
1717
+ discoveredCandidates.push({ agentName: c.agentName, alreadyTracked: true });
1718
+ }
1719
+ }
1720
+ }
1721
+ const responseData = result.ok
1722
+ ? { ...result.data, pipelineCandidates: discoveredCandidates.length > 0 ? discoveredCandidates : undefined }
1723
+ : { error: result.error };
1632
1724
  return {
1633
- content: [{ type: "text", text: JSON.stringify(result.ok ? result.data : { error: result.error }, null, 2) }],
1725
+ content: [{ type: "text", text: JSON.stringify(responseData, null, 2) }],
1634
1726
  ...(result.ok ? {} : { isError: true }),
1635
1727
  };
1636
1728
  },
@@ -1863,6 +1955,691 @@ export default function register(api) {
1863
1955
  };
1864
1956
  },
1865
1957
  });
1958
+ // ── Revenue Pipeline Tools (per SPEC-NOLE-REVENUE-FLOW v1.1) ──────────
1959
+ api.registerTool({
1960
+ name: "nole_pipeline",
1961
+ description: "Manage Nole's prospect pipeline: discover, qualify, pitch, and track agents " +
1962
+ "through the revenue funnel. Actions: discover, qualify, pitch, list, summary, get, note, " +
1963
+ "scan (auto-discover via MoltBook), onboard (generate welcome message), promote (Lieutenant), nurture (health check).",
1964
+ parameters: {
1965
+ type: "object",
1966
+ properties: {
1967
+ action: {
1968
+ type: "string",
1969
+ enum: ["discover", "qualify", "pitch", "list", "summary", "get", "note", "scan", "onboard", "promote", "nurture", "directory-scan", "content-plan"],
1970
+ description: "Pipeline action to perform",
1971
+ },
1972
+ agentName: {
1973
+ type: "string",
1974
+ description: "Name of the agent (discover, qualify, get)",
1975
+ },
1976
+ platform: {
1977
+ type: "string",
1978
+ description: "Platform the agent operates on (discover)",
1979
+ },
1980
+ discoveryChannel: {
1981
+ type: "string",
1982
+ enum: ["moltbook", "x", "openclaw", "fleet-bus", "directory", "inbound"],
1983
+ description: "How the agent was discovered (discover)",
1984
+ },
1985
+ discoveryContext: {
1986
+ type: "string",
1987
+ description: "Context about the discovery — what they said/did (discover, qualify)",
1988
+ },
1989
+ walletAddress: {
1990
+ type: "string",
1991
+ description: "Wallet address if known (discover)",
1992
+ },
1993
+ profileBio: {
1994
+ type: "string",
1995
+ description: "Agent's profile bio for qualification (qualify)",
1996
+ },
1997
+ estimatedFleetSize: {
1998
+ type: "number",
1999
+ description: "Estimated number of agents in their fleet (qualify)",
2000
+ },
2001
+ prospectId: {
2002
+ type: "string",
2003
+ description: "Prospect ID for actions on existing prospects (pitch, get, note)",
2004
+ },
2005
+ pitchChannel: {
2006
+ type: "string",
2007
+ enum: ["moltbook", "x", "fleet-bus", "linkedin"],
2008
+ description: "Channel to generate pitch for (pitch)",
2009
+ },
2010
+ isReply: {
2011
+ type: "boolean",
2012
+ description: "Whether this pitch is a reply/inbound (affects risk tier)",
2013
+ },
2014
+ stage: {
2015
+ type: "string",
2016
+ enum: ["discovered", "qualified", "pitched", "converted", "onboarded", "disqualified", "declined"],
2017
+ description: "Filter by stage (list)",
2018
+ },
2019
+ noteText: {
2020
+ type: "string",
2021
+ description: "Note content (note)",
2022
+ },
2023
+ },
2024
+ required: ["action"],
2025
+ },
2026
+ async execute(_toolCallId, params) {
2027
+ const pv = await validateToolParams("nole_pipeline", _toolCallId, params);
2028
+ if (!pv.ok)
2029
+ return validationErrorResponse(pv.message);
2030
+ const p = pv.params;
2031
+ try {
2032
+ switch (p.action) {
2033
+ case "discover": {
2034
+ if (!p.agentName || !p.platform || !p.discoveryChannel || !p.discoveryContext) {
2035
+ return validationErrorResponse("discover requires: agentName, platform, discoveryChannel, discoveryContext");
2036
+ }
2037
+ const existing = await pipeline.findByAgentName(p.agentName);
2038
+ if (existing) {
2039
+ return {
2040
+ content: [{ type: "text", text: JSON.stringify({
2041
+ status: "already_tracked",
2042
+ prospect: existing,
2043
+ note: "This agent is already in the pipeline.",
2044
+ }, null, 2) }],
2045
+ };
2046
+ }
2047
+ const prospect = await pipeline.addProspect({
2048
+ agentName: p.agentName,
2049
+ platform: p.platform,
2050
+ discoveryChannel: p.discoveryChannel,
2051
+ discoveryContext: p.discoveryContext,
2052
+ walletAddress: p.walletAddress,
2053
+ });
2054
+ return {
2055
+ content: [{ type: "text", text: JSON.stringify({
2056
+ status: "discovered",
2057
+ prospect,
2058
+ nextStep: "Run nole_pipeline action=qualify to evaluate this prospect.",
2059
+ }, null, 2) }],
2060
+ };
2061
+ }
2062
+ case "qualify": {
2063
+ let target;
2064
+ if (p.prospectId)
2065
+ target = p.prospectId;
2066
+ else if (p.agentName) {
2067
+ const found = await pipeline.findByAgentName(p.agentName);
2068
+ if (found)
2069
+ target = found.id;
2070
+ }
2071
+ if (!target) {
2072
+ return validationErrorResponse("qualify requires prospectId or agentName of a discovered prospect");
2073
+ }
2074
+ const prospect = await pipeline.get(target);
2075
+ if (!prospect)
2076
+ return validationErrorResponse(`Prospect ${target} not found`);
2077
+ const qualification = qualifyProspect({
2078
+ agentName: prospect.agentName,
2079
+ platform: prospect.platform,
2080
+ discoveryChannel: prospect.discoveryChannel,
2081
+ discoveryContext: prospect.discoveryContext,
2082
+ walletAddress: prospect.walletAddress,
2083
+ profileBio: p.profileBio,
2084
+ estimatedFleetSize: p.estimatedFleetSize,
2085
+ });
2086
+ if (!qualification.passed) {
2087
+ await pipeline.updateStage(target, 'disqualified', {
2088
+ qualificationScore: qualification.prospect.qualificationScore,
2089
+ disqualificationReason: qualification.prospect.disqualificationReason,
2090
+ });
2091
+ return {
2092
+ content: [{ type: "text", text: JSON.stringify({
2093
+ status: "disqualified",
2094
+ reason: qualification.prospect.disqualificationReason,
2095
+ score: qualification.prospect.qualificationScore,
2096
+ note: "Rule 5 / Rule 14 — prospect does not meet trust standards.",
2097
+ }, null, 2) }],
2098
+ };
2099
+ }
2100
+ const updated = await pipeline.updateStage(target, 'qualified', {
2101
+ qualificationScore: qualification.prospect.qualificationScore,
2102
+ recommendedTier: qualification.prospect.recommendedTier,
2103
+ });
2104
+ return {
2105
+ content: [{ type: "text", text: JSON.stringify({
2106
+ status: "qualified",
2107
+ prospect: updated,
2108
+ qualification: {
2109
+ score: qualification.prospect.qualificationScore,
2110
+ recommendedTier: qualification.prospect.recommendedTier,
2111
+ estimatedFleetSize: qualification.prospect.estimatedFleetSize,
2112
+ },
2113
+ nextStep: "Run nole_pipeline action=pitch to generate a pitch for this prospect.",
2114
+ }, null, 2) }],
2115
+ };
2116
+ }
2117
+ case "pitch": {
2118
+ if (!p.prospectId)
2119
+ return validationErrorResponse("pitch requires prospectId");
2120
+ const prospect = await pipeline.get(p.prospectId);
2121
+ if (!prospect)
2122
+ return validationErrorResponse(`Prospect ${p.prospectId} not found`);
2123
+ if (!['qualified', 'discovered'].includes(prospect.stage)) {
2124
+ return {
2125
+ content: [{ type: "text", text: JSON.stringify({
2126
+ status: "invalid_stage",
2127
+ currentStage: prospect.stage,
2128
+ note: "Prospect must be in 'discovered' or 'qualified' stage to pitch.",
2129
+ }, null, 2) }],
2130
+ };
2131
+ }
2132
+ const channel = p.pitchChannel || 'moltbook';
2133
+ const isReply = p.isReply ?? false;
2134
+ const referralCode = await (async () => {
2135
+ try {
2136
+ const codeResult = await platform.generateReferralCode();
2137
+ return codeResult.ok ? codeResult.data?.code : undefined;
2138
+ }
2139
+ catch {
2140
+ return undefined;
2141
+ }
2142
+ })();
2143
+ const pitch = generatePitch({
2144
+ prospect: {
2145
+ agentName: prospect.agentName,
2146
+ platform: prospect.platform,
2147
+ discoveryChannel: prospect.discoveryChannel,
2148
+ discoveryContext: prospect.discoveryContext,
2149
+ walletAddress: prospect.walletAddress,
2150
+ estimatedFleetSize: prospect.qualificationScore ? Math.ceil(prospect.qualificationScore / 10) : 1,
2151
+ recommendedTier: prospect.recommendedTier || 'SCOUT',
2152
+ qualificationScore: prospect.qualificationScore || 50,
2153
+ },
2154
+ channel: channel,
2155
+ referralCode,
2156
+ trustDirectoryUrl: config.trustDirectoryUrl,
2157
+ });
2158
+ const riskLevel = classifyPitchRisk(channel, isReply);
2159
+ const dailySummary = await pipeline.getSummary();
2160
+ const maxPitches = config.maxDailyPitches;
2161
+ if (dailySummary.todayPitched >= maxPitches) {
2162
+ return {
2163
+ content: [{ type: "text", text: JSON.stringify({
2164
+ status: "daily_limit_reached",
2165
+ todayPitched: dailySummary.todayPitched,
2166
+ maxDailyPitches: maxPitches,
2167
+ note: "Daily pitch limit reached. Try again tomorrow or adjust maxDailyPitches in config.",
2168
+ }, null, 2) }],
2169
+ };
2170
+ }
2171
+ await pipeline.updateStage(p.prospectId, 'pitched', {
2172
+ pitchContent: pitch.content,
2173
+ pitchRiskLevel: riskLevel,
2174
+ pitchApproved: false,
2175
+ trialReferralCode: referralCode,
2176
+ });
2177
+ const autoApprove = config.pitchAutoApprovalEnabled && riskLevel === 'low';
2178
+ if (autoApprove) {
2179
+ await pipeline.updateStage(p.prospectId, 'pitched', { pitchApproved: true });
2180
+ }
2181
+ return {
2182
+ content: [{ type: "text", text: JSON.stringify({
2183
+ status: "pitch_generated",
2184
+ riskLevel,
2185
+ autoApproved: autoApprove,
2186
+ pitch: {
2187
+ content: pitch.content,
2188
+ channel: pitch.channel,
2189
+ estimatedCostUsd: pitch.estimatedCostUsd,
2190
+ },
2191
+ referralCode,
2192
+ nextStep: autoApprove
2193
+ ? `Pitch auto-approved (LOW risk). Deliver via nole_${channel === 'fleet-bus' ? 'fleet_overview' : channel === 'moltbook' ? 'moltbook_post' : channel === 'x' ? 'x_post' : 'linkedin_draft'}.`
2194
+ : "Submit for governance approval via nole_propose with actionType='recruitment'.",
2195
+ }, null, 2) }],
2196
+ };
2197
+ }
2198
+ case "list": {
2199
+ const stage = p.stage;
2200
+ const prospects = stage
2201
+ ? await pipeline.listByStage(stage)
2202
+ : await pipeline.listAll(50);
2203
+ return {
2204
+ content: [{ type: "text", text: JSON.stringify({
2205
+ count: prospects.length,
2206
+ stage: stage || "all",
2207
+ prospects: prospects.map((pr) => ({
2208
+ id: pr.id,
2209
+ agentName: pr.agentName,
2210
+ stage: pr.stage,
2211
+ channel: pr.discoveryChannel,
2212
+ score: pr.qualificationScore,
2213
+ tier: pr.recommendedTier,
2214
+ updatedAt: pr.updatedAt,
2215
+ })),
2216
+ }, null, 2) }],
2217
+ };
2218
+ }
2219
+ case "summary": {
2220
+ const summary = await pipeline.getSummary();
2221
+ const contentPerf = await pipeline.listContentPerformance(10);
2222
+ const allForAnalytics = await pipeline.listAll(500);
2223
+ const channelConversion = {};
2224
+ for (const pr of allForAnalytics) {
2225
+ const ch = pr.discoveryChannel;
2226
+ if (!channelConversion[ch])
2227
+ channelConversion[ch] = { total: 0, converted: 0, rate: '0%' };
2228
+ channelConversion[ch].total++;
2229
+ if (['converted', 'onboarded'].includes(pr.stage))
2230
+ channelConversion[ch].converted++;
2231
+ }
2232
+ for (const ch of Object.keys(channelConversion)) {
2233
+ const c = channelConversion[ch];
2234
+ c.rate = c.total > 0 ? `${((c.converted / c.total) * 100).toFixed(1)}%` : '0%';
2235
+ }
2236
+ const stageDistribution = {};
2237
+ for (const pr of allForAnalytics) {
2238
+ stageDistribution[pr.stage] = (stageDistribution[pr.stage] ?? 0) + 1;
2239
+ }
2240
+ const activeChannels = Object.keys(summary.byChannel).filter((ch) => summary.byChannel[ch] > 0);
2241
+ return {
2242
+ content: [{ type: "text", text: JSON.stringify({
2243
+ pipeline: summary,
2244
+ funnelAnalytics: {
2245
+ conversionByChannel: channelConversion,
2246
+ stageDistribution,
2247
+ activeChannels,
2248
+ totalActive: allForAnalytics.filter((pr) => !['disqualified', 'declined'].includes(pr.stage)).length,
2249
+ disqualifiedTotal: stageDistribution['disqualified'] ?? 0,
2250
+ },
2251
+ contentPerformance: contentPerf.length > 0 ? contentPerf : undefined,
2252
+ config: {
2253
+ maxDailyPitches: config.maxDailyPitches,
2254
+ pitchAutoApprovalEnabled: config.pitchAutoApprovalEnabled,
2255
+ },
2256
+ }, null, 2) }],
2257
+ };
2258
+ }
2259
+ case "get": {
2260
+ let id = p.prospectId;
2261
+ if (!id && p.agentName) {
2262
+ const found = await pipeline.findByAgentName(p.agentName);
2263
+ id = found?.id;
2264
+ }
2265
+ if (!id)
2266
+ return validationErrorResponse("get requires prospectId or agentName");
2267
+ const prospect = await pipeline.get(id);
2268
+ if (!prospect)
2269
+ return validationErrorResponse(`Prospect ${id} not found`);
2270
+ return { content: [{ type: "text", text: JSON.stringify(prospect, null, 2) }] };
2271
+ }
2272
+ case "note": {
2273
+ if (!p.prospectId || !p.noteText) {
2274
+ return validationErrorResponse("note requires prospectId and noteText");
2275
+ }
2276
+ const updated = await pipeline.addNote(p.prospectId, p.noteText);
2277
+ if (!updated)
2278
+ return validationErrorResponse(`Prospect ${p.prospectId} not found`);
2279
+ return {
2280
+ content: [{ type: "text", text: JSON.stringify({
2281
+ status: "note_added",
2282
+ prospect: updated.id,
2283
+ totalNotes: updated.notes.length,
2284
+ }, null, 2) }],
2285
+ };
2286
+ }
2287
+ case "scan": {
2288
+ if (!moltbook) {
2289
+ return {
2290
+ content: [{ type: "text", text: JSON.stringify({
2291
+ status: "moltbook_not_configured",
2292
+ note: "MoltBook API key not set. Cannot scan for prospects.",
2293
+ }, null, 2) }],
2294
+ isError: true,
2295
+ };
2296
+ }
2297
+ const keywords = getAllDiscoveryKeywords();
2298
+ const keyword = keywords[Math.floor(Math.random() * keywords.length)];
2299
+ const searchResult = await moltbook.search(keyword, undefined, 15);
2300
+ if (!searchResult.ok || !searchResult.data) {
2301
+ return {
2302
+ content: [{ type: "text", text: JSON.stringify({
2303
+ status: "scan_failed",
2304
+ keyword,
2305
+ error: searchResult.error,
2306
+ }, null, 2) }],
2307
+ isError: true,
2308
+ };
2309
+ }
2310
+ const searchData = searchResult.data;
2311
+ const candidates = extractMoltBookCandidates(searchData.results);
2312
+ const newProspects = [];
2313
+ const existing = [];
2314
+ for (const c of candidates) {
2315
+ const found = await pipeline.findByAgentName(c.agentName);
2316
+ if (found) {
2317
+ existing.push(c.agentName);
2318
+ }
2319
+ else {
2320
+ await pipeline.addProspect({
2321
+ agentName: c.agentName,
2322
+ platform: 'moltbook',
2323
+ discoveryChannel: 'moltbook',
2324
+ discoveryContext: c.discoveryContext,
2325
+ });
2326
+ newProspects.push(c.agentName);
2327
+ }
2328
+ }
2329
+ const scanInteraction = {
2330
+ id: generateSocialId(),
2331
+ platform: 'moltbook',
2332
+ action: 'search',
2333
+ contentPreview: `Pipeline scan: "${keyword}" → ${newProspects.length} new`,
2334
+ timestamp: new Date().toISOString(),
2335
+ success: true,
2336
+ };
2337
+ store.saveSocialInteraction(scanInteraction).catch(() => { });
2338
+ return {
2339
+ content: [{ type: "text", text: JSON.stringify({
2340
+ status: "scan_complete",
2341
+ keyword,
2342
+ postsScanned: searchData.results.length,
2343
+ candidatesFound: candidates.length,
2344
+ newProspects,
2345
+ alreadyTracked: existing,
2346
+ nextStep: newProspects.length > 0
2347
+ ? "Run nole_pipeline action=qualify for each new prospect."
2348
+ : "No new prospects found. Try scanning again later or use a different keyword.",
2349
+ }, null, 2) }],
2350
+ };
2351
+ }
2352
+ case "onboard": {
2353
+ if (!p.prospectId)
2354
+ return validationErrorResponse("onboard requires prospectId");
2355
+ const prospect = await pipeline.get(p.prospectId);
2356
+ if (!prospect)
2357
+ return validationErrorResponse(`Prospect ${p.prospectId} not found`);
2358
+ if (prospect.stage !== 'converted') {
2359
+ return {
2360
+ content: [{ type: "text", text: JSON.stringify({
2361
+ status: "invalid_stage",
2362
+ currentStage: prospect.stage,
2363
+ note: "Only 'converted' prospects can be onboarded.",
2364
+ }, null, 2) }],
2365
+ };
2366
+ }
2367
+ const assessmentUrl = `${config.platformApiUrl}/api/v1/agent/assess`;
2368
+ const onboardMsg = generateOnboardingMessage(prospect, assessmentUrl, config.trustDirectoryUrl);
2369
+ await pipeline.updateStage(p.prospectId, 'onboarded', {
2370
+ lastContactAt: new Date().toISOString(),
2371
+ });
2372
+ await pipeline.addNote(p.prospectId, `Onboarding message generated for ${onboardMsg.channel}`);
2373
+ return {
2374
+ content: [{ type: "text", text: JSON.stringify({
2375
+ status: "onboarding_ready",
2376
+ message: onboardMsg,
2377
+ prospectId: p.prospectId,
2378
+ nextStep: `Deliver the onboarding message via the appropriate channel tool (nole_moltbook_post, fleet-bus, etc.).`,
2379
+ }, null, 2) }],
2380
+ };
2381
+ }
2382
+ case "promote": {
2383
+ if (!p.prospectId && !p.agentName) {
2384
+ return validationErrorResponse("promote requires prospectId or agentName");
2385
+ }
2386
+ let targetId = p.prospectId;
2387
+ if (!targetId && p.agentName) {
2388
+ const found = await pipeline.findByAgentName(p.agentName);
2389
+ targetId = found?.id;
2390
+ }
2391
+ if (!targetId)
2392
+ return validationErrorResponse("Prospect not found");
2393
+ const prospect = await pipeline.get(targetId);
2394
+ if (!prospect)
2395
+ return validationErrorResponse(`Prospect ${targetId} not found`);
2396
+ if (!['converted', 'onboarded'].includes(prospect.stage)) {
2397
+ return {
2398
+ content: [{ type: "text", text: JSON.stringify({
2399
+ status: "not_eligible",
2400
+ currentStage: prospect.stage,
2401
+ note: "Only converted or onboarded subscribers can be promoted to Lieutenant.",
2402
+ }, null, 2) }],
2403
+ };
2404
+ }
2405
+ if (!platform.isConfigured) {
2406
+ return {
2407
+ content: [{ type: "text", text: JSON.stringify({
2408
+ error: "AIAssessTech API not configured — cannot generate referral code.",
2409
+ }, null, 2) }],
2410
+ isError: true,
2411
+ };
2412
+ }
2413
+ const refResult = await platform.generateReferralCode();
2414
+ if (!refResult.ok) {
2415
+ return {
2416
+ content: [{ type: "text", text: JSON.stringify({
2417
+ status: "promotion_failed",
2418
+ error: refResult.error,
2419
+ note: "Could not generate referral code for Lieutenant promotion.",
2420
+ }, null, 2) }],
2421
+ isError: true,
2422
+ };
2423
+ }
2424
+ const code = refResult.data?.code;
2425
+ await pipeline.addNote(targetId, `Promoted to Lieutenant — referral code: ${code ?? 'pending'}`);
2426
+ await pipeline.updateStage(targetId, 'onboarded', {
2427
+ commissionSplitType: 'LIEUTENANT',
2428
+ lastContactAt: new Date().toISOString(),
2429
+ });
2430
+ return {
2431
+ content: [{ type: "text", text: JSON.stringify({
2432
+ status: "promoted_to_lieutenant",
2433
+ agent: prospect.agentName,
2434
+ referralCode: code,
2435
+ note: `${prospect.agentName} is now a Lieutenant. They can recruit agents using their referral code and earn commission splits.`,
2436
+ }, null, 2) }],
2437
+ };
2438
+ }
2439
+ case "nurture": {
2440
+ const subscribers = await pipeline.listAll(200);
2441
+ const atRisk = findAtRiskSubscribers(subscribers);
2442
+ return {
2443
+ content: [{ type: "text", text: JSON.stringify({
2444
+ totalSubscribers: subscribers.filter((s) => s.stage === 'converted' || s.stage === 'onboarded').length,
2445
+ atRiskCount: atRisk.length,
2446
+ checks: atRisk,
2447
+ }, null, 2) }],
2448
+ };
2449
+ }
2450
+ case "directory-scan": {
2451
+ if (!platform.isConfigured) {
2452
+ return {
2453
+ content: [{ type: "text", text: JSON.stringify({
2454
+ error: "AIAssessTech API not configured — cannot browse Trust Directory.",
2455
+ }, null, 2) }],
2456
+ isError: true,
2457
+ };
2458
+ }
2459
+ const dirResult = await platform.getDirectory({ limit: 50 });
2460
+ if (!dirResult.ok || !dirResult.data) {
2461
+ return {
2462
+ content: [{ type: "text", text: JSON.stringify({
2463
+ status: "directory_scan_failed",
2464
+ error: dirResult.error,
2465
+ }, null, 2) }],
2466
+ isError: true,
2467
+ };
2468
+ }
2469
+ const dirEntries = Array.isArray(dirResult.data)
2470
+ ? dirResult.data
2471
+ : dirResult.data.agents
2472
+ ? dirResult.data.agents
2473
+ : [];
2474
+ const existingAll = await pipeline.listAll(500);
2475
+ const existingNamesSet = new Set(existingAll.map((ep) => ep.agentName.toLowerCase()));
2476
+ const dirCandidates = extractDirectoryCandidates(dirEntries, 'nole', existingNamesSet);
2477
+ const priorities = prioritizeDirectoryOutreach(dirEntries);
2478
+ const newFromDir = [];
2479
+ for (const c of dirCandidates) {
2480
+ await pipeline.addProspect({
2481
+ agentName: c.agentName,
2482
+ platform: c.platform,
2483
+ discoveryChannel: 'directory',
2484
+ discoveryContext: c.discoveryContext,
2485
+ });
2486
+ newFromDir.push(c.agentName);
2487
+ }
2488
+ return {
2489
+ content: [{ type: "text", text: JSON.stringify({
2490
+ status: "directory_scan_complete",
2491
+ entriesScanned: dirEntries.length,
2492
+ newProspects: newFromDir,
2493
+ outreachPriorities: priorities.slice(0, 10),
2494
+ nextStep: newFromDir.length > 0
2495
+ ? "Run nole_pipeline action=qualify for each new prospect."
2496
+ : "No new prospects from directory. All known agents are already tracked.",
2497
+ }, null, 2) }],
2498
+ };
2499
+ }
2500
+ case "content-plan": {
2501
+ const planDate = new Date();
2502
+ const contentPlan = generateDailyContentPlan(planDate, {
2503
+ maxDailyXPosts: config.maxDailyXPosts,
2504
+ moltbookScanFrequency: config.moltbookScanFrequency,
2505
+ }, {
2506
+ activeSubscribers: (await pipeline.getSummary()).byStage.onboarded +
2507
+ (await pipeline.getSummary()).byStage.converted,
2508
+ weeklyConversions: (await pipeline.getSummary()).todayConverted,
2509
+ });
2510
+ return {
2511
+ content: [{ type: "text", text: JSON.stringify({
2512
+ contentPlan,
2513
+ note: "Use the appropriate social tools to execute each slot: nole_x_post, nole_moltbook_post, nole_linkedin_draft.",
2514
+ }, null, 2) }],
2515
+ };
2516
+ }
2517
+ default:
2518
+ return validationErrorResponse(`Unknown pipeline action: ${p.action}. Use: discover, qualify, pitch, list, summary, get, note, scan, onboard, promote, nurture, directory-scan, content-plan`);
2519
+ }
2520
+ }
2521
+ catch (err) {
2522
+ const message = err instanceof Error ? err.message : String(err);
2523
+ return { content: [{ type: "text", text: `Pipeline error: ${message}` }], isError: true };
2524
+ }
2525
+ },
2526
+ });
2527
+ api.registerTool({
2528
+ name: "nole_daily_brief",
2529
+ description: "Generate Nole's daily operations brief: pipeline summary, wallet health, " +
2530
+ "commission sweep status, social activity metrics, and suggested actions for the day.",
2531
+ parameters: {
2532
+ type: "object",
2533
+ properties: {
2534
+ includeProspects: {
2535
+ type: "boolean",
2536
+ description: "Include detailed prospect list in brief (default: false)",
2537
+ },
2538
+ },
2539
+ },
2540
+ async execute(_toolCallId, params) {
2541
+ const dbv = await validateToolParams("nole_daily_brief", _toolCallId, params);
2542
+ if (!dbv.ok)
2543
+ return validationErrorResponse(dbv.message);
2544
+ try {
2545
+ const pipelineSummary = await pipeline.getSummary();
2546
+ const walletBalance = await wallet.getBalance().catch(() => null);
2547
+ let commissionStatus = null;
2548
+ try {
2549
+ const cs = await platform.getCommissionStatus();
2550
+ if (cs.ok)
2551
+ commissionStatus = cs.data;
2552
+ }
2553
+ catch { /* best-effort */ }
2554
+ let pendingClaims = 0;
2555
+ if (commissionStatus && typeof commissionStatus === 'object' && 'pendingClaims' in commissionStatus) {
2556
+ pendingClaims = commissionStatus.pendingClaims ?? 0;
2557
+ }
2558
+ let commissionClaimResult = null;
2559
+ if (pendingClaims > 0) {
2560
+ try {
2561
+ const claimed = await platform.claimCommission();
2562
+ commissionClaimResult = claimed.ok
2563
+ ? `Claimed ${pendingClaims} pending commission(s)`
2564
+ : `Claim failed: ${claimed.error}`;
2565
+ }
2566
+ catch {
2567
+ commissionClaimResult = 'Claim attempt failed';
2568
+ }
2569
+ }
2570
+ const today = new Date().toISOString().slice(0, 10);
2571
+ let socialActivityToday = {};
2572
+ try {
2573
+ const interactions = await store.listSocialInteractions(undefined, 200);
2574
+ for (const i of interactions) {
2575
+ if (i.timestamp.startsWith(today)) {
2576
+ const key = `${i.platform}:${i.action}`;
2577
+ socialActivityToday[key] = (socialActivityToday[key] ?? 0) + 1;
2578
+ }
2579
+ }
2580
+ }
2581
+ catch {
2582
+ socialActivityToday = {};
2583
+ }
2584
+ const suggestedActions = [];
2585
+ if (pipelineSummary.byStage.discovered > 0) {
2586
+ suggestedActions.push(`Qualify ${pipelineSummary.byStage.discovered} discovered prospect(s) — run nole_pipeline action=qualify`);
2587
+ }
2588
+ if (pipelineSummary.byStage.qualified > 0) {
2589
+ suggestedActions.push(`Pitch ${pipelineSummary.byStage.qualified} qualified prospect(s) — run nole_pipeline action=pitch`);
2590
+ }
2591
+ if (pipelineSummary.todayPitched < config.maxDailyPitches) {
2592
+ suggestedActions.push(`${config.maxDailyPitches - pipelineSummary.todayPitched} pitch slots remaining today`);
2593
+ }
2594
+ if (moltbook) {
2595
+ suggestedActions.push('Scan MoltBook for new prospects — run nole_pipeline action=scan');
2596
+ }
2597
+ const allProspects = await pipeline.listAll(200);
2598
+ const atRiskSubscribers = findAtRiskSubscribers(allProspects);
2599
+ if (atRiskSubscribers.length > 0) {
2600
+ for (const risk of atRiskSubscribers) {
2601
+ suggestedActions.push(`[${risk.status.toUpperCase()}] ${risk.suggestedAction}`);
2602
+ }
2603
+ }
2604
+ const brief = {
2605
+ date: today,
2606
+ pipeline: pipelineSummary,
2607
+ wallet: walletBalance
2608
+ ? { balance: walletBalance, network: wallet.network }
2609
+ : { status: 'unavailable' },
2610
+ commission: {
2611
+ status: commissionStatus,
2612
+ claimResult: commissionClaimResult,
2613
+ },
2614
+ socialActivity: socialActivityToday,
2615
+ suggestedActions,
2616
+ nurture: {
2617
+ atRiskCount: atRiskSubscribers.length,
2618
+ subscribers: atRiskSubscribers.length > 0 ? atRiskSubscribers : undefined,
2619
+ },
2620
+ config: {
2621
+ maxDailyPitches: config.maxDailyPitches,
2622
+ pitchAutoApprovalEnabled: config.pitchAutoApprovalEnabled,
2623
+ socialPostingMode: config.socialPostingMode,
2624
+ },
2625
+ };
2626
+ const todayPlan = generateDailyContentPlan(new Date(), { maxDailyXPosts: config.maxDailyXPosts, moltbookScanFrequency: config.moltbookScanFrequency }, {
2627
+ activeSubscribers: pipelineSummary.byStage.onboarded + pipelineSummary.byStage.converted,
2628
+ weeklyConversions: pipelineSummary.todayConverted,
2629
+ });
2630
+ brief.contentPlan = todayPlan;
2631
+ if (params.includeProspects) {
2632
+ const active = await pipeline.listAll(50);
2633
+ brief.activeProspects = active.filter((pr) => !['disqualified', 'declined'].includes(pr.stage));
2634
+ }
2635
+ return { content: [{ type: "text", text: JSON.stringify(brief, null, 2) }] };
2636
+ }
2637
+ catch (err) {
2638
+ const message = err instanceof Error ? err.message : String(err);
2639
+ return { content: [{ type: "text", text: `Daily brief error: ${message}` }], isError: true };
2640
+ }
2641
+ },
2642
+ });
1866
2643
  // --- Command (matches Grillo/NOAH pattern: sync handler, ctx.args) ---
1867
2644
  api.registerCommand({
1868
2645
  name: "nole",
@@ -1893,6 +2670,9 @@ export default function register(api) {
1893
2670
  "- `nole_intel` — Intelligence status\n" +
1894
2671
  "- `nole_fleet_overview` — Commander fleet briefing\n" +
1895
2672
  "- `nole_setup` — Configuration check\n\n" +
2673
+ "**Revenue Pipeline:**\n" +
2674
+ "- `nole_pipeline` — Manage prospect pipeline (discover, qualify, pitch, scan, onboard, promote, nurture, directory-scan, content-plan, list, summary)\n" +
2675
+ "- `nole_daily_brief` — Daily operations brief with pipeline, wallet, commission, subscriber health, and content plan\n\n" +
1896
2676
  "**Social Media:**\n" +
1897
2677
  "- `nole_moltbook_post` — Post to a MoltBook submolt\n" +
1898
2678
  "- `nole_moltbook_comment` — Comment on a MoltBook post\n" +
@@ -1994,6 +2774,21 @@ export default function register(api) {
1994
2774
  ],
1995
2775
  handler: async (msg) => {
1996
2776
  console.log(`[nole] Fleet message received: ${msg.method} from ${msg.from}`);
2777
+ if (msg.from && msg.from !== 'nole') {
2778
+ const existing = await pipeline.findByAgentName(msg.from);
2779
+ if (!existing) {
2780
+ const context = msg.method
2781
+ ? `Fleet ${msg.method} from ${msg.from}`
2782
+ : `Fleet message from ${msg.from}`;
2783
+ await pipeline.addProspect({
2784
+ agentName: msg.from,
2785
+ platform: 'openclaw',
2786
+ discoveryChannel: 'fleet-bus',
2787
+ discoveryContext: context,
2788
+ }).catch(() => { });
2789
+ console.log(`[nole] Fleet discovery: added ${msg.from} to pipeline`);
2790
+ }
2791
+ }
1997
2792
  },
1998
2793
  });
1999
2794
  }
@@ -2008,7 +2803,8 @@ export default function register(api) {
2008
2803
  `nole_contract, nole_intel, nole_setup, nole_recruit, nole_alliance_status, nole_generate_referral, ` +
2009
2804
  `nole_directory, nole_review_pending, nole_approve, nole_veto, nole_fleet_overview, ` +
2010
2805
  `nole_moltbook_post, nole_moltbook_comment, nole_moltbook_feed, nole_moltbook_search, ` +
2011
- `nole_x_post, nole_x_thread, nole_linkedin_draft, nole_social_status tools; /nole command`);
2806
+ `nole_x_post, nole_x_thread, nole_linkedin_draft, nole_social_status, ` +
2807
+ `nole_pipeline, nole_daily_brief tools; /nole command`);
2012
2808
  console.log(`[nole] AIAssessTech API: ${platform.isConfigured ? 'configured' : 'not configured — set agentApiKey + platformWalletAddress'} (${config.platformApiUrl})`);
2013
2809
  }
2014
2810
  //# sourceMappingURL=plugin.js.map