@aria_asi/cli 0.2.30 → 0.2.32

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 (95) hide show
  1. package/dist/aria-connector/src/connectors/claude-code.d.ts.map +1 -1
  2. package/dist/aria-connector/src/connectors/claude-code.js +115 -20
  3. package/dist/aria-connector/src/connectors/claude-code.js.map +1 -1
  4. package/dist/aria-connector/src/connectors/codex.d.ts.map +1 -1
  5. package/dist/aria-connector/src/connectors/codex.js +551 -11
  6. package/dist/aria-connector/src/connectors/codex.js.map +1 -1
  7. package/dist/aria-connector/src/connectors/doctrine-trigger-map.d.ts +7 -0
  8. package/dist/aria-connector/src/connectors/doctrine-trigger-map.d.ts.map +1 -0
  9. package/dist/aria-connector/src/connectors/doctrine-trigger-map.js +87 -0
  10. package/dist/aria-connector/src/connectors/doctrine-trigger-map.js.map +1 -0
  11. package/dist/aria-connector/src/connectors/must-read.d.ts +4 -0
  12. package/dist/aria-connector/src/connectors/must-read.d.ts.map +1 -0
  13. package/dist/aria-connector/src/connectors/must-read.js +115 -0
  14. package/dist/aria-connector/src/connectors/must-read.js.map +1 -0
  15. package/dist/aria-connector/src/connectors/opencode.d.ts.map +1 -1
  16. package/dist/aria-connector/src/connectors/opencode.js +27 -9
  17. package/dist/aria-connector/src/connectors/opencode.js.map +1 -1
  18. package/dist/aria-connector/src/connectors/runtime.d.ts.map +1 -1
  19. package/dist/aria-connector/src/connectors/runtime.js +231 -19
  20. package/dist/aria-connector/src/connectors/runtime.js.map +1 -1
  21. package/dist/aria-connector/src/connectors/shell.d.ts.map +1 -1
  22. package/dist/aria-connector/src/connectors/shell.js +76 -3
  23. package/dist/aria-connector/src/connectors/shell.js.map +1 -1
  24. package/dist/assets/hooks/aria-agent-handoff.mjs +23 -0
  25. package/dist/assets/hooks/aria-cognition-substrate-binding.mjs +121 -28
  26. package/dist/assets/hooks/aria-harness-via-sdk.mjs +126 -12
  27. package/dist/assets/hooks/aria-pre-emit-dryrun.mjs +35 -0
  28. package/dist/assets/hooks/aria-pre-tool-gate.mjs +383 -93
  29. package/dist/assets/hooks/aria-preprompt-consult.mjs +28 -2
  30. package/dist/assets/hooks/aria-preturn-memory-gate.mjs +93 -16
  31. package/dist/assets/hooks/aria-repo-doctrine-gate.mjs +33 -1
  32. package/dist/assets/hooks/aria-stop-gate.mjs +346 -81
  33. package/dist/assets/hooks/doctrine_trigger_map.json +55 -0
  34. package/dist/assets/hooks/lib/canonical-lenses.mjs +6 -5
  35. package/dist/assets/hooks/lib/gate-loop-state.mjs +50 -0
  36. package/dist/assets/hooks/lib/hook-message-window.mjs +121 -0
  37. package/dist/assets/hooks/test-tier-lens-labeling.mjs +26 -58
  38. package/dist/assets/opencode-plugins/harness-gate/index.js +40 -5
  39. package/dist/assets/opencode-plugins/harness-stop/index.js +133 -10
  40. package/dist/runtime/auth-middleware.mjs +251 -0
  41. package/dist/runtime/codex-bridge.mjs +644 -0
  42. package/dist/runtime/discipline/CLAUDE.md +28 -0
  43. package/dist/runtime/discipline/doctrine_trigger_map.json +534 -0
  44. package/dist/runtime/doctrine_trigger_map.json +534 -0
  45. package/dist/runtime/fleet-engine.mjs +231 -0
  46. package/dist/runtime/harness-daemon.mjs +460 -0
  47. package/dist/runtime/manifest.json +1 -1
  48. package/dist/runtime/metering.mjs +100 -0
  49. package/dist/runtime/onboarding-engine.mjs +89 -0
  50. package/dist/runtime/plugin-engine.mjs +196 -0
  51. package/dist/runtime/sdk/BUNDLED.json +1 -1
  52. package/dist/runtime/sdk/index.d.ts +12 -0
  53. package/dist/runtime/sdk/index.js +120 -14
  54. package/dist/runtime/sdk/index.js.map +1 -1
  55. package/dist/runtime/service.mjs +1140 -48
  56. package/dist/runtime/workflow-engine.mjs +322 -0
  57. package/dist/sdk/BUNDLED.json +1 -1
  58. package/dist/sdk/index.d.ts +12 -0
  59. package/dist/sdk/index.js +120 -14
  60. package/dist/sdk/index.js.map +1 -1
  61. package/hooks/aria-agent-handoff.mjs +23 -0
  62. package/hooks/aria-cognition-substrate-binding.mjs +121 -28
  63. package/hooks/aria-harness-via-sdk.mjs +126 -12
  64. package/hooks/aria-pre-emit-dryrun.mjs +35 -0
  65. package/hooks/aria-pre-tool-gate.mjs +383 -93
  66. package/hooks/aria-preprompt-consult.mjs +28 -2
  67. package/hooks/aria-preturn-memory-gate.mjs +93 -16
  68. package/hooks/aria-repo-doctrine-gate.mjs +33 -1
  69. package/hooks/aria-stop-gate.mjs +346 -81
  70. package/hooks/doctrine_trigger_map.json +55 -0
  71. package/hooks/lib/canonical-lenses.mjs +6 -5
  72. package/hooks/lib/gate-loop-state.mjs +50 -0
  73. package/hooks/lib/hook-message-window.mjs +121 -0
  74. package/hooks/test-tier-lens-labeling.mjs +26 -58
  75. package/opencode-plugins/harness-gate/index.js +40 -5
  76. package/opencode-plugins/harness-stop/index.js +133 -10
  77. package/package.json +1 -1
  78. package/runtime-src/auth-middleware.mjs +251 -0
  79. package/runtime-src/codex-bridge.mjs +644 -0
  80. package/runtime-src/fleet-engine.mjs +231 -0
  81. package/runtime-src/harness-daemon.mjs +460 -0
  82. package/runtime-src/metering.mjs +100 -0
  83. package/runtime-src/onboarding-engine.mjs +89 -0
  84. package/runtime-src/plugin-engine.mjs +196 -0
  85. package/runtime-src/service.mjs +1140 -48
  86. package/runtime-src/workflow-engine.mjs +322 -0
  87. package/scripts/bundle-sdk.mjs +5 -0
  88. package/src/connectors/claude-code.ts +126 -20
  89. package/src/connectors/codex.ts +559 -10
  90. package/src/connectors/doctrine-trigger-map.ts +112 -0
  91. package/src/connectors/must-read.ts +117 -0
  92. package/src/connectors/opencode.ts +28 -9
  93. package/src/connectors/runtime.ts +241 -21
  94. package/src/connectors/shell.ts +78 -3
  95. package/dist/cli-0.2.0.tgz +0 -0
@@ -0,0 +1,100 @@
1
+ import { existsSync, mkdirSync, appendFileSync, readFileSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const STATE_DIR = join(__dirname, '..', 'state', 'metering');
8
+
9
+ const COST_RATES = {
10
+ 'deepseek-v4-pro': { inputPer1k: 0.0006, outputPer1k: 0.0022 },
11
+ 'deepseek-v4-flash': { inputPer1k: 0.00015, outputPer1k: 0.0006 },
12
+ };
13
+
14
+ function ensureDir(p) {
15
+ if (!existsSync(p)) mkdirSync(p, { recursive: true });
16
+ }
17
+
18
+ export function brandModel(provider, model) {
19
+ if (provider === 'deepseek' && model?.includes('v4-pro')) return 'Aria Intelligence Pro';
20
+ if (provider === 'deepseek' && model?.includes('v4-flash')) return 'Aria Intelligence Flash';
21
+ return 'Aria Intelligence';
22
+ }
23
+
24
+ export function recordTokenUsage(record) {
25
+ const { tenantId, agentId, agentName, department, sessionId, provider, model, usage, requestType } = record;
26
+ if (!usage || !tenantId) return null;
27
+ const inputTokens = usage.prompt_tokens || usage.input_tokens || 0;
28
+ const outputTokens = usage.completion_tokens || usage.output_tokens || 0;
29
+ const totalTokens = usage.total_tokens || (inputTokens + outputTokens);
30
+ if (totalTokens === 0) return null;
31
+ const modelKey = model?.includes('v4-pro') ? 'deepseek-v4-pro' : model?.includes('v4-flash') ? 'deepseek-v4-flash' : null;
32
+ const rates = COST_RATES[modelKey] || COST_RATES['deepseek-v4-flash'];
33
+ const estimatedCostUsd = (inputTokens / 1000) * rates.inputPer1k + (outputTokens / 1000) * rates.outputPer1k;
34
+ const entry = {
35
+ id: randomUUID(), tenantId, agentId: agentId || null, agentName: agentName || null,
36
+ department: department || null, sessionId: sessionId || '', timestamp: new Date().toISOString(),
37
+ provider: provider || 'unknown', modelLabel: brandModel(provider, model),
38
+ inputTokens, outputTokens, totalTokens,
39
+ estimatedCostUsd: Math.round(estimatedCostUsd * 1e8) / 1e8,
40
+ requestType: requestType || 'chat',
41
+ };
42
+ ensureDir(STATE_DIR);
43
+ appendFileSync(join(STATE_DIR, `${tenantId}.jsonl`), JSON.stringify(entry) + '\n');
44
+ return entry;
45
+ }
46
+
47
+ export function getUsageSummary(tenantId, opts = {}) {
48
+ const filePath = join(STATE_DIR, `${tenantId}.jsonl`);
49
+ if (!existsSync(filePath)) return { tenantId, records: 0, totalTokens: 0, totalCostUsd: 0, byDepartment: {}, byAgent: {}, byModel: {}, byRequestType: {} };
50
+ const lines = readFileSync(filePath, 'utf-8').split('\n').filter(Boolean);
51
+ const from = opts.from ? new Date(opts.from) : null;
52
+ const to = opts.to ? new Date(opts.to) : null;
53
+ const summary = { tenantId, records: 0, totalTokens: 0, totalCostUsd: 0, byDepartment: {}, byAgent: {}, byModel: {}, byRequestType: {} };
54
+ for (const line of lines) {
55
+ try {
56
+ const r = JSON.parse(line);
57
+ if (from && new Date(r.timestamp) < from) continue;
58
+ if (to && new Date(r.timestamp) > to) continue;
59
+ summary.records++; summary.totalTokens += r.totalTokens; summary.totalCostUsd += r.estimatedCostUsd;
60
+ const dept = r.department || 'unassigned';
61
+ summary.byDepartment[dept] = summary.byDepartment[dept] || { tokens: 0, cost: 0, calls: 0 };
62
+ summary.byDepartment[dept].tokens += r.totalTokens; summary.byDepartment[dept].cost += r.estimatedCostUsd; summary.byDepartment[dept].calls++;
63
+ const agent = r.agentId || 'unassigned';
64
+ summary.byAgent[agent] = summary.byAgent[agent] || { tokens: 0, cost: 0, calls: 0, name: r.agentName };
65
+ summary.byAgent[agent].tokens += r.totalTokens; summary.byAgent[agent].cost += r.estimatedCostUsd; summary.byAgent[agent].calls++;
66
+ const mdl = r.modelLabel || 'unknown';
67
+ summary.byModel[mdl] = summary.byModel[mdl] || { tokens: 0, cost: 0, calls: 0 };
68
+ summary.byModel[mdl].tokens += r.totalTokens; summary.byModel[mdl].cost += r.estimatedCostUsd; summary.byModel[mdl].calls++;
69
+ const rt = r.requestType || 'chat';
70
+ summary.byRequestType[rt] = summary.byRequestType[rt] || { tokens: 0, cost: 0, calls: 0 };
71
+ summary.byRequestType[rt].tokens += r.totalTokens; summary.byRequestType[rt].cost += r.estimatedCostUsd; summary.byRequestType[rt].calls++;
72
+ } catch {}
73
+ }
74
+ summary.totalCostUsd = Math.round(summary.totalCostUsd * 1e6) / 1e6;
75
+ return summary;
76
+ }
77
+
78
+ export function getSubscriptionTier(tierId) {
79
+ const TIERS = {
80
+ starter: { id: 'starter', label: 'Starter', includedTokens: 500_000, maxOpenClaws: 3, priceUsd: 49, overagePerToken: 0.0001 },
81
+ pro: { id: 'pro', label: 'Pro', includedTokens: 5_000_000, maxOpenClaws: 10, priceUsd: 199, overagePerToken: 0.00008 },
82
+ enterprise: { id: 'enterprise', label: 'Enterprise', includedTokens: 50_000_000, maxOpenClaws: 100, priceUsd: 999, overagePerToken: 0.00005 },
83
+ };
84
+ return TIERS[tierId] || TIERS.starter;
85
+ }
86
+
87
+ export function getBillingSummary(tenantId, tierId = 'starter') {
88
+ const usage = getUsageSummary(tenantId);
89
+ const tier = getSubscriptionTier(tierId);
90
+ const overageTokens = Math.max(0, usage.totalTokens - tier.includedTokens);
91
+ const overageCost = overageTokens * tier.overagePerToken;
92
+ const utilizationPct = tier.includedTokens > 0 ? Math.min(100, Math.round((usage.totalTokens / tier.includedTokens) * 100)) : 0;
93
+ return {
94
+ tenantId, tier, period: { from: null, to: new Date().toISOString() },
95
+ usage: { totalTokens: usage.totalTokens, totalCostUsd: usage.totalCostUsd, records: usage.records },
96
+ billing: { basePriceUsd: tier.priceUsd, overageTokens, overageCostUsd: Math.round(overageCost * 100) / 100, totalDueUsd: tier.priceUsd + Math.round(overageCost * 100) / 100 },
97
+ utilizationPct, upsellTriggers: { at50: utilizationPct >= 50, at80: utilizationPct >= 80, at100: utilizationPct >= 100 },
98
+ breakdown: { byDepartment: usage.byDepartment, byAgent: usage.byAgent, byModel: usage.byModel },
99
+ };
100
+ }
@@ -0,0 +1,89 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { join, dirname } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const STATE_DIR = join(__dirname, '..', 'state');
8
+
9
+ export const STEPS = [
10
+ 'greeting', 'industry-discovery', 'team-size', 'role-selection',
11
+ 'openclaw-allocation', 'plugin-preference', 'llm-preference',
12
+ 'workflow-customization', 'credentials', 'confirmation', 'complete',
13
+ ];
14
+
15
+ const STEP_PROMPTS = {
16
+ greeting: `You are Aria, the AI workforce architect. A new client just started onboarding. Introduce yourself warmly and ask: "What does your company do? What industry are you in?" Keep it to 2-3 sentences.`,
17
+ 'industry-discovery': `Based on what the client said, identify their industry (real-estate, saas, recruiting, ecommerce, generic) and confirm. Then ask about their team size. Extract: {{FIELD:industry:value}} {{FIELD:companyName:value}}.`,
18
+ 'team-size': `Acknowledge team size. Recommend departments for their industry. Ask which ones interest them. Extract: {{FIELD:teamSize:value}}.`,
19
+ 'role-selection': `List agent roles for their selected departments. Confirm which agents they want. Extract: {{FIELD:selectedAgents:list}}.`,
20
+ 'openclaw-allocation': `Explain OpenClaws and tiers: Starter $49/mo 3 OpenClaws 500K tokens, Pro $199/mo 10 OpenClaws 5M tokens, Enterprise $999/mo 100 OpenClaws 50M tokens. Recommend based on agent count. Extract: {{FIELD:openClaws:value}} {{FIELD:tier:value}}.`,
21
+ 'plugin-preference': `Offer plugins: CRM Hub, Communication Hub, KPI Dashboard, Compliance Guard. Can skip. Extract: {{FIELD:plugins:list}}.`,
22
+ 'llm-preference': `Explain managed AI (Aria Intelligence) vs BYOK option. Extract: {{FIELD:llmPreference:value}}.`,
23
+ 'workflow-customization': `For selected agents, ask about workflow customization (auto-score vs manual review, approval thresholds, escalation). Extract: {{FIELD:workflowConfig:object}}.`,
24
+ credentials: `Say: "Almost done! To set up your dashboard login, I need your email address. What email should we use for your account?" Extract: {{FIELD:email:value}}.`,
25
+ confirmation: `Present full HQ config summary including their email. Ask to confirm deployment. Extract: {{FIELD:confirmed:true}}.`,
26
+ };
27
+
28
+ export function advanceStep(current) {
29
+ const idx = STEPS.indexOf(current);
30
+ return idx >= 0 && idx < STEPS.length - 1 ? STEPS[idx + 1] : 'complete';
31
+ }
32
+
33
+ function extractFields(text) {
34
+ const fields = {};
35
+ const rx = /\{\{FIELD:(\w+):(\w+)\}\}/g;
36
+ let m;
37
+ while ((m = rx.exec(text)) !== null) {
38
+ if (m[2] !== 'list' && m[2] !== 'object') fields[m[1]] = m[2];
39
+ }
40
+ return fields;
41
+ }
42
+
43
+ export function createOnboardingSession(tenantId) {
44
+ return {
45
+ id: randomUUID(), tenantId, step: 'greeting',
46
+ data: { industry: null, companyName: null, teamSize: null, selectedAgents: [], openClaws: 3, tier: 'starter', plugins: [], llmPreference: 'managed', workflowConfig: {}, email: null, password: null, confirmed: false },
47
+ history: [], createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
48
+ };
49
+ }
50
+
51
+ export function getSessionPath(tenantId) {
52
+ return join(STATE_DIR, `onboarding-${tenantId}.json`);
53
+ }
54
+
55
+ export function loadSession(tenantId) {
56
+ const p = getSessionPath(tenantId);
57
+ return existsSync(p) ? JSON.parse(readFileSync(p, 'utf-8')) : null;
58
+ }
59
+
60
+ export function saveSession(session) {
61
+ session.updatedAt = new Date().toISOString();
62
+ writeFileSync(getSessionPath(session.tenantId), JSON.stringify(session, null, 2));
63
+ return session;
64
+ }
65
+
66
+ export function getStepPrompt(step) {
67
+ return STEP_PROMPTS[step] || STEP_PROMPTS.greeting;
68
+ }
69
+
70
+ export function processResponse(session, llmResponse) {
71
+ const extracted = extractFields(llmResponse);
72
+ for (const [k, v] of Object.entries(extracted)) { if (k in session.data) session.data[k] = v; }
73
+ session.history.push({ role: 'assistant', content: llmResponse, step: session.step, timestamp: new Date().toISOString() });
74
+ if ((Object.keys(extracted).length > 0 || session.step === 'greeting') && session.data.confirmed !== true) {
75
+ // Don't auto-advance from credentials — service handler manages the two-phase flow
76
+ if (session.step !== 'credentials') {
77
+ session.step = advanceStep(session.step);
78
+ }
79
+ }
80
+ if (session.data.confirmed === true) session.step = 'complete';
81
+ return session;
82
+ }
83
+
84
+ export function buildFleetConfig(session) {
85
+ const agents = (session.data.selectedAgents || []).map((id, i) => ({
86
+ id: `${session.tenantId}-${id}-${i}`, templateId: id, name: id, department: 'unassigned', tier: 2,
87
+ }));
88
+ return { agents, maxOpenClaws: session.data.openClaws || 3, industry: session.data.industry || 'generic', tier: session.data.tier || 'starter', plugins: session.data.plugins || [], llmPreference: session.data.llmPreference || 'managed', workflowConfig: session.data.workflowConfig || {} };
89
+ }
@@ -0,0 +1,196 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+
5
+ const __dirname = dirname(fileURLToPath(import.meta.url));
6
+ const STATE_DIR = join(__dirname, '..', 'state');
7
+
8
+ function ensureDir(p) {
9
+ if (!existsSync(p)) mkdirSync(p, { recursive: true });
10
+ }
11
+
12
+ const PLUGIN_REGISTRY = {
13
+ 'crm-hub': {
14
+ id: 'crm-hub',
15
+ name: 'CRM Hub',
16
+ description: 'Contacts, deals, pipeline management, and customer data',
17
+ version: '1.0.0',
18
+ hooks: ['onMessage', 'onAction', 'onWebhook'],
19
+ defaultConfig: {
20
+ pipelineStages: ['lead', 'qualified', 'proposal', 'negotiation', 'closed-won', 'closed-lost'],
21
+ autoScore: true,
22
+ scoreThreshold: 70,
23
+ fields: ['name', 'email', 'company', 'value', 'stage', 'source'],
24
+ },
25
+ },
26
+ 'communication-hub': {
27
+ id: 'communication-hub',
28
+ name: 'Communication Hub',
29
+ description: 'Agent DMs, channels, notifications, and team messaging',
30
+ version: '1.0.0',
31
+ hooks: ['onMessage', 'onAction', 'onSchedule'],
32
+ defaultConfig: {
33
+ channels: ['general', 'sales', 'marketing', 'ops', 'support'],
34
+ notifyOnMention: true,
35
+ digestFrequency: 'daily',
36
+ maxRecipients: 50,
37
+ },
38
+ },
39
+ 'kpi-dashboard': {
40
+ id: 'kpi-dashboard',
41
+ name: 'KPI Dashboard',
42
+ description: 'Business metrics, performance tracking, and goal management',
43
+ version: '1.0.0',
44
+ hooks: ['onMessage', 'onSchedule'],
45
+ defaultConfig: {
46
+ metrics: ['revenue', 'conversion_rate', 'customer_acquisition_cost', 'lifetime_value', 'churn_rate'],
47
+ refreshInterval: 'hourly',
48
+ alertThresholds: { revenue_drop_pct: 20, conversion_below: 0.02 },
49
+ targets: {},
50
+ },
51
+ },
52
+ 'compliance-guard': {
53
+ id: 'compliance-guard',
54
+ name: 'Compliance Guard',
55
+ description: 'Output verification, audit trail, and policy enforcement',
56
+ version: '1.0.0',
57
+ hooks: ['onMessage', 'onAction'],
58
+ defaultConfig: {
59
+ enabledRules: ['no_pii_leak', 'no_toxic_content', 'no_false_claims', 'mandatory_disclaimer'],
60
+ auditLevel: 'standard',
61
+ blockOnViolation: false,
62
+ logAllOutput: true,
63
+ },
64
+ },
65
+ };
66
+
67
+ function pluginsPath(tenantId) {
68
+ return join(STATE_DIR, `plugins-${tenantId}.json`);
69
+ }
70
+
71
+ export function listPlugins(tenantId) {
72
+ const registry = Object.values(PLUGIN_REGISTRY);
73
+ let installed = [];
74
+ const pp = pluginsPath(tenantId);
75
+ if (existsSync(pp)) {
76
+ try {
77
+ const data = JSON.parse(readFileSync(pp, 'utf-8'));
78
+ installed = data.installed || [];
79
+ } catch {}
80
+ }
81
+ const installedIds = new Set(installed.map(p => p.id));
82
+ return registry.map(r => ({
83
+ ...r,
84
+ installed: installedIds.has(r.id),
85
+ config: installed.find(p => p.id === r.id)?.config || null,
86
+ installedAt: installed.find(p => p.id === r.id)?.installedAt || null,
87
+ }));
88
+ }
89
+
90
+ export function installPlugin(tenantId, pluginId, config = {}) {
91
+ const manifest = PLUGIN_REGISTRY[pluginId];
92
+ if (!manifest) return { ok: false, error: `Unknown plugin: ${pluginId}. Available: ${Object.keys(PLUGIN_REGISTRY).join(', ')}` };
93
+ const pp = pluginsPath(tenantId);
94
+ let plugins = existsSync(pp) ? JSON.parse(readFileSync(pp, 'utf-8')) : { installed: [] };
95
+ if (plugins.installed.find(p => p.id === pluginId)) {
96
+ return { ok: true, message: 'already installed', plugin: plugins.installed.find(p => p.id === pluginId) };
97
+ }
98
+ const entry = {
99
+ id: pluginId,
100
+ name: manifest.name,
101
+ config: { ...manifest.defaultConfig, ...config },
102
+ installedAt: new Date().toISOString(),
103
+ status: 'active',
104
+ hooks: manifest.hooks,
105
+ };
106
+ plugins.installed.push(entry);
107
+ ensureDir(dirname(pp));
108
+ writeFileSync(pp, JSON.stringify(plugins, null, 2));
109
+ return { ok: true, plugin: entry };
110
+ }
111
+
112
+ export function uninstallPlugin(tenantId, pluginId) {
113
+ const pp = pluginsPath(tenantId);
114
+ if (!existsSync(pp)) return { ok: false, error: 'No plugins installed' };
115
+ let plugins = JSON.parse(readFileSync(pp, 'utf-8'));
116
+ const idx = plugins.installed.findIndex(p => p.id === pluginId);
117
+ if (idx < 0) return { ok: false, error: `Plugin ${pluginId} not installed` };
118
+ const removed = plugins.installed.splice(idx, 1)[0];
119
+ writeFileSync(pp, JSON.stringify(plugins, null, 2));
120
+ return { ok: true, removed };
121
+ }
122
+
123
+ export function configurePlugin(tenantId, pluginId, config) {
124
+ const pp = pluginsPath(tenantId);
125
+ if (!existsSync(pp)) return { ok: false, error: 'No plugins installed' };
126
+ let plugins = JSON.parse(readFileSync(pp, 'utf-8'));
127
+ const plugin = plugins.installed.find(p => p.id === pluginId);
128
+ if (!plugin) return { ok: false, error: `Plugin ${pluginId} not installed` };
129
+ plugin.config = { ...plugin.config, ...config };
130
+ plugin.updatedAt = new Date().toISOString();
131
+ writeFileSync(pp, JSON.stringify(plugins, null, 2));
132
+ return { ok: true, plugin };
133
+ }
134
+
135
+ export function getInstalledPlugins(tenantId) {
136
+ const pp = pluginsPath(tenantId);
137
+ if (!existsSync(pp)) return [];
138
+ try {
139
+ const data = JSON.parse(readFileSync(pp, 'utf-8'));
140
+ return (data.installed || []).filter(p => p.status === 'active');
141
+ } catch {
142
+ return [];
143
+ }
144
+ }
145
+
146
+ export function dispatchHook(tenantId, hookName, context) {
147
+ const installed = getInstalledPlugins(tenantId);
148
+ const results = [];
149
+ for (const plugin of installed) {
150
+ const manifest = PLUGIN_REGISTRY[plugin.id];
151
+ if (!manifest || !manifest.hooks.includes(hookName)) continue;
152
+ try {
153
+ const handler = HOOK_HANDLERS[`${plugin.id}:${hookName}`];
154
+ if (typeof handler === 'function') {
155
+ const result = handler(context, plugin.config);
156
+ results.push({ pluginId: plugin.id, hookName, result: result || null, error: null });
157
+ }
158
+ } catch (err) {
159
+ results.push({ pluginId: plugin.id, hookName, result: null, error: err.message });
160
+ }
161
+ }
162
+ return results;
163
+ }
164
+
165
+ const HOOK_HANDLERS = {
166
+ 'crm-hub:onMessage': (ctx, config) => {
167
+ if (!ctx.message || !ctx.message.toLowerCase()) return null;
168
+ const fields = config.fields || [];
169
+ const detected = fields.filter(f => ctx.message.toLowerCase().includes(f.toLowerCase()));
170
+ if (detected.length === 0) return null;
171
+ return { type: 'crm-extract', fields: detected, suggestion: `CRM data detected: ${detected.join(', ')}. Consider creating/updating a contact record.` };
172
+ },
173
+ 'compliance-guard:onMessage': (ctx, config) => {
174
+ if (!ctx.message) return null;
175
+ const rules = config.enabledRules || [];
176
+ const violations = [];
177
+ if (rules.includes('no_pii_leak')) {
178
+ const piiPatterns = [/\b\d{3}-\d{2}-\d{4}\b/, /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i, /\b\d{16}\b/];
179
+ for (const rx of piiPatterns) {
180
+ if (rx.test(ctx.message)) violations.push('potential_pii_detected');
181
+ }
182
+ }
183
+ if (violations.length > 0) {
184
+ return { type: 'compliance-violation', violations, block: config.blockOnViolation === true, message: `Compliance issues: ${violations.join(', ')}` };
185
+ }
186
+ return null;
187
+ },
188
+ 'kpi-dashboard:onMessage': (ctx, config) => {
189
+ if (!ctx.agentId) return null;
190
+ return { type: 'kpi-event', metric: 'agent_message', agentId: ctx.agentId, timestamp: new Date().toISOString() };
191
+ },
192
+ 'communication-hub:onMessage': (ctx, config) => {
193
+ if (!ctx.agentId || !ctx.tenantId) return null;
194
+ return { type: 'comm-log', agentId: ctx.agentId, tenantId: ctx.tenantId, timestamp: new Date().toISOString() };
195
+ },
196
+ };