@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,979 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* hbo-core-store.ts
|
|
3
|
+
* Unified SQLite store for HBO (Helios Business Operations) core domain data.
|
|
4
|
+
*
|
|
5
|
+
* Covers: tasks, approvals, budget policies, cost events, business agents,
|
|
6
|
+
* OKRs, leads, CRM contacts, accounts, and opportunities.
|
|
7
|
+
*
|
|
8
|
+
* Storage engine: node:sqlite (DatabaseSync) when available (Node ≥ 22.13
|
|
9
|
+
* without flags, or Node ≥ 22.5 with --experimental-sqlite), otherwise
|
|
10
|
+
* better-sqlite3 (Node ≥ 16, requires native compilation via node-gyp but
|
|
11
|
+
* is already a declared dependency).
|
|
12
|
+
*
|
|
13
|
+
* Runs natively on Windows, macOS, and Linux for all users regardless of
|
|
14
|
+
* Node version.
|
|
15
|
+
*
|
|
16
|
+
* Data model: Memgraph is primary. This store is the fallback when Memgraph
|
|
17
|
+
* is unavailable. Writes go to SQLite first (authoritative on Memgraph failure),
|
|
18
|
+
* then fire-and-forget to Memgraph as a projection.
|
|
19
|
+
*
|
|
20
|
+
* Pattern:
|
|
21
|
+
* - Lazy singleton DB open via getDb()
|
|
22
|
+
* - Schema initialised once on first open (initSchema)
|
|
23
|
+
* - All writes use INSERT OR REPLACE for idempotency
|
|
24
|
+
* - All records are stored with a data_json column for full fidelity
|
|
25
|
+
* - Read helpers parse data_json and merge with top-level indexed columns
|
|
26
|
+
* - No hardcoded company IDs anywhere
|
|
27
|
+
*/
|
|
28
|
+
import * as path from 'path';
|
|
29
|
+
import * as os from 'os';
|
|
30
|
+
import { mkdirSync } from 'fs';
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Engine detection — node:sqlite (Node 22+) preferred, better-sqlite3 fallback
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
// _engine tracks which SQLite library is in use so initSchema can apply the
|
|
37
|
+
// correct WAL pragma syntax:
|
|
38
|
+
// node:sqlite → db.exec('PRAGMA journal_mode = WAL')
|
|
39
|
+
// better-sqlite3 → db.pragma('journal_mode = WAL')
|
|
40
|
+
let _engine: 'node-sqlite' | 'better-sqlite3' | null = null;
|
|
41
|
+
|
|
42
|
+
function _openDatabase(dbPath: string): any {
|
|
43
|
+
// Attempt 1: node:sqlite built-in (Node >= 22.13 no flags needed; Node >= 22.5 with --experimental-sqlite)
|
|
44
|
+
try {
|
|
45
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
46
|
+
const { DatabaseSync } = require('node:sqlite');
|
|
47
|
+
const db = new DatabaseSync(dbPath);
|
|
48
|
+
_engine = 'node-sqlite';
|
|
49
|
+
return db;
|
|
50
|
+
} catch (_nodeSqliteErr) {
|
|
51
|
+
// node:sqlite unavailable (Node < 22.13 without flag, or Node < 22.5)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Attempt 2: better-sqlite3 — synchronous, compiled native module, Node ≥ 16
|
|
55
|
+
try {
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
57
|
+
const BetterSqlite3 = require('better-sqlite3');
|
|
58
|
+
const db = new BetterSqlite3(dbPath);
|
|
59
|
+
_engine = 'better-sqlite3';
|
|
60
|
+
return db;
|
|
61
|
+
} catch (_betterSqliteErr) {
|
|
62
|
+
// better-sqlite3 unavailable (native build failed or not installed)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
throw new Error(
|
|
66
|
+
'[hbo-core-store] No SQLite engine available. ' +
|
|
67
|
+
'Requires Node >= 22.13 (no flags) or Node >= 22.5 with --experimental-sqlite, ' +
|
|
68
|
+
'OR better-sqlite3 installed. Run: npm install better-sqlite3'
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ---------------------------------------------------------------------------
|
|
73
|
+
// DB singleton
|
|
74
|
+
// ---------------------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
let _db: any | null = null;
|
|
77
|
+
|
|
78
|
+
function getDb(): any {
|
|
79
|
+
if (_db) return _db;
|
|
80
|
+
|
|
81
|
+
const dbPath = path.join(
|
|
82
|
+
process.env.HELIOS_ROOT ?? path.join(os.homedir(), 'helios-agent'),
|
|
83
|
+
'data',
|
|
84
|
+
'hbo-core.db',
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
mkdirSync(path.join(dbPath, '..'), { recursive: true });
|
|
88
|
+
|
|
89
|
+
_db = _openDatabase(dbPath);
|
|
90
|
+
|
|
91
|
+
// WAL mode for concurrent readers + single writer.
|
|
92
|
+
// node:sqlite uses .exec('PRAGMA ...'); better-sqlite3 uses .pragma('...')
|
|
93
|
+
if (_engine === 'better-sqlite3') {
|
|
94
|
+
_db.pragma('journal_mode = WAL');
|
|
95
|
+
_db.pragma('foreign_keys = ON');
|
|
96
|
+
} else {
|
|
97
|
+
_db.exec('PRAGMA journal_mode = WAL');
|
|
98
|
+
_db.exec('PRAGMA foreign_keys = ON');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
initSchema(_db);
|
|
102
|
+
return _db;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Schema
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
function initSchema(db: any): void {
|
|
110
|
+
db.exec(`
|
|
111
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
112
|
+
id TEXT PRIMARY KEY,
|
|
113
|
+
company_id TEXT NOT NULL,
|
|
114
|
+
title TEXT,
|
|
115
|
+
status TEXT,
|
|
116
|
+
assignee_agent_id TEXT,
|
|
117
|
+
goal_id TEXT,
|
|
118
|
+
origin_kind TEXT,
|
|
119
|
+
helios_issue_id TEXT,
|
|
120
|
+
execution_locked_at INTEGER,
|
|
121
|
+
created_at INTEGER,
|
|
122
|
+
updated_at INTEGER,
|
|
123
|
+
data_json TEXT
|
|
124
|
+
);
|
|
125
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_company_status ON tasks(company_id, status);
|
|
126
|
+
CREATE INDEX IF NOT EXISTS idx_tasks_assignee ON tasks(assignee_agent_id, company_id);
|
|
127
|
+
|
|
128
|
+
CREATE TABLE IF NOT EXISTS approvals (
|
|
129
|
+
id TEXT PRIMARY KEY,
|
|
130
|
+
company_id TEXT NOT NULL,
|
|
131
|
+
type TEXT,
|
|
132
|
+
status TEXT,
|
|
133
|
+
strategy_id TEXT,
|
|
134
|
+
requested_by TEXT,
|
|
135
|
+
created_at INTEGER,
|
|
136
|
+
updated_at INTEGER,
|
|
137
|
+
data_json TEXT
|
|
138
|
+
);
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_approvals_company_status ON approvals(company_id, status);
|
|
140
|
+
|
|
141
|
+
CREATE TABLE IF NOT EXISTS budget_policies (
|
|
142
|
+
id TEXT PRIMARY KEY,
|
|
143
|
+
company_id TEXT NOT NULL,
|
|
144
|
+
agent_id TEXT,
|
|
145
|
+
spent_cents INTEGER DEFAULT 0,
|
|
146
|
+
percent_used REAL DEFAULT 0,
|
|
147
|
+
updated_at INTEGER,
|
|
148
|
+
data_json TEXT
|
|
149
|
+
);
|
|
150
|
+
CREATE INDEX IF NOT EXISTS idx_budget_company_agent ON budget_policies(company_id, agent_id);
|
|
151
|
+
|
|
152
|
+
CREATE TABLE IF NOT EXISTS cost_events (
|
|
153
|
+
id TEXT PRIMARY KEY,
|
|
154
|
+
company_id TEXT NOT NULL,
|
|
155
|
+
agent_id TEXT,
|
|
156
|
+
feature TEXT,
|
|
157
|
+
model TEXT,
|
|
158
|
+
amount_usd REAL,
|
|
159
|
+
created_at INTEGER,
|
|
160
|
+
data_json TEXT
|
|
161
|
+
);
|
|
162
|
+
CREATE INDEX IF NOT EXISTS idx_cost_company ON cost_events(company_id, created_at);
|
|
163
|
+
|
|
164
|
+
CREATE TABLE IF NOT EXISTS business_agents (
|
|
165
|
+
id TEXT PRIMARY KEY,
|
|
166
|
+
company_id TEXT NOT NULL,
|
|
167
|
+
role TEXT,
|
|
168
|
+
status TEXT,
|
|
169
|
+
current_task_id TEXT,
|
|
170
|
+
skill TEXT,
|
|
171
|
+
adapter TEXT,
|
|
172
|
+
updated_at INTEGER,
|
|
173
|
+
data_json TEXT
|
|
174
|
+
);
|
|
175
|
+
CREATE INDEX IF NOT EXISTS idx_agents_company_status ON business_agents(company_id, status);
|
|
176
|
+
|
|
177
|
+
CREATE TABLE IF NOT EXISTS okrs (
|
|
178
|
+
id TEXT PRIMARY KEY,
|
|
179
|
+
company_id TEXT NOT NULL,
|
|
180
|
+
type TEXT,
|
|
181
|
+
status TEXT,
|
|
182
|
+
title TEXT,
|
|
183
|
+
updated_at INTEGER,
|
|
184
|
+
data_json TEXT
|
|
185
|
+
);
|
|
186
|
+
CREATE INDEX IF NOT EXISTS idx_okrs_company_type ON okrs(company_id, type, status);
|
|
187
|
+
|
|
188
|
+
CREATE TABLE IF NOT EXISTS goals (
|
|
189
|
+
id TEXT PRIMARY KEY,
|
|
190
|
+
company_id TEXT NOT NULL,
|
|
191
|
+
title TEXT,
|
|
192
|
+
description TEXT,
|
|
193
|
+
level TEXT,
|
|
194
|
+
status TEXT,
|
|
195
|
+
parent_id TEXT,
|
|
196
|
+
created_at INTEGER,
|
|
197
|
+
updated_at INTEGER,
|
|
198
|
+
data_json TEXT
|
|
199
|
+
);
|
|
200
|
+
CREATE INDEX IF NOT EXISTS idx_goals_company_status ON goals(company_id, status);
|
|
201
|
+
CREATE INDEX IF NOT EXISTS idx_goals_parent ON goals(company_id, parent_id);
|
|
202
|
+
|
|
203
|
+
CREATE TABLE IF NOT EXISTS leads (
|
|
204
|
+
id TEXT PRIMARY KEY,
|
|
205
|
+
company_id TEXT NOT NULL,
|
|
206
|
+
status TEXT,
|
|
207
|
+
email TEXT,
|
|
208
|
+
name TEXT,
|
|
209
|
+
updated_at INTEGER,
|
|
210
|
+
data_json TEXT
|
|
211
|
+
);
|
|
212
|
+
CREATE INDEX IF NOT EXISTS idx_leads_company_status ON leads(company_id, status);
|
|
213
|
+
|
|
214
|
+
CREATE TABLE IF NOT EXISTS crm_contacts (
|
|
215
|
+
id TEXT PRIMARY KEY,
|
|
216
|
+
company_id TEXT NOT NULL,
|
|
217
|
+
email TEXT,
|
|
218
|
+
name TEXT,
|
|
219
|
+
updated_at INTEGER,
|
|
220
|
+
data_json TEXT
|
|
221
|
+
);
|
|
222
|
+
CREATE INDEX IF NOT EXISTS idx_crm_company ON crm_contacts(company_id);
|
|
223
|
+
|
|
224
|
+
CREATE TABLE IF NOT EXISTS accounts (
|
|
225
|
+
id TEXT PRIMARY KEY,
|
|
226
|
+
company_id TEXT NOT NULL,
|
|
227
|
+
name TEXT,
|
|
228
|
+
updated_at INTEGER,
|
|
229
|
+
data_json TEXT
|
|
230
|
+
);
|
|
231
|
+
|
|
232
|
+
CREATE TABLE IF NOT EXISTS opportunities (
|
|
233
|
+
id TEXT PRIMARY KEY,
|
|
234
|
+
company_id TEXT NOT NULL,
|
|
235
|
+
title TEXT,
|
|
236
|
+
status TEXT,
|
|
237
|
+
updated_at INTEGER,
|
|
238
|
+
data_json TEXT
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
CREATE TABLE IF NOT EXISTS helios_projects (
|
|
242
|
+
id TEXT PRIMARY KEY,
|
|
243
|
+
company_id TEXT NOT NULL,
|
|
244
|
+
name TEXT,
|
|
245
|
+
pillar_id TEXT,
|
|
246
|
+
goal_id TEXT,
|
|
247
|
+
status TEXT,
|
|
248
|
+
phase TEXT,
|
|
249
|
+
created_at INTEGER,
|
|
250
|
+
updated_at INTEGER,
|
|
251
|
+
data_json TEXT
|
|
252
|
+
);
|
|
253
|
+
CREATE INDEX IF NOT EXISTS idx_helios_projects_company ON helios_projects(company_id, status);
|
|
254
|
+
|
|
255
|
+
CREATE TABLE IF NOT EXISTS project_documents (
|
|
256
|
+
id TEXT PRIMARY KEY,
|
|
257
|
+
project_id TEXT NOT NULL,
|
|
258
|
+
company_id TEXT,
|
|
259
|
+
purpose TEXT,
|
|
260
|
+
approach TEXT,
|
|
261
|
+
intent_anchor TEXT,
|
|
262
|
+
success_criteria TEXT,
|
|
263
|
+
exclusions TEXT,
|
|
264
|
+
content TEXT,
|
|
265
|
+
version INTEGER DEFAULT 1,
|
|
266
|
+
updated_at INTEGER,
|
|
267
|
+
data_json TEXT
|
|
268
|
+
);
|
|
269
|
+
CREATE INDEX IF NOT EXISTS idx_project_docs_project ON project_documents(project_id);
|
|
270
|
+
|
|
271
|
+
CREATE TABLE IF NOT EXISTS routines (
|
|
272
|
+
id TEXT PRIMARY KEY,
|
|
273
|
+
company_id TEXT NOT NULL,
|
|
274
|
+
agent_id TEXT,
|
|
275
|
+
name TEXT,
|
|
276
|
+
cron_expr TEXT,
|
|
277
|
+
status TEXT DEFAULT 'active',
|
|
278
|
+
concurrency_policy TEXT DEFAULT 'skip_if_active',
|
|
279
|
+
catch_up_policy TEXT DEFAULT 'skip_missed',
|
|
280
|
+
catch_up_cap INTEGER DEFAULT 0,
|
|
281
|
+
next_run_at TEXT,
|
|
282
|
+
last_run_at TEXT,
|
|
283
|
+
created_at INTEGER,
|
|
284
|
+
updated_at INTEGER,
|
|
285
|
+
data_json TEXT
|
|
286
|
+
);
|
|
287
|
+
CREATE INDEX IF NOT EXISTS idx_routines_company_status ON routines(company_id, status);
|
|
288
|
+
|
|
289
|
+
CREATE TABLE IF NOT EXISTS comments (
|
|
290
|
+
id TEXT PRIMARY KEY,
|
|
291
|
+
task_id TEXT,
|
|
292
|
+
approval_id TEXT,
|
|
293
|
+
company_id TEXT NOT NULL,
|
|
294
|
+
body TEXT NOT NULL,
|
|
295
|
+
author_type TEXT DEFAULT 'user',
|
|
296
|
+
author_id TEXT,
|
|
297
|
+
presentation_kind TEXT,
|
|
298
|
+
presentation_tone TEXT,
|
|
299
|
+
sync_status TEXT DEFAULT 'pending',
|
|
300
|
+
created_at INTEGER,
|
|
301
|
+
data_json TEXT
|
|
302
|
+
);
|
|
303
|
+
CREATE INDEX IF NOT EXISTS idx_comments_task ON comments(task_id, created_at);
|
|
304
|
+
CREATE INDEX IF NOT EXISTS idx_comments_approval ON comments(approval_id, created_at);
|
|
305
|
+
CREATE INDEX IF NOT EXISTS idx_comments_sync ON comments(company_id, sync_status);
|
|
306
|
+
`);
|
|
307
|
+
|
|
308
|
+
// H-7 migration: add created_at column to existing goals tables that predate this commit.
|
|
309
|
+
// ALTER TABLE ADD COLUMN is idempotent in SQLite when caught — safe to run on every startup.
|
|
310
|
+
try { db.exec(`ALTER TABLE goals ADD COLUMN created_at INTEGER`); } catch (_) { /* column already exists */ }
|
|
311
|
+
// C-2 migration: project_documents table may not exist on older installs (added in Phase 3 audit fix).
|
|
312
|
+
// CREATE TABLE IF NOT EXISTS handles this; no ALTER needed since it's a new table.
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// ---------------------------------------------------------------------------
|
|
316
|
+
// Helpers
|
|
317
|
+
// ---------------------------------------------------------------------------
|
|
318
|
+
|
|
319
|
+
function _parseJson<T>(value: string | null | undefined, fallback: T): T {
|
|
320
|
+
if (!value) return fallback;
|
|
321
|
+
try { return JSON.parse(value); } catch { return fallback; }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// H-8 fix: these are the SQLite column names that _mergeRow overlays from the DB row.
|
|
325
|
+
// When passing a _mergeRow result back into a CREATE/upsert function, strip these so
|
|
326
|
+
// data_json doesn't accumulate duplicate snake_case keys alongside the camelCase ones.
|
|
327
|
+
const _SQLITE_COLUMN_KEYS = new Set([
|
|
328
|
+
'id', 'company_id', 'title', 'description', 'level', 'status', 'parent_id',
|
|
329
|
+
'created_at', 'updated_at', 'data_json',
|
|
330
|
+
// tasks
|
|
331
|
+
'assignee_agent_id', 'goal_id', 'origin_kind', 'helios_issue_id', 'execution_locked_at',
|
|
332
|
+
// approvals
|
|
333
|
+
'type', 'strategy_id', 'requested_by',
|
|
334
|
+
// cost_events
|
|
335
|
+
'agent_id', 'feature', 'model', 'amount_usd',
|
|
336
|
+
// business_agents
|
|
337
|
+
'role', 'last_heartbeat_at', 'pause_reason',
|
|
338
|
+
// okrs / leads / crm_contacts / accounts / opportunities
|
|
339
|
+
'email', 'name', 'amount',
|
|
340
|
+
// helios_projects (C-4 fix: prevents H-8 data pollution for projects)
|
|
341
|
+
'pillar_id', 'phase',
|
|
342
|
+
// project_documents
|
|
343
|
+
'project_id', 'section', 'value', 'updated_by',
|
|
344
|
+
'intent_anchor', 'success_criteria', 'exclusions', 'content', 'purpose', 'approach', 'version',
|
|
345
|
+
// routines
|
|
346
|
+
'agent_id', 'cron_expr', 'concurrency_policy', 'catch_up_policy', 'catch_up_cap',
|
|
347
|
+
'next_run_at', 'last_run_at',
|
|
348
|
+
// comments (P8C-09)
|
|
349
|
+
'task_id', 'approval_id', 'author_type', 'author_id',
|
|
350
|
+
'presentation_kind', 'presentation_tone', 'sync_status',
|
|
351
|
+
]);
|
|
352
|
+
|
|
353
|
+
function _stripSqliteCols(obj: Record<string, any>): Record<string, any> {
|
|
354
|
+
const result: Record<string, any> = {};
|
|
355
|
+
for (const key of Object.keys(obj)) {
|
|
356
|
+
if (!_SQLITE_COLUMN_KEYS.has(key)) result[key] = obj[key];
|
|
357
|
+
}
|
|
358
|
+
return result;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function _mergeRow(row: any): Record<string, any> {
|
|
362
|
+
const base = _parseJson<Record<string, any>>(row.data_json, {});
|
|
363
|
+
const merged: Record<string, any> = { ...base };
|
|
364
|
+
for (const key of Object.keys(row)) {
|
|
365
|
+
if (key !== 'data_json') merged[key] = row[key];
|
|
366
|
+
}
|
|
367
|
+
return merged;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function _statusParams(statuses: string[]): { placeholders: string; params: string[] } {
|
|
371
|
+
const placeholders = statuses.map(() => '?').join(', ');
|
|
372
|
+
return { placeholders, params: statuses };
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// ---------------------------------------------------------------------------
|
|
376
|
+
// Tasks
|
|
377
|
+
// ---------------------------------------------------------------------------
|
|
378
|
+
|
|
379
|
+
export function createTask(task: Record<string, any>): void {
|
|
380
|
+
const companyId = task.company_id ?? task.companyId;
|
|
381
|
+
if (!companyId) throw new Error(`[hbo-core-store] createTask: company_id is required (task id=${task.id})`);
|
|
382
|
+
const db = getDb();
|
|
383
|
+
const now = Date.now();
|
|
384
|
+
db.prepare(`
|
|
385
|
+
INSERT OR REPLACE INTO tasks
|
|
386
|
+
(id, company_id, title, status, assignee_agent_id, goal_id,
|
|
387
|
+
origin_kind, helios_issue_id, execution_locked_at, created_at, updated_at, data_json)
|
|
388
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
389
|
+
`).run(
|
|
390
|
+
task.id,
|
|
391
|
+
companyId,
|
|
392
|
+
task.title ?? null,
|
|
393
|
+
task.status ?? null,
|
|
394
|
+
task.assignee_agent_id ?? task.assigneeAgentId ?? null,
|
|
395
|
+
task.goal_id ?? task.goalId ?? null,
|
|
396
|
+
task.origin_kind ?? task.originKind ?? null,
|
|
397
|
+
task.helios_issue_id ?? task.heliosIssueId ?? null,
|
|
398
|
+
task.execution_locked_at ?? task.executionLockedAt ?? null,
|
|
399
|
+
task.created_at ?? task.createdAt ?? now,
|
|
400
|
+
task.updated_at ?? task.updatedAt ?? now,
|
|
401
|
+
JSON.stringify(task),
|
|
402
|
+
);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
export function getTask(id: string, companyId: string): Record<string, any> | null {
|
|
406
|
+
const db = getDb();
|
|
407
|
+
const row = db.prepare('SELECT * FROM tasks WHERE id = ? AND company_id = ?').get(id, companyId) as any;
|
|
408
|
+
return row ? _mergeRow(row) : null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function updateTask(id: string, companyId: string, update: Record<string, any>): void {
|
|
412
|
+
// Upsert: if not in SQLite yet, create with update applied rather than silently no-op
|
|
413
|
+
const existing = getTask(id, companyId) ?? { id, company_id: companyId };
|
|
414
|
+
const merged = { ...existing, ...update, id, company_id: companyId, updated_at: Date.now() };
|
|
415
|
+
createTask(merged);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
export function getTasksByCompanyStatus(
|
|
419
|
+
companyId: string,
|
|
420
|
+
status: string | string[],
|
|
421
|
+
): Record<string, any>[] {
|
|
422
|
+
const db = getDb();
|
|
423
|
+
const statuses = Array.isArray(status) ? status : [status];
|
|
424
|
+
const { placeholders, params } = _statusParams(statuses);
|
|
425
|
+
return (db.prepare(
|
|
426
|
+
`SELECT * FROM tasks WHERE company_id = ? AND status IN (${placeholders})`
|
|
427
|
+
).all(companyId, ...params) as any[]).map(_mergeRow);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
export function deleteTask(id: string, companyId: string): void {
|
|
431
|
+
const db = getDb();
|
|
432
|
+
db.prepare('DELETE FROM tasks WHERE id = ? AND company_id = ?').run(id, companyId);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// ---------------------------------------------------------------------------
|
|
436
|
+
// Approvals
|
|
437
|
+
// ---------------------------------------------------------------------------
|
|
438
|
+
|
|
439
|
+
export function createApproval(approval: Record<string, any>): void {
|
|
440
|
+
const companyId = approval.company_id ?? approval.companyId;
|
|
441
|
+
if (!companyId) throw new Error(`[hbo-core-store] createApproval: company_id is required (approval id=${approval.id})`);
|
|
442
|
+
const db = getDb();
|
|
443
|
+
const now = Date.now();
|
|
444
|
+
db.prepare(`
|
|
445
|
+
INSERT OR REPLACE INTO approvals
|
|
446
|
+
(id, company_id, type, status, strategy_id, requested_by, created_at, updated_at, data_json)
|
|
447
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
448
|
+
`).run(
|
|
449
|
+
approval.id,
|
|
450
|
+
companyId,
|
|
451
|
+
approval.type ?? null,
|
|
452
|
+
approval.status ?? null,
|
|
453
|
+
approval.strategy_id ?? approval.strategyId ?? null,
|
|
454
|
+
approval.requested_by ?? approval.requestedBy ?? null,
|
|
455
|
+
approval.created_at ?? approval.createdAt ?? now,
|
|
456
|
+
approval.updated_at ?? approval.updatedAt ?? now,
|
|
457
|
+
JSON.stringify(approval),
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function getApproval(id: string, companyId: string): Record<string, any> | null {
|
|
462
|
+
const db = getDb();
|
|
463
|
+
const row = db.prepare('SELECT * FROM approvals WHERE id = ? AND company_id = ?').get(id, companyId) as any;
|
|
464
|
+
return row ? _mergeRow(row) : null;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
export function updateApproval(id: string, companyId: string, update: Record<string, any>): void {
|
|
468
|
+
const existing = getApproval(id, companyId) ?? { id, company_id: companyId };
|
|
469
|
+
const merged = { ...existing, ...update, id, company_id: companyId, updated_at: Date.now() };
|
|
470
|
+
createApproval(merged);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
export function getApprovalsByCompanyStatus(
|
|
474
|
+
companyId: string,
|
|
475
|
+
status: string | string[],
|
|
476
|
+
): Record<string, any>[] {
|
|
477
|
+
const db = getDb();
|
|
478
|
+
const statuses = Array.isArray(status) ? status : [status];
|
|
479
|
+
const { placeholders, params } = _statusParams(statuses);
|
|
480
|
+
return (db.prepare(
|
|
481
|
+
`SELECT * FROM approvals WHERE company_id = ? AND status IN (${placeholders})`
|
|
482
|
+
).all(companyId, ...params) as any[]).map(_mergeRow);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// ---------------------------------------------------------------------------
|
|
486
|
+
// Budget Policies
|
|
487
|
+
// ---------------------------------------------------------------------------
|
|
488
|
+
|
|
489
|
+
export function upsertBudgetPolicy(policy: Record<string, any>): void {
|
|
490
|
+
const companyId = policy.company_id ?? policy.companyId;
|
|
491
|
+
if (!companyId) throw new Error(`[hbo-core-store] upsertBudgetPolicy: company_id is required`);
|
|
492
|
+
const db = getDb();
|
|
493
|
+
db.prepare(`
|
|
494
|
+
INSERT OR REPLACE INTO budget_policies
|
|
495
|
+
(id, company_id, agent_id, spent_cents, percent_used, updated_at, data_json)
|
|
496
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
497
|
+
`).run(
|
|
498
|
+
policy.id,
|
|
499
|
+
companyId,
|
|
500
|
+
policy.agent_id ?? policy.agentId ?? null,
|
|
501
|
+
policy.spent_cents ?? policy.spentCents ?? 0,
|
|
502
|
+
policy.percent_used ?? policy.percentUsed ?? 0,
|
|
503
|
+
policy.updated_at ?? policy.updatedAt ?? Date.now(),
|
|
504
|
+
JSON.stringify(policy),
|
|
505
|
+
);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
export function getBudgetPolicy(id: string, companyId: string): Record<string, any> | null {
|
|
509
|
+
const db = getDb();
|
|
510
|
+
const row = db.prepare('SELECT * FROM budget_policies WHERE id = ? AND company_id = ?').get(id, companyId) as any;
|
|
511
|
+
return row ? _mergeRow(row) : null;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
export function getBudgetPoliciesByCompany(companyId: string): Record<string, any>[] {
|
|
515
|
+
const db = getDb();
|
|
516
|
+
return (db.prepare('SELECT * FROM budget_policies WHERE company_id = ?').all(companyId) as any[]).map(_mergeRow);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
// Cost Events
|
|
521
|
+
// ---------------------------------------------------------------------------
|
|
522
|
+
|
|
523
|
+
export function createCostEvent(event: Record<string, any>): void {
|
|
524
|
+
const companyId = event.company_id ?? event.companyId;
|
|
525
|
+
if (!companyId) throw new Error(`[hbo-core-store] createCostEvent: company_id is required`);
|
|
526
|
+
const db = getDb();
|
|
527
|
+
const now = Date.now();
|
|
528
|
+
db.prepare(`
|
|
529
|
+
INSERT OR REPLACE INTO cost_events
|
|
530
|
+
(id, company_id, agent_id, feature, model, amount_usd, created_at, data_json)
|
|
531
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
532
|
+
`).run(
|
|
533
|
+
event.id,
|
|
534
|
+
companyId,
|
|
535
|
+
event.agent_id ?? event.agentId ?? null,
|
|
536
|
+
event.feature ?? null,
|
|
537
|
+
event.model ?? null,
|
|
538
|
+
event.amount_usd ?? event.amountUsd ?? null,
|
|
539
|
+
event.created_at ?? event.createdAt ?? now,
|
|
540
|
+
JSON.stringify(event),
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export function getCostEventsByCompanyRange(
|
|
545
|
+
companyId: string,
|
|
546
|
+
startMs: number,
|
|
547
|
+
endMs: number,
|
|
548
|
+
): Record<string, any>[] {
|
|
549
|
+
const db = getDb();
|
|
550
|
+
return (db.prepare(
|
|
551
|
+
'SELECT * FROM cost_events WHERE company_id = ? AND created_at >= ? AND created_at <= ? ORDER BY created_at ASC'
|
|
552
|
+
).all(companyId, startMs, endMs) as any[]).map(_mergeRow);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ---------------------------------------------------------------------------
|
|
556
|
+
// Business Agents
|
|
557
|
+
// ---------------------------------------------------------------------------
|
|
558
|
+
|
|
559
|
+
export function upsertBusinessAgent(agent: Record<string, any>): void {
|
|
560
|
+
const companyId = agent.company_id ?? agent.companyId;
|
|
561
|
+
if (!companyId) throw new Error(`[hbo-core-store] upsertBusinessAgent: company_id is required`);
|
|
562
|
+
const db = getDb();
|
|
563
|
+
db.prepare(`
|
|
564
|
+
INSERT OR REPLACE INTO business_agents
|
|
565
|
+
(id, company_id, role, status, current_task_id, skill, adapter, updated_at, data_json)
|
|
566
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
567
|
+
`).run(
|
|
568
|
+
agent.id,
|
|
569
|
+
companyId,
|
|
570
|
+
agent.role ?? null,
|
|
571
|
+
agent.status ?? null,
|
|
572
|
+
agent.current_task_id ?? agent.currentTaskId ?? null,
|
|
573
|
+
agent.skill ?? null,
|
|
574
|
+
agent.adapter ?? null,
|
|
575
|
+
agent.updated_at ?? agent.updatedAt ?? Date.now(),
|
|
576
|
+
JSON.stringify(agent),
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
export function getBusinessAgent(id: string, companyId: string): Record<string, any> | null {
|
|
581
|
+
const db = getDb();
|
|
582
|
+
const row = db.prepare('SELECT * FROM business_agents WHERE id = ? AND company_id = ?').get(id, companyId) as any;
|
|
583
|
+
return row ? _mergeRow(row) : null;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
export function getBusinessAgentsByCompany(companyId: string): Record<string, any>[] {
|
|
587
|
+
const db = getDb();
|
|
588
|
+
return (db.prepare('SELECT * FROM business_agents WHERE company_id = ?').all(companyId) as any[]).map(_mergeRow);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// ---------------------------------------------------------------------------
|
|
592
|
+
// OKRs
|
|
593
|
+
// ---------------------------------------------------------------------------
|
|
594
|
+
|
|
595
|
+
export function upsertOKR(okr: Record<string, any>): void {
|
|
596
|
+
const companyId = okr.company_id ?? okr.companyId;
|
|
597
|
+
if (!companyId) throw new Error(`[hbo-core-store] upsertOKR: company_id is required`);
|
|
598
|
+
const db = getDb();
|
|
599
|
+
db.prepare(`
|
|
600
|
+
INSERT OR REPLACE INTO okrs
|
|
601
|
+
(id, company_id, type, status, title, updated_at, data_json)
|
|
602
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
603
|
+
`).run(
|
|
604
|
+
okr.id,
|
|
605
|
+
companyId,
|
|
606
|
+
okr.type ?? null,
|
|
607
|
+
okr.status ?? null,
|
|
608
|
+
okr.title ?? null,
|
|
609
|
+
okr.updated_at ?? okr.updatedAt ?? Date.now(),
|
|
610
|
+
JSON.stringify(okr),
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
export function getOKRsByCompanyType(
|
|
615
|
+
companyId: string,
|
|
616
|
+
type?: string,
|
|
617
|
+
status?: string,
|
|
618
|
+
): Record<string, any>[] {
|
|
619
|
+
const db = getDb();
|
|
620
|
+
if (type !== undefined && status !== undefined) {
|
|
621
|
+
return (db.prepare('SELECT * FROM okrs WHERE company_id = ? AND type = ? AND status = ?').all(companyId, type, status) as any[]).map(_mergeRow);
|
|
622
|
+
}
|
|
623
|
+
if (type !== undefined) {
|
|
624
|
+
return (db.prepare('SELECT * FROM okrs WHERE company_id = ? AND type = ?').all(companyId, type) as any[]).map(_mergeRow);
|
|
625
|
+
}
|
|
626
|
+
if (status !== undefined) {
|
|
627
|
+
return (db.prepare('SELECT * FROM okrs WHERE company_id = ? AND status = ?').all(companyId, status) as any[]).map(_mergeRow);
|
|
628
|
+
}
|
|
629
|
+
return (db.prepare('SELECT * FROM okrs WHERE company_id = ?').all(companyId) as any[]).map(_mergeRow);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// ---------------------------------------------------------------------------
|
|
633
|
+
// Goals
|
|
634
|
+
// ---------------------------------------------------------------------------
|
|
635
|
+
|
|
636
|
+
export function createGoal(goal: Record<string, any>): void {
|
|
637
|
+
const companyId = goal.company_id ?? goal.companyId;
|
|
638
|
+
if (!companyId) throw new Error(`[hbo-core-store] createGoal: company_id is required (goal id=${goal.id})`);
|
|
639
|
+
const db = getDb();
|
|
640
|
+
const now = Date.now();
|
|
641
|
+
db.prepare(`
|
|
642
|
+
INSERT OR REPLACE INTO goals
|
|
643
|
+
(id, company_id, title, description, level, status, parent_id, created_at, updated_at, data_json)
|
|
644
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
645
|
+
`).run(
|
|
646
|
+
goal.id,
|
|
647
|
+
companyId,
|
|
648
|
+
goal.title ?? null,
|
|
649
|
+
goal.description ?? goal.desc ?? null,
|
|
650
|
+
goal.level ?? null,
|
|
651
|
+
goal.status ?? null,
|
|
652
|
+
goal.parent_id ?? goal.parentId ?? goal.parentGoalId ?? null,
|
|
653
|
+
goal.created_at ?? goal.createdAt ?? now,
|
|
654
|
+
goal.updated_at ?? goal.updatedAt ?? now,
|
|
655
|
+
JSON.stringify(goal),
|
|
656
|
+
);
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export function getGoal(id: string, companyId: string): Record<string, any> | null {
|
|
660
|
+
const db = getDb();
|
|
661
|
+
const row = db.prepare('SELECT * FROM goals WHERE id = ? AND company_id = ?').get(id, companyId) as any;
|
|
662
|
+
return row ? _mergeRow(row) : null;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
export function updateGoal(id: string, companyId: string, update: Record<string, any>): void {
|
|
666
|
+
const existing = getGoal(id, companyId) ?? { id, company_id: companyId };
|
|
667
|
+
// H-8 fix: strip SQLite column keys from the existing merged row before re-creating,
|
|
668
|
+
// so data_json doesn't accumulate duplicate snake_case keys from _mergeRow.
|
|
669
|
+
const cleanExisting = _stripSqliteCols(existing);
|
|
670
|
+
createGoal({ ...cleanExisting, ...update, id, company_id: companyId, updated_at: Date.now() });
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
export function deleteGoal(id: string, companyId: string): void {
|
|
674
|
+
const db = getDb();
|
|
675
|
+
db.prepare('DELETE FROM goals WHERE id = ? AND company_id = ?').run(id, companyId);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export function getGoalsByCompany(companyId: string): Record<string, any>[] {
|
|
679
|
+
const db = getDb();
|
|
680
|
+
return (db.prepare('SELECT * FROM goals WHERE company_id = ?').all(companyId) as any[]).map(_mergeRow);
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ---------------------------------------------------------------------------
|
|
684
|
+
// HeliosProjects (P3-SQL: SQLite mirror of HeliosProject Memgraph nodes)
|
|
685
|
+
// ---------------------------------------------------------------------------
|
|
686
|
+
|
|
687
|
+
export function upsertProject(project: Record<string, any>): void {
|
|
688
|
+
const companyId = project.company_id ?? project.companyId;
|
|
689
|
+
if (!companyId) throw new Error(`[hbo-core-store] upsertProject: company_id is required`);
|
|
690
|
+
const db = getDb();
|
|
691
|
+
const now = Date.now();
|
|
692
|
+
db.prepare(`
|
|
693
|
+
INSERT OR REPLACE INTO helios_projects
|
|
694
|
+
(id, company_id, name, pillar_id, goal_id, status, phase, created_at, updated_at, data_json)
|
|
695
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
696
|
+
`).run(
|
|
697
|
+
project.id,
|
|
698
|
+
companyId,
|
|
699
|
+
project.name ?? null,
|
|
700
|
+
project.pillar_id ?? project.pillarId ?? null,
|
|
701
|
+
project.goal_id ?? project.goalId ?? null,
|
|
702
|
+
project.status ?? 'planning',
|
|
703
|
+
project.phase ?? 'planning',
|
|
704
|
+
project.created_at ?? project.createdAt ?? now,
|
|
705
|
+
project.updated_at ?? project.updatedAt ?? now,
|
|
706
|
+
JSON.stringify(project),
|
|
707
|
+
);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
export function getProject(id: string, companyId: string): Record<string, any> | null {
|
|
711
|
+
const db = getDb();
|
|
712
|
+
const row = db.prepare('SELECT * FROM helios_projects WHERE id = ? AND company_id = ?').get(id, companyId) as any;
|
|
713
|
+
return row ? _mergeRow(row) : null;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
export function getProjectsByCompany(companyId: string): Record<string, any>[] {
|
|
717
|
+
const db = getDb();
|
|
718
|
+
return (db.prepare('SELECT * FROM helios_projects WHERE company_id = ? ORDER BY created_at DESC LIMIT 100').all(companyId) as any[]).map(_mergeRow);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
export function updateProject(id: string, companyId: string, update: Record<string, any>): void {
|
|
722
|
+
// M-4 fix: SQLite project state can now advance (status, phase, name changes)
|
|
723
|
+
const existing = getProject(id, companyId) ?? { id, company_id: companyId };
|
|
724
|
+
const cleanExisting = _stripSqliteCols(existing);
|
|
725
|
+
upsertProject({ ...cleanExisting, ...update, id, company_id: companyId, updated_at: Date.now() });
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// ---------------------------------------------------------------------------
|
|
729
|
+
// ProjectDocuments (C-2: SQLite mirror of ProjectDocument Memgraph nodes)
|
|
730
|
+
// ---------------------------------------------------------------------------
|
|
731
|
+
|
|
732
|
+
export function upsertProjectDocument(doc: Record<string, any>): void {
|
|
733
|
+
const projectId = doc.project_id ?? doc.projectId;
|
|
734
|
+
if (!projectId) throw new Error(`[hbo-core-store] upsertProjectDocument: project_id is required`);
|
|
735
|
+
const db = getDb();
|
|
736
|
+
const id = doc.id ?? `pdoc:${projectId}:main`;
|
|
737
|
+
const now = Date.now();
|
|
738
|
+
db.prepare(`
|
|
739
|
+
INSERT OR REPLACE INTO project_documents
|
|
740
|
+
(id, project_id, company_id, purpose, approach, intent_anchor,
|
|
741
|
+
success_criteria, exclusions, content, version, updated_at, data_json)
|
|
742
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
743
|
+
`).run(
|
|
744
|
+
id,
|
|
745
|
+
projectId,
|
|
746
|
+
doc.company_id ?? doc.companyId ?? null,
|
|
747
|
+
doc.purpose ?? null,
|
|
748
|
+
doc.approach ?? null,
|
|
749
|
+
doc.intent_anchor ?? doc.intentAnchor ?? null,
|
|
750
|
+
doc.success_criteria ?? doc.successCriteria ?? '[]',
|
|
751
|
+
doc.exclusions ?? '[]',
|
|
752
|
+
doc.content ?? null,
|
|
753
|
+
doc.version ?? 1,
|
|
754
|
+
doc.updated_at ?? doc.updatedAt ?? now,
|
|
755
|
+
JSON.stringify(doc),
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export function getProjectDocument(projectId: string): Record<string, any> | null {
|
|
760
|
+
const db = getDb();
|
|
761
|
+
const row = db.prepare('SELECT * FROM project_documents WHERE project_id = ? ORDER BY updated_at DESC LIMIT 1').get(projectId) as any;
|
|
762
|
+
return row ? _mergeRow(row) : null;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// ---------------------------------------------------------------------------
|
|
766
|
+
// Routines (P5-SQL: SQLite mirror of Routine Memgraph nodes)
|
|
767
|
+
// ---------------------------------------------------------------------------
|
|
768
|
+
|
|
769
|
+
export function upsertRoutine(routine: Record<string, any>): void {
|
|
770
|
+
const companyId = routine.company_id ?? routine.companyId;
|
|
771
|
+
if (!companyId) throw new Error(`[hbo-core-store] upsertRoutine: company_id is required`);
|
|
772
|
+
const db = getDb();
|
|
773
|
+
const now = Date.now();
|
|
774
|
+
db.prepare(`
|
|
775
|
+
INSERT OR REPLACE INTO routines
|
|
776
|
+
(id, company_id, agent_id, name, cron_expr, status, concurrency_policy,
|
|
777
|
+
catch_up_policy, catch_up_cap, next_run_at, last_run_at, created_at, updated_at, data_json)
|
|
778
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
779
|
+
`).run(
|
|
780
|
+
routine.id,
|
|
781
|
+
companyId,
|
|
782
|
+
routine.agent_id ?? routine.agentId ?? null,
|
|
783
|
+
routine.name ?? routine.title ?? null,
|
|
784
|
+
routine.cron_expr ?? routine.cronExpr ?? null,
|
|
785
|
+
routine.status ?? 'active',
|
|
786
|
+
routine.concurrency_policy ?? routine.concurrencyPolicy ?? 'skip_if_active',
|
|
787
|
+
routine.catch_up_policy ?? routine.catchUpPolicy ?? 'skip_missed',
|
|
788
|
+
routine.catch_up_cap ?? routine.catchUpCap ?? 0,
|
|
789
|
+
routine.next_run_at ?? routine.nextRunAt ?? null,
|
|
790
|
+
routine.last_run_at ?? routine.lastRunAt ?? null,
|
|
791
|
+
routine.created_at ?? routine.createdAt ?? now,
|
|
792
|
+
routine.updated_at ?? routine.updatedAt ?? now,
|
|
793
|
+
JSON.stringify(routine),
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
export function getRoutine(id: string, companyId: string): Record<string, any> | null {
|
|
798
|
+
const db = getDb();
|
|
799
|
+
const row = db.prepare('SELECT * FROM routines WHERE id = ? AND company_id = ?').get(id, companyId) as any;
|
|
800
|
+
return row ? _mergeRow(row) : null;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
export function getRoutinesByCompany(companyId: string, status?: string): Record<string, any>[] {
|
|
804
|
+
const db = getDb();
|
|
805
|
+
if (status) {
|
|
806
|
+
return (db.prepare('SELECT * FROM routines WHERE company_id = ? AND status = ? ORDER BY created_at DESC').all(companyId, status) as any[]).map(_mergeRow);
|
|
807
|
+
}
|
|
808
|
+
return (db.prepare('SELECT * FROM routines WHERE company_id = ? ORDER BY created_at DESC').all(companyId) as any[]).map(_mergeRow);
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
export function updateRoutine(id: string, companyId: string, update: Record<string, any>): void {
|
|
812
|
+
const existing = getRoutine(id, companyId) ?? { id, company_id: companyId };
|
|
813
|
+
const cleanExisting = _stripSqliteCols(existing);
|
|
814
|
+
upsertRoutine({ ...cleanExisting, ...update, id, company_id: companyId, updated_at: Date.now() });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ---------------------------------------------------------------------------
|
|
818
|
+
// Comments (P8C-09: offline queue for task + approval comments)
|
|
819
|
+
// ---------------------------------------------------------------------------
|
|
820
|
+
|
|
821
|
+
export function createComment(comment: Record<string, any>): void {
|
|
822
|
+
const companyId = comment.company_id ?? comment.companyId;
|
|
823
|
+
if (!companyId) throw new Error(`[hbo-core-store] createComment: company_id is required`);
|
|
824
|
+
const db = getDb();
|
|
825
|
+
const now = Date.now();
|
|
826
|
+
db.prepare(`
|
|
827
|
+
INSERT OR REPLACE INTO comments
|
|
828
|
+
(id, task_id, approval_id, company_id, body, author_type, author_id,
|
|
829
|
+
presentation_kind, presentation_tone, sync_status, created_at, data_json)
|
|
830
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
831
|
+
`).run(
|
|
832
|
+
comment.id,
|
|
833
|
+
comment.task_id ?? comment.taskId ?? null,
|
|
834
|
+
comment.approval_id ?? comment.approvalId ?? null,
|
|
835
|
+
companyId,
|
|
836
|
+
comment.body ?? '',
|
|
837
|
+
comment.author_type ?? comment.authorType ?? 'user',
|
|
838
|
+
comment.author_id ?? comment.authorId ?? null,
|
|
839
|
+
comment.presentation_kind ?? comment.presentationKind ?? null,
|
|
840
|
+
comment.presentation_tone ?? comment.presentationTone ?? null,
|
|
841
|
+
comment.sync_status ?? comment.syncStatus ?? 'pending',
|
|
842
|
+
comment.created_at ?? comment.createdAt ?? now,
|
|
843
|
+
JSON.stringify(comment),
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
export function getCommentsByTask(taskId: string, companyId: string): Record<string, any>[] {
|
|
848
|
+
const db = getDb();
|
|
849
|
+
return (db.prepare('SELECT * FROM comments WHERE task_id = ? AND company_id = ? ORDER BY created_at ASC').all(taskId, companyId) as any[]).map(_mergeRow);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
export function getCommentsByApproval(approvalId: string, companyId: string): Record<string, any>[] {
|
|
853
|
+
const db = getDb();
|
|
854
|
+
return (db.prepare('SELECT * FROM comments WHERE approval_id = ? AND company_id = ? ORDER BY created_at ASC').all(approvalId, companyId) as any[]).map(_mergeRow);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
export function getPendingComments(companyId: string): Record<string, any>[] {
|
|
858
|
+
const db = getDb();
|
|
859
|
+
return (db.prepare("SELECT * FROM comments WHERE company_id = ? AND sync_status = 'pending' ORDER BY created_at ASC").all(companyId) as any[]).map(_mergeRow);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
export function markCommentSynced(id: string): void {
|
|
863
|
+
const db = getDb();
|
|
864
|
+
db.prepare("UPDATE comments SET sync_status = 'synced' WHERE id = ?").run(id);
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// ---------------------------------------------------------------------------
|
|
868
|
+
// Leads
|
|
869
|
+
// ---------------------------------------------------------------------------
|
|
870
|
+
|
|
871
|
+
export function upsertLead(lead: Record<string, any>): void {
|
|
872
|
+
const companyId = lead.company_id ?? lead.companyId;
|
|
873
|
+
if (!companyId) throw new Error(`[hbo-core-store] upsertLead: company_id is required`);
|
|
874
|
+
const db = getDb();
|
|
875
|
+
db.prepare(`
|
|
876
|
+
INSERT OR REPLACE INTO leads
|
|
877
|
+
(id, company_id, status, email, name, updated_at, data_json)
|
|
878
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
879
|
+
`).run(
|
|
880
|
+
lead.id,
|
|
881
|
+
companyId,
|
|
882
|
+
lead.status ?? null,
|
|
883
|
+
lead.email ?? null,
|
|
884
|
+
lead.name ?? null,
|
|
885
|
+
lead.updated_at ?? lead.updatedAt ?? Date.now(),
|
|
886
|
+
JSON.stringify(lead),
|
|
887
|
+
);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
export function getLeadsByCompanyStatus(companyId: string, statuses: string[]): Record<string, any>[] {
|
|
891
|
+
const db = getDb();
|
|
892
|
+
const { placeholders, params } = _statusParams(statuses);
|
|
893
|
+
return (db.prepare(
|
|
894
|
+
`SELECT * FROM leads WHERE company_id = ? AND status IN (${placeholders})`
|
|
895
|
+
).all(companyId, ...params) as any[]).map(_mergeRow);
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ---------------------------------------------------------------------------
|
|
899
|
+
// CRM Contacts
|
|
900
|
+
// ---------------------------------------------------------------------------
|
|
901
|
+
|
|
902
|
+
export function upsertCRMContact(contact: Record<string, any>): void {
|
|
903
|
+
const companyId = contact.company_id ?? contact.companyId;
|
|
904
|
+
if (!companyId) throw new Error(`[hbo-core-store] upsertCRMContact: company_id is required`);
|
|
905
|
+
const db = getDb();
|
|
906
|
+
db.prepare(`
|
|
907
|
+
INSERT OR REPLACE INTO crm_contacts
|
|
908
|
+
(id, company_id, email, name, updated_at, data_json)
|
|
909
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
910
|
+
`).run(
|
|
911
|
+
contact.id,
|
|
912
|
+
companyId,
|
|
913
|
+
contact.email ?? null,
|
|
914
|
+
contact.name ?? null,
|
|
915
|
+
contact.updated_at ?? contact.updatedAt ?? Date.now(),
|
|
916
|
+
JSON.stringify(contact),
|
|
917
|
+
);
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
export function getCRMContactsByCompany(companyId: string): Record<string, any>[] {
|
|
921
|
+
const db = getDb();
|
|
922
|
+
return (db.prepare('SELECT * FROM crm_contacts WHERE company_id = ?').all(companyId) as any[]).map(_mergeRow);
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ---------------------------------------------------------------------------
|
|
926
|
+
// Accounts
|
|
927
|
+
// ---------------------------------------------------------------------------
|
|
928
|
+
|
|
929
|
+
export function upsertAccount(account: Record<string, any>): void {
|
|
930
|
+
const companyId = account.company_id ?? account.companyId;
|
|
931
|
+
if (!companyId) throw new Error(`[hbo-core-store] upsertAccount: company_id is required`);
|
|
932
|
+
const db = getDb();
|
|
933
|
+
db.prepare(`
|
|
934
|
+
INSERT OR REPLACE INTO accounts
|
|
935
|
+
(id, company_id, name, updated_at, data_json)
|
|
936
|
+
VALUES (?, ?, ?, ?, ?)
|
|
937
|
+
`).run(
|
|
938
|
+
account.id,
|
|
939
|
+
companyId,
|
|
940
|
+
account.name ?? null,
|
|
941
|
+
account.updated_at ?? account.updatedAt ?? Date.now(),
|
|
942
|
+
JSON.stringify(account),
|
|
943
|
+
);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// ---------------------------------------------------------------------------
|
|
947
|
+
// Opportunities
|
|
948
|
+
// ---------------------------------------------------------------------------
|
|
949
|
+
|
|
950
|
+
export function upsertOpportunity(opp: Record<string, any>): void {
|
|
951
|
+
const companyId = opp.company_id ?? opp.companyId;
|
|
952
|
+
if (!companyId) throw new Error(`[hbo-core-store] upsertOpportunity: company_id is required`);
|
|
953
|
+
const db = getDb();
|
|
954
|
+
db.prepare(`
|
|
955
|
+
INSERT OR REPLACE INTO opportunities
|
|
956
|
+
(id, company_id, title, status, updated_at, data_json)
|
|
957
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
958
|
+
`).run(
|
|
959
|
+
opp.id,
|
|
960
|
+
companyId,
|
|
961
|
+
opp.title ?? null,
|
|
962
|
+
opp.status ?? null,
|
|
963
|
+
opp.updated_at ?? opp.updatedAt ?? Date.now(),
|
|
964
|
+
JSON.stringify(opp),
|
|
965
|
+
);
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
// ---------------------------------------------------------------------------
|
|
969
|
+
// Test / lifecycle helpers
|
|
970
|
+
// ---------------------------------------------------------------------------
|
|
971
|
+
|
|
972
|
+
/**
|
|
973
|
+
* Reset the DB singleton. ONLY call this in tests.
|
|
974
|
+
*/
|
|
975
|
+
export function resetHboCoreStore(): void {
|
|
976
|
+
try { _db?.close(); } catch { /* ignore */ }
|
|
977
|
+
_db = null;
|
|
978
|
+
_engine = null;
|
|
979
|
+
}
|