chainlesschain 0.37.9 → 0.37.11
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/README.md +309 -19
- package/bin/chainlesschain.js +4 -0
- package/package.json +1 -1
- package/src/commands/a2a.js +374 -0
- package/src/commands/audit.js +286 -0
- package/src/commands/auth.js +387 -0
- package/src/commands/bi.js +240 -0
- package/src/commands/browse.js +184 -0
- package/src/commands/cowork.js +317 -0
- package/src/commands/did.js +376 -0
- package/src/commands/economy.js +375 -0
- package/src/commands/encrypt.js +233 -0
- package/src/commands/evolution.js +398 -0
- package/src/commands/export.js +125 -0
- package/src/commands/git.js +215 -0
- package/src/commands/hmemory.js +273 -0
- package/src/commands/hook.js +260 -0
- package/src/commands/import.js +259 -0
- package/src/commands/init.js +184 -0
- package/src/commands/instinct.js +202 -0
- package/src/commands/llm.js +155 -4
- package/src/commands/lowcode.js +320 -0
- package/src/commands/mcp.js +302 -0
- package/src/commands/memory.js +282 -0
- package/src/commands/note.js +187 -0
- package/src/commands/org.js +505 -0
- package/src/commands/p2p.js +274 -0
- package/src/commands/plugin.js +451 -0
- package/src/commands/sandbox.js +366 -0
- package/src/commands/search.js +237 -0
- package/src/commands/session.js +238 -0
- package/src/commands/skill.js +254 -201
- package/src/commands/sync.js +249 -0
- package/src/commands/tokens.js +214 -0
- package/src/commands/wallet.js +416 -0
- package/src/commands/workflow.js +359 -0
- package/src/commands/zkp.js +277 -0
- package/src/index.js +93 -1
- package/src/lib/a2a-protocol.js +371 -0
- package/src/lib/agent-coordinator.js +273 -0
- package/src/lib/agent-economy.js +369 -0
- package/src/lib/app-builder.js +377 -0
- package/src/lib/audit-logger.js +364 -0
- package/src/lib/bi-engine.js +299 -0
- package/src/lib/bm25-search.js +322 -0
- package/src/lib/browser-automation.js +216 -0
- package/src/lib/cowork/ab-comparator-cli.js +180 -0
- package/src/lib/cowork/code-knowledge-graph-cli.js +232 -0
- package/src/lib/cowork/debate-review-cli.js +144 -0
- package/src/lib/cowork/decision-kb-cli.js +153 -0
- package/src/lib/cowork/project-style-analyzer-cli.js +168 -0
- package/src/lib/cowork-adapter.js +106 -0
- package/src/lib/crypto-manager.js +246 -0
- package/src/lib/did-manager.js +270 -0
- package/src/lib/ensure-utf8.js +59 -0
- package/src/lib/evolution-system.js +508 -0
- package/src/lib/git-integration.js +220 -0
- package/src/lib/hierarchical-memory.js +471 -0
- package/src/lib/hook-manager.js +387 -0
- package/src/lib/instinct-manager.js +190 -0
- package/src/lib/knowledge-exporter.js +302 -0
- package/src/lib/knowledge-importer.js +293 -0
- package/src/lib/llm-providers.js +325 -0
- package/src/lib/mcp-client.js +413 -0
- package/src/lib/memory-manager.js +211 -0
- package/src/lib/note-versioning.js +244 -0
- package/src/lib/org-manager.js +424 -0
- package/src/lib/p2p-manager.js +317 -0
- package/src/lib/pdf-parser.js +96 -0
- package/src/lib/permission-engine.js +374 -0
- package/src/lib/plan-mode.js +333 -0
- package/src/lib/plugin-manager.js +430 -0
- package/src/lib/project-detector.js +53 -0
- package/src/lib/response-cache.js +156 -0
- package/src/lib/sandbox-v2.js +503 -0
- package/src/lib/service-container.js +183 -0
- package/src/lib/session-manager.js +189 -0
- package/src/lib/skill-loader.js +274 -0
- package/src/lib/sync-manager.js +347 -0
- package/src/lib/token-tracker.js +200 -0
- package/src/lib/wallet-manager.js +348 -0
- package/src/lib/workflow-engine.js +503 -0
- package/src/lib/zkp-engine.js +241 -0
- package/src/repl/agent-repl.js +259 -124
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Audit Logger — records security events and operations for compliance.
|
|
3
|
+
* Provides event logging, querying, statistics, and export.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from "crypto";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Event types for audit logging.
|
|
10
|
+
*/
|
|
11
|
+
export const EVENT_TYPES = {
|
|
12
|
+
AUTH: "auth",
|
|
13
|
+
PERMISSION: "permission",
|
|
14
|
+
DATA: "data",
|
|
15
|
+
SYSTEM: "system",
|
|
16
|
+
FILE: "file",
|
|
17
|
+
DID: "did",
|
|
18
|
+
CRYPTO: "crypto",
|
|
19
|
+
API: "api",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Risk levels.
|
|
24
|
+
*/
|
|
25
|
+
export const RISK_LEVELS = {
|
|
26
|
+
LOW: "low",
|
|
27
|
+
MEDIUM: "medium",
|
|
28
|
+
HIGH: "high",
|
|
29
|
+
CRITICAL: "critical",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* High-risk operations that trigger elevated risk assessment.
|
|
34
|
+
*/
|
|
35
|
+
const HIGH_RISK_OPERATIONS = new Set([
|
|
36
|
+
"delete_identity",
|
|
37
|
+
"grant_admin",
|
|
38
|
+
"revoke_all",
|
|
39
|
+
"delete_role",
|
|
40
|
+
"db_encrypt",
|
|
41
|
+
"db_decrypt",
|
|
42
|
+
"config_change",
|
|
43
|
+
"export_secrets",
|
|
44
|
+
"bulk_delete",
|
|
45
|
+
"password_reset",
|
|
46
|
+
"schema_change",
|
|
47
|
+
]);
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Ensure audit tables exist.
|
|
51
|
+
*/
|
|
52
|
+
export function ensureAuditTables(db) {
|
|
53
|
+
db.exec(`
|
|
54
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
55
|
+
id TEXT PRIMARY KEY,
|
|
56
|
+
event_type TEXT NOT NULL,
|
|
57
|
+
operation TEXT NOT NULL,
|
|
58
|
+
actor TEXT,
|
|
59
|
+
target TEXT,
|
|
60
|
+
details TEXT,
|
|
61
|
+
risk_level TEXT DEFAULT 'low',
|
|
62
|
+
ip_address TEXT,
|
|
63
|
+
user_agent TEXT,
|
|
64
|
+
success INTEGER DEFAULT 1,
|
|
65
|
+
error_message TEXT,
|
|
66
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
67
|
+
)
|
|
68
|
+
`);
|
|
69
|
+
|
|
70
|
+
db.exec(`
|
|
71
|
+
CREATE INDEX IF NOT EXISTS idx_audit_event_type ON audit_log(event_type)
|
|
72
|
+
`);
|
|
73
|
+
db.exec(`
|
|
74
|
+
CREATE INDEX IF NOT EXISTS idx_audit_created_at ON audit_log(created_at)
|
|
75
|
+
`);
|
|
76
|
+
db.exec(`
|
|
77
|
+
CREATE INDEX IF NOT EXISTS idx_audit_risk_level ON audit_log(risk_level)
|
|
78
|
+
`);
|
|
79
|
+
db.exec(`
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_log(actor)
|
|
81
|
+
`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Assess risk level for an operation.
|
|
86
|
+
*/
|
|
87
|
+
export function assessRisk(eventType, operation, details) {
|
|
88
|
+
if (HIGH_RISK_OPERATIONS.has(operation)) {
|
|
89
|
+
return RISK_LEVELS.HIGH;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (eventType === EVENT_TYPES.AUTH && operation.includes("fail")) {
|
|
93
|
+
return RISK_LEVELS.MEDIUM;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (eventType === EVENT_TYPES.PERMISSION) {
|
|
97
|
+
return RISK_LEVELS.MEDIUM;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (details && typeof details === "object") {
|
|
101
|
+
if (details.bulkCount && details.bulkCount > 100) {
|
|
102
|
+
return RISK_LEVELS.HIGH;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return RISK_LEVELS.LOW;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Sanitize sensitive data from log details.
|
|
111
|
+
*/
|
|
112
|
+
export function sanitizeDetails(details) {
|
|
113
|
+
if (!details || typeof details !== "object") return details;
|
|
114
|
+
|
|
115
|
+
const sanitized = { ...details };
|
|
116
|
+
const sensitiveKeys = [
|
|
117
|
+
"password",
|
|
118
|
+
"secret",
|
|
119
|
+
"secretKey",
|
|
120
|
+
"secret_key",
|
|
121
|
+
"privateKey",
|
|
122
|
+
"private_key",
|
|
123
|
+
"token",
|
|
124
|
+
"apiKey",
|
|
125
|
+
"api_key",
|
|
126
|
+
"mnemonic",
|
|
127
|
+
];
|
|
128
|
+
|
|
129
|
+
for (const key of sensitiveKeys) {
|
|
130
|
+
if (sanitized[key]) {
|
|
131
|
+
sanitized[key] = "[REDACTED]";
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return sanitized;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Log an audit event.
|
|
140
|
+
*/
|
|
141
|
+
export function logEvent(db, event) {
|
|
142
|
+
ensureAuditTables(db);
|
|
143
|
+
|
|
144
|
+
const id = crypto.randomUUID();
|
|
145
|
+
const sanitized = sanitizeDetails(event.details);
|
|
146
|
+
const risk =
|
|
147
|
+
event.riskLevel ||
|
|
148
|
+
assessRisk(event.eventType, event.operation, event.details);
|
|
149
|
+
|
|
150
|
+
db.prepare(
|
|
151
|
+
`INSERT INTO audit_log (id, event_type, operation, actor, target, details, risk_level, ip_address, user_agent, success, error_message)
|
|
152
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
153
|
+
).run(
|
|
154
|
+
id,
|
|
155
|
+
event.eventType || EVENT_TYPES.SYSTEM,
|
|
156
|
+
event.operation || "unknown",
|
|
157
|
+
event.actor || null,
|
|
158
|
+
event.target || null,
|
|
159
|
+
sanitized ? JSON.stringify(sanitized) : null,
|
|
160
|
+
risk,
|
|
161
|
+
event.ipAddress || null,
|
|
162
|
+
event.userAgent || null,
|
|
163
|
+
event.success !== false ? 1 : 0,
|
|
164
|
+
event.errorMessage || null,
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return { id, riskLevel: risk, createdAt: new Date().toISOString() };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Query audit logs with filters.
|
|
172
|
+
*/
|
|
173
|
+
export function queryLogs(db, filters = {}) {
|
|
174
|
+
ensureAuditTables(db);
|
|
175
|
+
|
|
176
|
+
let sql = "SELECT * FROM audit_log WHERE 1=1";
|
|
177
|
+
const params = [];
|
|
178
|
+
|
|
179
|
+
if (filters.eventType) {
|
|
180
|
+
sql += " AND event_type = ?";
|
|
181
|
+
params.push(filters.eventType);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (filters.operation) {
|
|
185
|
+
sql += " AND operation LIKE ?";
|
|
186
|
+
params.push(`%${filters.operation}%`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (filters.actor) {
|
|
190
|
+
sql += " AND actor LIKE ?";
|
|
191
|
+
params.push(`%${filters.actor}%`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (filters.riskLevel) {
|
|
195
|
+
sql += " AND risk_level = ?";
|
|
196
|
+
params.push(filters.riskLevel);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (filters.success !== undefined) {
|
|
200
|
+
sql += " AND success = ?";
|
|
201
|
+
params.push(filters.success ? 1 : 0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (filters.startDate) {
|
|
205
|
+
sql += " AND created_at >= ?";
|
|
206
|
+
params.push(filters.startDate);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (filters.endDate) {
|
|
210
|
+
sql += " AND created_at <= ?";
|
|
211
|
+
params.push(filters.endDate);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (filters.search) {
|
|
215
|
+
// Search in operation field (primary search field for CLI)
|
|
216
|
+
sql += " AND operation LIKE ?";
|
|
217
|
+
params.push(`%${filters.search}%`);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
sql += " ORDER BY created_at DESC";
|
|
221
|
+
|
|
222
|
+
if (filters.limit) {
|
|
223
|
+
sql += " LIMIT ?";
|
|
224
|
+
params.push(filters.limit);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (filters.offset) {
|
|
228
|
+
sql += " OFFSET ?";
|
|
229
|
+
params.push(filters.offset);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const rows = db.prepare(sql).all(...params);
|
|
233
|
+
return rows.map((r) => ({
|
|
234
|
+
...r,
|
|
235
|
+
details: r.details ? JSON.parse(r.details) : null,
|
|
236
|
+
success: r.success === 1,
|
|
237
|
+
}));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Get audit statistics.
|
|
242
|
+
*/
|
|
243
|
+
export function getStatistics(db, startDate, endDate) {
|
|
244
|
+
ensureAuditTables(db);
|
|
245
|
+
|
|
246
|
+
let dateFilter = "";
|
|
247
|
+
const params = [];
|
|
248
|
+
|
|
249
|
+
if (startDate) {
|
|
250
|
+
dateFilter += " AND created_at >= ?";
|
|
251
|
+
params.push(startDate);
|
|
252
|
+
}
|
|
253
|
+
if (endDate) {
|
|
254
|
+
dateFilter += " AND created_at <= ?";
|
|
255
|
+
params.push(endDate);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const total = db
|
|
259
|
+
.prepare(`SELECT COUNT(*) as c FROM audit_log WHERE 1=1 ${dateFilter}`)
|
|
260
|
+
.get(...params).c;
|
|
261
|
+
|
|
262
|
+
const byEventType = db
|
|
263
|
+
.prepare(
|
|
264
|
+
`SELECT event_type, COUNT(*) as count FROM audit_log WHERE 1=1 ${dateFilter} GROUP BY event_type ORDER BY count DESC`,
|
|
265
|
+
)
|
|
266
|
+
.all(...params);
|
|
267
|
+
|
|
268
|
+
const byRiskLevel = db
|
|
269
|
+
.prepare(
|
|
270
|
+
`SELECT risk_level, COUNT(*) as count FROM audit_log WHERE 1=1 ${dateFilter} GROUP BY risk_level ORDER BY count DESC`,
|
|
271
|
+
)
|
|
272
|
+
.all(...params);
|
|
273
|
+
|
|
274
|
+
const failures = db
|
|
275
|
+
.prepare(
|
|
276
|
+
`SELECT COUNT(*) as c FROM audit_log WHERE success = 0 ${dateFilter}`,
|
|
277
|
+
)
|
|
278
|
+
.get(...params).c;
|
|
279
|
+
|
|
280
|
+
const highRiskHigh = db
|
|
281
|
+
.prepare(
|
|
282
|
+
`SELECT COUNT(*) as c FROM audit_log WHERE risk_level = 'high' ${dateFilter}`,
|
|
283
|
+
)
|
|
284
|
+
.get(...params).c;
|
|
285
|
+
const highRiskCritical = db
|
|
286
|
+
.prepare(
|
|
287
|
+
`SELECT COUNT(*) as c FROM audit_log WHERE risk_level = 'critical' ${dateFilter}`,
|
|
288
|
+
)
|
|
289
|
+
.get(...params).c;
|
|
290
|
+
const highRisk = highRiskHigh + highRiskCritical;
|
|
291
|
+
|
|
292
|
+
return {
|
|
293
|
+
total,
|
|
294
|
+
failures,
|
|
295
|
+
highRisk,
|
|
296
|
+
byEventType: Object.fromEntries(
|
|
297
|
+
byEventType.map((r) => [r.event_type, r.count]),
|
|
298
|
+
),
|
|
299
|
+
byRiskLevel: Object.fromEntries(
|
|
300
|
+
byRiskLevel.map((r) => [r.risk_level, r.count]),
|
|
301
|
+
),
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Export audit logs as JSON or CSV.
|
|
307
|
+
*/
|
|
308
|
+
export function exportLogs(db, format = "json", filters = {}) {
|
|
309
|
+
const logs = queryLogs(db, { ...filters, limit: filters.limit || 10000 });
|
|
310
|
+
|
|
311
|
+
if (format === "csv") {
|
|
312
|
+
const headers = [
|
|
313
|
+
"id",
|
|
314
|
+
"event_type",
|
|
315
|
+
"operation",
|
|
316
|
+
"actor",
|
|
317
|
+
"target",
|
|
318
|
+
"risk_level",
|
|
319
|
+
"success",
|
|
320
|
+
"error_message",
|
|
321
|
+
"created_at",
|
|
322
|
+
];
|
|
323
|
+
const csvRows = [headers.join(",")];
|
|
324
|
+
|
|
325
|
+
for (const log of logs) {
|
|
326
|
+
const row = headers.map((h) => {
|
|
327
|
+
const val = log[h];
|
|
328
|
+
if (val === null || val === undefined) return "";
|
|
329
|
+
const str = String(val);
|
|
330
|
+
return str.includes(",") || str.includes('"')
|
|
331
|
+
? `"${str.replace(/"/g, '""')}"`
|
|
332
|
+
: str;
|
|
333
|
+
});
|
|
334
|
+
csvRows.push(row.join(","));
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return csvRows.join("\n");
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return JSON.stringify(logs, null, 2);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Delete old audit logs.
|
|
345
|
+
*/
|
|
346
|
+
export function purgeLogs(db, daysToKeep = 90) {
|
|
347
|
+
ensureAuditTables(db);
|
|
348
|
+
|
|
349
|
+
const cutoff = new Date();
|
|
350
|
+
cutoff.setDate(cutoff.getDate() - daysToKeep);
|
|
351
|
+
const cutoffStr = cutoff.toISOString().replace("T", " ").slice(0, 19);
|
|
352
|
+
|
|
353
|
+
const result = db
|
|
354
|
+
.prepare("DELETE FROM audit_log WHERE created_at < ?")
|
|
355
|
+
.run(cutoffStr);
|
|
356
|
+
return result.changes;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Get the most recent audit events.
|
|
361
|
+
*/
|
|
362
|
+
export function getRecentEvents(db, limit = 20) {
|
|
363
|
+
return queryLogs(db, { limit });
|
|
364
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BI Engine — Business intelligence with NL→SQL queries, dashboards,
|
|
3
|
+
* reports, anomaly detection, trend prediction, and scheduling.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import crypto from "crypto";
|
|
7
|
+
|
|
8
|
+
/* ── In-memory stores ──────────────────────────────────────── */
|
|
9
|
+
const _dashboards = new Map();
|
|
10
|
+
const _reports = new Map();
|
|
11
|
+
const _scheduledReports = new Map();
|
|
12
|
+
|
|
13
|
+
const _templates = [
|
|
14
|
+
{
|
|
15
|
+
id: "tpl-kpi",
|
|
16
|
+
name: "KPI Dashboard",
|
|
17
|
+
description: "Key performance indicators overview",
|
|
18
|
+
widgets: ["metric-card", "sparkline", "gauge"],
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
id: "tpl-sales",
|
|
22
|
+
name: "Sales Report",
|
|
23
|
+
description: "Sales pipeline and revenue analysis",
|
|
24
|
+
widgets: ["bar-chart", "funnel", "table"],
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
id: "tpl-ops",
|
|
28
|
+
name: "Operations Dashboard",
|
|
29
|
+
description: "System health and operational metrics",
|
|
30
|
+
widgets: ["heatmap", "timeline", "alert-list"],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "tpl-hr",
|
|
34
|
+
name: "HR Analytics",
|
|
35
|
+
description: "Workforce analytics and headcount",
|
|
36
|
+
widgets: ["pie-chart", "trend-line", "scorecard"],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "tpl-finance",
|
|
40
|
+
name: "Financial Overview",
|
|
41
|
+
description: "Revenue, expenses, and cash flow",
|
|
42
|
+
widgets: ["waterfall", "stacked-bar", "summary-table"],
|
|
43
|
+
},
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
/* ── Schema ────────────────────────────────────────────────── */
|
|
47
|
+
|
|
48
|
+
export function ensureBITables(db) {
|
|
49
|
+
db.exec(`
|
|
50
|
+
CREATE TABLE IF NOT EXISTS bi_dashboards (
|
|
51
|
+
id TEXT PRIMARY KEY,
|
|
52
|
+
name TEXT NOT NULL,
|
|
53
|
+
widgets TEXT,
|
|
54
|
+
layout TEXT,
|
|
55
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
56
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
57
|
+
)
|
|
58
|
+
`);
|
|
59
|
+
db.exec(`
|
|
60
|
+
CREATE TABLE IF NOT EXISTS bi_reports (
|
|
61
|
+
id TEXT PRIMARY KEY,
|
|
62
|
+
name TEXT NOT NULL,
|
|
63
|
+
query TEXT,
|
|
64
|
+
result TEXT,
|
|
65
|
+
format TEXT DEFAULT 'pdf',
|
|
66
|
+
created_at TEXT DEFAULT (datetime('now'))
|
|
67
|
+
)
|
|
68
|
+
`);
|
|
69
|
+
db.exec(`
|
|
70
|
+
CREATE TABLE IF NOT EXISTS bi_scheduled (
|
|
71
|
+
id TEXT PRIMARY KEY,
|
|
72
|
+
report_id TEXT NOT NULL,
|
|
73
|
+
cron TEXT NOT NULL,
|
|
74
|
+
recipients TEXT,
|
|
75
|
+
last_run TEXT,
|
|
76
|
+
status TEXT DEFAULT 'active'
|
|
77
|
+
)
|
|
78
|
+
`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* ── NL → SQL Query ────────────────────────────────────────── */
|
|
82
|
+
|
|
83
|
+
export function nlQuery(query) {
|
|
84
|
+
if (!query || typeof query !== "string") {
|
|
85
|
+
throw new Error("Query must be a non-empty string");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Mock NL→SQL translation: generate a SELECT LIKE query
|
|
89
|
+
const sanitized = query.replace(/['"]/g, "").trim();
|
|
90
|
+
const words = sanitized.split(/\s+/).slice(0, 3).join("_").toLowerCase();
|
|
91
|
+
const generatedSQL = `SELECT * FROM data WHERE content LIKE '%${words}%'`;
|
|
92
|
+
|
|
93
|
+
const id = crypto.randomUUID();
|
|
94
|
+
const results = [];
|
|
95
|
+
const visualization = {
|
|
96
|
+
type: "table",
|
|
97
|
+
title: query,
|
|
98
|
+
columns: ["id", "content", "value"],
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
id,
|
|
103
|
+
query,
|
|
104
|
+
generatedSQL,
|
|
105
|
+
results,
|
|
106
|
+
rowCount: results.length,
|
|
107
|
+
visualization,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/* ── Reports ───────────────────────────────────────────────── */
|
|
112
|
+
|
|
113
|
+
export function generateReport(db, name, options) {
|
|
114
|
+
const id = crypto.randomUUID();
|
|
115
|
+
const now = new Date().toISOString();
|
|
116
|
+
const format = (options && options.format) || "pdf";
|
|
117
|
+
|
|
118
|
+
const sectionNames = (options && options.sections) || [
|
|
119
|
+
"summary",
|
|
120
|
+
"details",
|
|
121
|
+
"conclusion",
|
|
122
|
+
];
|
|
123
|
+
const sections = sectionNames.map((s) => ({
|
|
124
|
+
name: s,
|
|
125
|
+
content: `Auto-generated ${s} section`,
|
|
126
|
+
generatedAt: now,
|
|
127
|
+
}));
|
|
128
|
+
|
|
129
|
+
const report = {
|
|
130
|
+
id,
|
|
131
|
+
name,
|
|
132
|
+
format,
|
|
133
|
+
sections,
|
|
134
|
+
generatedAt: now,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
_reports.set(id, report);
|
|
138
|
+
|
|
139
|
+
db.prepare(
|
|
140
|
+
`INSERT INTO bi_reports (id, name, query, result, format, created_at)
|
|
141
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
142
|
+
).run(id, name, "", JSON.stringify(report), format, now);
|
|
143
|
+
|
|
144
|
+
return report;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/* ── Dashboards ────────────────────────────────────────────── */
|
|
148
|
+
|
|
149
|
+
export function createDashboard(db, name, widgets, layout) {
|
|
150
|
+
const id = crypto.randomUUID();
|
|
151
|
+
const now = new Date().toISOString();
|
|
152
|
+
|
|
153
|
+
const dashboard = {
|
|
154
|
+
id,
|
|
155
|
+
name,
|
|
156
|
+
widgets: widgets || [],
|
|
157
|
+
layout: layout || { type: "grid", columns: 2 },
|
|
158
|
+
createdAt: now,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
_dashboards.set(id, dashboard);
|
|
162
|
+
|
|
163
|
+
db.prepare(
|
|
164
|
+
`INSERT INTO bi_dashboards (id, name, widgets, layout, created_at, updated_at)
|
|
165
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
166
|
+
).run(
|
|
167
|
+
id,
|
|
168
|
+
name,
|
|
169
|
+
JSON.stringify(dashboard.widgets),
|
|
170
|
+
JSON.stringify(dashboard.layout),
|
|
171
|
+
now,
|
|
172
|
+
now,
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
return dashboard;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/* ── Anomaly Detection ─────────────────────────────────────── */
|
|
179
|
+
|
|
180
|
+
export function detectAnomaly(data, options) {
|
|
181
|
+
if (!Array.isArray(data) || data.length === 0) {
|
|
182
|
+
throw new Error("Data must be a non-empty array of numbers");
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const threshold = (options && options.threshold) || 2;
|
|
186
|
+
const n = data.length;
|
|
187
|
+
const mean = data.reduce((s, v) => s + v, 0) / n;
|
|
188
|
+
const variance = data.reduce((s, v) => s + (v - mean) ** 2, 0) / n;
|
|
189
|
+
const std = Math.sqrt(variance);
|
|
190
|
+
|
|
191
|
+
const anomalies = [];
|
|
192
|
+
if (std > 0) {
|
|
193
|
+
for (let i = 0; i < data.length; i++) {
|
|
194
|
+
const zScore = Math.abs((data[i] - mean) / std);
|
|
195
|
+
if (zScore > threshold) {
|
|
196
|
+
anomalies.push({ index: i, value: data[i], zScore });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return { anomalies, mean, std, threshold };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* ── Trend Prediction ──────────────────────────────────────── */
|
|
205
|
+
|
|
206
|
+
export function predictTrend(data, periods) {
|
|
207
|
+
if (!Array.isArray(data) || data.length < 2) {
|
|
208
|
+
throw new Error("Data must be an array with at least 2 points");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const n = data.length;
|
|
212
|
+
const periodsToPredict = periods || 3;
|
|
213
|
+
|
|
214
|
+
// Simple linear regression: y = slope * x + intercept
|
|
215
|
+
let sumX = 0;
|
|
216
|
+
let sumY = 0;
|
|
217
|
+
let sumXY = 0;
|
|
218
|
+
let sumX2 = 0;
|
|
219
|
+
|
|
220
|
+
for (let i = 0; i < n; i++) {
|
|
221
|
+
sumX += i;
|
|
222
|
+
sumY += data[i];
|
|
223
|
+
sumXY += i * data[i];
|
|
224
|
+
sumX2 += i * i;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const slope = (n * sumXY - sumX * sumY) / (n * sumX2 - sumX * sumX);
|
|
228
|
+
const intercept = (sumY - slope * sumX) / n;
|
|
229
|
+
|
|
230
|
+
const predictions = [];
|
|
231
|
+
for (let i = 0; i < periodsToPredict; i++) {
|
|
232
|
+
const x = n + i;
|
|
233
|
+
predictions.push(Math.round((slope * x + intercept) * 100) / 100);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let trend;
|
|
237
|
+
if (Math.abs(slope) < 0.001) {
|
|
238
|
+
trend = "flat";
|
|
239
|
+
} else if (slope > 0) {
|
|
240
|
+
trend = "up";
|
|
241
|
+
} else {
|
|
242
|
+
trend = "down";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
return { predictions, trend, slope: Math.round(slope * 1000) / 1000 };
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/* ── Templates ─────────────────────────────────────────────── */
|
|
249
|
+
|
|
250
|
+
export function listTemplates() {
|
|
251
|
+
return [..._templates];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* ── Scheduling ────────────────────────────────────────────── */
|
|
255
|
+
|
|
256
|
+
export function scheduleReport(db, reportId, cron, recipients) {
|
|
257
|
+
const id = crypto.randomUUID();
|
|
258
|
+
const schedule = {
|
|
259
|
+
id,
|
|
260
|
+
reportId,
|
|
261
|
+
cron,
|
|
262
|
+
recipients: recipients || [],
|
|
263
|
+
lastRun: null,
|
|
264
|
+
status: "active",
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
_scheduledReports.set(id, schedule);
|
|
268
|
+
|
|
269
|
+
db.prepare(
|
|
270
|
+
`INSERT INTO bi_scheduled (id, report_id, cron, recipients, last_run, status)
|
|
271
|
+
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
272
|
+
).run(id, reportId, cron, JSON.stringify(schedule.recipients), "", "active");
|
|
273
|
+
|
|
274
|
+
return schedule;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* ── Export ─────────────────────────────────────────────────── */
|
|
278
|
+
|
|
279
|
+
export function exportReport(reportId, format) {
|
|
280
|
+
const report = _reports.get(reportId);
|
|
281
|
+
if (!report) throw new Error(`Report not found: ${reportId}`);
|
|
282
|
+
|
|
283
|
+
const exportFormat = format || report.format || "pdf";
|
|
284
|
+
return {
|
|
285
|
+
reportId,
|
|
286
|
+
format: exportFormat,
|
|
287
|
+
filename: `${report.name.replace(/\s+/g, "_")}.${exportFormat}`,
|
|
288
|
+
size: 0,
|
|
289
|
+
exportedAt: new Date().toISOString(),
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/* ── Reset (for testing) ───────────────────────────────────── */
|
|
294
|
+
|
|
295
|
+
export function _resetState() {
|
|
296
|
+
_dashboards.clear();
|
|
297
|
+
_reports.clear();
|
|
298
|
+
_scheduledReports.clear();
|
|
299
|
+
}
|