@cgh567/agent 2.4.2 → 2.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.
- package/agents/business/talisman-ceo.md +183 -0
- package/agents/business/talisman-comms.md +257 -0
- package/agents/business/talisman-cto.md +153 -0
- package/agents/business/talisman-finance.md +246 -0
- package/agents/business/talisman-marketing.md +240 -0
- package/agents/business/talisman-sales.md +242 -0
- package/agents/business/talisman-support.md +236 -0
- package/bin/helios-rpc.js +19 -0
- package/daemon/adapters/helios-rpc-adapter.js +5 -12
- package/daemon/adapters/tui_wakeup.js +8 -0
- package/daemon/context-enrichment.js +27 -0
- package/daemon/daemon-manager.js +1 -1
- package/daemon/db/email-infrastructure-migrate.js +192 -0
- package/daemon/db/hbo-core-migrate.js +189 -0
- package/daemon/helios-api.js +863 -64
- package/daemon/helios-company-daemon.js +233 -33
- package/daemon/lib/blast-radius-analyzer.js +75 -0
- package/daemon/lib/domain-bootstrap-orchestrator.js +267 -0
- package/daemon/lib/forensic-log.js +113 -0
- package/daemon/lib/goal-research-pipeline.js +644 -0
- package/daemon/lib/harada/cascade-judge.js +84 -1
- package/daemon/lib/harada/cascade-research-dispatcher.js +282 -0
- package/daemon/lib/harada/pillar-dispatcher.js +23 -2
- package/daemon/lib/hbo-bridge.js +74 -6
- package/daemon/lib/headroom-middleware.js +129 -0
- package/daemon/lib/headroom-proxy-manager.js +309 -0
- package/daemon/lib/hed-engine.js +25 -0
- package/daemon/lib/intelligence/department-page-generator.js +46 -1
- package/daemon/lib/interpretation-engine.js +92 -0
- package/daemon/lib/mental-model-cache.js +96 -0
- package/daemon/lib/project-factory.js +47 -0
- package/daemon/lib/session-log-reader.js +93 -0
- package/daemon/lib/standard-work-bootstrap.js +87 -1
- package/daemon/lib/task-completion-processor.js +23 -0
- package/daemon/lib/wizard-engine.js +57 -6
- package/daemon/package.json +2 -1
- package/daemon/routes/agents.js +51 -6
- package/daemon/routes/channels.js +116 -2
- package/daemon/routes/crm.js +85 -0
- package/daemon/routes/dashboard.js +62 -16
- package/daemon/routes/dept.js +10 -1
- package/daemon/routes/email-triage.js +19 -10
- package/daemon/routes/hbo.js +618 -58
- package/daemon/routes/hed.js +133 -0
- package/daemon/routes/inbox.js +397 -8
- package/daemon/routes/project.js +580 -66
- package/daemon/routes/routines.js +14 -0
- package/daemon/routes/tasks.js +15 -1
- package/daemon/schema-apply.js +174 -0
- package/daemon/schema-definitions.js +433 -0
- package/daemon/schema-migrations-hbo.js +20 -0
- package/daemon/schema-migrations-hed.js +18 -0
- package/daemon/schema-migrations-proj.js +153 -0
- package/extensions/__tests__/codebase-index.test.ts +73 -0
- package/extensions/__tests__/extension-command-registration.test.ts +35 -0
- package/extensions/__tests__/git-push-guard.test.ts +68 -0
- package/extensions/context-compaction.ts +104 -76
- package/extensions/cortex/__tests__/cortex-core.test.ts +100 -0
- package/extensions/cortex/wal-replay.ts +91 -0
- package/extensions/email/actions/draft-response.ts +21 -1
- package/extensions/email/auth/accounts.ts +5 -11
- package/extensions/email/auth/inbox-dog.ts +5 -2
- package/extensions/email/backfill.ts +20 -13
- package/extensions/email/providers/gmail.ts +164 -0
- package/extensions/email/providers/google-calendar.ts +34 -5
- package/extensions/helios-browser/__tests__/browser-routing.test.ts +57 -0
- package/extensions/helios-browser/backends/playwright.ts +3 -1
- package/extensions/helios-governance/__tests__/governance-gates.test.ts +40 -0
- package/extensions/helios-governance/__tests__/tournament-consumer.test.js +66 -0
- package/extensions/hema-dispatch-v3/headroom-compress.ts +103 -0
- package/extensions/hema-dispatch-v3/index.ts +46 -72
- package/extensions/interview/__tests__/server.test.ts +117 -0
- package/extensions/lib/helios-root.cjs +46 -0
- package/extensions/subagent-mesh/__tests__/handlers.test.ts +98 -0
- package/extensions/warm-tick/warm-tick-maintenance.ts +164 -0
- package/lib/__tests__/bulk-ingest.live.test.ts +66 -0
- package/lib/__tests__/crash-fixes.test.ts +49 -0
- package/lib/__tests__/hbo-core-store.test.js +238 -0
- package/lib/__tests__/maintenance-mission-wiring.test.ts +35 -0
- package/lib/broker/__tests__/jit-subscription.test.js +44 -1
- package/lib/broker/__tests__/lifecycle-channels.test.js +25 -1
- package/lib/compression/__tests__/ccr-store.test.js +138 -0
- package/lib/compression/__tests__/pipeline.test.js +280 -0
- package/lib/compression/__tests__/smart-crusher.test.js +242 -0
- package/lib/compression/dist/server.js +34 -1
- package/lib/compression/dist/start-server.js +77 -0
- package/lib/event-bus.mts +1 -1
- package/lib/graph/learning/headroom-learn-bridge.js +175 -0
- package/lib/graph-availability.js +62 -0
- package/lib/hbo-core-store.compiled.js +834 -0
- package/lib/hbo-core-store.js +124 -0
- package/lib/hbo-core-store.ts +979 -0
- package/lib/mission-loop/__tests__/research-handler.test.ts +143 -0
- package/lib/skill-sync.js +6 -1
- package/lib/startup-integrity.js +9 -2
- package/lib/triage-core/__tests__/classifier-fixture.test.ts +254 -0
- package/lib/triage-core/__tests__/classifier-post-norm.test.ts +1 -1
- package/lib/triage-core/__tests__/classifier.test.ts +45 -7
- package/lib/triage-core/__tests__/correction-detector.test.ts +36 -0
- package/lib/triage-core/__tests__/d6-dunbar-boost.test.ts +5 -5
- package/lib/triage-core/__tests__/orchestrator-pipeline.test.ts +107 -0
- package/lib/triage-core/__tests__/orchestrator.test.ts +113 -1
- package/lib/triage-core/__tests__/signals.test.ts +357 -0
- package/lib/triage-core/__tests__/sql-parity.test.ts +216 -0
- package/lib/triage-core/backfill-cost-estimator.ts +91 -0
- package/lib/triage-core/backfill-orchestrator.ts +119 -0
- package/lib/triage-core/classifier.ts +41 -8
- package/lib/triage-core/cos/cross-channel-escalation.ts +2 -3
- package/lib/triage-core/cos/response-debt.ts +2 -2
- package/lib/triage-core/graph/__tests__/batch-persistence.test.ts +283 -0
- package/lib/triage-core/graph/batch-persistence.ts +66 -2
- package/lib/triage-core/graph/betweenness-worker.js +75 -0
- package/lib/triage-core/graph/graph-rank-sql.ts +67 -0
- package/lib/triage-core/graph/persistence.ts +1 -1
- package/lib/triage-core/graph/schema-v2.ts +2 -0
- package/lib/triage-core/graph/schema.cypher +11 -0
- package/lib/triage-core/graph/triage-query.ts +1 -1
- package/lib/triage-core/learning.ts +15 -20
- package/lib/triage-core/mental-model/bedrock-config.ts +78 -0
- package/lib/triage-core/mental-model/cos-integration.ts +1 -1
- package/lib/triage-core/mental-model/entity-extractor.ts +51 -4
- package/lib/triage-core/mental-model/identity-resolver.ts +5 -5
- package/lib/triage-core/mental-model/key-facts.ts +1 -2
- package/lib/triage-core/mental-model/model-assembler-sql.ts +200 -0
- package/lib/triage-core/mental-model/model-assembler.ts +16 -3
- package/lib/triage-core/orchestrator.ts +8 -15
- package/lib/triage-core/scheduled-sends.ts +39 -2
- package/lib/triage-core/signals/comms-style.ts +1 -1
- package/lib/triage-core/signals/cross-channel-escalation.ts +2 -2
- package/lib/triage-core/signals/favee-type.ts +6 -1
- package/lib/triage-core/signals/goal-relevance.ts +31 -2
- package/lib/triage-core/signals/personal-importance.ts +1 -1
- package/lib/triage-core/signals/referral-chain.ts +0 -1
- package/lib/triage-core/signals/relationship-decay.ts +4 -0
- package/lib/triage-core/signals/relationship-health.ts +6 -1
- package/lib/triage-core/signals/trajectory-signal.ts +38 -3
- package/lib/triage-core/tournament-runner.js +11 -1
- package/lib/triage-core/triage-llm-factory.ts +110 -0
- package/lib/triage-core/triage-local-llm.ts +145 -0
- package/lib/triage-core/triage-sql-store.ts +337 -0
- package/lib/triage-core/types.ts +2 -2
- package/lib/unified-graph.atomic.test.ts +52 -0
- package/lib/unified-graph.failure-categories.test.ts +55 -0
- package/package.json +18 -7
- package/prebuilds/darwin-arm64/better_sqlite3.node +0 -0
- package/prebuilds/linux-x64/better_sqlite3.node +0 -0
- package/prebuilds/win32-x64/better_sqlite3.node +0 -0
- package/skills/helios-bookkeeping/SKILL.md +321 -0
- package/skills/helios-briefer/SKILL.md +44 -0
- package/skills/helios-client-relations/SKILL.md +322 -0
- package/skills/helios-personal-triager/SKILL.md +45 -0
- package/skills/helios-recruitment/SKILL.md +317 -0
- package/skills/helios-relationship-nudger/SKILL.md +77 -0
- package/skills/helios-researcher/SKILL.md +44 -0
- package/skills/helios-scheduler/SKILL.md +58 -0
- package/skills/helios-tax-analyst/SKILL.md +280 -0
- package/lib/triage-core/orchestrator.ts.bak-r005-r006-r008 +0 -1823
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SessionLogReader — reads OpenCode session logs from opencode.db
|
|
8
|
+
* to reconstruct agent decision traces for HED audit.
|
|
9
|
+
* Uses better-sqlite3 (already a dependency via daemon) for read-only access.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// OpenCode stores sessions at platform-specific paths:
|
|
13
|
+
// Linux/WSL: ~/.local/share/opencode/opencode.db
|
|
14
|
+
// macOS: ~/Library/Application Support/opencode/opencode.db
|
|
15
|
+
// Windows: %APPDATA%\opencode\opencode.db
|
|
16
|
+
function getOpenCodeDbPath() {
|
|
17
|
+
if (process.platform === 'linux') {
|
|
18
|
+
return path.join(os.homedir(), '.local', 'share', 'opencode', 'opencode.db');
|
|
19
|
+
}
|
|
20
|
+
if (process.platform === 'darwin') {
|
|
21
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'opencode', 'opencode.db');
|
|
22
|
+
}
|
|
23
|
+
return path.join(os.homedir(), 'AppData', 'Local', 'opencode', 'opencode.db');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function readSessionLog(sessionKey) {
|
|
27
|
+
if (!sessionKey) return [];
|
|
28
|
+
let Database;
|
|
29
|
+
try {
|
|
30
|
+
Database = require('better-sqlite3');
|
|
31
|
+
} catch {
|
|
32
|
+
console.warn('[session-log-reader] better-sqlite3 not available — cannot read session log');
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
const dbPath = getOpenCodeDbPath();
|
|
36
|
+
let db;
|
|
37
|
+
try {
|
|
38
|
+
db = new Database(dbPath, { readonly: true, fileMustExist: true });
|
|
39
|
+
} catch (err) {
|
|
40
|
+
console.warn('[session-log-reader] cannot open opencode.db:', err.message);
|
|
41
|
+
return [];
|
|
42
|
+
}
|
|
43
|
+
try {
|
|
44
|
+
const messages = db.prepare(
|
|
45
|
+
`SELECT role, content, created_at FROM messages WHERE session_id = ? ORDER BY created_at ASC`
|
|
46
|
+
).all(sessionKey);
|
|
47
|
+
return messages.map((m, i) => ({
|
|
48
|
+
index: i,
|
|
49
|
+
role: m.role,
|
|
50
|
+
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
51
|
+
createdAt: m.created_at
|
|
52
|
+
}));
|
|
53
|
+
} catch (err) {
|
|
54
|
+
console.warn('[session-log-reader] query failed:', err.message);
|
|
55
|
+
return [];
|
|
56
|
+
} finally {
|
|
57
|
+
db.close();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Classify messages into XEPV sequence: eXplore, Execute, Plan, Verify
|
|
62
|
+
function classifyXEPV(messages) {
|
|
63
|
+
return messages.map(m => {
|
|
64
|
+
const content = m.content || '';
|
|
65
|
+
// Tool calls in content indicate action type
|
|
66
|
+
if (content.includes('"edit"') || content.includes('"write"')) return { ...m, xepv: 'X_Execute' };
|
|
67
|
+
if (content.includes('"read"') || content.includes('"grep"') || content.includes('"search_codebase"') || content.includes('"glob"')) return { ...m, xepv: 'X_Explore' };
|
|
68
|
+
if (content.includes('"bash"') && (content.includes('test') || content.includes('vitest') || content.includes('grep -n'))) return { ...m, xepv: 'X_Verify' };
|
|
69
|
+
if (m.role === 'assistant' && (content.includes('plan') || content.includes('approach') || content.includes('strategy') || content.includes('first'))) return { ...m, xepv: 'X_Plan' };
|
|
70
|
+
return { ...m, xepv: 'X_Unknown' };
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Find the branch point where the agent deviated from the declared target
|
|
75
|
+
function findBranchPoint(xepvMessages, opTarget) {
|
|
76
|
+
const exploreBeforeExecute = xepvMessages.filter(m => m.xepv === 'X_Explore');
|
|
77
|
+
for (const msg of exploreBeforeExecute) {
|
|
78
|
+
// If the agent explored files NOT in the declared target, that's the branch point
|
|
79
|
+
if (opTarget && !msg.content.includes(opTarget)) {
|
|
80
|
+
return { message: msg, reason: `Agent explored ${msg.content.slice(0, 200)} instead of declared target ${opTarget}` };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Compute P-ratio: plan steps vs execution steps
|
|
87
|
+
function computePRatio(xepvMessages) {
|
|
88
|
+
const plan = xepvMessages.filter(m => m.xepv === 'X_Plan').length;
|
|
89
|
+
const execute = xepvMessages.filter(m => m.xepv === 'X_Execute').length;
|
|
90
|
+
return { planSteps: plan, executeSteps: execute, ratio: execute > 0 ? plan / execute : plan };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
module.exports = { readSessionLog, classifyXEPV, findBranchPoint, computePRatio };
|
|
@@ -106,7 +106,93 @@ const PROVISIONAL_WORK = [
|
|
|
106
106
|
],
|
|
107
107
|
qualityCriteria: ['Body updated with completion summary.'],
|
|
108
108
|
escalationTriggers: ['Task request is ambiguous — set andoning=true, andonCategory=ambiguous_instruction.']
|
|
109
|
-
}
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
taskOriginKind: 'harada_l2_research',
|
|
112
|
+
agentId: 'any',
|
|
113
|
+
estimatedDurationMs: 600_000,
|
|
114
|
+
p90DurationMs: 1_200_000,
|
|
115
|
+
steps: [
|
|
116
|
+
'1. Read your task body carefully — it contains the GoalResearchBrief, pillar domain, and CEO critique from the previous round (if any).',
|
|
117
|
+
'2. Read the full GoalPillar node for your assigned pillar using GET /api/hbo/mandala?companyId=<cid>.',
|
|
118
|
+
'3. Perform a SearXNG web search: "<pillar.name> <company domain> strategy best practices 2024 2025".',
|
|
119
|
+
'4. Query Memgraph for existing memory on this pillar: MATCH (p:GoalPillar {id: $pillarId})-[:HAS_FACT]->(f:KeyFact) RETURN f.text LIMIT 10.',
|
|
120
|
+
'5. Cross-reference the GoalResearchBrief tournament winner with your search results.',
|
|
121
|
+
'6. Synthesise a 50-word purpose statement for this pillar anchored to the company goal.',
|
|
122
|
+
'7. Choose a strategy direction in ≤20 chars (e.g., "Land & Expand", "Cost Leadership").',
|
|
123
|
+
'8. Define 3 key actions the company must execute over the next 90 days for this pillar.',
|
|
124
|
+
'9. If a CEO critique was provided in the task body, explicitly address each point.',
|
|
125
|
+
'10. Submit: POST /api/hbo/pillar/{pillarId}/l2content with { l2Strategy, l2Content, l2ReviewStatus: "pending_review" }.',
|
|
126
|
+
'11. Write a one-sentence completion summary in your task result.',
|
|
127
|
+
],
|
|
128
|
+
qualityCriteria: [
|
|
129
|
+
'l2Strategy is ≤20 characters and is a recognisable strategic archetype.',
|
|
130
|
+
'l2Content directly references the GoalResearchBrief tournament winner.',
|
|
131
|
+
'All three 90-day actions are specific and measurable.',
|
|
132
|
+
'CEO critique (if present) is explicitly addressed point-by-point.',
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
taskOriginKind: 'harada_l2_review',
|
|
137
|
+
agentId: 'any_ceo',
|
|
138
|
+
estimatedDurationMs: 120_000,
|
|
139
|
+
p90DurationMs: 300_000,
|
|
140
|
+
steps: [
|
|
141
|
+
'1. Read the GoalPillar l2Content and l2Strategy for the pillar under review.',
|
|
142
|
+
'2. Read the GoalResearchBrief tournament winner for context.',
|
|
143
|
+
'3. Evaluate against three criteria: (a) Does the strategy align with the company goal? (b) Does l2Content reference the tournament winner? (c) Are the 90-day actions specific and measurable?',
|
|
144
|
+
'4. If l2ReviewCycles >= 2: escalate with AnomalySignal (harada_cascade_blocked) instead of requesting another revision.',
|
|
145
|
+
'5. Verdict PASS: POST /api/hbo/pillar/{pillarId}/l2review with { verdict: "pass", reviewCritique: "<one sentence summary>" }.',
|
|
146
|
+
'6. Verdict FAIL: POST /api/hbo/pillar/{pillarId}/l2review with { verdict: "fail", reviewCritique: "<specific actionable critique>" }. This re-dispatches harada_l2_research with your critique in the task body.',
|
|
147
|
+
],
|
|
148
|
+
qualityCriteria: [
|
|
149
|
+
'Verdict is exactly "pass" or "fail" — no other values.',
|
|
150
|
+
'reviewCritique is ≤100 words and is specific enough for the researcher to act on.',
|
|
151
|
+
'harada_cascade_blocked is raised when l2ReviewCycles >= 2 — never a third research pass.',
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
taskOriginKind: 'harada_l3_research',
|
|
156
|
+
agentId: 'any',
|
|
157
|
+
estimatedDurationMs: 480_000,
|
|
158
|
+
p90DurationMs: 900_000,
|
|
159
|
+
steps: [
|
|
160
|
+
'1. Read your task body — it contains the pillar l2Content, l2Strategy, action cell description, and dept head critique (if any).',
|
|
161
|
+
'2. Read the ActionCell node for context using GET /api/hbo/actioncell/{cellId}.',
|
|
162
|
+
'3. Perform a SearXNG web search: "<cell.description> <pillar.name> execution tactics implementation".',
|
|
163
|
+
'4. Focus on concrete execution tactics, not high-level strategy.',
|
|
164
|
+
'5. Query Memgraph for relevant prior work: MATCH (ac:ActionCell {id: $cellId})-[:HAS_FACT]->(f:KeyFact) RETURN f.text LIMIT 5.',
|
|
165
|
+
'6. Synthesise a ≤100-word execution plan for this action cell.',
|
|
166
|
+
'7. Ensure your plan references the pillar l2Strategy for coherence.',
|
|
167
|
+
'8. If a dept head critique was in the task body, address it explicitly.',
|
|
168
|
+
'9. Submit: POST /api/hbo/actioncell/{cellId}/l3content with { l3Content, l3ReviewStatus: "pending_review" }.',
|
|
169
|
+
'10. Write a one-sentence completion summary.',
|
|
170
|
+
],
|
|
171
|
+
qualityCriteria: [
|
|
172
|
+
'l3Content is ≤100 words and is an execution plan, not a strategy statement.',
|
|
173
|
+
'l3Content explicitly references the pillar l2Strategy.',
|
|
174
|
+
'Dept head critique (if present) is addressed point-by-point.',
|
|
175
|
+
],
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
taskOriginKind: 'harada_l3_review',
|
|
179
|
+
agentId: 'any',
|
|
180
|
+
estimatedDurationMs: 120_000,
|
|
181
|
+
p90DurationMs: 300_000,
|
|
182
|
+
steps: [
|
|
183
|
+
'1. Read the ActionCell l3Content for the cell under review.',
|
|
184
|
+
'2. Read the parent GoalPillar l2Strategy for alignment context.',
|
|
185
|
+
'3. Evaluate against two criteria: (a) Does l3Content align with the pillar l2Strategy? (b) Is the execution plan specific enough to guide agent task dispatch?',
|
|
186
|
+
'4. If l3ReviewCycles >= 2: escalate with harada_cascade_blocked AnomalySignal.',
|
|
187
|
+
'5. Verdict PASS: POST /api/hbo/actioncell/{cellId}/l3review with { verdict: "pass", reviewCritique: "<one sentence>" }.',
|
|
188
|
+
'6. Verdict FAIL: POST /api/hbo/actioncell/{cellId}/l3review with { verdict: "fail", reviewCritique: "<specific critique>" }.',
|
|
189
|
+
],
|
|
190
|
+
qualityCriteria: [
|
|
191
|
+
'Verdict is exactly "pass" or "fail".',
|
|
192
|
+
'reviewCritique is ≤80 words.',
|
|
193
|
+
'harada_cascade_blocked is raised when l3ReviewCycles >= 2.',
|
|
194
|
+
],
|
|
195
|
+
},
|
|
110
196
|
];
|
|
111
197
|
|
|
112
198
|
/**
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const logger = require('./logger');
|
|
3
|
+
const hboStore = require('../../lib/hbo-core-store');
|
|
3
4
|
const { MandalaManager } = require('./harada/mandala');
|
|
4
5
|
/**
|
|
5
6
|
* daemon/lib/task-completion-processor.js — Task Completion → Graph Persistence Pipeline
|
|
@@ -35,9 +36,11 @@ class TaskCompletionProcessor {
|
|
|
35
36
|
/**
|
|
36
37
|
* @param {object} opts
|
|
37
38
|
* @param {Function} opts.mgQuery - Memgraph query function from daemon
|
|
39
|
+
* @param {object} [opts.activityLogger] - Optional ActivityLogger for recording task.complete events
|
|
38
40
|
*/
|
|
39
41
|
constructor(opts = {}) {
|
|
40
42
|
this._mg = opts.mgQuery;
|
|
43
|
+
this.activityLogger = opts.activityLogger ?? null;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
46
|
/**
|
|
@@ -207,6 +210,16 @@ class TaskCompletionProcessor {
|
|
|
207
210
|
process.stderr.write('[task-completion] routineDisciplineScore increment warn: ' + (e?.message || e) + '\n');
|
|
208
211
|
}
|
|
209
212
|
}
|
|
213
|
+
|
|
214
|
+
// ── H-01: Record task.complete in ActivityLogger ─────────────────────────
|
|
215
|
+
this.activityLogger?.record({
|
|
216
|
+
action: 'task.complete',
|
|
217
|
+
actor: agentId,
|
|
218
|
+
entityId: taskId,
|
|
219
|
+
companyId: companyId,
|
|
220
|
+
outcome: exitCode === 0 ? 'success' : 'error',
|
|
221
|
+
meta: { exitCode, originKind },
|
|
222
|
+
});
|
|
210
223
|
} catch (err) {
|
|
211
224
|
// Non-fatal — log but don't fail the task completion
|
|
212
225
|
process.stdout.write(JSON.stringify({
|
|
@@ -259,6 +272,16 @@ class TaskCompletionProcessor {
|
|
|
259
272
|
SET t.resultSummary = $summary, t.hasFullResult = true`,
|
|
260
273
|
{ taskId, summary: output.slice(0, 2000) }
|
|
261
274
|
);
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
hboStore.updateTask?.(taskId, companyId || '', {
|
|
278
|
+
status: 'done',
|
|
279
|
+
resultSummary: output.slice(0, 2000),
|
|
280
|
+
hasFullResult: true,
|
|
281
|
+
});
|
|
282
|
+
} catch (err) {
|
|
283
|
+
logger.warn(`[completion-processor] hbo-core mirror failed for ${taskId}: ${err.message}`);
|
|
284
|
+
}
|
|
262
285
|
}
|
|
263
286
|
|
|
264
287
|
// ── 2. Update goal progress ───────────────────────────────────────────────────
|
|
@@ -26,13 +26,65 @@ const { randomUUID } = require('crypto');
|
|
|
26
26
|
const { EventEmitter } = require('events');
|
|
27
27
|
const { SIGNAL_KEY } = require('./signal-key');
|
|
28
28
|
|
|
29
|
-
const { initDefaultGoal } = require('../../skills/helios-business-operator/lib/business-goal-service.js');
|
|
30
29
|
const { CompanyBeliefService } = require('./company-belief-service');
|
|
30
|
+
const hboStore = require('../../lib/hbo-core-store');
|
|
31
31
|
|
|
32
32
|
const HELIOS_ROOT = path.resolve(__dirname, '..', '..');
|
|
33
33
|
const COMPANIES_DIR = path.join(__dirname, '..', 'companies');
|
|
34
34
|
const HELIOS_RPC_BIN = path.join(HELIOS_ROOT, 'bin', 'helios-rpc.js');
|
|
35
35
|
|
|
36
|
+
async function initWizardBusinessGoal(mgQuery, companyId, goalTitle) {
|
|
37
|
+
if (!goalTitle || !goalTitle.trim()) return null;
|
|
38
|
+
|
|
39
|
+
const existing = await mgQuery(
|
|
40
|
+
`MATCH (g:BusinessGoal {level: 'company', companyId: $companyId})
|
|
41
|
+
RETURN g.id AS id, g.title AS title LIMIT toInteger(1)`,
|
|
42
|
+
{ companyId }
|
|
43
|
+
);
|
|
44
|
+
const existingRows = existing?.rows || [];
|
|
45
|
+
if (existingRows.length > 0) return { created: false };
|
|
46
|
+
|
|
47
|
+
const goalId = `bg:${randomUUID()}`;
|
|
48
|
+
const title = goalTitle.trim();
|
|
49
|
+
await mgQuery(
|
|
50
|
+
`CREATE (g:BusinessGoal {
|
|
51
|
+
id: $id,
|
|
52
|
+
companyId: $companyId,
|
|
53
|
+
title: $title,
|
|
54
|
+
description: $description,
|
|
55
|
+
level: 'company',
|
|
56
|
+
status: 'active',
|
|
57
|
+
parentId: null,
|
|
58
|
+
parentGoalId: null,
|
|
59
|
+
createdAt: datetime(),
|
|
60
|
+
updatedAt: datetime()
|
|
61
|
+
})`,
|
|
62
|
+
{
|
|
63
|
+
id: goalId,
|
|
64
|
+
companyId,
|
|
65
|
+
title,
|
|
66
|
+
description: 'Top-level company objective',
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
hboStore.createGoal?.({
|
|
72
|
+
id: goalId,
|
|
73
|
+
companyId,
|
|
74
|
+
title,
|
|
75
|
+
description: 'Top-level company objective',
|
|
76
|
+
level: 'company',
|
|
77
|
+
status: 'active',
|
|
78
|
+
parentId: null,
|
|
79
|
+
createdAt: Date.now(),
|
|
80
|
+
});
|
|
81
|
+
} catch (storeErr) {
|
|
82
|
+
process.stderr.write(`[wizard-engine] goal mirror failed: ${storeErr.message}\n`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return { created: true, goalId };
|
|
86
|
+
}
|
|
87
|
+
|
|
36
88
|
// ── Default agent template — the 7 core HBO roles every company gets ─────────
|
|
37
89
|
//
|
|
38
90
|
// Skills use 'helios-business-operator' so the helios_rpc adapter runs the
|
|
@@ -916,13 +968,12 @@ class WizardEngine extends EventEmitter {
|
|
|
916
968
|
|
|
917
969
|
await seedToMemgraph(this._mgQuery, config);
|
|
918
970
|
|
|
919
|
-
//
|
|
920
|
-
//
|
|
921
|
-
// to the company and links the wizard's stated goal text as the top-level objective.
|
|
971
|
+
// Create a daemon-visible BusinessGoal after CompanyGoal seeding.
|
|
972
|
+
// Uses this wizard's mgQuery path and mirrors to hbo-core.db.
|
|
922
973
|
// Previously this function had zero call sites — it was dead code.
|
|
923
974
|
if (goal && goal.trim()) {
|
|
924
975
|
try {
|
|
925
|
-
await
|
|
976
|
+
await initWizardBusinessGoal(this._mgQuery, companyId, goal.trim());
|
|
926
977
|
this._emit('wizard:seed_company', {
|
|
927
978
|
message: `Default BusinessGoal initialized for company "${companyId}"`,
|
|
928
979
|
companyId,
|
|
@@ -930,7 +981,7 @@ class WizardEngine extends EventEmitter {
|
|
|
930
981
|
} catch (goalErr) {
|
|
931
982
|
// Non-fatal: CompanyGoal already seeded; BusinessGoal hierarchy is supplemental.
|
|
932
983
|
this._emit('wizard:seed_company', {
|
|
933
|
-
message: `
|
|
984
|
+
message: `BusinessGoal initialization note: ${goalErr.message}`,
|
|
934
985
|
companyId,
|
|
935
986
|
});
|
|
936
987
|
}
|
package/daemon/package.json
CHANGED
package/daemon/routes/agents.js
CHANGED
|
@@ -4,8 +4,10 @@
|
|
|
4
4
|
* routes/agents.js — Agent-related route dispatch
|
|
5
5
|
*
|
|
6
6
|
* Routes handled:
|
|
7
|
-
* GET /api/agents
|
|
8
|
-
*
|
|
7
|
+
* GET /api/agents — list all agents for a company
|
|
8
|
+
* GET /api/agents/:id — single agent detail (P7-A1)
|
|
9
|
+
* GET /api/agents/:id/runs — HeartbeatRun history (P7-A2)
|
|
10
|
+
* POST /api/agents/pause-all — pause all agents (handled by hbo.js; stubbed here)
|
|
9
11
|
* POST /api/agents/:id/sync-skills
|
|
10
12
|
* POST /api/agents/:id/approve
|
|
11
13
|
* POST /api/agents/:id/terminate
|
|
@@ -14,7 +16,8 @@
|
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
18
|
module.exports = function createAgentsRouter(handlers) {
|
|
17
|
-
const { handleGetAgents,
|
|
19
|
+
const { handleGetAgents, handleGetAgent, handleGetAgentRuns,
|
|
20
|
+
handleSyncSkills, handleApproveAgent, handleTerminateAgent,
|
|
18
21
|
handlePauseAgent, handleResumeAgent, handlePauseAll } = handlers;
|
|
19
22
|
|
|
20
23
|
return async function agentsRoute(req, res, ctx, pathname, method) {
|
|
@@ -24,7 +27,25 @@ module.exports = function createAgentsRouter(handlers) {
|
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
if (method === 'POST' && pathname === '/api/agents/pause-all') {
|
|
27
|
-
|
|
30
|
+
// pause-all is also handled by hbo.js; guard against null stub
|
|
31
|
+
if (handlePauseAll) {
|
|
32
|
+
await handlePauseAll(req, res, ctx);
|
|
33
|
+
} else {
|
|
34
|
+
// delegated to hbo.js — return false so hboRoute can handle it
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// P7-A2: GET /api/agents/:id/runs — must be matched BEFORE /:id to avoid conflict
|
|
41
|
+
const runsMatch = pathname.match(/^\/api\/agents\/([^/]+)\/runs$/);
|
|
42
|
+
if (method === 'GET' && runsMatch) {
|
|
43
|
+
if (handleGetAgentRuns) {
|
|
44
|
+
await handleGetAgentRuns(req, res, ctx, decodeURIComponent(runsMatch[1]));
|
|
45
|
+
} else {
|
|
46
|
+
res.writeHead(501, { 'Content-Type': 'application/json' });
|
|
47
|
+
res.end(JSON.stringify({ error: 'Agent runs endpoint not configured' }));
|
|
48
|
+
}
|
|
28
49
|
return true;
|
|
29
50
|
}
|
|
30
51
|
|
|
@@ -48,13 +69,37 @@ module.exports = function createAgentsRouter(handlers) {
|
|
|
48
69
|
|
|
49
70
|
const pauseMatch = pathname.match(/^\/api\/agents\/([^/]+)\/pause$/);
|
|
50
71
|
if (method === 'POST' && pauseMatch) {
|
|
51
|
-
|
|
72
|
+
if (handlePauseAgent) {
|
|
73
|
+
await handlePauseAgent(req, res, ctx, decodeURIComponent(pauseMatch[1]));
|
|
74
|
+
} else {
|
|
75
|
+
// H-4 fix: explicit error response instead of silent hang
|
|
76
|
+
res.writeHead(501, { 'Content-Type': 'application/json' });
|
|
77
|
+
res.end(JSON.stringify({ error: 'Agent pause handler not configured' }));
|
|
78
|
+
}
|
|
52
79
|
return true;
|
|
53
80
|
}
|
|
54
81
|
|
|
55
82
|
const resumeMatch = pathname.match(/^\/api\/agents\/([^/]+)\/resume$/);
|
|
56
83
|
if (method === 'POST' && resumeMatch) {
|
|
57
|
-
|
|
84
|
+
if (handleResumeAgent) {
|
|
85
|
+
await handleResumeAgent(req, res, ctx, decodeURIComponent(resumeMatch[1]));
|
|
86
|
+
} else {
|
|
87
|
+
// H-4 fix: explicit error response instead of silent hang
|
|
88
|
+
res.writeHead(501, { 'Content-Type': 'application/json' });
|
|
89
|
+
res.end(JSON.stringify({ error: 'Agent resume handler not configured' }));
|
|
90
|
+
}
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// P7-A1: GET /api/agents/:id — must be LAST to avoid matching sub-routes above
|
|
95
|
+
const agentDetailMatch = pathname.match(/^\/api\/agents\/([^/]+)$/);
|
|
96
|
+
if (method === 'GET' && agentDetailMatch) {
|
|
97
|
+
if (handleGetAgent) {
|
|
98
|
+
await handleGetAgent(req, res, ctx, decodeURIComponent(agentDetailMatch[1]));
|
|
99
|
+
} else {
|
|
100
|
+
res.writeHead(501, { 'Content-Type': 'application/json' });
|
|
101
|
+
res.end(JSON.stringify({ error: 'Agent detail endpoint not configured' }));
|
|
102
|
+
}
|
|
58
103
|
return true;
|
|
59
104
|
}
|
|
60
105
|
|
|
@@ -34,7 +34,7 @@ const ACCOUNTS_FILE = path.join(ACCOUNTS_DIR, 'accounts.json');
|
|
|
34
34
|
// Serialise all accounts.json mutations to prevent concurrent-write corruption
|
|
35
35
|
let _accountsWriteLock = Promise.resolve();
|
|
36
36
|
function withAccountsLock(fn) {
|
|
37
|
-
_accountsWriteLock = _accountsWriteLock.then(fn).catch(
|
|
37
|
+
_accountsWriteLock = _accountsWriteLock.then(fn).catch(err => { console.error('[channels] accounts lock error:', err); });
|
|
38
38
|
return _accountsWriteLock;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -348,6 +348,108 @@ async function handleSaveSlackChannel(req, res, companyId, body, mgQuery) {
|
|
|
348
348
|
|
|
349
349
|
// ── Route dispatcher ───────────────────────────────────────────────────────
|
|
350
350
|
|
|
351
|
+
// ── iMessage channel handlers ──────────────────────────────────────────────
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* POST /api/companies/:id/channels/imessage/setup
|
|
355
|
+
*
|
|
356
|
+
* Verifies that the daemon can read the macOS iMessage database (chat.db).
|
|
357
|
+
* Requirements: macOS, Messages.app installed, Full Disk Access granted to this process.
|
|
358
|
+
* Returns: { connected: true, messageCount: N } on success,
|
|
359
|
+
* { error: 'fda_denied', message: '...' } if FDA is not granted,
|
|
360
|
+
* { error: 'not_macos' } on non-macOS platforms.
|
|
361
|
+
*/
|
|
362
|
+
async function handleImessageSetup(req, res) {
|
|
363
|
+
// iMessage is macOS-only — chat.db only exists on macOS
|
|
364
|
+
if (process.platform !== 'darwin') {
|
|
365
|
+
jsonErr(res, 400, 'not_macos', req);
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const chatDbPath = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
|
|
370
|
+
|
|
371
|
+
let db;
|
|
372
|
+
try {
|
|
373
|
+
// Dynamically require better-sqlite3 so non-macOS builds don't fail at import time.
|
|
374
|
+
// If it's not available, fall back to graceful error.
|
|
375
|
+
let Database;
|
|
376
|
+
try {
|
|
377
|
+
Database = require('better-sqlite3');
|
|
378
|
+
} catch (_) {
|
|
379
|
+
// Try bun:sqlite as fallback (daemon may run under Bun)
|
|
380
|
+
try {
|
|
381
|
+
const bun = require('bun:sqlite');
|
|
382
|
+
Database = bun.Database;
|
|
383
|
+
} catch (__) {
|
|
384
|
+
jsonErr(res, 500, 'sqlite_unavailable', req);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
db = new Database(chatDbPath, { readonly: true });
|
|
390
|
+
// Quick sanity query — counts messages to verify FDA access
|
|
391
|
+
const row = db.prepare('SELECT COUNT(*) as count FROM message').get();
|
|
392
|
+
const messageCount = row ? row.count : 0;
|
|
393
|
+
|
|
394
|
+
jsonOk(res, { connected: true, messageCount }, req);
|
|
395
|
+
} catch (err) {
|
|
396
|
+
const code = err && (err.code || err.message || '');
|
|
397
|
+
// SQLITE_CANTOPEN is thrown when FDA is not granted (chat.db unreadable)
|
|
398
|
+
if (
|
|
399
|
+
typeof code === 'string' &&
|
|
400
|
+
(code.includes('SQLITE_CANTOPEN') || code.includes('EPERM') || code.includes('EACCES') || code.includes('authorization denied'))
|
|
401
|
+
) {
|
|
402
|
+
res.writeHead(403, getCorsHeaders(req));
|
|
403
|
+
res.end(JSON.stringify({
|
|
404
|
+
error: 'fda_denied',
|
|
405
|
+
message: 'Helios needs Full Disk Access to read iMessages. Open System Settings → Privacy & Security → Full Disk Access and add Helios.',
|
|
406
|
+
}));
|
|
407
|
+
} else {
|
|
408
|
+
jsonErr(res, 500, err.message || 'Unknown error reading chat.db', req);
|
|
409
|
+
}
|
|
410
|
+
} finally {
|
|
411
|
+
if (db) {
|
|
412
|
+
try { db.close(); } catch (_) {}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* GET /api/companies/:id/channels/imessage/status
|
|
419
|
+
*
|
|
420
|
+
* Returns imessage connection status without writing anything.
|
|
421
|
+
*/
|
|
422
|
+
async function handleImessageStatus(req, res) {
|
|
423
|
+
if (process.platform !== 'darwin') {
|
|
424
|
+
jsonOk(res, { connected: false, available: false }, req);
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const chatDbPath = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
|
|
429
|
+
|
|
430
|
+
let db;
|
|
431
|
+
let connected = false;
|
|
432
|
+
try {
|
|
433
|
+
let Database;
|
|
434
|
+
try { Database = require('better-sqlite3'); } catch (_) {
|
|
435
|
+
try { Database = require('bun:sqlite').Database; } catch (__) {}
|
|
436
|
+
}
|
|
437
|
+
if (Database) {
|
|
438
|
+
db = new Database(chatDbPath, { readonly: true });
|
|
439
|
+
db.prepare('SELECT 1 FROM message LIMIT 1').get();
|
|
440
|
+
connected = true;
|
|
441
|
+
}
|
|
442
|
+
} catch (_) {
|
|
443
|
+
connected = false;
|
|
444
|
+
} finally {
|
|
445
|
+
if (db) { try { db.close(); } catch (_) {} }
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
jsonOk(res, { connected, available: true }, req);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// ── Route dispatcher ───────────────────────────────────────────────────────
|
|
452
|
+
|
|
351
453
|
/**
|
|
352
454
|
* channelsRoute(req, res, ctx, pathname, method)
|
|
353
455
|
* Returns true if this route handled the request, false otherwise.
|
|
@@ -362,8 +464,10 @@ function channelsRoute(req, res, ctx, pathname, method) {
|
|
|
362
464
|
|
|
363
465
|
const emailMatch = pathname.match(/^\/api\/companies\/([^/]+)\/channels\/email(\/oauth\/(start|complete))?$/);
|
|
364
466
|
const slackMatch = pathname.match(/^\/api\/companies\/([^/]+)\/channels\/slack$/);
|
|
467
|
+
const imessageSetupMatch = pathname.match(/^\/api\/companies\/([^/]+)\/channels\/imessage\/setup$/);
|
|
468
|
+
const imessageStatusMatch = pathname.match(/^\/api\/companies\/([^/]+)\/channels\/imessage\/status$/);
|
|
365
469
|
|
|
366
|
-
if (!emailMatch && !slackMatch) return Promise.resolve(false);
|
|
470
|
+
if (!emailMatch && !slackMatch && !imessageSetupMatch && !imessageStatusMatch) return Promise.resolve(false);
|
|
367
471
|
|
|
368
472
|
const mgQuery = ctx && ctx.mgQuery;
|
|
369
473
|
|
|
@@ -396,6 +500,14 @@ function channelsRoute(req, res, ctx, pathname, method) {
|
|
|
396
500
|
}
|
|
397
501
|
}
|
|
398
502
|
|
|
503
|
+
if (imessageSetupMatch && method === 'POST') {
|
|
504
|
+
return handleImessageSetup(req, res).then(() => true);
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
if (imessageStatusMatch && method === 'GET') {
|
|
508
|
+
return handleImessageStatus(req, res).then(() => true);
|
|
509
|
+
}
|
|
510
|
+
|
|
399
511
|
return Promise.resolve(false);
|
|
400
512
|
}
|
|
401
513
|
|
|
@@ -431,3 +543,5 @@ module.exports.handleEmailOAuthStart = handleEmailOAuthStart;
|
|
|
431
543
|
module.exports.handleEmailOAuthComplete = handleEmailOAuthComplete;
|
|
432
544
|
module.exports.handleGetSlackChannel = handleGetSlackChannel;
|
|
433
545
|
module.exports.handleSaveSlackChannel = handleSaveSlackChannel;
|
|
546
|
+
module.exports.handleImessageSetup = handleImessageSetup;
|
|
547
|
+
module.exports.handleImessageStatus = handleImessageStatus;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* routes/crm.js — P5-S5c
|
|
5
|
+
* POST /api/crm/contacts — upsert a CRM contact into Memgraph.
|
|
6
|
+
* Accepts CRMSyncEntry shape from crmSyncService.ts dual-write.
|
|
7
|
+
* C7: Writes to Memgraph via mgQuery (CREATE or MERGE on email/id).
|
|
8
|
+
* C15: Non-blocking from desktop perspective — desktop SQLite is authoritative;
|
|
9
|
+
* this route is the async Memgraph leg.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
module.exports = function createCrmRouter({ parseBody, jsonResponse }) {
|
|
13
|
+
return async function crmRoute(req, res, ctx, pathname, method) {
|
|
14
|
+
// POST /api/crm/contacts — upsert contact node into Memgraph
|
|
15
|
+
if (method === 'POST' && pathname === '/api/crm/contacts') {
|
|
16
|
+
try {
|
|
17
|
+
const body = await parseBody(req);
|
|
18
|
+
if (!body || !body.id || !body.name) {
|
|
19
|
+
jsonResponse(res, 400, { error: 'id and name are required' });
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const mg = ctx.mgQuery;
|
|
24
|
+
if (!mg) {
|
|
25
|
+
// Memgraph not connected — return 503 so desktop queues for retry
|
|
26
|
+
jsonResponse(res, 503, { error: 'Memgraph not connected' });
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const contact = {
|
|
31
|
+
id: String(body.id),
|
|
32
|
+
name: String(body.name),
|
|
33
|
+
email: body.email ? String(body.email) : null,
|
|
34
|
+
phone: body.phone ? String(body.phone) : null,
|
|
35
|
+
company: body.company ? String(body.company) : null,
|
|
36
|
+
jobTitle: body.jobTitle ? String(body.jobTitle) : null,
|
|
37
|
+
stage: body.stage ? String(body.stage) : 'lead',
|
|
38
|
+
source: body.source ? String(body.source) : 'desktop',
|
|
39
|
+
companyId: body.companyId ? String(body.companyId) : (ctx.cid || null),
|
|
40
|
+
dunbarLayer: body.dunbarLayer ? String(body.dunbarLayer) : null,
|
|
41
|
+
healthStatus: body.healthStatus ? String(body.healthStatus) : null,
|
|
42
|
+
personId: body.personId ? String(body.personId) : null,
|
|
43
|
+
syncedAt: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// MERGE on id to make upsert idempotent; SET all mutable fields
|
|
47
|
+
await mg(
|
|
48
|
+
`MERGE (c:CRMContact {id: $id})
|
|
49
|
+
SET c.name = $name,
|
|
50
|
+
c.email = $email,
|
|
51
|
+
c.phone = $phone,
|
|
52
|
+
c.company = $company,
|
|
53
|
+
c.jobTitle = $jobTitle,
|
|
54
|
+
c.stage = $stage,
|
|
55
|
+
c.source = $source,
|
|
56
|
+
c.companyId = $companyId,
|
|
57
|
+
c.dunbarLayer = $dunbarLayer,
|
|
58
|
+
c.healthStatus = $healthStatus,
|
|
59
|
+
c.personId = $personId,
|
|
60
|
+
c.syncedAt = $syncedAt`,
|
|
61
|
+
contact
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
// If personId is known, create a link to the Person node
|
|
65
|
+
if (contact.personId) {
|
|
66
|
+
await mg(
|
|
67
|
+
`MATCH (c:CRMContact {id: $crmId}), (p:Person {id: $personId})
|
|
68
|
+
MERGE (c)-[:TRACKS]->(p)`,
|
|
69
|
+
{ crmId: contact.id, personId: contact.personId }
|
|
70
|
+
).catch(() => {
|
|
71
|
+
// Non-fatal — Person node may not exist yet
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
jsonResponse(res, 201, { ok: true, id: contact.id });
|
|
76
|
+
return true;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
jsonResponse(res, 500, { error: err.message });
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return false;
|
|
84
|
+
};
|
|
85
|
+
};
|